mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-01 19:18:28 -04:00
Squashed commit of the following:
commit990d86d481Author: bbedward <bbedward@gmail.com> Date: Sat Jul 18 10:43:22 2026 -0400 flake: update-common commit526cb157fdAuthor: bbedward <bbedward@gmail.com> Date: Thu Jul 16 17:56:40 2026 -0400 i18n: update sync scrirpt for dank-qml-common commit92ba96d9f9Author: bbedward <bbedward@gmail.com> Date: Thu Jul 16 09:24:37 2026 -0400 qs: integrate with dank-qml-common
This commit is contained in:
@@ -1,935 +1,3 @@
|
||||
import Qt.labs.folderlistmodel
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||
property string docsDir: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
|
||||
property string musicDir: StandardPaths.writableLocation(StandardPaths.MusicLocation)
|
||||
property string videosDir: StandardPaths.writableLocation(StandardPaths.MoviesLocation)
|
||||
property string picsDir: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
|
||||
property string downloadDir: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
|
||||
property string desktopDir: StandardPaths.writableLocation(StandardPaths.DesktopLocation)
|
||||
property string currentPath: ""
|
||||
property var fileExtensions: ["*.*"]
|
||||
property alias filterExtensions: root.fileExtensions
|
||||
property string browserTitle: "Select File"
|
||||
property string browserIcon: "folder_open"
|
||||
property string browserType: "generic"
|
||||
property bool showHiddenFiles: false
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool backButtonFocused: false
|
||||
property bool saveMode: false
|
||||
property string defaultFileName: ""
|
||||
property int keyboardSelectionIndex: -1
|
||||
property bool keyboardSelectionRequested: false
|
||||
property bool showKeyboardHints: false
|
||||
property bool showFileInfo: false
|
||||
property string selectedFilePath: ""
|
||||
property string selectedFileName: ""
|
||||
property bool selectedFileIsDir: false
|
||||
property bool showOverwriteConfirmation: false
|
||||
property string pendingFilePath: ""
|
||||
property bool showSidebar: true
|
||||
property string viewMode: "grid"
|
||||
property string sortBy: "name"
|
||||
property bool sortAscending: true
|
||||
property int iconSizeIndex: 1
|
||||
property var iconSizes: [80, 120, 160, 200]
|
||||
property bool pathEditMode: false
|
||||
property bool pathInputHasFocus: false
|
||||
property int actualGridColumns: 5
|
||||
property bool _initialized: false
|
||||
property bool closeOnEscape: true
|
||||
property var windowControls: null
|
||||
|
||||
signal fileSelected(string path)
|
||||
signal closeRequested
|
||||
|
||||
function encodeFileUrl(path) {
|
||||
if (!path)
|
||||
return "";
|
||||
return "file://" + path.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
loadSettings();
|
||||
currentPath = getLastPath();
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
currentPath = getLastPath();
|
||||
selectedIndex = -1;
|
||||
keyboardNavigationActive = false;
|
||||
backButtonFocused = false;
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
const type = browserType || "default";
|
||||
const settings = CacheData.fileBrowserSettings[type];
|
||||
const isImageBrowser = ["wallpaper", "profile"].includes(browserType);
|
||||
|
||||
if (settings) {
|
||||
viewMode = settings.viewMode || (isImageBrowser ? "grid" : "list");
|
||||
sortBy = settings.sortBy || "name";
|
||||
sortAscending = settings.sortAscending !== undefined ? settings.sortAscending : true;
|
||||
iconSizeIndex = settings.iconSizeIndex !== undefined ? settings.iconSizeIndex : 1;
|
||||
showSidebar = settings.showSidebar !== undefined ? settings.showSidebar : true;
|
||||
} else {
|
||||
viewMode = isImageBrowser ? "grid" : "list";
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
if (!_initialized)
|
||||
return;
|
||||
const type = browserType || "default";
|
||||
let settings = CacheData.fileBrowserSettings;
|
||||
if (!settings[type]) {
|
||||
settings[type] = {};
|
||||
}
|
||||
settings[type].viewMode = viewMode;
|
||||
settings[type].sortBy = sortBy;
|
||||
settings[type].sortAscending = sortAscending;
|
||||
settings[type].iconSizeIndex = iconSizeIndex;
|
||||
settings[type].showSidebar = showSidebar;
|
||||
settings[type].lastPath = currentPath;
|
||||
CacheData.fileBrowserSettings = settings;
|
||||
|
||||
if (browserType === "wallpaper") {
|
||||
CacheData.wallpaperLastPath = currentPath;
|
||||
} else if (browserType === "profile") {
|
||||
CacheData.profileLastPath = currentPath;
|
||||
}
|
||||
|
||||
CacheData.saveCache();
|
||||
}
|
||||
|
||||
onViewModeChanged: saveSettings()
|
||||
onSortByChanged: saveSettings()
|
||||
onSortAscendingChanged: saveSettings()
|
||||
onIconSizeIndexChanged: saveSettings()
|
||||
onShowSidebarChanged: saveSettings()
|
||||
|
||||
function isImageFile(fileName) {
|
||||
if (!fileName)
|
||||
return false;
|
||||
const ext = fileName.toLowerCase().split('.').pop();
|
||||
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext);
|
||||
}
|
||||
|
||||
function getLastPath() {
|
||||
const type = browserType || "default";
|
||||
const settings = CacheData.fileBrowserSettings[type];
|
||||
const lastPath = settings?.lastPath || "";
|
||||
return (lastPath && lastPath !== "") ? lastPath : homeDir;
|
||||
}
|
||||
|
||||
function saveLastPath(path) {
|
||||
const type = browserType || "default";
|
||||
let settings = CacheData.fileBrowserSettings;
|
||||
if (!settings[type]) {
|
||||
settings[type] = {};
|
||||
}
|
||||
settings[type].lastPath = path;
|
||||
CacheData.fileBrowserSettings = settings;
|
||||
CacheData.saveCache();
|
||||
|
||||
if (browserType === "wallpaper") {
|
||||
CacheData.wallpaperLastPath = path;
|
||||
} else if (browserType === "profile") {
|
||||
CacheData.profileLastPath = path;
|
||||
}
|
||||
}
|
||||
|
||||
function setSelectedFileData(path, name, isDir) {
|
||||
selectedFilePath = path;
|
||||
selectedFileName = name;
|
||||
selectedFileIsDir = isDir;
|
||||
}
|
||||
|
||||
function openItemContextMenu(sender, localX, localY, path, name, isDir) {
|
||||
if (!sender)
|
||||
return;
|
||||
const pos = sender.mapToItem(root, localX, localY);
|
||||
itemContextMenu.showAt(root, pos.x, pos.y, path, name, isDir);
|
||||
}
|
||||
|
||||
function navigateUp() {
|
||||
const path = currentPath;
|
||||
if (path === homeDir)
|
||||
return;
|
||||
const lastSlash = path.lastIndexOf('/');
|
||||
if (lastSlash <= 0)
|
||||
return;
|
||||
const newPath = path.substring(0, lastSlash);
|
||||
if (newPath.length < homeDir.length) {
|
||||
currentPath = homeDir;
|
||||
saveLastPath(homeDir);
|
||||
} else {
|
||||
currentPath = newPath;
|
||||
saveLastPath(newPath);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTo(path) {
|
||||
currentPath = path;
|
||||
saveLastPath(path);
|
||||
selectedIndex = -1;
|
||||
backButtonFocused = false;
|
||||
}
|
||||
|
||||
function keyboardFileSelection(index) {
|
||||
if (index < 0)
|
||||
return;
|
||||
keyboardSelectionTimer.targetIndex = index;
|
||||
keyboardSelectionTimer.start();
|
||||
}
|
||||
|
||||
function executeKeyboardSelection(index) {
|
||||
keyboardSelectionIndex = index;
|
||||
keyboardSelectionRequested = true;
|
||||
}
|
||||
|
||||
function activateFile(path, name, isDir) {
|
||||
if (isDir) {
|
||||
navigateTo(path);
|
||||
return;
|
||||
}
|
||||
if (saveMode) {
|
||||
saveRow.fileName = name;
|
||||
pendingFilePath = path;
|
||||
showOverwriteConfirmation = true;
|
||||
} else {
|
||||
fileSelected(path);
|
||||
closeRequested();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSaveFile(filePath) {
|
||||
var normalizedPath = filePath;
|
||||
if (!normalizedPath.startsWith("file://")) {
|
||||
normalizedPath = encodeFileUrl(filePath);
|
||||
}
|
||||
|
||||
var exists = false;
|
||||
var fileName = filePath.split('/').pop();
|
||||
|
||||
for (var i = 0; i < folderModel.count; i++) {
|
||||
if (folderModel.get(i, "fileName") === fileName && !folderModel.get(i, "fileIsDir")) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
pendingFilePath = normalizedPath;
|
||||
showOverwriteConfirmation = true;
|
||||
} else {
|
||||
fileSelected(normalizedPath);
|
||||
closeRequested();
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentPathChanged: {
|
||||
selectedFilePath = "";
|
||||
selectedFileName = "";
|
||||
selectedFileIsDir = false;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
onSelectedIndexChanged: {
|
||||
if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) {
|
||||
selectedFilePath = "";
|
||||
selectedFileName = "";
|
||||
selectedFileIsDir = false;
|
||||
}
|
||||
}
|
||||
|
||||
property var steamPaths: [StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.steam/steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/snap/steam/common/.local/share/Steam/steamapps/workshop/content/431960"]
|
||||
|
||||
property var quickAccessLocations: [
|
||||
{
|
||||
"name": I18n.tr("Home"),
|
||||
"path": homeDir,
|
||||
"icon": "home"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Documents"),
|
||||
"path": docsDir,
|
||||
"icon": "description"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Downloads"),
|
||||
"path": downloadDir,
|
||||
"icon": "download"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Pictures"),
|
||||
"path": picsDir,
|
||||
"icon": "image"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Music"),
|
||||
"path": musicDir,
|
||||
"icon": "music_note"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Videos"),
|
||||
"path": videosDir,
|
||||
"icon": "movie"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Desktop"),
|
||||
"path": desktopDir,
|
||||
"icon": "computer"
|
||||
}
|
||||
]
|
||||
|
||||
FolderListModel {
|
||||
id: folderModel
|
||||
|
||||
showDirsFirst: true
|
||||
showDotAndDotDot: false
|
||||
showHidden: root.showHiddenFiles
|
||||
caseSensitive: false
|
||||
nameFilters: fileExtensions
|
||||
showFiles: true
|
||||
showDirs: true
|
||||
folder: encodeFileUrl(currentPath || homeDir)
|
||||
sortField: {
|
||||
switch (sortBy) {
|
||||
case "name":
|
||||
return FolderListModel.Name;
|
||||
case "size":
|
||||
return FolderListModel.Size;
|
||||
case "modified":
|
||||
return FolderListModel.Time;
|
||||
case "type":
|
||||
return FolderListModel.Type;
|
||||
default:
|
||||
return FolderListModel.Name;
|
||||
}
|
||||
}
|
||||
sortReversed: !sortAscending
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: keyboardController
|
||||
|
||||
property int totalItems: folderModel.count
|
||||
property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns)
|
||||
|
||||
function handleKey(event) {
|
||||
if (event.key === Qt.Key_Escape && root.closeOnEscape) {
|
||||
closeRequested();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (event.key === Qt.Key_F10) {
|
||||
showKeyboardHints = !showKeyboardHints;
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) {
|
||||
showFileInfo = !showFileInfo;
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) {
|
||||
if (currentPath !== homeDir) {
|
||||
navigateUp();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!keyboardNavigationActive) {
|
||||
const isInitKey = event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key === Qt.Key_Right || (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier);
|
||||
|
||||
if (isInitKey) {
|
||||
keyboardNavigationActive = true;
|
||||
if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
} else {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (event.key) {
|
||||
case Qt.Key_Tab:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
} else {
|
||||
selectedIndex = 0;
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Backtab:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = totalItems - 1;
|
||||
} else if (selectedIndex > 0) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
} else {
|
||||
selectedIndex = totalItems - 1;
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_N:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_P:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_J:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_K:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_H:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (!backButtonFocused && selectedIndex > 0) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_L:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_Left:
|
||||
if (pathInputHasFocus)
|
||||
return;
|
||||
if (backButtonFocused)
|
||||
return;
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Right:
|
||||
if (pathInputHasFocus)
|
||||
return;
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Up:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
if (gridColumns === 1) {
|
||||
selectedIndex = 0;
|
||||
} else {
|
||||
var col = selectedIndex % gridColumns;
|
||||
selectedIndex = Math.min(col, totalItems - 1);
|
||||
}
|
||||
} else if (selectedIndex >= gridColumns) {
|
||||
selectedIndex -= gridColumns;
|
||||
} else if (selectedIndex > 0 && gridColumns === 1) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Down:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
} else if (gridColumns === 1) {
|
||||
if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
}
|
||||
} else {
|
||||
var newIndex = selectedIndex + gridColumns;
|
||||
if (newIndex < totalItems) {
|
||||
selectedIndex = newIndex;
|
||||
} else {
|
||||
var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns;
|
||||
var col = selectedIndex % gridColumns;
|
||||
var targetIndex = lastRowStart + col;
|
||||
if (targetIndex < totalItems && targetIndex > selectedIndex) {
|
||||
selectedIndex = targetIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
case Qt.Key_Space:
|
||||
if (backButtonFocused) {
|
||||
navigateUp();
|
||||
} else if (selectedIndex >= 0 && selectedIndex < totalItems) {
|
||||
root.keyboardFileSelection(selectedIndex);
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: keyboardSelectionTimer
|
||||
|
||||
property int targetIndex: -1
|
||||
|
||||
interval: 1
|
||||
onTriggered: {
|
||||
executeKeyboardSelection(targetIndex);
|
||||
}
|
||||
}
|
||||
|
||||
focus: true
|
||||
|
||||
Keys.onPressed: event => {
|
||||
keyboardController.handleKey(event);
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: if (windowControls)
|
||||
windowControls.tryStartMove()
|
||||
onDoubleClicked: if (windowControls)
|
||||
windowControls.tryToggleMaximize()
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
|
||||
DankIcon {
|
||||
name: browserIcon
|
||||
size: Theme.iconSizeLarge
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: browserTitle
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: showHiddenFiles ? "visibility_off" : "visibility"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: showHiddenFiles ? Theme.primary : Theme.surfaceText
|
||||
onClicked: showHiddenFiles = !showHiddenFiles
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: viewMode === "grid" ? "view_list" : "grid_view"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: viewMode = viewMode === "grid" ? "list" : "grid"
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: iconSizeIndex === 0 ? "photo_size_select_small" : iconSizeIndex === 1 ? "photo_size_select_large" : iconSizeIndex === 2 ? "photo_size_select_actual" : "zoom_in"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
visible: viewMode === "grid"
|
||||
onClicked: iconSizeIndex = (iconSizeIndex + 1) % iconSizes.length
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: "info"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: root.showKeyboardHints = !root.showKeyboardHints
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
visible: windowControls?.supported ?? false
|
||||
circular: false
|
||||
iconName: windowControls?.targetWindow?.maximized ? "fullscreen_exit" : "fullscreen"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: if (windowControls)
|
||||
windowControls.tryToggleMaximize()
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: root.closeRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - 49
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: root.saveMode ? 40 + Theme.spacingL * 2 : 0
|
||||
spacing: 0
|
||||
|
||||
Row {
|
||||
width: showSidebar ? 201 : 0
|
||||
height: parent.height
|
||||
spacing: 0
|
||||
visible: showSidebar
|
||||
|
||||
FileBrowserSidebar {
|
||||
height: parent.height
|
||||
quickAccessLocations: root.quickAccessLocations
|
||||
currentPath: root.currentPath
|
||||
onLocationSelected: path => navigateTo(path)
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: 1
|
||||
height: parent.height
|
||||
color: Theme.outline
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - (showSidebar ? 201 : 0)
|
||||
height: parent.height
|
||||
spacing: 0
|
||||
|
||||
FileBrowserNavigation {
|
||||
width: parent.width
|
||||
currentPath: root.currentPath
|
||||
homeDir: root.homeDir
|
||||
backButtonFocused: root.backButtonFocused
|
||||
keyboardNavigationActive: root.keyboardNavigationActive
|
||||
showSidebar: root.showSidebar
|
||||
pathEditMode: root.pathEditMode
|
||||
onNavigateUp: root.navigateUp()
|
||||
onNavigateTo: path => root.navigateTo(path)
|
||||
onPathInputFocusChanged: hasFocus => {
|
||||
root.pathInputHasFocus = hasFocus;
|
||||
if (hasFocus) {
|
||||
root.pathEditMode = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
Item {
|
||||
id: gridContainer
|
||||
width: parent.width
|
||||
height: parent.height - 41
|
||||
clip: true
|
||||
|
||||
property real gridCellWidth: iconSizes[iconSizeIndex] + 24
|
||||
property real gridCellHeight: iconSizes[iconSizeIndex] + 56
|
||||
property real availableGridWidth: width - Theme.spacingM * 2
|
||||
property int gridColumns: Math.max(1, Math.floor(availableGridWidth / gridCellWidth))
|
||||
property real gridLeftMargin: Theme.spacingM + Math.max(0, (availableGridWidth - (gridColumns * gridCellWidth)) / 2)
|
||||
|
||||
onGridColumnsChanged: {
|
||||
root.actualGridColumns = gridColumns;
|
||||
}
|
||||
Component.onCompleted: {
|
||||
root.actualGridColumns = gridColumns;
|
||||
}
|
||||
|
||||
DankGridView {
|
||||
id: fileGrid
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: gridContainer.gridLeftMargin
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
visible: viewMode === "grid"
|
||||
cellWidth: gridContainer.gridCellWidth
|
||||
cellHeight: gridContainer.gridCellHeight
|
||||
cacheBuffer: 260
|
||||
model: folderModel
|
||||
currentIndex: selectedIndex
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive && currentIndex >= 0)
|
||||
positionViewAtIndex(currentIndex, GridView.Contain);
|
||||
}
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: gridScrollbar
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: DankScrollbar {
|
||||
policy: ScrollBar.AlwaysOff
|
||||
}
|
||||
|
||||
delegate: FileBrowserGridDelegate {
|
||||
iconSizes: root.iconSizes
|
||||
iconSizeIndex: root.iconSizeIndex
|
||||
selectedIndex: root.selectedIndex
|
||||
keyboardNavigationActive: root.keyboardNavigationActive
|
||||
onItemClicked: (index, path, name, isDir) => {
|
||||
selectedIndex = index;
|
||||
setSelectedFileData(path, name, isDir);
|
||||
root.activateFile(path, name, isDir);
|
||||
}
|
||||
onItemSelected: (index, path, name, isDir) => {
|
||||
setSelectedFileData(path, name, isDir);
|
||||
}
|
||||
onItemContextMenuRequested: (sender, localX, localY, path, name, isDir) => {
|
||||
root.openItemContextMenu(sender, localX, localY, path, name, isDir);
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onKeyboardSelectionRequestedChanged() {
|
||||
if (root.keyboardSelectionRequested && root.keyboardSelectionIndex === index) {
|
||||
root.keyboardSelectionRequested = false;
|
||||
selectedIndex = index;
|
||||
setSelectedFileData(filePath, fileName, fileIsDir);
|
||||
root.activateFile(filePath, fileName, fileIsDir);
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: fileList
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
visible: viewMode === "list"
|
||||
spacing: Theme.spacingXXS
|
||||
model: folderModel
|
||||
currentIndex: selectedIndex
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive && currentIndex >= 0)
|
||||
positionViewAtIndex(currentIndex, ListView.Contain);
|
||||
}
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: listScrollbar
|
||||
}
|
||||
|
||||
delegate: FileBrowserListDelegate {
|
||||
width: fileList.width
|
||||
selectedIndex: root.selectedIndex
|
||||
keyboardNavigationActive: root.keyboardNavigationActive
|
||||
onItemClicked: (index, path, name, isDir) => {
|
||||
selectedIndex = index;
|
||||
setSelectedFileData(path, name, isDir);
|
||||
root.activateFile(path, name, isDir);
|
||||
}
|
||||
onItemSelected: (index, path, name, isDir) => {
|
||||
setSelectedFileData(path, name, isDir);
|
||||
}
|
||||
onItemContextMenuRequested: (sender, localX, localY, path, name, isDir) => {
|
||||
root.openItemContextMenu(sender, localX, localY, path, name, isDir);
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onKeyboardSelectionRequestedChanged() {
|
||||
if (root.keyboardSelectionRequested && root.keyboardSelectionIndex === index) {
|
||||
root.keyboardSelectionRequested = false;
|
||||
selectedIndex = index;
|
||||
setSelectedFileData(filePath, fileName, fileIsDir);
|
||||
root.activateFile(filePath, fileName, fileIsDir);
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserSaveRow {
|
||||
id: saveRow
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
saveMode: root.saveMode
|
||||
defaultFileName: root.defaultFileName
|
||||
currentPath: root.currentPath
|
||||
onSaveRequested: filePath => handleSaveFile(filePath)
|
||||
}
|
||||
|
||||
KeyboardHints {
|
||||
id: keyboardHints
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
showHints: root.showKeyboardHints
|
||||
}
|
||||
|
||||
FileInfo {
|
||||
id: fileInfo
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
width: 300
|
||||
showFileInfo: root.showFileInfo
|
||||
selectedIndex: root.selectedIndex
|
||||
sourceFolderModel: folderModel
|
||||
currentPath: root.currentPath
|
||||
currentFileName: root.selectedFileName
|
||||
currentFileIsDir: root.selectedFileIsDir
|
||||
currentFileExtension: {
|
||||
if (root.selectedFileIsDir || !root.selectedFileName)
|
||||
return "";
|
||||
|
||||
var lastDot = root.selectedFileName.lastIndexOf('.');
|
||||
return lastDot > 0 ? root.selectedFileName.substring(lastDot + 1).toLowerCase() : "";
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserSortMenu {
|
||||
id: sortMenu
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 120
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
sortBy: root.sortBy
|
||||
sortAscending: root.sortAscending
|
||||
onSortBySelected: value => {
|
||||
root.sortBy = value;
|
||||
}
|
||||
onSortOrderSelected: ascending => {
|
||||
root.sortAscending = ascending;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserOverwriteDialog {
|
||||
anchors.fill: parent
|
||||
showDialog: showOverwriteConfirmation
|
||||
pendingFilePath: root.pendingFilePath
|
||||
onConfirmed: filePath => {
|
||||
showOverwriteConfirmation = false;
|
||||
fileSelected(filePath);
|
||||
pendingFilePath = "";
|
||||
Qt.callLater(() => root.closeRequested());
|
||||
}
|
||||
onCancelled: {
|
||||
showOverwriteConfirmation = false;
|
||||
pendingFilePath = "";
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserItemContextMenu {
|
||||
id: itemContextMenu
|
||||
parentFocusItem: root
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserContent {}
|
||||
|
||||
@@ -1,252 +1,3 @@
|
||||
import QtQuick
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
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)
|
||||
signal itemContextMenuRequested(var sender, real localX, real localY, string path, string name, bool isDir)
|
||||
|
||||
function getFileExtension(fileName) {
|
||||
const parts = fileName.split('.');
|
||||
if (parts.length > 1) {
|
||||
return parts[parts.length - 1].toLowerCase();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function determineFileType(fileName) {
|
||||
const ext = getFileExtension(fileName);
|
||||
|
||||
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico", "jxl", "avif", "heif", "exr"];
|
||||
if (imageExts.includes(ext)) {
|
||||
return "image";
|
||||
}
|
||||
|
||||
const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"];
|
||||
if (videoExts.includes(ext)) {
|
||||
return "video";
|
||||
}
|
||||
|
||||
const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"];
|
||||
if (audioExts.includes(ext)) {
|
||||
return "audio";
|
||||
}
|
||||
|
||||
const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"];
|
||||
if (codeExts.includes(ext)) {
|
||||
return "code";
|
||||
}
|
||||
|
||||
const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"];
|
||||
if (docExts.includes(ext)) {
|
||||
return "document";
|
||||
}
|
||||
|
||||
const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"];
|
||||
if (archiveExts.includes(ext)) {
|
||||
return "archive";
|
||||
}
|
||||
|
||||
if (!ext || fileName.indexOf('.') === -1) {
|
||||
return "binary";
|
||||
}
|
||||
|
||||
return "file";
|
||||
}
|
||||
|
||||
function isImageFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
return determineFileType(fileName) === "image";
|
||||
}
|
||||
|
||||
function isVideoFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
return determineFileType(fileName) === "video";
|
||||
}
|
||||
|
||||
property bool isImage: isImageFile(delegateRoot.fileName)
|
||||
property bool isVideo: isVideoFile(delegateRoot.fileName)
|
||||
|
||||
property string _xdgCacheHome: Paths.strip(Paths.xdgCache)
|
||||
property string _thumbnailSize: iconSizeIndex >= 2 ? "x-large" : "large"
|
||||
property int _thumbnailPx: iconSizeIndex >= 2 ? 512 : 256
|
||||
property string videoThumbnailPath: {
|
||||
if (!delegateRoot.fileIsDir && isVideo) {
|
||||
const hash = Qt.md5("file://" + delegateRoot.filePath);
|
||||
return _xdgCacheHome + "/thumbnails/" + _thumbnailSize + "/" + hash + ".png";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
property string _videoThumb: ""
|
||||
property bool _thumbGenAttempted: false
|
||||
|
||||
// Probe the thumbnail optimistically; Image.Error triggers generation
|
||||
onVideoThumbnailPathChanged: {
|
||||
_thumbGenAttempted = false;
|
||||
_videoThumb = videoThumbnailPath;
|
||||
}
|
||||
|
||||
function generateVideoThumbnail() {
|
||||
if (_thumbGenAttempted)
|
||||
return;
|
||||
_thumbGenAttempted = true;
|
||||
_videoThumb = "";
|
||||
const thumbPath = videoThumbnailPath;
|
||||
const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize;
|
||||
const script = "mkdir -p \"$1\" && ffmpegthumbnailer -i \"$2\" -o \"$3\" -s " + _thumbnailPx + " -f";
|
||||
Proc.runCommand(null, ["sh", "-c", script, "thumb", thumbDir, delegateRoot.filePath, thumbPath], function (output, exitCode) {
|
||||
if (exitCode === 0)
|
||||
_videoThumb = thumbPath;
|
||||
});
|
||||
}
|
||||
|
||||
function getIconForFile(fileName) {
|
||||
const lowerName = fileName.toLowerCase();
|
||||
if (lowerName.startsWith("dockerfile")) {
|
||||
return "docker";
|
||||
}
|
||||
const ext = fileName.split('.').pop();
|
||||
return ext || "";
|
||||
}
|
||||
|
||||
width: weMode ? 245 : iconSizes[iconSizeIndex] + 16
|
||||
height: weMode ? 205 : iconSizes[iconSizeIndex] + 48
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
|
||||
return Theme.surfacePressed;
|
||||
|
||||
return mouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : Theme.withAlpha(Theme.surfaceContainerHigh, 0);
|
||||
}
|
||||
border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
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
|
||||
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
id: gridPreviewImage
|
||||
anchors.fill: parent
|
||||
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"]
|
||||
property int weExtIndex: 0
|
||||
property string imagePath: {
|
||||
if (weMode && delegateRoot.fileIsDir)
|
||||
return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
|
||||
if (_videoThumb)
|
||||
return _videoThumb;
|
||||
return "";
|
||||
}
|
||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||
onStatusChanged: {
|
||||
if (status !== Image.Error)
|
||||
return;
|
||||
if (weMode && delegateRoot.fileIsDir) {
|
||||
if (weExtIndex < weExtensions.length - 1) {
|
||||
weExtIndex++;
|
||||
} else {
|
||||
imagePath = "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_videoThumb)
|
||||
generateVideoThumbnail();
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: weMode ? 225 : iconSizes[iconSizeIndex]
|
||||
sourceSize.height: weMode ? 225 : iconSizes[iconSizeIndex]
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready && ((!delegateRoot.fileIsDir && isVideo) || (weMode && delegateRoot.fileIsDir))
|
||||
}
|
||||
|
||||
CachingImage {
|
||||
anchors.fill: parent
|
||||
imagePath: !delegateRoot.fileIsDir && isImage ? delegateRoot.filePath : ""
|
||||
maxCacheSize: 256
|
||||
animate: false
|
||||
visible: !delegateRoot.fileIsDir && isImage
|
||||
}
|
||||
}
|
||||
|
||||
DankNFIcon {
|
||||
anchors.centerIn: parent
|
||||
name: delegateRoot.fileIsDir ? "folder" : getIconForFile(delegateRoot.fileName)
|
||||
size: iconSizes[iconSizeIndex] * 0.45
|
||||
color: delegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
|
||||
visible: (!delegateRoot.fileIsDir && !isImage && !(isVideo && gridPreviewImage.status === Image.Ready)) || (delegateRoot.fileIsDir && !weMode)
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: delegateRoot.fileName || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: delegateRoot.width - Theme.spacingM
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
switch (mouse.button) {
|
||||
case Qt.LeftButton:
|
||||
itemClicked(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
|
||||
break;
|
||||
case Qt.RightButton:
|
||||
itemContextMenuRequested(delegateRoot, mouse.x, mouse.y, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserGridDelegate {}
|
||||
|
||||
@@ -1,153 +1,3 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property string filePath: ""
|
||||
property string fileName: ""
|
||||
property bool fileIsDir: false
|
||||
property var parentFocusItem: null
|
||||
|
||||
signal trashed
|
||||
signal menuClosed
|
||||
|
||||
readonly property var menuItems: [
|
||||
{
|
||||
text: I18n.tr("Move to Trash"),
|
||||
icon: "delete",
|
||||
action: trashItem,
|
||||
enabled: filePath.length > 0,
|
||||
dangerous: true
|
||||
},
|
||||
{
|
||||
text: I18n.tr("Copy path"),
|
||||
icon: "content_copy",
|
||||
action: copyPath,
|
||||
enabled: filePath.length > 0
|
||||
}
|
||||
]
|
||||
|
||||
function showAt(parentItem, localX, localY, path, name, isDir) {
|
||||
if (!parentItem)
|
||||
return;
|
||||
parent = parentItem;
|
||||
filePath = path || "";
|
||||
fileName = name || "";
|
||||
fileIsDir = !!isDir;
|
||||
x = Math.max(0, Math.min(parentItem.width - width, localX));
|
||||
y = Math.max(0, Math.min(parentItem.height - height, localY));
|
||||
open();
|
||||
}
|
||||
|
||||
function trashItem() {
|
||||
if (!filePath)
|
||||
return;
|
||||
TrashService.trashPath(filePath, ok => {
|
||||
if (ok)
|
||||
root.trashed();
|
||||
});
|
||||
close();
|
||||
}
|
||||
|
||||
function copyPath() {
|
||||
if (!filePath)
|
||||
return;
|
||||
Quickshell.execDetached(["dms", "cl", "copy", filePath]);
|
||||
close();
|
||||
}
|
||||
|
||||
width: 220
|
||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
padding: 0
|
||||
modal: false
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
|
||||
onClosed: {
|
||||
closePolicy = Popup.CloseOnEscape;
|
||||
menuClosed();
|
||||
if (parentFocusItem)
|
||||
Qt.callLater(() => parentFocusItem.forceActiveFocus());
|
||||
}
|
||||
|
||||
onOpened: outsideClickTimer.start()
|
||||
|
||||
Timer {
|
||||
id: outsideClickTimer
|
||||
interval: 100
|
||||
onTriggered: root.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
contentItem: Rectangle {
|
||||
color: Theme.floatingSurface
|
||||
radius: Theme.cornerRadius
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Repeater {
|
||||
model: root.menuItems
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
opacity: modelData.enabled ? 1 : 0.5
|
||||
color: {
|
||||
if (!modelData.enabled || !area.containsMouse)
|
||||
return "transparent";
|
||||
if (modelData.dangerous)
|
||||
return Theme.errorHover;
|
||||
return BlurService.hoverColor(Theme.widgetBaseHoverColor);
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: modelData.icon
|
||||
size: 16
|
||||
color: modelData.dangerous && area.containsMouse && modelData.enabled ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData.text
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: modelData.dangerous && area.containsMouse && modelData.enabled ? Theme.error : Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: area
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: modelData.enabled
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: modelData.action()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserItemContextMenu {}
|
||||
|
||||
@@ -1,256 +1,3 @@
|
||||
import QtQuick
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
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)
|
||||
signal itemContextMenuRequested(var sender, real localX, real localY, string path, string name, bool isDir)
|
||||
|
||||
function getFileExtension(fileName) {
|
||||
const parts = fileName.split('.');
|
||||
if (parts.length > 1) {
|
||||
return parts[parts.length - 1].toLowerCase();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function determineFileType(fileName) {
|
||||
const ext = getFileExtension(fileName);
|
||||
|
||||
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico", "jxl", "avif", "heif", "exr"];
|
||||
if (imageExts.includes(ext)) {
|
||||
return "image";
|
||||
}
|
||||
|
||||
const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"];
|
||||
if (videoExts.includes(ext)) {
|
||||
return "video";
|
||||
}
|
||||
|
||||
const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"];
|
||||
if (audioExts.includes(ext)) {
|
||||
return "audio";
|
||||
}
|
||||
|
||||
const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"];
|
||||
if (codeExts.includes(ext)) {
|
||||
return "code";
|
||||
}
|
||||
|
||||
const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"];
|
||||
if (docExts.includes(ext)) {
|
||||
return "document";
|
||||
}
|
||||
|
||||
const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"];
|
||||
if (archiveExts.includes(ext)) {
|
||||
return "archive";
|
||||
}
|
||||
|
||||
if (!ext || fileName.indexOf('.') === -1) {
|
||||
return "binary";
|
||||
}
|
||||
|
||||
return "file";
|
||||
}
|
||||
|
||||
function isImageFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
return determineFileType(fileName) === "image";
|
||||
}
|
||||
|
||||
function isVideoFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
return determineFileType(fileName) === "video";
|
||||
}
|
||||
|
||||
property bool isImage: isImageFile(listDelegateRoot.fileName)
|
||||
property bool isVideo: isVideoFile(listDelegateRoot.fileName)
|
||||
|
||||
property string _xdgCacheHome: Paths.strip(Paths.xdgCache)
|
||||
property string videoThumbnailPath: {
|
||||
if (!listDelegateRoot.fileIsDir && isVideo) {
|
||||
const hash = Qt.md5("file://" + listDelegateRoot.filePath);
|
||||
return _xdgCacheHome + "/thumbnails/normal/" + hash + ".png";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
property string _videoThumb: ""
|
||||
property bool _thumbGenAttempted: false
|
||||
|
||||
// Probe the thumbnail optimistically; Image.Error triggers generation
|
||||
onVideoThumbnailPathChanged: {
|
||||
_thumbGenAttempted = false;
|
||||
_videoThumb = videoThumbnailPath;
|
||||
}
|
||||
|
||||
function generateVideoThumbnail() {
|
||||
if (_thumbGenAttempted)
|
||||
return;
|
||||
_thumbGenAttempted = true;
|
||||
_videoThumb = "";
|
||||
const thumbPath = videoThumbnailPath;
|
||||
const thumbDir = _xdgCacheHome + "/thumbnails/normal";
|
||||
const script = "mkdir -p \"$1\" && ffmpegthumbnailer -i \"$2\" -o \"$3\" -s 128 -f";
|
||||
Proc.runCommand(null, ["sh", "-c", script, "thumb", thumbDir, listDelegateRoot.filePath, thumbPath], function (output, exitCode) {
|
||||
if (exitCode === 0)
|
||||
_videoThumb = thumbPath;
|
||||
});
|
||||
}
|
||||
|
||||
function getIconForFile(fileName) {
|
||||
const lowerName = fileName.toLowerCase();
|
||||
if (lowerName.startsWith("dockerfile")) {
|
||||
return "docker";
|
||||
}
|
||||
const ext = fileName.split('.').pop();
|
||||
return ext || "";
|
||||
}
|
||||
|
||||
function formatFileSize(size) {
|
||||
if (size < 1024)
|
||||
return size + " B";
|
||||
if (size < 1024 * 1024)
|
||||
return (size / 1024).toFixed(1) + " KB";
|
||||
if (size < 1024 * 1024 * 1024)
|
||||
return (size / (1024 * 1024)).toFixed(1) + " MB";
|
||||
return (size / (1024 * 1024 * 1024)).toFixed(1) + " GB";
|
||||
}
|
||||
|
||||
height: 44
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex)
|
||||
return Theme.surfacePressed;
|
||||
return listMouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : Theme.withAlpha(Theme.surfaceContainerHigh, 0);
|
||||
}
|
||||
border.color: keyboardNavigationActive && listDelegateRoot.index === selectedIndex ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
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
|
||||
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
id: listPreviewImage
|
||||
anchors.fill: parent
|
||||
property string imagePath: _videoThumb
|
||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: 32
|
||||
sourceSize.height: 32
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready && !listDelegateRoot.fileIsDir && isVideo
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error && _videoThumb)
|
||||
generateVideoThumbnail();
|
||||
}
|
||||
}
|
||||
|
||||
CachingImage {
|
||||
anchors.fill: parent
|
||||
imagePath: !listDelegateRoot.fileIsDir && isImage ? listDelegateRoot.filePath : ""
|
||||
maxCacheSize: 256
|
||||
animate: false
|
||||
visible: !listDelegateRoot.fileIsDir && isImage
|
||||
}
|
||||
}
|
||||
|
||||
DankNFIcon {
|
||||
anchors.centerIn: parent
|
||||
name: listDelegateRoot.fileIsDir ? "folder" : getIconForFile(listDelegateRoot.fileName)
|
||||
size: Theme.iconSize - 2
|
||||
color: listDelegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
|
||||
visible: listDelegateRoot.fileIsDir || (!isImage && !(isVideo && listPreviewImage.status === Image.Ready))
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: listDelegateRoot.fileName || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width - 280
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
maximumLineCount: 1
|
||||
clip: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: listDelegateRoot.fileIsDir ? "" : formatFileSize(listDelegateRoot.fileSize)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: 70
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Qt.formatDateTime(listDelegateRoot.fileModified, "MMM d, yyyy h:mm AP")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: 140
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: listMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
switch (mouse.button) {
|
||||
case Qt.LeftButton:
|
||||
itemClicked(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
|
||||
break;
|
||||
case Qt.RightButton:
|
||||
itemContextMenuRequested(listDelegateRoot, mouse.x, mouse.y, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserListDelegate {}
|
||||
|
||||
@@ -1,100 +1,3 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
FloatingWindow {
|
||||
id: fileBrowserModal
|
||||
|
||||
property bool disablePopupTransparency: true
|
||||
property string browserTitle: "Select File"
|
||||
property string browserIcon: "folder_open"
|
||||
property string browserType: "generic"
|
||||
property var fileExtensions: ["*.*"]
|
||||
property alias filterExtensions: fileBrowserModal.fileExtensions
|
||||
property bool showHiddenFiles: false
|
||||
property bool saveMode: false
|
||||
property string defaultFileName: ""
|
||||
property var parentModal: null
|
||||
parentWindow: parentModal
|
||||
property bool shouldHaveFocus: visible
|
||||
property bool allowFocusOverride: false
|
||||
property bool shouldBeVisible: visible
|
||||
property bool allowStacking: true
|
||||
|
||||
signal fileSelected(string path)
|
||||
signal dialogClosed
|
||||
|
||||
function open() {
|
||||
visible = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
objectName: "fileBrowserModal"
|
||||
title: "Files - " + browserTitle
|
||||
minimumSize: Qt.size(500, 400)
|
||||
implicitWidth: 800
|
||||
implicitHeight: 600
|
||||
color: Theme.surfaceContainer
|
||||
visible: false
|
||||
|
||||
onClosed: close()
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
if (parentModal && "shouldHaveFocus" in parentModal) {
|
||||
parentModal.shouldHaveFocus = false;
|
||||
parentModal.allowFocusOverride = true;
|
||||
}
|
||||
Qt.callLater(() => {
|
||||
if (content) {
|
||||
content.reset();
|
||||
content.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (parentModal && "allowFocusOverride" in parentModal) {
|
||||
parentModal.allowFocusOverride = false;
|
||||
parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible);
|
||||
}
|
||||
dialogClosed();
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: fileBrowserModal.visible
|
||||
sourceComponent: FileBrowserContent {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
closeOnEscape: false
|
||||
windowControls: fileBrowserModal.windowControlsRef
|
||||
|
||||
browserTitle: fileBrowserModal.browserTitle
|
||||
browserIcon: fileBrowserModal.browserIcon
|
||||
browserType: fileBrowserModal.browserType
|
||||
fileExtensions: fileBrowserModal.fileExtensions
|
||||
showHiddenFiles: fileBrowserModal.showHiddenFiles
|
||||
saveMode: fileBrowserModal.saveMode
|
||||
defaultFileName: fileBrowserModal.defaultFileName
|
||||
|
||||
Component.onCompleted: initialize()
|
||||
|
||||
onFileSelected: path => fileBrowserModal.fileSelected(path)
|
||||
onCloseRequested: fileBrowserModal.close()
|
||||
}
|
||||
}
|
||||
|
||||
property alias content: contentLoader.item
|
||||
property alias windowControlsRef: windowControls
|
||||
|
||||
FloatingWindowControls {
|
||||
id: windowControls
|
||||
targetWindow: fileBrowserModal
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserModal {}
|
||||
|
||||
@@ -1,130 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
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 : Theme.withAlpha(Theme.surfaceVariant, 0)
|
||||
opacity: currentPath !== homeDir ? 1 : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "arrow_back"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: backButtonMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: currentPath !== homeDir
|
||||
cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: currentPath !== homeDir
|
||||
onClicked: navigation.navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Math.max(0, (parent?.width ?? 0) - 40 - Theme.spacingS - (showSidebar ? 0 : 80))
|
||||
height: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: pathEditMode ? Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) : Theme.withAlpha(Theme.surfaceContainer, 0)
|
||||
border.color: pathEditMode ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserNavigation {}
|
||||
|
||||
@@ -1,127 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
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 ? Qt.lighter(Theme.surfaceVariant, 1.2) : 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserOverwriteDialog {}
|
||||
|
||||
@@ -1,75 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
Row {
|
||||
id: saveRow
|
||||
|
||||
property bool saveMode: false
|
||||
property string defaultFileName: ""
|
||||
property string currentPath: ""
|
||||
property alias fileName: fileNameInput.text
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserSaveRow {}
|
||||
|
||||
@@ -1,70 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
StyledRect {
|
||||
id: sidebar
|
||||
|
||||
property var quickAccessLocations: []
|
||||
property string currentPath: ""
|
||||
signal locationSelected(string path)
|
||||
|
||||
width: 200
|
||||
color: Theme.nestedSurface
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Quick Access")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
leftPadding: Theme.spacingS
|
||||
bottomPadding: Theme.spacingXS
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: quickAccessLocations
|
||||
|
||||
StyledRect {
|
||||
width: parent?.width ?? 0
|
||||
height: 38
|
||||
radius: Theme.cornerRadius
|
||||
color: quickAccessMouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : (currentPath === modelData?.path ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0))
|
||||
|
||||
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 ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserSidebar {}
|
||||
|
||||
@@ -1,189 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
StyledRect {
|
||||
id: sortMenu
|
||||
|
||||
property string sortBy: "name"
|
||||
property bool sortAscending: true
|
||||
property color surfaceColor: Theme.surfaceContainer
|
||||
|
||||
signal sortBySelected(string value)
|
||||
signal sortOrderSelected(bool ascending)
|
||||
|
||||
width: 200
|
||||
height: sortColumn.height + Theme.spacingM * 2
|
||||
color: surfaceColor
|
||||
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 : Theme.withAlpha(Theme.surfacePressed, 0))
|
||||
|
||||
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 : Theme.withAlpha(Theme.surfacePressed, 0))
|
||||
|
||||
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 : Theme.withAlpha(Theme.surfacePressed, 0))
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserSortMenu {}
|
||||
|
||||
@@ -1,236 +1,3 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool showFileInfo: false
|
||||
property int selectedIndex: -1
|
||||
property var sourceFolderModel: null
|
||||
property string currentPath: ""
|
||||
|
||||
height: 200
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, 0.95)
|
||||
border.color: Theme.secondary
|
||||
border.width: 2
|
||||
opacity: showFileInfo ? 1 : 0
|
||||
z: 100
|
||||
|
||||
onShowFileInfoChanged: {
|
||||
if (showFileInfo && currentFileName && currentPath) {
|
||||
const fullPath = currentPath + "/" + currentFileName;
|
||||
fileStatProcess.selectedFilePath = fullPath;
|
||||
fileStatProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fileStatProcess
|
||||
command: ["stat", "-c", "%y|%A|%s|%n", selectedFilePath]
|
||||
property string selectedFilePath: ""
|
||||
property var fileStats: null
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text && text.trim()) {
|
||||
const parts = text.trim().split('|');
|
||||
if (parts.length >= 4) {
|
||||
fileStatProcess.fileStats = {
|
||||
"modifiedTime": parts[0],
|
||||
"permissions": parts[1],
|
||||
"size": parseInt(parts[2]) || 0,
|
||||
"fullPath": parts[3]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function (exitCode) {}
|
||||
}
|
||||
|
||||
property string currentFileName: ""
|
||||
property bool currentFileIsDir: false
|
||||
property string currentFileExtension: ""
|
||||
|
||||
onCurrentFileNameChanged: {
|
||||
if (showFileInfo && currentFileName && currentPath) {
|
||||
const fullPath = currentPath + "/" + currentFileName;
|
||||
if (fullPath !== fileStatProcess.selectedFilePath) {
|
||||
fileStatProcess.selectedFilePath = fullPath;
|
||||
fileStatProcess.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateFileInfo(filePath, fileName, isDirectory) {
|
||||
if (filePath && filePath !== fileStatProcess.selectedFilePath) {
|
||||
fileStatProcess.selectedFilePath = filePath;
|
||||
currentFileName = fileName || "";
|
||||
currentFileIsDir = isDirectory || false;
|
||||
|
||||
let ext = "";
|
||||
if (!isDirectory && fileName) {
|
||||
const lastDot = fileName.lastIndexOf('.');
|
||||
if (lastDot > 0) {
|
||||
ext = fileName.substring(lastDot + 1).toLowerCase();
|
||||
}
|
||||
}
|
||||
currentFileExtension = ext;
|
||||
|
||||
if (showFileInfo) {
|
||||
fileStatProcess.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var currentFileDisplayData: {
|
||||
if (selectedIndex < 0 || !sourceFolderModel) {
|
||||
return {
|
||||
"exists": false,
|
||||
"name": "No selection",
|
||||
"type": "",
|
||||
"size": "",
|
||||
"modified": "",
|
||||
"permissions": "",
|
||||
"extension": "",
|
||||
"position": "N/A"
|
||||
};
|
||||
}
|
||||
|
||||
const hasValidFile = currentFileName !== "";
|
||||
return {
|
||||
"exists": hasValidFile,
|
||||
"name": hasValidFile ? currentFileName : "Loading...",
|
||||
"type": currentFileIsDir ? "Directory" : "File",
|
||||
"size": fileStatProcess.fileStats ? formatFileSize(fileStatProcess.fileStats.size) : "Calculating...",
|
||||
"modified": fileStatProcess.fileStats ? formatDateTime(fileStatProcess.fileStats.modifiedTime) : "Loading...",
|
||||
"permissions": fileStatProcess.fileStats ? fileStatProcess.fileStats.permissions : "Loading...",
|
||||
"extension": currentFileExtension,
|
||||
"position": sourceFolderModel ? ((selectedIndex + 1) + " of " + sourceFolderModel.count) : "N/A"
|
||||
};
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "info"
|
||||
size: Theme.iconSize
|
||||
color: Theme.secondary
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("File Information")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideMiddle
|
||||
wrapMode: Text.NoWrap
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.type + (currentFileDisplayData.extension ? " (." + currentFileDisplayData.extension + ")" : "")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.size
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
visible: currentFileDisplayData.exists && !currentFileIsDir
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.modified
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
visible: currentFileDisplayData.exists
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.permissions
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
visible: currentFileDisplayData.exists
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.position
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("F1/I: Toggle • F10: Help")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingM
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0 || !bytes) {
|
||||
return "0 B";
|
||||
}
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatDateTime(dateTimeString) {
|
||||
if (!dateTimeString) {
|
||||
return "Unknown";
|
||||
}
|
||||
const parts = dateTimeString.split(' ');
|
||||
if (parts.length >= 2) {
|
||||
return parts[0] + " " + parts[1].split('.')[0];
|
||||
}
|
||||
return dateTimeString;
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileInfo {}
|
||||
|
||||
@@ -1,50 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool showHints: false
|
||||
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, 0.95)
|
||||
border.color: Theme.primary
|
||||
border.width: 2
|
||||
opacity: showHints ? 1 : 0
|
||||
z: 100
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.KeyboardHints {}
|
||||
|
||||
Reference in New Issue
Block a user