kek test
This commit is contained in:
@@ -0,0 +1,201 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
function getAppId(fileName) {
|
||||||
|
return fileName.endsWith(".desktop") ? fileName.replace(".desktop", "") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileType(fileName, isDir) {
|
||||||
|
if (isDir)
|
||||||
|
return "directory";
|
||||||
|
let ext = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : "";
|
||||||
|
if (ext === "desktop")
|
||||||
|
return "desktop";
|
||||||
|
|
||||||
|
const map = {
|
||||||
|
"image": ["png", "jpg", "jpeg", "svg", "gif", "bmp", "webp", "ico", "tiff", "tif", "heic", "heif", "raw", "psd", "ai", "xcf"],
|
||||||
|
"video": ["mp4", "mkv", "webm", "avi", "mov", "flv", "wmv", "m4v", "mpg", "mpeg", "3gp", "vob", "ogv", "ts"],
|
||||||
|
"audio": ["mp3", "wav", "flac", "aac", "ogg", "m4a", "wma", "opus", "alac", "mid", "midi", "amr"],
|
||||||
|
"archive": ["zip", "tar", "gz", "rar", "7z", "xz", "bz2", "tgz", "iso", "img", "dmg", "deb", "rpm", "apk"],
|
||||||
|
"document": ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "odt", "ods", "odp", "rtf", "epub", "mobi", "djvu"],
|
||||||
|
"text": ["txt", "md", "rst", "tex", "log", "json", "xml", "yaml", "yml", "toml", "ini", "conf", "cfg", "env", "csv", "tsv"],
|
||||||
|
"code": ["qml", "cpp", "c", "h", "hpp", "py", "js", "ts", "jsx", "tsx", "java", "rs", "go", "rb", "php", "cs", "swift", "kt", "sh", "bash", "zsh", "fish", "html", "htm", "css", "scss", "sass", "less", "vue", "svelte", "sql", "graphql", "lua", "pl", "dart", "r", "dockerfile", "make"],
|
||||||
|
"executable": ["exe", "msi", "bat", "cmd", "appimage", "run", "bin", "out", "so", "dll"],
|
||||||
|
"font": ["ttf", "otf", "woff", "woff2"]
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [type, extensions] of Object.entries(map)) {
|
||||||
|
if (extensions.includes(ext))
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconName(fileName, isDir) {
|
||||||
|
if (isDir)
|
||||||
|
return "folder";
|
||||||
|
let ext = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : "";
|
||||||
|
|
||||||
|
const map = {
|
||||||
|
// Images
|
||||||
|
"png": "image-x-generic",
|
||||||
|
"jpg": "image-x-generic",
|
||||||
|
"jpeg": "image-x-generic",
|
||||||
|
"svg": "image-svg+xml",
|
||||||
|
"gif": "image-x-generic",
|
||||||
|
"bmp": "image-x-generic",
|
||||||
|
"webp": "image-x-generic",
|
||||||
|
"ico": "image-x-generic",
|
||||||
|
"tiff": "image-x-generic",
|
||||||
|
"tif": "image-x-generic",
|
||||||
|
"heic": "image-x-generic",
|
||||||
|
"heif": "image-x-generic",
|
||||||
|
"raw": "image-x-generic",
|
||||||
|
"psd": "image-vnd.adobe.photoshop",
|
||||||
|
"ai": "application-illustrator",
|
||||||
|
"xcf": "image-x-xcf",
|
||||||
|
|
||||||
|
// Vidéos
|
||||||
|
"mp4": "video-x-generic",
|
||||||
|
"mkv": "video-x-generic",
|
||||||
|
"webm": "video-x-generic",
|
||||||
|
"avi": "video-x-generic",
|
||||||
|
"mov": "video-x-generic",
|
||||||
|
"flv": "video-x-generic",
|
||||||
|
"wmv": "video-x-generic",
|
||||||
|
"m4v": "video-x-generic",
|
||||||
|
"mpg": "video-x-generic",
|
||||||
|
"mpeg": "video-x-generic",
|
||||||
|
"3gp": "video-x-generic",
|
||||||
|
"vob": "video-x-generic",
|
||||||
|
"ogv": "video-x-generic",
|
||||||
|
"ts": "video-x-generic",
|
||||||
|
|
||||||
|
// Audio
|
||||||
|
"mp3": "audio-x-generic",
|
||||||
|
"wav": "audio-x-generic",
|
||||||
|
"flac": "audio-x-generic",
|
||||||
|
"aac": "audio-x-generic",
|
||||||
|
"ogg": "audio-x-generic",
|
||||||
|
"m4a": "audio-x-generic",
|
||||||
|
"wma": "audio-x-generic",
|
||||||
|
"opus": "audio-x-generic",
|
||||||
|
"alac": "audio-x-generic",
|
||||||
|
"mid": "audio-midi",
|
||||||
|
"midi": "audio-midi",
|
||||||
|
"amr": "audio-x-generic",
|
||||||
|
|
||||||
|
// Archives & Images
|
||||||
|
"zip": "application-zip",
|
||||||
|
"tar": "application-x-tar",
|
||||||
|
"gz": "application-gzip",
|
||||||
|
"rar": "application-vnd.rar",
|
||||||
|
"7z": "application-x-7z-compressed",
|
||||||
|
"xz": "application-x-xz",
|
||||||
|
"bz2": "application-x-bzip2",
|
||||||
|
"tgz": "application-x-compressed-tar",
|
||||||
|
"iso": "application-x-cd-image",
|
||||||
|
"img": "application-x-cd-image",
|
||||||
|
"dmg": "application-x-apple-diskimage",
|
||||||
|
"deb": "application-vnd.debian.binary-package",
|
||||||
|
"rpm": "application-x-rpm",
|
||||||
|
"apk": "application-vnd.android.package-archive",
|
||||||
|
|
||||||
|
// Documents
|
||||||
|
"pdf": "application-pdf",
|
||||||
|
"doc": "application-msword",
|
||||||
|
"docx": "application-vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"xls": "application-vnd.ms-excel",
|
||||||
|
"xlsx": "application-vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"ppt": "application-vnd.ms-powerpoint",
|
||||||
|
"pptx": "application-vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
"odt": "application-vnd.oasis.opendocument.text",
|
||||||
|
"ods": "application-vnd.oasis.opendocument.spreadsheet",
|
||||||
|
"odp": "application-vnd.oasis.opendocument.presentation",
|
||||||
|
"rtf": "application-rtf",
|
||||||
|
"epub": "application-epub+zip",
|
||||||
|
"mobi": "application-x-mobipocket-ebook",
|
||||||
|
"djvu": "image-vnd.djvu",
|
||||||
|
"csv": "text-csv",
|
||||||
|
"tsv": "text-tab-separated-values",
|
||||||
|
|
||||||
|
// Data & Config
|
||||||
|
"txt": "text-x-generic",
|
||||||
|
"md": "text-markdown",
|
||||||
|
"rst": "text-x-rst",
|
||||||
|
"tex": "text-x-tex",
|
||||||
|
"log": "text-x-log",
|
||||||
|
"json": "application-json",
|
||||||
|
"xml": "text-xml",
|
||||||
|
"yaml": "text-x-yaml",
|
||||||
|
"yml": "text-x-yaml",
|
||||||
|
"toml": "text-x-toml",
|
||||||
|
"ini": "text-x-generic",
|
||||||
|
"conf": "text-x-generic",
|
||||||
|
"cfg": "text-x-generic",
|
||||||
|
"env": "text-x-generic",
|
||||||
|
|
||||||
|
// Code
|
||||||
|
"qml": "text-x-qml",
|
||||||
|
"cpp": "text-x-c++src",
|
||||||
|
"c": "text-x-csrc",
|
||||||
|
"h": "text-x-chdr",
|
||||||
|
"hpp": "text-x-c++hdr",
|
||||||
|
"py": "text-x-python",
|
||||||
|
"js": "text-x-javascript",
|
||||||
|
"ts": "text-x-typescript",
|
||||||
|
"jsx": "text-x-javascript",
|
||||||
|
"tsx": "text-x-typescript",
|
||||||
|
"java": "text-x-java",
|
||||||
|
"rs": "text-x-rust",
|
||||||
|
"go": "text-x-go",
|
||||||
|
"rb": "text-x-ruby",
|
||||||
|
"php": "application-x-php",
|
||||||
|
"cs": "text-x-csharp",
|
||||||
|
"swift": "text-x-swift",
|
||||||
|
"kt": "text-x-kotlin",
|
||||||
|
"sh": "application-x-shellscript",
|
||||||
|
"bash": "application-x-shellscript",
|
||||||
|
"zsh": "application-x-shellscript",
|
||||||
|
"fish": "application-x-shellscript",
|
||||||
|
"html": "text-html",
|
||||||
|
"htm": "text-html",
|
||||||
|
"css": "text-css",
|
||||||
|
"scss": "text-x-scss",
|
||||||
|
"sass": "text-x-sass",
|
||||||
|
"less": "text-x-less",
|
||||||
|
"vue": "text-html",
|
||||||
|
"svelte": "text-html",
|
||||||
|
"sql": "application-x-sql",
|
||||||
|
"graphql": "text-x-generic",
|
||||||
|
"lua": "text-x-lua",
|
||||||
|
"pl": "text-x-perl",
|
||||||
|
"dart": "text-x-dart",
|
||||||
|
"r": "text-x-r",
|
||||||
|
"dockerfile": "text-x-generic",
|
||||||
|
"make": "text-x-makefile",
|
||||||
|
|
||||||
|
// Executables
|
||||||
|
"exe": "application-x-executable",
|
||||||
|
"msi": "application-x-msi",
|
||||||
|
"bat": "application-x-ms-dos-executable",
|
||||||
|
"cmd": "application-x-ms-dos-executable",
|
||||||
|
"appimage": "application-x-executable",
|
||||||
|
"run": "application-x-executable",
|
||||||
|
"bin": "application-x-executable",
|
||||||
|
"out": "application-x-executable",
|
||||||
|
"so": "application-x-sharedlib",
|
||||||
|
"dll": "application-x-sharedlib",
|
||||||
|
|
||||||
|
// Fonts
|
||||||
|
"ttf": "font-x-generic",
|
||||||
|
"otf": "font-x-generic",
|
||||||
|
"woff": "font-x-generic",
|
||||||
|
"woff2": "font-x-generic"
|
||||||
|
};
|
||||||
|
return map[ext] || "text-x-generic";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
function fileNameForPath(str) {
|
||||||
|
if (typeof str !== "string")
|
||||||
|
return "";
|
||||||
|
const trimmed = trimFileProtocol(str);
|
||||||
|
return trimmed.split(/[\\/]/).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimFileExt(str) {
|
||||||
|
if (typeof str !== "string")
|
||||||
|
return "";
|
||||||
|
const trimmed = trimFileProtocol(str);
|
||||||
|
const lastDot = trimmed.lastIndexOf(".");
|
||||||
|
if (lastDot > -1 && lastDot > trimmed.lastIndexOf("/")) {
|
||||||
|
return trimmed.slice(0, lastDot);
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimFileProtocol(str) {
|
||||||
|
return str.startsWith("file://") ? str.slice(7) : str;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import qs.Components
|
||||||
|
import qs.Config
|
||||||
|
import qs.Paths
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
z: 998
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
property real menuX: 0
|
||||||
|
property real menuY: 0
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: root.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomClippingRect {
|
||||||
|
id: popupBackground
|
||||||
|
readonly property real padding: 4
|
||||||
|
|
||||||
|
x: root.menuX
|
||||||
|
y: root.menuY
|
||||||
|
|
||||||
|
color: DynamicColors.tPalette.m3surface
|
||||||
|
radius: Appearance.rounding.normal
|
||||||
|
|
||||||
|
implicitWidth: menuLayout.implicitWidth + padding * 2
|
||||||
|
implicitHeight: menuLayout.implicitHeight + padding * 2
|
||||||
|
|
||||||
|
Behavior on opacity { Anim {} }
|
||||||
|
opacity: root.visible ? 1 : 0
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: menuLayout
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: popupBackground.padding
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12
|
||||||
|
MaterialIcon { text: "terminal"; font.pointSize: 20 }
|
||||||
|
CustomText { text: "Open terminal"; Layout.fillWidth: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", FileUtils.trimFileProtocol(Paths.desktop)])
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 1
|
||||||
|
color: DynamicColors.palette.m3outlineVariant
|
||||||
|
Layout.topMargin: 4
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12
|
||||||
|
MaterialIcon { text: "settings"; font.pointSize: 20 }
|
||||||
|
CustomText { text: "Sleex settings"; Layout.fillWidth: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
Quickshell.execDetached(["qs", "-p", "/usr/share/sleex/settings.qml"])
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 1
|
||||||
|
color: Appearance.m3colors.m3outlineVariant
|
||||||
|
Layout.topMargin: 4
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12
|
||||||
|
MaterialIcon { text: "logout"; font.pointSize: 20 }
|
||||||
|
CustomText { text: "Logout"; Layout.fillWidth: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
Hyprland.dispatch("global quickshell:sessionOpen")
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 1
|
||||||
|
color: Appearance.m3colors.m3outlineVariant
|
||||||
|
Layout.topMargin: 4
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12
|
||||||
|
MaterialIcon { text: Config.options.background.showDesktopIcons ? "visibility_off" : "visibility"; font.pointSize: 20 }
|
||||||
|
CustomText { text: Config.options.background.showDesktopIcons ? "Hide icons" : "Show icons"; Layout.fillWidth: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
Config.options.background.showDesktopIcons = !Config.options.background.showDesktopIcons
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAt(mouseX, mouseY, parentW, parentH) {
|
||||||
|
menuX = Math.min(mouseX, parentW - popupBackground.implicitWidth)
|
||||||
|
menuY = Math.min(mouseY, parentH - popupBackground.implicitHeight)
|
||||||
|
visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
visible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Components
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: contextMenu
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
z: 999
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
property string targetFilePath: ""
|
||||||
|
property bool targetIsDir: false
|
||||||
|
property var targetAppEntry: null
|
||||||
|
|
||||||
|
property var targetPaths: []
|
||||||
|
|
||||||
|
signal openFileRequested(string path, bool isDir)
|
||||||
|
signal renameRequested(string path)
|
||||||
|
|
||||||
|
property real menuX: 0
|
||||||
|
property real menuY: 0
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: contextMenu.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomClippingRect {
|
||||||
|
id: popupBackground
|
||||||
|
readonly property real padding: 4
|
||||||
|
|
||||||
|
x: contextMenu.menuX
|
||||||
|
y: contextMenu.menuY
|
||||||
|
|
||||||
|
color: DynamicColors.tPalette.m3surface
|
||||||
|
radius: Appearance.rounding.normal
|
||||||
|
|
||||||
|
implicitWidth: menuLayout.implicitWidth + padding * 2
|
||||||
|
implicitHeight: menuLayout.implicitHeight + padding * 2
|
||||||
|
|
||||||
|
Behavior on opacity { Anim {} }
|
||||||
|
opacity: contextMenu.visible ? 1 : 0
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: menuLayout
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: popupBackground.padding
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12
|
||||||
|
MaterialIcon { text: "open_in_new"; font.pointSize: 20 }
|
||||||
|
CustomText { text: "Open"; Layout.fillWidth: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
for (let i = 0; i < contextMenu.targetPaths.length; i++) {
|
||||||
|
let p = contextMenu.targetPaths[i];
|
||||||
|
if (p === contextMenu.targetFilePath) {
|
||||||
|
if (p.endsWith(".desktop") && contextMenu.targetAppEntry) contextMenu.targetAppEntry.execute()
|
||||||
|
else contextMenu.openFileRequested(p, contextMenu.targetIsDir)
|
||||||
|
} else {
|
||||||
|
Quickshell.execDetached(["xdg-open", p])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12
|
||||||
|
MaterialIcon { text: contextMenu.targetIsDir ? "terminal" : "apps"; font.pointSize: 20 }
|
||||||
|
CustomText { text: contextMenu.targetIsDir ? "Open in terminal" : "Open with..."; Layout.fillWidth: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
Quickshell.execDetached(["xdg-open", contextMenu.targetFilePath])
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 1
|
||||||
|
color: DynamicColors.palette.m3outlineVariant
|
||||||
|
Layout.topMargin: 4
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12
|
||||||
|
MaterialIcon { text: "content_copy"; font.pointSize: 20 }
|
||||||
|
CustomText { text: "Copy path"; Layout.fillWidth: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")])
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: contextMenu.targetPaths.length === 1
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12
|
||||||
|
MaterialIcon { text: "edit"; font.pointSize: 20 }
|
||||||
|
CustomText { text: "Rename"; Layout.fillWidth: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
contextMenu.renameRequested(contextMenu.targetFilePath)
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 1
|
||||||
|
color: Appearance.m3colors.m3outlineVariant
|
||||||
|
Layout.topMargin: 4
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
id: deleteButton
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colBackgroundHover: Appearance.colors.colError
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
spacing: 8
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12
|
||||||
|
MaterialIcon {
|
||||||
|
text: "delete";
|
||||||
|
font.pointSize: 20;
|
||||||
|
color: deleteButton.hovered ? Appearance.colors.colOnError : Appearance.colors.colError
|
||||||
|
}
|
||||||
|
CustomText {
|
||||||
|
text: "Move to trash";
|
||||||
|
Layout.fillWidth: true;
|
||||||
|
color: deleteButton.hovered ? Appearance.colors.colOnError : Appearance.colors.colError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
let cmd = ["gio", "trash"].concat(contextMenu.targetPaths)
|
||||||
|
Quickshell.execDetached(cmd)
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAt(mouseX, mouseY, path, isDir, appEnt, parentW, parentH, selectionArray) {
|
||||||
|
targetFilePath = path
|
||||||
|
targetIsDir = isDir
|
||||||
|
targetAppEntry = appEnt
|
||||||
|
|
||||||
|
targetPaths = (selectionArray && selectionArray.length > 0) ? selectionArray : [path]
|
||||||
|
|
||||||
|
menuX = Math.min(mouseX, parentW - popupBackground.implicitWidth)
|
||||||
|
menuY = Math.min(mouseY, parentH - popupBackground.implicitHeight)
|
||||||
|
|
||||||
|
visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
visible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Config
|
||||||
|
import qs.Components
|
||||||
|
import qs.Helpers
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: delegateRoot
|
||||||
|
|
||||||
|
property var appEntry: fileName.endsWith(".desktop") ? DesktopEntries.byId(DesktopUtils.getAppId(fileName)) : null
|
||||||
|
property bool fileIsDir: model.isDir
|
||||||
|
property string fileName: model.fileName
|
||||||
|
property string filePath: model.filePath
|
||||||
|
property int gridX: model.gridX
|
||||||
|
property int gridY: model.gridY
|
||||||
|
property bool isSnapping: snapAnimX.running || snapAnimY.running
|
||||||
|
property string resolvedIcon: {
|
||||||
|
if (fileName.endsWith(".desktop")) {
|
||||||
|
if (appEntry && appEntry.icon && appEntry.icon !== "")
|
||||||
|
return appEntry.icon;
|
||||||
|
return AppSearch.guessIcon(DesktopUtils.getAppId(fileName));
|
||||||
|
} else if (DesktopUtils.getFileType(fileName, fileIsDir) === "image") {
|
||||||
|
return "file://" + filePath;
|
||||||
|
} else {
|
||||||
|
return DesktopUtils.getIconName(fileName, fileIsDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function compensateAndSnap(absVisX, absVisY) {
|
||||||
|
dragContainer.x = absVisX - delegateRoot.x;
|
||||||
|
dragContainer.y = absVisY - delegateRoot.y;
|
||||||
|
snapAnimX.start();
|
||||||
|
snapAnimY.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDragX() {
|
||||||
|
return dragContainer.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDragY() {
|
||||||
|
return dragContainer.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
height: root.cellHeight
|
||||||
|
width: root.cellWidth
|
||||||
|
x: gridX * root.cellWidth
|
||||||
|
y: gridY * root.cellHeight
|
||||||
|
|
||||||
|
Behavior on x {
|
||||||
|
enabled: !mouseArea.drag.active && !isSnapping && !root.selectedIcons.includes(filePath)
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on y {
|
||||||
|
enabled: !mouseArea.drag.active && !isSnapping && !root.selectedIcons.includes(filePath)
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: dragContainer
|
||||||
|
|
||||||
|
height: parent.height
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
states: State {
|
||||||
|
when: mouseArea.drag.active
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
opacity: 0.8
|
||||||
|
scale: 1.1
|
||||||
|
target: dragContainer
|
||||||
|
z: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transform: Translate {
|
||||||
|
x: (root.selectedIcons.includes(filePath) && root.dragLeader !== "" && root.dragLeader !== filePath) ? root.groupDragX : 0
|
||||||
|
y: (root.selectedIcons.includes(filePath) && root.dragLeader !== "" && root.dragLeader !== filePath) ? root.groupDragY : 0
|
||||||
|
}
|
||||||
|
transitions: Transition {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onXChanged: {
|
||||||
|
if (mouseArea.drag.active) {
|
||||||
|
root.dragLeader = filePath;
|
||||||
|
root.groupDragX = x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onYChanged: {
|
||||||
|
if (mouseArea.drag.active) {
|
||||||
|
root.dragLeader = filePath;
|
||||||
|
root.groupDragY = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyAnimation {
|
||||||
|
id: snapAnimX
|
||||||
|
|
||||||
|
duration: 250
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
property: "x"
|
||||||
|
target: dragContainer
|
||||||
|
to: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyAnimation {
|
||||||
|
id: snapAnimY
|
||||||
|
|
||||||
|
duration: 250
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
property: "y"
|
||||||
|
target: dragContainer
|
||||||
|
to: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
implicitSize: 48
|
||||||
|
source: {
|
||||||
|
if (delegateRoot.resolvedIcon.startsWith("file://") || delegateRoot.resolvedIcon.startsWith("/")) {
|
||||||
|
return delegateRoot.resolvedIcon;
|
||||||
|
} else {
|
||||||
|
return Quickshell.iconPath(delegateRoot.resolvedIcon, fileIsDir ? "folder" : "text-x-generic");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
height: 40
|
||||||
|
width: 88
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "white"
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
maximumLineCount: 2
|
||||||
|
style: Text.Outline
|
||||||
|
styleColor: "black"
|
||||||
|
text: (appEntry && appEntry.name !== "") ? appEntry.name : fileName
|
||||||
|
visible: !renameLoader.active
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: renameLoader
|
||||||
|
|
||||||
|
active: root.editingFilePath === filePath
|
||||||
|
anchors.centerIn: parent
|
||||||
|
height: 24
|
||||||
|
width: 110
|
||||||
|
|
||||||
|
sourceComponent: CustomTextInput {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
color: "white"
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: fileName
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
forceActiveFocus();
|
||||||
|
selectAll();
|
||||||
|
}
|
||||||
|
Keys.onPressed: function (event) {
|
||||||
|
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
if (text.trim() !== "" && text !== fileName) {
|
||||||
|
let newName = text.trim();
|
||||||
|
let newPath = filePath.substring(0, filePath.lastIndexOf('/') + 1) + newName;
|
||||||
|
|
||||||
|
Quickshell.execDetached(["mv", filePath, newPath]);
|
||||||
|
}
|
||||||
|
root.editingFilePath = "";
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (event.key === Qt.Key_Escape) {
|
||||||
|
root.editingFilePath = "";
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onActiveFocusChanged: {
|
||||||
|
if (!activeFocus && root.editingFilePath === filePath) {
|
||||||
|
root.editingFilePath = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 4
|
||||||
|
color: "white"
|
||||||
|
opacity: root.selectedIcons.includes(filePath) ? 0.2 : 0.0
|
||||||
|
radius: 8
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
drag.target: dragContainer
|
||||||
|
hoverEnabled: true
|
||||||
|
|
||||||
|
onClicked: mouse => {
|
||||||
|
root.forceActiveFocus();
|
||||||
|
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
if (!root.selectedIcons.includes(filePath)) {
|
||||||
|
root.selectedIcons = [filePath];
|
||||||
|
}
|
||||||
|
let pos = mapToItem(root, mouse.x, mouse.y);
|
||||||
|
root.contextMenu.openAt(pos.x, pos.y, filePath, fileIsDir, appEntry, root.width, root.height, root.selectedIcons);
|
||||||
|
} else {
|
||||||
|
root.selectedIcons = [filePath];
|
||||||
|
root.contextMenu.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onDoubleClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
if (filePath.endsWith(".desktop") && appEntry)
|
||||||
|
appEntry.execute();
|
||||||
|
else
|
||||||
|
root.exec(filePath, fileIsDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onPressed: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton && !root.selectedIcons.includes(filePath)) {
|
||||||
|
root.selectedIcons = [filePath];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onReleased: {
|
||||||
|
if (drag.active) {
|
||||||
|
let absoluteX = delegateRoot.x + dragContainer.x;
|
||||||
|
let absoluteY = delegateRoot.y + dragContainer.y;
|
||||||
|
let snapX = Math.max(0, Math.round(absoluteX / root.cellWidth));
|
||||||
|
let snapY = Math.max(0, Math.round(absoluteY / root.cellHeight));
|
||||||
|
|
||||||
|
root.performMassDrop(filePath, snapX, snapY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 4
|
||||||
|
color: "white"
|
||||||
|
opacity: parent.containsMouse ? 0.1 : 0.0
|
||||||
|
radius: 8
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Modules
|
||||||
|
import qs.Helpers
|
||||||
|
import qs.Paths
|
||||||
|
import ZShell.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property int cellHeight: 110
|
||||||
|
property int cellWidth: 100
|
||||||
|
property var contextMenu: desktopMenu
|
||||||
|
property string dragLeader: ""
|
||||||
|
property string editingFilePath: ""
|
||||||
|
property real groupDragX: 0
|
||||||
|
property real groupDragY: 0
|
||||||
|
property var selectedIcons: []
|
||||||
|
property real startX: 0
|
||||||
|
property real startY: 0
|
||||||
|
|
||||||
|
function exec(filePath, isDir) {
|
||||||
|
const cmd = ["xdg-open", filePath];
|
||||||
|
Quickshell.execDetached(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
function performMassDrop(leaderPath, targetX, targetY) {
|
||||||
|
let maxCol = Math.max(0, Math.floor(gridArea.width / cellWidth) - 1);
|
||||||
|
let maxRow = Math.max(0, Math.floor(gridArea.height / cellHeight) - 1);
|
||||||
|
|
||||||
|
let visuals = [];
|
||||||
|
for (let i = 0; i < gridArea.children.length; i++) {
|
||||||
|
let child = gridArea.children[i];
|
||||||
|
if (child.filePath && root.selectedIcons.includes(child.filePath)) {
|
||||||
|
let isLeader = (root.dragLeader === child.filePath);
|
||||||
|
let offsetX = isLeader ? child.getDragX() : root.groupDragX;
|
||||||
|
let offsetY = isLeader ? child.getDragY() : root.groupDragY;
|
||||||
|
visuals.push({
|
||||||
|
childRef: child,
|
||||||
|
absX: child.x + offsetX,
|
||||||
|
absY: child.y + offsetY
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
desktopModel.massMove(root.selectedIcons, leaderPath, targetX, targetY, maxCol, maxRow);
|
||||||
|
|
||||||
|
for (let i = 0; i < visuals.length; i++) {
|
||||||
|
visuals[i].childRef.compensateAndSnap(visuals[i].absX, visuals[i].absY);
|
||||||
|
}
|
||||||
|
|
||||||
|
root.dragLeader = "";
|
||||||
|
root.groupDragX = 0;
|
||||||
|
root.groupDragY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (event.key === Qt.Key_F2 && selectedIcons.length > 0)
|
||||||
|
editingFilePath = selectedIcons[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopModel {
|
||||||
|
id: desktopModel
|
||||||
|
|
||||||
|
Component.onCompleted: loadDirectory(FileUtils.trimFileProtocol(Paths.desktop))
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: lasso
|
||||||
|
|
||||||
|
border.color: Appearance.colors.colPrimary
|
||||||
|
border.width: 1
|
||||||
|
color: DynamicColors.tPalette.m3primary
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
visible: false
|
||||||
|
z: 99
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
onPositionChanged: mouse => {
|
||||||
|
if (lasso.visible) {
|
||||||
|
lasso.x = Math.min(mouse.x, root.startX);
|
||||||
|
lasso.y = Math.min(mouse.y, root.startY);
|
||||||
|
lasso.width = Math.abs(mouse.x - root.startX);
|
||||||
|
lasso.height = Math.abs(mouse.y - root.startY);
|
||||||
|
|
||||||
|
let minCol = Math.floor((lasso.x - gridArea.x) / cellWidth);
|
||||||
|
let maxCol = Math.floor((lasso.x + lasso.width - gridArea.x) / cellWidth);
|
||||||
|
let minRow = Math.floor((lasso.y - gridArea.y) / cellHeight);
|
||||||
|
let maxRow = Math.floor((lasso.y + lasso.height - gridArea.y) / cellHeight);
|
||||||
|
|
||||||
|
let newSelection = [];
|
||||||
|
for (let i = 0; i < gridArea.children.length; i++) {
|
||||||
|
let child = gridArea.children[i];
|
||||||
|
if (child.filePath !== undefined && child.gridX >= minCol && child.gridX <= maxCol && child.gridY >= minRow && child.gridY <= maxRow) {
|
||||||
|
newSelection.push(child.filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root.selectedIcons = newSelection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onPressed: mouse => {
|
||||||
|
root.editingFilePath = "";
|
||||||
|
desktopMenu.close();
|
||||||
|
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
root.selectedIcons = [];
|
||||||
|
bgContextMenu.openAt(mouse.x, mouse.y, root.width, root.height);
|
||||||
|
} else {
|
||||||
|
bgContextMenu.close();
|
||||||
|
root.selectedIcons = [];
|
||||||
|
root.startX = mouse.x;
|
||||||
|
root.startY = mouse.y;
|
||||||
|
lasso.x = mouse.x;
|
||||||
|
lasso.y = mouse.y;
|
||||||
|
lasso.width = 0;
|
||||||
|
lasso.height = 0;
|
||||||
|
lasso.visible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onReleased: {
|
||||||
|
lasso.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: gridArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 20
|
||||||
|
anchors.topMargin: 40
|
||||||
|
visible: true
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: desktopModel
|
||||||
|
|
||||||
|
delegate: DesktopIconDelegate {
|
||||||
|
property int itemIndex: index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopIconContextMenu {
|
||||||
|
id: desktopMenu
|
||||||
|
|
||||||
|
onOpenFileRequested: (path, isDir) => root.exec(path, isDir)
|
||||||
|
onRenameRequested: path => {
|
||||||
|
root.editingFilePath = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackgroundContextMenu {
|
||||||
|
id: bgContextMenu
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ CustomRect {
|
|||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
animate: true
|
animate: true
|
||||||
color: Players.active?.isPlaying ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface
|
color: Players.active?.isPlaying ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface
|
||||||
font.pointSize: Appearance.font.size.normal
|
font.pointSize: Appearance.font.size.larger
|
||||||
text: Players.active?.isPlaying ? "music_note" : "music_off"
|
text: Players.active?.isPlaying ? "music_note" : "music_off"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-62
@@ -19,91 +19,47 @@ RowLayout {
|
|||||||
property bool warning: percentage * 100 >= warningThreshold
|
property bool warning: percentage * 100 >= warningThreshold
|
||||||
property int warningThreshold: 80
|
property int warningThreshold: 80
|
||||||
|
|
||||||
clip: true
|
|
||||||
percentage: 0
|
percentage: 0
|
||||||
|
|
||||||
Behavior on animatedPercentage {
|
Behavior on animatedPercentage {
|
||||||
Anim {
|
Anim {
|
||||||
duration: Appearance.anim.durations.large
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: animatedPercentage = percentage
|
Component.onCompleted: animatedPercentage = percentage
|
||||||
onPercentageChanged: animatedPercentage = percentage
|
onPercentageChanged: {
|
||||||
|
const next = percentage;
|
||||||
|
|
||||||
// Canvas {
|
if (Math.abs(next - animatedPercentage) >= 0.05)
|
||||||
// id: gaugeCanvas
|
animatedPercentage = next;
|
||||||
//
|
}
|
||||||
// anchors.centerIn: parent
|
|
||||||
// height: width
|
|
||||||
// width: Math.min(parent.width, parent.height)
|
|
||||||
//
|
|
||||||
// Component.onCompleted: requestPaint()
|
|
||||||
// onPaint: {
|
|
||||||
// const ctx = getContext("2d");
|
|
||||||
// ctx.reset();
|
|
||||||
// const cx = width / 2;
|
|
||||||
// const cy = (height / 2) + 1;
|
|
||||||
// const radius = (Math.min(width, height) - 12) / 2;
|
|
||||||
// const lineWidth = 3;
|
|
||||||
// ctx.beginPath();
|
|
||||||
// ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep);
|
|
||||||
// ctx.lineWidth = lineWidth;
|
|
||||||
// ctx.lineCap = "round";
|
|
||||||
// ctx.strokeStyle = DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2);
|
|
||||||
// ctx.stroke();
|
|
||||||
// if (root.animatedPercentage > 0) {
|
|
||||||
// ctx.beginPath();
|
|
||||||
// ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep * root.animatedPercentage);
|
|
||||||
// ctx.lineWidth = lineWidth;
|
|
||||||
// ctx.lineCap = "round";
|
|
||||||
// ctx.strokeStyle = root.accentColor;
|
|
||||||
// ctx.stroke();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Connections {
|
|
||||||
// function onAnimatedPercentageChanged() {
|
|
||||||
// gaugeCanvas.requestPaint();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// target: root
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Connections {
|
|
||||||
// function onPaletteChanged() {
|
|
||||||
// gaugeCanvas.requestPaint();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// target: DynamicColors
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
id: icon
|
id: icon
|
||||||
|
|
||||||
color: DynamicColors.palette.m3onSurface
|
color: DynamicColors.palette.m3onSurface
|
||||||
font.pointSize: 12
|
font.pointSize: Appearance.font.size.larger
|
||||||
text: root.icon
|
text: root.icon
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomClippingRect {
|
CustomClippingRect {
|
||||||
Layout.preferredHeight: root.height
|
Layout.preferredHeight: root.height - Appearance.padding.small
|
||||||
Layout.preferredWidth: 5
|
Layout.preferredWidth: 4
|
||||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
||||||
radius: Appearance.rounding.full
|
radius: Appearance.rounding.full
|
||||||
|
|
||||||
CustomRect {
|
CustomRect {
|
||||||
anchors.bottom: parent.bottom
|
id: fill
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
color: root.mainColor
|
|
||||||
implicitHeight: root.percentage * parent.height
|
|
||||||
radius: implicitHeight / 2
|
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
anchors.fill: parent
|
||||||
Anim {
|
antialiasing: false
|
||||||
}
|
color: root.mainColor
|
||||||
|
implicitHeight: Math.ceil(root.percentage * parent.height)
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
|
||||||
|
transform: Scale {
|
||||||
|
origin.y: fill.height
|
||||||
|
yScale: Math.max(0.001, root.animatedPercentage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ CustomRect {
|
|||||||
spacing: Appearance.spacing.small
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
font.pointSize: Appearance.font.size.normal
|
font.pointSize: Appearance.font.size.larger
|
||||||
text: "package_2"
|
text: "package_2"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Quickshell
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import qs.Config
|
import qs.Config
|
||||||
|
import qs.Modules.DesktopIcons
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
active: Config.background.enabled
|
active: Config.background.enabled
|
||||||
@@ -30,6 +31,9 @@ Loader {
|
|||||||
|
|
||||||
WallBackground {
|
WallBackground {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DesktopIcons {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Singleton {
|
|||||||
readonly property string cache: `${Quickshell.env("XDG_CACHE_HOME") || `${home}/.cache`}/zshell`
|
readonly property string cache: `${Quickshell.env("XDG_CACHE_HOME") || `${home}/.cache`}/zshell`
|
||||||
readonly property string config: `${Quickshell.env("XDG_CONFIG_HOME") || `${home}/.config`}/zshell`
|
readonly property string config: `${Quickshell.env("XDG_CONFIG_HOME") || `${home}/.config`}/zshell`
|
||||||
readonly property string data: `${Quickshell.env("XDG_DATA_HOME") || `${home}/.local/share`}/zshell`
|
readonly property string data: `${Quickshell.env("XDG_DATA_HOME") || `${home}/.local/share`}/zshell`
|
||||||
|
readonly property string desktop: `${Quickshell.env("XDG_DATA_HOME") || `${home}/Desktop`}`
|
||||||
readonly property string home: Quickshell.env("HOME")
|
readonly property string home: Quickshell.env("HOME")
|
||||||
readonly property string imagecache: `${cache}/imagecache`
|
readonly property string imagecache: `${cache}/imagecache`
|
||||||
readonly property string libdir: Quickshell.env("ZSHELL_LIB_DIR") || "/usr/lib/zshell"
|
readonly property string libdir: Quickshell.env("ZSHELL_LIB_DIR") || "/usr/lib/zshell"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
|||||||
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
||||||
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
||||||
pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)
|
pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)
|
||||||
|
pkg_check_modules(GLIB REQUIRED glib-2.0 gobject-2.0 gio-2.0)
|
||||||
if(NOT Cava_FOUND)
|
if(NOT Cava_FOUND)
|
||||||
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
|
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ qml_module(ZShell-services
|
|||||||
audiocollector.hpp audiocollector.cpp
|
audiocollector.hpp audiocollector.cpp
|
||||||
audioprovider.hpp audioprovider.cpp
|
audioprovider.hpp audioprovider.cpp
|
||||||
cavaprovider.hpp cavaprovider.cpp
|
cavaprovider.hpp cavaprovider.cpp
|
||||||
|
desktopmodel.hpp desktopmodel.cpp
|
||||||
|
desktopstatemanager.hpp desktopstatemanager.cpp
|
||||||
LIBRARIES
|
LIBRARIES
|
||||||
|
Qt6::Core
|
||||||
|
Qt6::Qml
|
||||||
PkgConfig::Pipewire
|
PkgConfig::Pipewire
|
||||||
PkgConfig::Aubio
|
PkgConfig::Aubio
|
||||||
PkgConfig::Cava
|
PkgConfig::Cava
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
#include "desktopmodel.hpp"
|
||||||
|
#include "desktopstatemanager.hpp"
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfoList>
|
||||||
|
|
||||||
|
namespace ZShell::services {
|
||||||
|
|
||||||
|
DesktopModel::DesktopModel(QObject *parent) : QAbstractListModel(parent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
int DesktopModel::rowCount(const QModelIndex &parent) const {
|
||||||
|
if (parent.isValid()) return 0;
|
||||||
|
return m_items.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant DesktopModel::data(const QModelIndex &index, int role) const {
|
||||||
|
if (!index.isValid() || index.row() >= m_items.size()) return QVariant();
|
||||||
|
|
||||||
|
const DesktopItem &item = m_items[index.row()];
|
||||||
|
switch (role) {
|
||||||
|
case FileNameRole: return item.fileName;
|
||||||
|
case FilePathRole: return item.filePath;
|
||||||
|
case IsDirRole: return item.isDir;
|
||||||
|
case GridXRole: return item.gridX;
|
||||||
|
case GridYRole: return item.gridY;
|
||||||
|
default: return QVariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> DesktopModel::roleNames() const {
|
||||||
|
QHash<int, QByteArray> roles;
|
||||||
|
roles[FileNameRole] = "fileName";
|
||||||
|
roles[FilePathRole] = "filePath";
|
||||||
|
roles[IsDirRole] = "isDir";
|
||||||
|
roles[GridXRole] = "gridX";
|
||||||
|
roles[GridYRole] = "gridY";
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopModel::loadDirectory(const QString &path) {
|
||||||
|
beginResetModel();
|
||||||
|
m_items.clear();
|
||||||
|
|
||||||
|
QDir dir(path);
|
||||||
|
dir.setFilter(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
|
||||||
|
QFileInfoList list = dir.entryInfoList();
|
||||||
|
|
||||||
|
DesktopStateManager sm;
|
||||||
|
QVariantMap savedLayout = sm.getLayout();
|
||||||
|
|
||||||
|
for (const QFileInfo &fileInfo : list) {
|
||||||
|
DesktopItem item;
|
||||||
|
item.fileName = fileInfo.fileName();
|
||||||
|
item.filePath = fileInfo.absoluteFilePath();
|
||||||
|
item.isDir = fileInfo.isDir();
|
||||||
|
|
||||||
|
if (savedLayout.contains(item.fileName)) {
|
||||||
|
QVariantMap pos = savedLayout[item.fileName].toMap();
|
||||||
|
item.gridX = pos["x"].toInt();
|
||||||
|
item.gridY = pos["y"].toInt();
|
||||||
|
} else {
|
||||||
|
// TODO: make getEmptySpot in C++ and call it here to get the initial position for new icons
|
||||||
|
item.gridX = 0;
|
||||||
|
item.gridY = 0;
|
||||||
|
}
|
||||||
|
m_items.append(item);
|
||||||
|
}
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopModel::moveIcon(int index, int newX, int newY) {
|
||||||
|
if (index < 0 || index >= m_items.size()) return;
|
||||||
|
|
||||||
|
m_items[index].gridX = newX;
|
||||||
|
m_items[index].gridY = newY;
|
||||||
|
|
||||||
|
QModelIndex modelIndex = createIndex(index, 0);
|
||||||
|
emit dataChanged(modelIndex, modelIndex, {GridXRole, GridYRole});
|
||||||
|
|
||||||
|
saveCurrentLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopModel::saveCurrentLayout() {
|
||||||
|
QVariantMap layout;
|
||||||
|
for (const auto& item : m_items) {
|
||||||
|
QVariantMap pos;
|
||||||
|
pos["x"] = item.gridX;
|
||||||
|
pos["y"] = item.gridY;
|
||||||
|
layout[item.fileName] = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopStateManager sm;
|
||||||
|
sm.saveLayout(layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopModel::massMove(const QVariantList& selectedPathsList, const QString& leaderPath, int targetX, int targetY, int maxCol, int maxRow) {
|
||||||
|
QStringList selectedPaths;
|
||||||
|
for (const QVariant& v : selectedPathsList) {
|
||||||
|
selectedPaths << v.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedPaths.isEmpty()) return;
|
||||||
|
|
||||||
|
int oldX = 0, oldY = 0;
|
||||||
|
for (const auto& item : m_items) {
|
||||||
|
if (item.filePath == leaderPath) {
|
||||||
|
oldX = item.gridX;
|
||||||
|
oldY = item.gridY;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int deltaX = targetX - oldX;
|
||||||
|
int deltaY = targetY - oldY;
|
||||||
|
|
||||||
|
if (deltaX == 0 && deltaY == 0) return;
|
||||||
|
|
||||||
|
if (selectedPaths.size() == 1 && targetX >= 0 && targetX <= maxCol && targetY >= 0 && targetY <= maxRow) {
|
||||||
|
QString movingPath = selectedPaths.first();
|
||||||
|
int movingIndex = -1;
|
||||||
|
int targetIndex = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < m_items.size(); ++i) {
|
||||||
|
if (m_items[i].filePath == movingPath) {
|
||||||
|
movingIndex = i;
|
||||||
|
} else if (m_items[i].gridX == targetX && m_items[i].gridY == targetY) {
|
||||||
|
targetIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetIndex != -1 && movingIndex != -1) {
|
||||||
|
m_items[targetIndex].gridX = oldX;
|
||||||
|
m_items[targetIndex].gridY = oldY;
|
||||||
|
m_items[movingIndex].gridX = targetX;
|
||||||
|
m_items[movingIndex].gridY = targetY;
|
||||||
|
|
||||||
|
emit dataChanged(index(0, 0), index(m_items.size() - 1, 0), {GridXRole, GridYRole});
|
||||||
|
saveCurrentLayout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<DesktopItem*> movingItems;
|
||||||
|
QSet<QString> occupied;
|
||||||
|
|
||||||
|
for (int i = 0; i < m_items.size(); ++i) {
|
||||||
|
if (selectedPaths.contains(m_items[i].filePath)) {
|
||||||
|
movingItems.append(&m_items[i]);
|
||||||
|
} else {
|
||||||
|
occupied.insert(QString::number(m_items[i].gridX) + "," + QString::number(m_items[i].gridY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto* item : movingItems) {
|
||||||
|
int newX = item->gridX + deltaX;
|
||||||
|
int newY = item->gridY + deltaY;
|
||||||
|
|
||||||
|
bool outOfBounds = newX < 0 || newX > maxCol || newY < 0 || newY > maxRow;
|
||||||
|
bool collision = occupied.contains(QString::number(newX) + "," + QString::number(newY));
|
||||||
|
|
||||||
|
if (outOfBounds || collision) {
|
||||||
|
bool found = false;
|
||||||
|
for (int x = 0; x <= maxCol && !found; ++x) {
|
||||||
|
for (int y = 0; y <= maxRow && !found; ++y) {
|
||||||
|
QString key = QString::number(x) + "," + QString::number(y);
|
||||||
|
if (!occupied.contains(key)) {
|
||||||
|
newX = x;
|
||||||
|
newY = y;
|
||||||
|
occupied.insert(key);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
occupied.insert(QString::number(newX) + "," + QString::number(newY));
|
||||||
|
}
|
||||||
|
|
||||||
|
item->gridX = newX;
|
||||||
|
item->gridY = newY;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit dataChanged(index(0, 0), index(m_items.size() - 1, 0), {GridXRole, GridYRole});
|
||||||
|
saveCurrentLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZShell::services
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
namespace ZShell::services {
|
||||||
|
|
||||||
|
struct DesktopItem {
|
||||||
|
QString fileName;
|
||||||
|
QString filePath;
|
||||||
|
bool isDir;
|
||||||
|
int gridX;
|
||||||
|
int gridY;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopModel : public QAbstractListModel {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum DesktopRoles {
|
||||||
|
FileNameRole = Qt::UserRole + 1,
|
||||||
|
FilePathRole,
|
||||||
|
IsDirRole,
|
||||||
|
GridXRole,
|
||||||
|
GridYRole
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit DesktopModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
Q_INVOKABLE void loadDirectory(const QString &path);
|
||||||
|
Q_INVOKABLE void moveIcon(int index, int newX, int newY);
|
||||||
|
Q_INVOKABLE void massMove(const QVariantList &selectedPathsList, const QString &leaderPath, int targetX, int targetY, int maxCol, int maxRow);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QList<DesktopItem> m_items;
|
||||||
|
void saveCurrentLayout();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZShell::services
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#include "desktopstatemanager.hpp"
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
namespace ZShell::services {
|
||||||
|
|
||||||
|
DesktopStateManager::DesktopStateManager(QObject *parent) : QObject(parent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DesktopStateManager::getConfigFilePath() const {
|
||||||
|
QString configDir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/sleex";
|
||||||
|
QDir dir(configDir);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
dir.mkpath(".");
|
||||||
|
}
|
||||||
|
return configDir + "/desktop_layout.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopStateManager::saveLayout(const QVariantMap& layout) {
|
||||||
|
QJsonObject jsonObj = QJsonObject::fromVariantMap(layout);
|
||||||
|
QJsonDocument doc(jsonObj);
|
||||||
|
QFile file(getConfigFilePath());
|
||||||
|
|
||||||
|
if (file.open(QIODevice::WriteOnly)) {
|
||||||
|
file.write(doc.toJson(QJsonDocument::Indented));
|
||||||
|
file.close();
|
||||||
|
} else {
|
||||||
|
qWarning() << "Sleex: Impossible de sauvegarder le layout du bureau dans" << getConfigFilePath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap DesktopStateManager::getLayout() {
|
||||||
|
QFile file(getConfigFilePath());
|
||||||
|
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
return QVariantMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray data = file.readAll();
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||||
|
if (doc.isObject()) {
|
||||||
|
return doc.object().toVariantMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
return QVariantMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZShell::services
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QVariantMap>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
namespace ZShell::services {
|
||||||
|
|
||||||
|
class DesktopStateManager : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_SINGLETON
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DesktopStateManager(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
Q_INVOKABLE void saveLayout(const QVariantMap& layout);
|
||||||
|
Q_INVOKABLE QVariantMap getLayout();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString getConfigFilePath() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZShell::services
|
||||||
Reference in New Issue
Block a user