mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-30 00:12:50 -05:00
add shell
This commit is contained in:
943
AppLauncher.qml
Normal file
943
AppLauncher.qml
Normal file
@@ -0,0 +1,943 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: launcher
|
||||||
|
|
||||||
|
property var theme
|
||||||
|
property bool isVisible: false
|
||||||
|
|
||||||
|
// Default theme fallback
|
||||||
|
property var defaultTheme: QtObject {
|
||||||
|
property color primary: "#D0BCFF"
|
||||||
|
property color background: "#10121E"
|
||||||
|
property color surfaceContainer: "#1D1B20"
|
||||||
|
property color surfaceText: "#E6E0E9"
|
||||||
|
property color surfaceVariant: "#49454F"
|
||||||
|
property color surfaceVariantText: "#CAC4D0"
|
||||||
|
property color outline: "#938F99"
|
||||||
|
property real cornerRadius: 12
|
||||||
|
property real cornerRadiusLarge: 16
|
||||||
|
property real cornerRadiusXLarge: 24
|
||||||
|
property real spacingXS: 4
|
||||||
|
property real spacingS: 8
|
||||||
|
property real spacingM: 12
|
||||||
|
property real spacingL: 16
|
||||||
|
property real spacingXL: 24
|
||||||
|
property real fontSizeLarge: 16
|
||||||
|
property real fontSizeMedium: 14
|
||||||
|
property real fontSizeSmall: 12
|
||||||
|
property real iconSize: 24
|
||||||
|
property real iconSizeLarge: 32
|
||||||
|
property real barHeight: 48
|
||||||
|
property string iconFont: "Material Symbols Rounded"
|
||||||
|
property int iconFontWeight: Font.Normal
|
||||||
|
property int shortDuration: 150
|
||||||
|
property int mediumDuration: 300
|
||||||
|
property int standardEasing: Easing.OutCubic
|
||||||
|
property int emphasizedEasing: Easing.OutQuart
|
||||||
|
}
|
||||||
|
|
||||||
|
property var activeTheme: theme || defaultTheme
|
||||||
|
|
||||||
|
// Full screen overlay setup for proper focus
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
bottom: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proper layer shell configuration
|
||||||
|
WlrLayershell.layer: WlrLayershell.Overlay
|
||||||
|
WlrLayershell.exclusiveZone: -1
|
||||||
|
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||||
|
WlrLayershell.namespace: "quickshell-launcher"
|
||||||
|
|
||||||
|
visible: isVisible
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
// Enhanced app management
|
||||||
|
property var currentApp: ({})
|
||||||
|
property var allApps: []
|
||||||
|
property var categories: ["All"]
|
||||||
|
property string selectedCategory: "All"
|
||||||
|
property var recentApps: []
|
||||||
|
property var pinnedApps: ["firefox", "code", "terminal", "file-manager"]
|
||||||
|
property bool showCategories: false
|
||||||
|
property string viewMode: "list" // "list" or "grid"
|
||||||
|
property var appCategories: ({
|
||||||
|
"AudioVideo": "Media",
|
||||||
|
"Audio": "Media",
|
||||||
|
"Video": "Media",
|
||||||
|
"Development": "Development",
|
||||||
|
"TextEditor": "Development",
|
||||||
|
"IDE": "Development",
|
||||||
|
"Programming": "Development",
|
||||||
|
"Education": "Education",
|
||||||
|
"Game": "Games",
|
||||||
|
"Graphics": "Graphics",
|
||||||
|
"Photography": "Graphics",
|
||||||
|
"Network": "Internet",
|
||||||
|
"WebBrowser": "Internet",
|
||||||
|
"Office": "Office",
|
||||||
|
"WordProcessor": "Office",
|
||||||
|
"Spreadsheet": "Office",
|
||||||
|
"Presentation": "Office",
|
||||||
|
"Science": "Science",
|
||||||
|
"Settings": "Settings",
|
||||||
|
"System": "System",
|
||||||
|
"Utility": "Utilities",
|
||||||
|
"Accessories": "Utilities",
|
||||||
|
"FileManager": "Utilities",
|
||||||
|
"TerminalEmulator": "Utilities"
|
||||||
|
})
|
||||||
|
|
||||||
|
ListModel { id: filteredModel }
|
||||||
|
ListModel { id: categoryModel }
|
||||||
|
|
||||||
|
// Background dim with click to close
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.3)
|
||||||
|
opacity: launcher.isVisible ? 1.0 : 0.0
|
||||||
|
visible: launcher.isVisible
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: activeTheme.shortDuration
|
||||||
|
easing.type: activeTheme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: launcher.isVisible
|
||||||
|
onClicked: launcher.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desktop applications scanning
|
||||||
|
Process {
|
||||||
|
id: desktopScanner
|
||||||
|
command: ["sh", "-c", `
|
||||||
|
for dir in "/usr/share/applications/" "/usr/local/share/applications/" "$HOME/.local/share/applications/" "/run/current-system/sw/share/applications/"; do
|
||||||
|
if [ -d "$dir" ]; then
|
||||||
|
find "$dir" -name "*.desktop" 2>/dev/null | while read file; do
|
||||||
|
echo "===FILE:$file"
|
||||||
|
sed -n '/^\\[Desktop Entry\\]/,/^\\[.*\\]/{/^\\[Desktop Entry\\]/d; /^\\[.*\\]/q; /^Name=/p; /^Exec=/p; /^Icon=/p; /^Hidden=/p; /^NoDisplay=/p; /^Categories=/p; /^Comment=/p}' "$file" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
`]
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: "\n"
|
||||||
|
onRead: (line) => {
|
||||||
|
if (line.startsWith("===FILE:")) {
|
||||||
|
// Save previous app if valid
|
||||||
|
if (currentApp.name && currentApp.exec && !currentApp.hidden && !currentApp.noDisplay) {
|
||||||
|
allApps.push({
|
||||||
|
name: currentApp.name,
|
||||||
|
exec: currentApp.exec,
|
||||||
|
icon: currentApp.icon || "application-x-executable",
|
||||||
|
comment: currentApp.comment || "",
|
||||||
|
categories: currentApp.categories || []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Start new app
|
||||||
|
currentApp = { name: "", exec: "", icon: "", comment: "", categories: [], hidden: false, noDisplay: false }
|
||||||
|
} else if (line.startsWith("Name=")) {
|
||||||
|
currentApp.name = line.substring(5)
|
||||||
|
} else if (line.startsWith("Exec=")) {
|
||||||
|
currentApp.exec = line.substring(5)
|
||||||
|
} else if (line.startsWith("Icon=")) {
|
||||||
|
currentApp.icon = line.substring(5)
|
||||||
|
} else if (line.startsWith("Comment=")) {
|
||||||
|
currentApp.comment = line.substring(8)
|
||||||
|
} else if (line.startsWith("Categories=")) {
|
||||||
|
currentApp.categories = line.substring(11).split(";").filter(cat => cat.length > 0)
|
||||||
|
} else if (line === "Hidden=true") {
|
||||||
|
currentApp.hidden = true
|
||||||
|
} else if (line === "NoDisplay=true") {
|
||||||
|
currentApp.noDisplay = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: {
|
||||||
|
// Save last app
|
||||||
|
if (currentApp.name && currentApp.exec && !currentApp.hidden && !currentApp.noDisplay) {
|
||||||
|
allApps.push({
|
||||||
|
name: currentApp.name,
|
||||||
|
exec: currentApp.exec,
|
||||||
|
icon: currentApp.icon || "application-x-executable",
|
||||||
|
comment: currentApp.comment || "",
|
||||||
|
categories: currentApp.categories || []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract unique categories
|
||||||
|
let uniqueCategories = new Set(["All"])
|
||||||
|
allApps.forEach(app => {
|
||||||
|
app.categories.forEach(cat => {
|
||||||
|
if (appCategories[cat]) {
|
||||||
|
uniqueCategories.add(appCategories[cat])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
categories = Array.from(uniqueCategories)
|
||||||
|
|
||||||
|
console.log("Loaded", allApps.length, "applications with", categories.length, "categories")
|
||||||
|
updateFilteredModel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFilteredModel() {
|
||||||
|
filteredModel.clear()
|
||||||
|
|
||||||
|
let apps = allApps
|
||||||
|
|
||||||
|
// Filter by category
|
||||||
|
if (selectedCategory !== "All") {
|
||||||
|
apps = apps.filter(app => {
|
||||||
|
return app.categories.some(cat => appCategories[cat] === selectedCategory)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by search
|
||||||
|
if (searchField.text.length > 0) {
|
||||||
|
const query = searchField.text.toLowerCase()
|
||||||
|
apps = apps.filter(app => {
|
||||||
|
return app.name.toLowerCase().includes(query) ||
|
||||||
|
(app.comment && app.comment.toLowerCase().includes(query))
|
||||||
|
}).sort((a, b) => {
|
||||||
|
// Sort by relevance
|
||||||
|
const aName = a.name.toLowerCase()
|
||||||
|
const bName = b.name.toLowerCase()
|
||||||
|
const aStartsWith = aName.startsWith(query)
|
||||||
|
const bStartsWith = bName.startsWith(query)
|
||||||
|
|
||||||
|
if (aStartsWith && !bStartsWith) return -1
|
||||||
|
if (!aStartsWith && bStartsWith) return 1
|
||||||
|
return aName.localeCompare(bName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort alphabetically if no search
|
||||||
|
if (searchField.text.length === 0) {
|
||||||
|
apps.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to model
|
||||||
|
apps.forEach(app => {
|
||||||
|
filteredModel.append(app)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main launcher panel with enhanced design
|
||||||
|
Rectangle {
|
||||||
|
id: launcherPanel
|
||||||
|
|
||||||
|
width: 520
|
||||||
|
height: 600
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
left: parent.left
|
||||||
|
topMargin: 50
|
||||||
|
leftMargin: activeTheme.spacingL
|
||||||
|
}
|
||||||
|
|
||||||
|
color: Qt.rgba(activeTheme.surfaceContainer.r, activeTheme.surfaceContainer.g, activeTheme.surfaceContainer.b, 0.98)
|
||||||
|
radius: activeTheme.cornerRadiusXLarge
|
||||||
|
|
||||||
|
// Material 3 elevation with multiple layers
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -3
|
||||||
|
color: "transparent"
|
||||||
|
radius: parent.radius + 3
|
||||||
|
border.color: Qt.rgba(0, 0, 0, 0.05)
|
||||||
|
border.width: 1
|
||||||
|
z: -3
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -2
|
||||||
|
color: "transparent"
|
||||||
|
radius: parent.radius + 2
|
||||||
|
border.color: Qt.rgba(0, 0, 0, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
z: -2
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "transparent"
|
||||||
|
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.12)
|
||||||
|
border.width: 1
|
||||||
|
radius: parent.radius
|
||||||
|
z: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animated entrance with spring effect
|
||||||
|
transform: [
|
||||||
|
Scale {
|
||||||
|
id: scaleTransform
|
||||||
|
origin.x: 0
|
||||||
|
origin.y: 0
|
||||||
|
xScale: launcher.isVisible ? 1.0 : 0.92
|
||||||
|
yScale: launcher.isVisible ? 1.0 : 0.92
|
||||||
|
|
||||||
|
Behavior on xScale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: activeTheme.mediumDuration
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
easing.overshoot: 1.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on yScale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: activeTheme.mediumDuration
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
easing.overshoot: 1.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Translate {
|
||||||
|
id: translateTransform
|
||||||
|
x: launcher.isVisible ? 0 : -30
|
||||||
|
y: launcher.isVisible ? 0 : -15
|
||||||
|
|
||||||
|
Behavior on x {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: activeTheme.mediumDuration
|
||||||
|
easing.type: activeTheme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on y {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: activeTheme.mediumDuration
|
||||||
|
easing.type: activeTheme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
opacity: launcher.isVisible ? 1.0 : 0.0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: activeTheme.mediumDuration
|
||||||
|
easing.type: activeTheme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content with focus management
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
// Handle keyboard shortcuts
|
||||||
|
Keys.onPressed: function(event) {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
launcher.hide()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: activeTheme.spacingXL
|
||||||
|
spacing: activeTheme.spacingL
|
||||||
|
|
||||||
|
// Header section
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
// App launcher title
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Applications"
|
||||||
|
font.pixelSize: activeTheme.fontSizeLarge + 4
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: activeTheme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { width: parent.width - 200; height: 1 }
|
||||||
|
|
||||||
|
// Quick stats
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: filteredModel.count + " apps"
|
||||||
|
font.pixelSize: activeTheme.fontSizeMedium
|
||||||
|
color: activeTheme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced search field
|
||||||
|
Rectangle {
|
||||||
|
id: searchContainer
|
||||||
|
width: parent.width
|
||||||
|
height: 52
|
||||||
|
radius: activeTheme.cornerRadiusLarge
|
||||||
|
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.6)
|
||||||
|
border.width: searchField.activeFocus ? 2 : 1
|
||||||
|
border.color: searchField.activeFocus ? activeTheme.primary :
|
||||||
|
Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.3)
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: activeTheme.shortDuration
|
||||||
|
easing.type: activeTheme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: activeTheme.spacingL
|
||||||
|
anchors.rightMargin: activeTheme.spacingL
|
||||||
|
spacing: activeTheme.spacingM
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "search"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: activeTheme.iconSize
|
||||||
|
color: searchField.activeFocus ? activeTheme.primary : activeTheme.surfaceVariantText
|
||||||
|
font.weight: activeTheme.iconFontWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: searchField
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - parent.spacing - activeTheme.iconSize - 32
|
||||||
|
height: parent.height - activeTheme.spacingS
|
||||||
|
|
||||||
|
color: activeTheme.surfaceText
|
||||||
|
font.pixelSize: activeTheme.fontSizeLarge
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
|
||||||
|
focus: launcher.isVisible
|
||||||
|
selectByMouse: true
|
||||||
|
activeFocusOnTab: true
|
||||||
|
|
||||||
|
// Placeholder text
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Search applications..."
|
||||||
|
color: activeTheme.surfaceVariantText
|
||||||
|
font.pixelSize: activeTheme.fontSizeLarge
|
||||||
|
visible: searchField.text.length === 0 && !searchField.activeFocus
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear button
|
||||||
|
Rectangle {
|
||||||
|
width: 24
|
||||||
|
height: 24
|
||||||
|
radius: 12
|
||||||
|
color: clearSearchArea.containsMouse ? Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.12) : "transparent"
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: searchField.text.length > 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "close"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: 16
|
||||||
|
color: clearSearchArea.containsMouse ? activeTheme.outline : activeTheme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: clearSearchArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: searchField.text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTextChanged: updateFilteredModel()
|
||||||
|
|
||||||
|
Keys.onPressed: function (event) {
|
||||||
|
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && filteredModel.count) {
|
||||||
|
launcher.launchApp(filteredModel.get(0).exec)
|
||||||
|
launcher.hide()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Escape) {
|
||||||
|
launcher.hide()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category filter and view mode controls
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
spacing: activeTheme.spacingM
|
||||||
|
visible: searchField.text.length === 0
|
||||||
|
|
||||||
|
// Category filter
|
||||||
|
Rectangle {
|
||||||
|
width: 200
|
||||||
|
height: 36
|
||||||
|
radius: activeTheme.cornerRadius
|
||||||
|
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
|
||||||
|
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: activeTheme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: activeTheme.spacingS
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "category"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: 18
|
||||||
|
color: activeTheme.surfaceVariantText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: selectedCategory
|
||||||
|
font.pixelSize: activeTheme.fontSizeMedium
|
||||||
|
color: activeTheme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: activeTheme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: showCategories ? "expand_less" : "expand_more"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: 18
|
||||||
|
color: activeTheme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: showCategories = !showCategories
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { width: parent.width - 300; height: 1 }
|
||||||
|
|
||||||
|
// View mode toggle
|
||||||
|
Row {
|
||||||
|
spacing: 4
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
// List view button
|
||||||
|
Rectangle {
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: activeTheme.cornerRadius
|
||||||
|
color: viewMode === "list" ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) :
|
||||||
|
listViewArea.containsMouse ? Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.08) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "view_list"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: 20
|
||||||
|
color: viewMode === "list" ? activeTheme.primary : activeTheme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: listViewArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: viewMode = "list"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid view button
|
||||||
|
Rectangle {
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: activeTheme.cornerRadius
|
||||||
|
color: viewMode === "grid" ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) :
|
||||||
|
gridViewArea.containsMouse ? Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.08) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "grid_view"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: 20
|
||||||
|
color: viewMode === "grid" ? activeTheme.primary : activeTheme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: gridViewArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: viewMode = "grid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category dropdown
|
||||||
|
Rectangle {
|
||||||
|
width: 200
|
||||||
|
height: Math.min(250, categories.length * 40 + activeTheme.spacingM * 2)
|
||||||
|
radius: activeTheme.cornerRadiusLarge
|
||||||
|
color: activeTheme.surfaceContainer
|
||||||
|
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
|
||||||
|
border.width: 1
|
||||||
|
visible: showCategories
|
||||||
|
z: 100
|
||||||
|
|
||||||
|
// Drop shadow
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -2
|
||||||
|
color: "transparent"
|
||||||
|
radius: parent.radius + 2
|
||||||
|
border.color: Qt.rgba(0, 0, 0, 0.1)
|
||||||
|
border.width: 1
|
||||||
|
z: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: activeTheme.spacingS
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
model: categories
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: ListView.view.width
|
||||||
|
height: 36
|
||||||
|
radius: activeTheme.cornerRadiusSmall
|
||||||
|
color: catArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: activeTheme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: modelData
|
||||||
|
font.pixelSize: activeTheme.fontSizeMedium
|
||||||
|
color: selectedCategory === modelData ? activeTheme.primary : activeTheme.surfaceText
|
||||||
|
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: catArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
selectedCategory = modelData
|
||||||
|
showCategories = false
|
||||||
|
updateFilteredModel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// App grid/list container
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - searchContainer.height - (searchField.text.length === 0 ? 128 : 60) - parent.spacing * 3
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
// List view
|
||||||
|
ListView {
|
||||||
|
id: appList
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: activeTheme.spacingS
|
||||||
|
spacing: activeTheme.spacingS
|
||||||
|
visible: viewMode === "list"
|
||||||
|
|
||||||
|
model: filteredModel
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: appList.width
|
||||||
|
height: 72
|
||||||
|
radius: activeTheme.cornerRadiusLarge
|
||||||
|
|
||||||
|
color: appMouseArea.hovered ?
|
||||||
|
Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) :
|
||||||
|
Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.03)
|
||||||
|
|
||||||
|
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: activeTheme.shortDuration
|
||||||
|
easing.type: activeTheme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: activeTheme.spacingM
|
||||||
|
spacing: activeTheme.spacingL
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 56
|
||||||
|
height: 56
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: Quickshell.iconPath(model.icon, "application-x-executable")
|
||||||
|
smooth: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
|
||||||
|
onStatusChanged: {
|
||||||
|
if (status === Image.Error && model.name.includes("Avahi")) {
|
||||||
|
console.log("Avahi icon failed to load:", model.icon, "->", source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for missing icons
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !parent.children[0].visible
|
||||||
|
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
|
||||||
|
radius: activeTheme.cornerRadiusLarge
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: model.name ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
|
font.pixelSize: activeTheme.iconSizeLarge
|
||||||
|
color: activeTheme.surfaceVariantText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - 56 - activeTheme.spacingL
|
||||||
|
spacing: activeTheme.spacingXS
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: model.name
|
||||||
|
font.pixelSize: activeTheme.fontSizeLarge
|
||||||
|
color: activeTheme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: model.comment || "Application"
|
||||||
|
font.pixelSize: activeTheme.fontSizeMedium
|
||||||
|
color: activeTheme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
visible: model.comment && model.comment.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: appMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
launcher.launchApp(model.exec)
|
||||||
|
launcher.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid view
|
||||||
|
GridView {
|
||||||
|
id: appGrid
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: activeTheme.spacingS
|
||||||
|
|
||||||
|
// Responsive cell sizes based on screen width
|
||||||
|
property int baseCellWidth: Math.max(100, Math.min(140, width / 8))
|
||||||
|
property int baseCellHeight: baseCellWidth + 20
|
||||||
|
|
||||||
|
cellWidth: baseCellWidth
|
||||||
|
cellHeight: baseCellHeight
|
||||||
|
visible: viewMode === "grid"
|
||||||
|
|
||||||
|
// Center the grid content
|
||||||
|
property int columnsCount: Math.floor(width / cellWidth)
|
||||||
|
property int remainingSpace: width - (columnsCount * cellWidth)
|
||||||
|
anchors.leftMargin: Math.max(activeTheme.spacingS, remainingSpace / 2)
|
||||||
|
anchors.rightMargin: anchors.leftMargin
|
||||||
|
|
||||||
|
model: filteredModel
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: appGrid.cellWidth - 8
|
||||||
|
height: appGrid.cellHeight - 8
|
||||||
|
radius: activeTheme.cornerRadiusLarge
|
||||||
|
color: gridAppArea.hovered ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) :
|
||||||
|
Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.03)
|
||||||
|
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: activeTheme.shortDuration
|
||||||
|
easing.type: activeTheme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: activeTheme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property int iconSize: Math.min(56, Math.max(32, appGrid.cellWidth * 0.6))
|
||||||
|
width: iconSize
|
||||||
|
height: iconSize
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: Quickshell.iconPath(model.icon, "application-x-executable")
|
||||||
|
smooth: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
|
||||||
|
onStatusChanged: {
|
||||||
|
if (status === Image.Error && model.name.includes("Avahi")) {
|
||||||
|
console.log("Avahi grid icon failed to load:", model.icon, "->", source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for missing icons
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !parent.children[0].visible
|
||||||
|
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
|
||||||
|
radius: activeTheme.cornerRadiusLarge
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: model.name ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
|
font.pixelSize: activeTheme.iconSizeLarge
|
||||||
|
color: activeTheme.surfaceVariantText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: 88
|
||||||
|
text: model.name
|
||||||
|
font.pixelSize: activeTheme.fontSizeSmall
|
||||||
|
color: activeTheme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
maximumLineCount: 2
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: gridAppArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
launcher.launchApp(model.exec)
|
||||||
|
launcher.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: appLauncher
|
||||||
|
|
||||||
|
function start(exec) {
|
||||||
|
// Clean up exec command (remove field codes)
|
||||||
|
var cleanExec = exec.replace(/%[fFuU]/g, "").trim()
|
||||||
|
command = ["sh", "-c", cleanExec]
|
||||||
|
running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
console.log("Failed to launch application, exit code:", exitCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchApp(exec) {
|
||||||
|
appLauncher.start(exec)
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
launcher.isVisible = true
|
||||||
|
Qt.callLater(function() {
|
||||||
|
searchField.forceActiveFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
launcher.isVisible = false
|
||||||
|
searchField.text = ""
|
||||||
|
showCategories = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (launcher.isVisible) {
|
||||||
|
hide()
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
desktopScanner.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
627
ClipboardHistory.qml
Normal file
627
ClipboardHistory.qml
Normal file
@@ -0,0 +1,627 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: clipboardHistory
|
||||||
|
|
||||||
|
property var theme
|
||||||
|
property bool isVisible: false
|
||||||
|
|
||||||
|
// Default theme fallback
|
||||||
|
property var defaultTheme: QtObject {
|
||||||
|
property color primary: "#D0BCFF"
|
||||||
|
property color background: "#10121E"
|
||||||
|
property color surfaceContainer: "#1D1B20"
|
||||||
|
property color surfaceText: "#E6E0E9"
|
||||||
|
property color surfaceVariant: "#49454F"
|
||||||
|
property color surfaceVariantText: "#CAC4D0"
|
||||||
|
property color outline: "#938F99"
|
||||||
|
property color error: "#F2B8B5"
|
||||||
|
property real cornerRadius: 12
|
||||||
|
property real cornerRadiusLarge: 16
|
||||||
|
property real cornerRadiusXLarge: 24
|
||||||
|
property real cornerRadiusSmall: 8
|
||||||
|
property real spacingXS: 4
|
||||||
|
property real spacingS: 8
|
||||||
|
property real spacingM: 12
|
||||||
|
property real spacingL: 16
|
||||||
|
property real spacingXL: 24
|
||||||
|
property real fontSizeLarge: 16
|
||||||
|
property real fontSizeMedium: 14
|
||||||
|
property real fontSizeSmall: 12
|
||||||
|
property real iconSize: 24
|
||||||
|
property real iconSizeLarge: 32
|
||||||
|
property string iconFont: "Material Symbols Rounded"
|
||||||
|
property int iconFontWeight: Font.Normal
|
||||||
|
property int shortDuration: 150
|
||||||
|
property int mediumDuration: 300
|
||||||
|
property int standardEasing: Easing.OutCubic
|
||||||
|
property int emphasizedEasing: Easing.OutQuart
|
||||||
|
}
|
||||||
|
|
||||||
|
property var activeTheme: theme || defaultTheme
|
||||||
|
|
||||||
|
// Window properties
|
||||||
|
color: "transparent"
|
||||||
|
visible: isVisible
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
bottom: true
|
||||||
|
}
|
||||||
|
|
||||||
|
WlrLayershell.layer: WlrLayershell.Overlay
|
||||||
|
WlrLayershell.exclusiveZone: -1
|
||||||
|
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
// Clipboard entries model
|
||||||
|
property var clipboardEntries: []
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: clipboardModel
|
||||||
|
}
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: filteredClipboardModel
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFilteredModel() {
|
||||||
|
filteredClipboardModel.clear()
|
||||||
|
for (let i = 0; i < clipboardModel.count; i++) {
|
||||||
|
const entry = clipboardModel.get(i).entry
|
||||||
|
if (searchField.text.trim().length === 0) {
|
||||||
|
filteredClipboardModel.append({"entry": entry})
|
||||||
|
} else {
|
||||||
|
const content = getEntryPreview(entry).toLowerCase()
|
||||||
|
if (content.includes(searchField.text.toLowerCase())) {
|
||||||
|
filteredClipboardModel.append({"entry": entry})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (isVisible) {
|
||||||
|
hide()
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
clipboardHistory.isVisible = true
|
||||||
|
searchField.focus = true
|
||||||
|
refreshClipboard()
|
||||||
|
console.log("ClipboardHistory: Opening and refreshing")
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
clipboardHistory.isVisible = false
|
||||||
|
searchField.focus = false
|
||||||
|
searchField.text = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshClipboard() {
|
||||||
|
clipboardProcess.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyEntry(entry) {
|
||||||
|
const entryId = entry.split('\t')[0]
|
||||||
|
copyProcess.command = ["sh", "-c", `cliphist decode ${entryId} | wl-copy`]
|
||||||
|
copyProcess.running = true
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteEntry(entry) {
|
||||||
|
const entryId = entry.split('\t')[0]
|
||||||
|
deleteProcess.command = ["cliphist", "delete-query", entryId]
|
||||||
|
deleteProcess.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAll() {
|
||||||
|
clearProcess.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEntryPreview(entry) {
|
||||||
|
// Remove cliphist ID prefix and clean up content
|
||||||
|
let content = entry.replace(/^\s*\d+\s+/, "")
|
||||||
|
|
||||||
|
// Handle different content types
|
||||||
|
if (content.includes("image/")) {
|
||||||
|
const match = content.match(/(\d+)x(\d+)/)
|
||||||
|
return match ? `Image ${match[1]}×${match[2]}` : "Image"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate long text
|
||||||
|
if (content.length > 100) {
|
||||||
|
return content.substring(0, 100) + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEntryType(entry) {
|
||||||
|
if (entry.includes("image/")) return "image"
|
||||||
|
if (entry.length > 200) return "long_text"
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background overlay
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.5)
|
||||||
|
opacity: clipboardHistory.isVisible ? 1.0 : 0.0
|
||||||
|
visible: clipboardHistory.isVisible
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: activeTheme.mediumDuration
|
||||||
|
easing.type: activeTheme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: clipboardHistory.isVisible
|
||||||
|
onClicked: clipboardHistory.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main clipboard container
|
||||||
|
Rectangle {
|
||||||
|
id: clipboardContainer
|
||||||
|
width: Math.min(600, parent.width - 200)
|
||||||
|
height: Math.min(500, parent.height - 100)
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
color: activeTheme.surfaceContainer
|
||||||
|
radius: activeTheme.cornerRadiusXLarge
|
||||||
|
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
opacity: clipboardHistory.isVisible ? 1.0 : 0.0
|
||||||
|
scale: clipboardHistory.isVisible ? 1.0 : 0.9
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: activeTheme.mediumDuration
|
||||||
|
easing.type: activeTheme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: activeTheme.mediumDuration
|
||||||
|
easing.type: activeTheme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header section
|
||||||
|
Column {
|
||||||
|
id: headerSection
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: activeTheme.spacingXL
|
||||||
|
spacing: activeTheme.spacingL
|
||||||
|
|
||||||
|
// Title and actions
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Clipboard History"
|
||||||
|
font.pixelSize: activeTheme.fontSizeLarge + 4
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: activeTheme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width - 180 - (clearAllButton.visible ? 48 : 0)
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all button
|
||||||
|
Rectangle {
|
||||||
|
id: clearAllButton
|
||||||
|
width: 40
|
||||||
|
height: 32
|
||||||
|
radius: activeTheme.cornerRadius
|
||||||
|
color: clearArea.containsMouse ? Qt.rgba(activeTheme.error.r, activeTheme.error.g, activeTheme.error.b, 0.12) : "transparent"
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: clipboardModel.count > 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "delete_sweep"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: activeTheme.iconSize
|
||||||
|
color: clearArea.containsMouse ? activeTheme.error : activeTheme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: clearArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: clearAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: activeTheme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
Rectangle {
|
||||||
|
width: 40
|
||||||
|
height: 32
|
||||||
|
radius: activeTheme.cornerRadius
|
||||||
|
color: closeArea.containsMouse ? Qt.rgba(activeTheme.error.r, activeTheme.error.g, activeTheme.error.b, 0.12) : "transparent"
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.rightMargin: 4
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "close"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: activeTheme.iconSize
|
||||||
|
color: closeArea.containsMouse ? activeTheme.error : activeTheme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: closeArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: clipboardHistory.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: activeTheme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search field
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 48
|
||||||
|
radius: activeTheme.cornerRadiusLarge
|
||||||
|
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
|
||||||
|
border.color: searchField.focus ? activeTheme.primary : Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
|
||||||
|
border.width: searchField.focus ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: activeTheme.spacingL
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: activeTheme.spacingM
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "search"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: activeTheme.iconSize
|
||||||
|
color: searchField.focus ? activeTheme.primary : Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.6)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: searchField
|
||||||
|
width: parent.parent.width - 80
|
||||||
|
height: parent.parent.height
|
||||||
|
font.pixelSize: activeTheme.fontSizeLarge
|
||||||
|
color: activeTheme.surfaceText
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
|
||||||
|
onTextChanged: updateFilteredModel()
|
||||||
|
|
||||||
|
Keys.onPressed: (event) => {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
clipboardHistory.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder text
|
||||||
|
Text {
|
||||||
|
text: "Search clipboard entries..."
|
||||||
|
font: searchField.font
|
||||||
|
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.6)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: searchField.text.length === 0 && !searchField.focus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: activeTheme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clipboard entries
|
||||||
|
Rectangle {
|
||||||
|
anchors.top: headerSection.bottom
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: activeTheme.spacingXL
|
||||||
|
anchors.topMargin: activeTheme.spacingL
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: clipboardList
|
||||||
|
model: filteredClipboardModel
|
||||||
|
spacing: activeTheme.spacingS
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: clipboardList.width
|
||||||
|
height: Math.max(60, contentColumn.implicitHeight + activeTheme.spacingM * 2)
|
||||||
|
radius: activeTheme.cornerRadius
|
||||||
|
color: entryArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) :
|
||||||
|
Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.05)
|
||||||
|
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.1)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
property string entryType: getEntryType(model.entry)
|
||||||
|
property string entryPreview: getEntryPreview(model.entry)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: activeTheme.spacingM
|
||||||
|
spacing: activeTheme.spacingL
|
||||||
|
|
||||||
|
// Entry type icon
|
||||||
|
Rectangle {
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: activeTheme.cornerRadius
|
||||||
|
color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: {
|
||||||
|
switch (entryType) {
|
||||||
|
case "image": return "image"
|
||||||
|
case "long_text": return "subject"
|
||||||
|
default: return "content_paste"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: activeTheme.iconSize - 4
|
||||||
|
color: activeTheme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry content
|
||||||
|
Column {
|
||||||
|
id: contentColumn
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - 100
|
||||||
|
spacing: activeTheme.spacingXS
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: {
|
||||||
|
switch (entryType) {
|
||||||
|
case "image": return "Image • " + entryPreview
|
||||||
|
case "long_text": return "Long Text"
|
||||||
|
default: return "Text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: activeTheme.fontSizeSmall
|
||||||
|
color: activeTheme.primary
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: entryPreview
|
||||||
|
font.pixelSize: activeTheme.fontSizeMedium
|
||||||
|
color: activeTheme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
maximumLineCount: entryType === "long_text" ? 3 : 2
|
||||||
|
elide: Text.ElideRight
|
||||||
|
visible: entryType !== "image"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: activeTheme.spacingXS
|
||||||
|
|
||||||
|
// Copy button
|
||||||
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: activeTheme.cornerRadiusSmall
|
||||||
|
color: copyArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "content_copy"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: activeTheme.iconSize - 8
|
||||||
|
color: copyArea.containsMouse ? activeTheme.primary : activeTheme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: copyArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: copyEntry(model.entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: activeTheme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: activeTheme.cornerRadiusSmall
|
||||||
|
color: deleteArea.containsMouse ? Qt.rgba(activeTheme.error.r, activeTheme.error.g, activeTheme.error.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "delete"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: activeTheme.iconSize - 8
|
||||||
|
color: deleteArea.containsMouse ? activeTheme.error : activeTheme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deleteArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: deleteEntry(model.entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: activeTheme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: entryArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: copyEntry(model.entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: activeTheme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty state
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: activeTheme.spacingL
|
||||||
|
visible: filteredClipboardModel.count === 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "content_paste_off"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: activeTheme.iconSizeLarge + 16
|
||||||
|
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "No clipboard history"
|
||||||
|
font.pixelSize: activeTheme.fontSizeLarge
|
||||||
|
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.6)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "Copy something to see it here"
|
||||||
|
font.pixelSize: activeTheme.fontSizeMedium
|
||||||
|
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clipboard processes
|
||||||
|
Process {
|
||||||
|
id: clipboardProcess
|
||||||
|
command: ["cliphist", "list"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: "\n"
|
||||||
|
onRead: (line) => {
|
||||||
|
if (line.trim()) {
|
||||||
|
clipboardHistory.clipboardEntries.push(line)
|
||||||
|
clipboardModel.append({"entry": line})
|
||||||
|
console.log("ClipboardHistory: Adding entry:", line.substring(0, 50) + "...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onStarted: {
|
||||||
|
clipboardHistory.clipboardEntries = []
|
||||||
|
clipboardModel.clear()
|
||||||
|
console.log("ClipboardHistory: Starting cliphist process...")
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: (exitCode) => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
console.log("ClipboardHistory: Loaded", clipboardModel.count, "entries")
|
||||||
|
updateFilteredModel()
|
||||||
|
} else {
|
||||||
|
console.warn("ClipboardHistory: Failed to load clipboard history")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: copyProcess
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: (exitCode) => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
console.warn("ClipboardHistory: Failed to copy entry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: deleteProcess
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: (exitCode) => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
refreshClipboard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: clearProcess
|
||||||
|
command: ["cliphist", "wipe"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: (exitCode) => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
clipboardHistory.clipboardEntries = []
|
||||||
|
clipboardModel.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: (event) => {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
420
MediaPlayer.qml
Normal file
420
MediaPlayer.qml
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import "Services"
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: mediaPlayer
|
||||||
|
|
||||||
|
property var theme
|
||||||
|
property bool isVisible: false
|
||||||
|
property MprisPlayer activePlayer: MprisController.activePlayer
|
||||||
|
property bool hasActiveMedia: MprisController.isPlaying && (activePlayer?.trackTitle || activePlayer?.trackArtist)
|
||||||
|
|
||||||
|
property var defaultTheme: QtObject {
|
||||||
|
property color primary: "#D0BCFF"
|
||||||
|
property color background: "#10121E"
|
||||||
|
property color surfaceContainer: "#1D1B20"
|
||||||
|
property color surfaceText: "#E6E0E9"
|
||||||
|
property color surfaceVariant: "#49454F"
|
||||||
|
property color surfaceVariantText: "#CAC4D0"
|
||||||
|
property color outline: "#938F99"
|
||||||
|
property color error: "#F2B8B5"
|
||||||
|
property real cornerRadius: 12
|
||||||
|
property real cornerRadiusLarge: 16
|
||||||
|
property real cornerRadiusXLarge: 24
|
||||||
|
property real cornerRadiusSmall: 8
|
||||||
|
property real spacingXS: 4
|
||||||
|
property real spacingS: 8
|
||||||
|
property real spacingM: 12
|
||||||
|
property real spacingL: 16
|
||||||
|
property real spacingXL: 24
|
||||||
|
property real fontSizeLarge: 16
|
||||||
|
property real fontSizeMedium: 14
|
||||||
|
property real fontSizeSmall: 12
|
||||||
|
property real iconSize: 24
|
||||||
|
property real iconSizeLarge: 32
|
||||||
|
property string iconFont: "Material Symbols Rounded"
|
||||||
|
property int iconFontWeight: Font.Normal
|
||||||
|
property int shortDuration: 150
|
||||||
|
property int mediumDuration: 300
|
||||||
|
property int standardEasing: Easing.OutCubic
|
||||||
|
property int emphasizedEasing: Easing.OutQuart
|
||||||
|
}
|
||||||
|
|
||||||
|
property var activeTheme: theme || defaultTheme
|
||||||
|
|
||||||
|
onHasActiveMediaChanged: {
|
||||||
|
if (!hasActiveMedia && isVisible) {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
bottom: true
|
||||||
|
}
|
||||||
|
|
||||||
|
WlrLayershell.layer: WlrLayershell.Overlay
|
||||||
|
WlrLayershell.exclusiveZone: -1
|
||||||
|
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||||
|
WlrLayershell.namespace: "quickshell-media-player"
|
||||||
|
|
||||||
|
visible: isVisible
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.3)
|
||||||
|
opacity: mediaPlayer.isVisible ? 1.0 : 0.0
|
||||||
|
visible: mediaPlayer.isVisible
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: activeTheme.shortDuration
|
||||||
|
easing.type: activeTheme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: mediaPlayer.isVisible
|
||||||
|
onClicked: mediaPlayer.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: mediaPanel
|
||||||
|
|
||||||
|
width: 480
|
||||||
|
height: 320
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
color: Qt.rgba(activeTheme.surfaceContainer.r, activeTheme.surfaceContainer.g, activeTheme.surfaceContainer.b, 0.98)
|
||||||
|
radius: activeTheme.cornerRadiusXLarge
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -2
|
||||||
|
color: "transparent"
|
||||||
|
radius: parent.radius + 2
|
||||||
|
border.color: Qt.rgba(0, 0, 0, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
z: -2
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "transparent"
|
||||||
|
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.12)
|
||||||
|
border.width: 1
|
||||||
|
radius: parent.radius
|
||||||
|
z: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
transform: [
|
||||||
|
Scale {
|
||||||
|
origin.x: mediaPanel.width / 2
|
||||||
|
origin.y: mediaPanel.height / 2
|
||||||
|
xScale: mediaPlayer.isVisible ? 1.0 : 0.9
|
||||||
|
yScale: mediaPlayer.isVisible ? 1.0 : 0.9
|
||||||
|
|
||||||
|
Behavior on xScale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: activeTheme.mediumDuration
|
||||||
|
easing.type: activeTheme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on yScale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: activeTheme.mediumDuration
|
||||||
|
easing.type: activeTheme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
opacity: mediaPlayer.isVisible ? 1.0 : 0.0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: activeTheme.mediumDuration
|
||||||
|
easing.type: activeTheme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: activeTheme.spacingXL
|
||||||
|
spacing: activeTheme.spacingL
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Now Playing"
|
||||||
|
font.pixelSize: activeTheme.fontSizeLarge + 4
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: activeTheme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { width: parent.width - 200; height: 1 }
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: activeTheme.cornerRadius
|
||||||
|
color: closeArea.containsMouse ? Qt.rgba(activeTheme.error.r, activeTheme.error.g, activeTheme.error.b, 0.12) : "transparent"
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "close"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: activeTheme.iconSize
|
||||||
|
color: closeArea.containsMouse ? activeTheme.error : activeTheme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: closeArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: mediaPlayer.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 80
|
||||||
|
spacing: activeTheme.spacingXL
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 180
|
||||||
|
height: parent.height
|
||||||
|
radius: activeTheme.cornerRadiusLarge
|
||||||
|
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: albumArt
|
||||||
|
anchors.fill: parent
|
||||||
|
source: activePlayer?.trackArtUrl || ""
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
smooth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: albumArt.status !== Image.Ready
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "album"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: 48
|
||||||
|
color: activeTheme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - 180 - activeTheme.spacingXL
|
||||||
|
height: parent.height
|
||||||
|
spacing: activeTheme.spacingM
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: activeTheme.spacingS
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: activePlayer?.trackTitle || "No title"
|
||||||
|
font.pixelSize: activeTheme.fontSizeLarge + 2
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: activeTheme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: activePlayer?.trackArtist || "Unknown artist"
|
||||||
|
font.pixelSize: activeTheme.fontSizeLarge
|
||||||
|
color: activeTheme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: activePlayer?.trackAlbum || ""
|
||||||
|
font.pixelSize: activeTheme.fontSizeMedium
|
||||||
|
color: activeTheme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { height: activeTheme.spacingM }
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: activeTheme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 6
|
||||||
|
radius: 3
|
||||||
|
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width * (activePlayer?.position / Math.max(activePlayer?.length || 1, 1))
|
||||||
|
height: parent.height
|
||||||
|
radius: parent.radius
|
||||||
|
color: activeTheme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: formatTime(activePlayer?.position || 0)
|
||||||
|
font.pixelSize: activeTheme.fontSizeSmall
|
||||||
|
color: activeTheme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { width: parent.width - 100; height: 1 }
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: formatTime(activePlayer?.length || 0)
|
||||||
|
font.pixelSize: activeTheme.fontSizeSmall
|
||||||
|
color: activeTheme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { height: activeTheme.spacingL }
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: activeTheme.spacingL
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 48
|
||||||
|
height: 48
|
||||||
|
radius: 24
|
||||||
|
color: prevArea.containsMouse ? Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "skip_previous"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: activeTheme.iconSize
|
||||||
|
color: activeTheme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: prevArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: activePlayer?.previous()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 56
|
||||||
|
height: 56
|
||||||
|
radius: 28
|
||||||
|
color: activeTheme.primary
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: activeTheme.iconSizeLarge
|
||||||
|
color: activeTheme.background
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: activePlayer?.togglePlaying()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 48
|
||||||
|
height: 48
|
||||||
|
radius: 24
|
||||||
|
color: nextArea.containsMouse ? Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "skip_next"
|
||||||
|
font.family: activeTheme.iconFont
|
||||||
|
font.pixelSize: activeTheme.iconSize
|
||||||
|
color: activeTheme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: nextArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: activePlayer?.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
running: activePlayer?.playbackState === MprisPlaybackState.Playing
|
||||||
|
interval: 1000
|
||||||
|
repeat: true
|
||||||
|
onTriggered: activePlayer?.positionChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(seconds) {
|
||||||
|
const mins = Math.floor(seconds / 60)
|
||||||
|
const secs = Math.floor(seconds % 60)
|
||||||
|
return mins + ":" + (secs < 10 ? "0" : "") + secs
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
mediaPlayer.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
mediaPlayer.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (mediaPlayer.isVisible) {
|
||||||
|
hide()
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
162
Services/MprisController.qml
Normal file
162
Services/MprisController.qml
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQml.Models
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that provides easy access to the active Mpris player.
|
||||||
|
*/
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
property MprisPlayer trackedPlayer: null
|
||||||
|
property MprisPlayer activePlayer: trackedPlayer ?? Mpris.players.values[0] ?? null
|
||||||
|
signal trackChanged(reverse: bool)
|
||||||
|
|
||||||
|
property bool __reverse: false
|
||||||
|
|
||||||
|
property var activeTrack
|
||||||
|
|
||||||
|
Instantiator {
|
||||||
|
model: Mpris.players
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
required property MprisPlayer modelData
|
||||||
|
target: modelData
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
console.log("MPRIS Player connected:", modelData.identity)
|
||||||
|
if (root.trackedPlayer == null || modelData.isPlaying) {
|
||||||
|
root.trackedPlayer = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) {
|
||||||
|
for (const player of Mpris.players.values) {
|
||||||
|
if (player.playbackState.isPlaying) {
|
||||||
|
root.trackedPlayer = player
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackedPlayer == null && Mpris.players.values.length != 0) {
|
||||||
|
trackedPlayer = Mpris.players.values[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPlaybackStateChanged() {
|
||||||
|
if (root.trackedPlayer !== modelData) root.trackedPlayer = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: activePlayer
|
||||||
|
|
||||||
|
function onPostTrackChanged() {
|
||||||
|
root.updateTrack()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTrackArtUrlChanged() {
|
||||||
|
if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) {
|
||||||
|
const r = root.__reverse
|
||||||
|
root.updateTrack()
|
||||||
|
root.__reverse = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onActivePlayerChanged: this.updateTrack()
|
||||||
|
|
||||||
|
function updateTrack() {
|
||||||
|
console.log(`MPRIS Track Update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtist}`)
|
||||||
|
this.activeTrack = {
|
||||||
|
uniqueId: this.activePlayer?.uniqueId ?? 0,
|
||||||
|
artUrl: this.activePlayer?.trackArtUrl ?? "",
|
||||||
|
title: this.activePlayer?.trackTitle || "Unknown Title",
|
||||||
|
artist: this.activePlayer?.trackArtist || "Unknown Artist",
|
||||||
|
album: this.activePlayer?.trackAlbum || "Unknown Album",
|
||||||
|
}
|
||||||
|
|
||||||
|
this.trackChanged(__reverse)
|
||||||
|
this.__reverse = false
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying
|
||||||
|
property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false
|
||||||
|
function togglePlaying() {
|
||||||
|
if (this.canTogglePlaying) this.activePlayer.togglePlaying()
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false
|
||||||
|
function previous() {
|
||||||
|
if (this.canGoPrevious) {
|
||||||
|
this.__reverse = true
|
||||||
|
this.activePlayer.previous()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool canGoNext: this.activePlayer?.canGoNext ?? false
|
||||||
|
function next() {
|
||||||
|
if (this.canGoNext) {
|
||||||
|
this.__reverse = false
|
||||||
|
this.activePlayer.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool canChangeVolume: this.activePlayer && this.activePlayer.volumeSupported && this.activePlayer.canControl
|
||||||
|
|
||||||
|
property bool loopSupported: this.activePlayer && this.activePlayer.loopSupported && this.activePlayer.canControl
|
||||||
|
property var loopState: this.activePlayer?.loopState ?? MprisLoopState.None
|
||||||
|
function setLoopState(loopState) {
|
||||||
|
if (this.loopSupported) {
|
||||||
|
this.activePlayer.loopState = loopState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool shuffleSupported: this.activePlayer && this.activePlayer.shuffleSupported && this.activePlayer.canControl
|
||||||
|
property bool hasShuffle: this.activePlayer?.shuffle ?? false
|
||||||
|
function setShuffle(shuffle) {
|
||||||
|
if (this.shuffleSupported) {
|
||||||
|
this.activePlayer.shuffle = shuffle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActivePlayer(player) {
|
||||||
|
const targetPlayer = player ?? Mpris.players[0]
|
||||||
|
console.log(`[Mpris] Active player ${targetPlayer} << ${activePlayer}`)
|
||||||
|
|
||||||
|
if (targetPlayer && this.activePlayer) {
|
||||||
|
this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer)
|
||||||
|
} else {
|
||||||
|
this.__reverse = false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.trackedPlayer = targetPlayer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug timer
|
||||||
|
Timer {
|
||||||
|
interval: 3000
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
console.log(`[MprisController] Players: ${Mpris.players.length}, Active: ${activePlayer?.identity || 'none'}, Playing: ${isPlaying}`)
|
||||||
|
if (activePlayer) {
|
||||||
|
console.log(` Track: ${activePlayer.trackTitle || 'Unknown'} by ${activePlayer.trackArtist || 'Unknown'}`)
|
||||||
|
console.log(` State: ${activePlayer.playbackState}`)
|
||||||
|
} else if (Mpris.players.length === 0) {
|
||||||
|
console.log(" No MPRIS players detected. Try:")
|
||||||
|
console.log(" - mpv --script-opts=mpris-title='{{media-title}}' file.mp3")
|
||||||
|
console.log(" - firefox/chromium (YouTube, Spotify Web)")
|
||||||
|
console.log(" - vlc file.mp3")
|
||||||
|
console.log(" Check available players: busctl --user list | grep mpris")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
85
Services/OSDetectionService.qml
Normal file
85
Services/OSDetectionService.qml
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: osService
|
||||||
|
|
||||||
|
property string osLogo: ""
|
||||||
|
property string osName: ""
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: osDetector
|
||||||
|
command: ["lsb_release", "-i", "-s"]
|
||||||
|
running: true
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: "\n"
|
||||||
|
onRead: (data) => {
|
||||||
|
if (data.trim()) {
|
||||||
|
let osId = data.trim().toLowerCase()
|
||||||
|
console.log("Detected OS:", osId)
|
||||||
|
setOSInfo(osId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: (exitCode) => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
osDetectorFallback.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: osDetectorFallback
|
||||||
|
command: ["sh", "-c", "cat /etc/os-release | grep '^ID=' | cut -d'=' -f2 | tr -d '\"'"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: "\n"
|
||||||
|
onRead: (data) => {
|
||||||
|
if (data.trim()) {
|
||||||
|
let osId = data.trim().toLowerCase()
|
||||||
|
console.log("Detected OS (fallback):", osId)
|
||||||
|
setOSInfo(osId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: (exitCode) => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
osService.osLogo = ""
|
||||||
|
osService.osName = "Linux"
|
||||||
|
console.log("OS detection failed, using generic icon")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOSInfo(osId) {
|
||||||
|
if (osId.includes("arch")) {
|
||||||
|
osService.osLogo = "\uf303"
|
||||||
|
osService.osName = "Arch Linux"
|
||||||
|
} else if (osId.includes("ubuntu")) {
|
||||||
|
osService.osLogo = "\uf31b"
|
||||||
|
osService.osName = "Ubuntu"
|
||||||
|
} else if (osId.includes("fedora")) {
|
||||||
|
osService.osLogo = "\uf30a"
|
||||||
|
osService.osName = "Fedora"
|
||||||
|
} else if (osId.includes("debian")) {
|
||||||
|
osService.osLogo = "\uf306"
|
||||||
|
osService.osName = "Debian"
|
||||||
|
} else if (osId.includes("opensuse")) {
|
||||||
|
osService.osLogo = "\uef6d"
|
||||||
|
osService.osName = "openSUSE"
|
||||||
|
} else if (osId.includes("manjaro")) {
|
||||||
|
osService.osLogo = "\uf312"
|
||||||
|
osService.osName = "Manjaro"
|
||||||
|
} else {
|
||||||
|
osService.osLogo = "\uf033"
|
||||||
|
osService.osName = "Linux"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
Services/WeatherService.qml
Normal file
78
Services/WeatherService.qml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: weatherService
|
||||||
|
|
||||||
|
property var weather: ({
|
||||||
|
available: false,
|
||||||
|
temp: 0,
|
||||||
|
tempF: 0,
|
||||||
|
city: "",
|
||||||
|
wCode: "113",
|
||||||
|
humidity: 0,
|
||||||
|
wind: "",
|
||||||
|
sunrise: "06:00",
|
||||||
|
sunset: "18:00",
|
||||||
|
uv: 0,
|
||||||
|
pressure: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: weatherFetcher
|
||||||
|
command: ["bash", "-c", "curl -s 'wttr.in/?format=j1' | jq '{current: .current_condition[0], location: .nearest_area[0], astronomy: .weather[0].astronomy[0]}'"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
if (text.trim() && text.trim().startsWith("{")) {
|
||||||
|
try {
|
||||||
|
let parsedData = JSON.parse(text.trim())
|
||||||
|
if (parsedData.current && parsedData.location) {
|
||||||
|
weatherService.weather = {
|
||||||
|
available: true,
|
||||||
|
temp: parseInt(parsedData.current.temp_C || 0),
|
||||||
|
tempF: parseInt(parsedData.current.temp_F || 0),
|
||||||
|
city: parsedData.location.areaName[0]?.value || "Unknown",
|
||||||
|
wCode: parsedData.current.weatherCode || "113",
|
||||||
|
humidity: parseInt(parsedData.current.humidity || 0),
|
||||||
|
wind: (parsedData.current.windspeedKmph || 0) + " km/h",
|
||||||
|
sunrise: parsedData.astronomy?.sunrise || "06:00",
|
||||||
|
sunset: parsedData.astronomy?.sunset || "18:00",
|
||||||
|
uv: parseInt(parsedData.current.uvIndex || 0),
|
||||||
|
pressure: parseInt(parsedData.current.pressure || 0)
|
||||||
|
}
|
||||||
|
console.log("Weather updated:", weatherService.weather.city, weatherService.weather.temp + "°C")
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to parse weather data:", e.message)
|
||||||
|
weatherService.weather.available = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("No valid weather data received")
|
||||||
|
weatherService.weather.available = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: (exitCode) => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
console.warn("Weather fetch failed with exit code:", exitCode)
|
||||||
|
weatherService.weather.available = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 600000 // 10 minutes
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
triggeredOnStart: true
|
||||||
|
onTriggered: {
|
||||||
|
weatherFetcher.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Services/qmldir
Normal file
1
Services/qmldir
Normal file
@@ -0,0 +1 @@
|
|||||||
|
singleton MprisController 1.0 MprisController.qml
|
||||||
58
Widgets/AppLauncherButton.qml
Normal file
58
Widgets/AppLauncherButton.qml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: archLauncher
|
||||||
|
|
||||||
|
property var theme
|
||||||
|
property var root
|
||||||
|
|
||||||
|
width: Math.max(120, launcherRow.implicitWidth + theme.spacingM * 2)
|
||||||
|
height: 32
|
||||||
|
radius: theme.cornerRadius
|
||||||
|
color: launcherArea.containsMouse ? Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.12) : Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.08)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: launcherRow
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: theme.spacingS
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.osLogo || "apps"
|
||||||
|
font.family: root.osLogo ? "NerdFont" : theme.iconFont
|
||||||
|
font.pixelSize: root.osLogo ? theme.iconSize - 2 : theme.iconSize - 2
|
||||||
|
font.weight: theme.iconFontWeight
|
||||||
|
color: theme.surfaceText
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Applications"
|
||||||
|
font.pixelSize: theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: launcherArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.appLauncher.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: theme.shortDuration
|
||||||
|
easing.type: theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Widgets/ClipboardButton.qml
Normal file
40
Widgets/ClipboardButton.qml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property var theme
|
||||||
|
property var root
|
||||||
|
|
||||||
|
width: 40
|
||||||
|
height: 32
|
||||||
|
radius: theme.cornerRadius
|
||||||
|
color: clipboardArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "content_paste"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: theme.iconSize - 6
|
||||||
|
font.weight: theme.iconFontWeight
|
||||||
|
color: theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: clipboardArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.clipboardHistoryPopup.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: theme.shortDuration
|
||||||
|
easing.type: theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
291
Widgets/ClockWidget.qml
Normal file
291
Widgets/ClockWidget.qml
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: clockContainer
|
||||||
|
|
||||||
|
property var theme
|
||||||
|
property var root
|
||||||
|
|
||||||
|
width: Math.min(root.hasActiveMedia ? 500 : (root.weather.available ? 280 : 200), parent.width - theme.spacingL * 2)
|
||||||
|
height: root.hasActiveMedia ? 80 : 32
|
||||||
|
radius: theme.cornerRadius
|
||||||
|
color: clockMouseArea.containsMouse && root.hasActiveMedia ?
|
||||||
|
Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) :
|
||||||
|
Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.08)
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: theme.shortDuration
|
||||||
|
easing.type: theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property date currentDate: new Date()
|
||||||
|
|
||||||
|
// Media player content (when active)
|
||||||
|
Column {
|
||||||
|
visible: root.hasActiveMedia
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - theme.spacingM * 2
|
||||||
|
spacing: theme.spacingXS
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 48
|
||||||
|
height: 48
|
||||||
|
radius: theme.cornerRadiusSmall
|
||||||
|
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: root.activePlayer?.trackArtUrl || ""
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
smooth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: parent.children[0].status !== Image.Ready
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "music_note"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: theme.iconSize
|
||||||
|
color: theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - 48 - theme.spacingS - 120
|
||||||
|
spacing: 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.activePlayer?.trackTitle || "Unknown Track"
|
||||||
|
font.pixelSize: theme.fontSizeMedium
|
||||||
|
color: theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.activePlayer?.trackArtist || "Unknown Artist"
|
||||||
|
font.pixelSize: theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7)
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: 14
|
||||||
|
color: prevBtnArea.containsMouse ? Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "skip_previous"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: 16
|
||||||
|
color: theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: prevBtnArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.activePlayer?.previous()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: 14
|
||||||
|
color: theme.primary
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: root.activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: 16
|
||||||
|
color: theme.background
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.activePlayer?.togglePlaying()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: 14
|
||||||
|
color: nextBtnArea.containsMouse ? Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "skip_next"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: 16
|
||||||
|
color: theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: nextBtnArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.activePlayer?.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 4
|
||||||
|
radius: 2
|
||||||
|
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: progressFill
|
||||||
|
width: parent.width * (root.activePlayer?.position / Math.max(root.activePlayer?.length || 1, 1))
|
||||||
|
height: parent.height
|
||||||
|
radius: parent.radius
|
||||||
|
color: theme.primary
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.OutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (root.activePlayer && root.activePlayer.length > 0) {
|
||||||
|
const newPosition = (mouse.x / width) * root.activePlayer.length
|
||||||
|
root.activePlayer.setPosition(newPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal clock/weather content (when no media)
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: theme.spacingM
|
||||||
|
visible: !root.hasActiveMedia
|
||||||
|
|
||||||
|
// Weather info (when available)
|
||||||
|
Row {
|
||||||
|
spacing: theme.spacingXS
|
||||||
|
visible: root.weather.available
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.weatherIcons[root.weather.wCode] || "clear_day"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: theme.iconSize - 2
|
||||||
|
color: theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: (root.useFahrenheit ? root.weather.tempF : root.weather.temp) + "°" + (root.useFahrenheit ? "F" : "C")
|
||||||
|
font.pixelSize: theme.fontSizeMedium
|
||||||
|
color: theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator when weather is available
|
||||||
|
Text {
|
||||||
|
text: "•"
|
||||||
|
font.pixelSize: theme.fontSizeMedium
|
||||||
|
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.5)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: root.weather.available
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time and date
|
||||||
|
Row {
|
||||||
|
spacing: theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: Qt.formatTime(clockContainer.currentDate, "h:mm AP")
|
||||||
|
font.pixelSize: theme.fontSizeMedium
|
||||||
|
color: theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "•"
|
||||||
|
font.pixelSize: theme.fontSizeMedium
|
||||||
|
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.5)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: Qt.formatDate(clockContainer.currentDate, "ddd d")
|
||||||
|
font.pixelSize: theme.fontSizeMedium
|
||||||
|
color: theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 1000
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
clockContainer.currentDate = new Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: clockMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: !root.hasActiveMedia ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
enabled: !root.hasActiveMedia
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.calendarVisible = !root.calendarVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Widgets/ColorPickerButton.qml
Normal file
40
Widgets/ColorPickerButton.qml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property var theme
|
||||||
|
property var root
|
||||||
|
|
||||||
|
width: 40
|
||||||
|
height: 32
|
||||||
|
radius: theme.cornerRadius
|
||||||
|
color: colorPickerArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "colorize"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: theme.iconSize - 6
|
||||||
|
font.weight: theme.iconFontWeight
|
||||||
|
color: theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: colorPickerArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.colorPickerProcess.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: theme.shortDuration
|
||||||
|
easing.type: theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
Widgets/NotificationButton.qml
Normal file
57
Widgets/NotificationButton.qml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property var theme
|
||||||
|
property var root
|
||||||
|
|
||||||
|
width: 40
|
||||||
|
height: 32
|
||||||
|
radius: theme.cornerRadius
|
||||||
|
color: notificationArea.containsMouse || root.notificationHistoryVisible ?
|
||||||
|
Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.16) :
|
||||||
|
Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
property bool hasUnread: root.notificationHistory.count > 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "notifications"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: theme.iconSize - 6
|
||||||
|
font.weight: theme.iconFontWeight
|
||||||
|
color: notificationArea.containsMouse || root.notificationHistoryVisible ?
|
||||||
|
theme.primary : theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 8
|
||||||
|
height: 8
|
||||||
|
radius: 4
|
||||||
|
color: theme.error
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.rightMargin: 6
|
||||||
|
anchors.topMargin: 6
|
||||||
|
visible: parent.hasUnread
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: notificationArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.notificationHistoryVisible = !root.notificationHistoryVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: theme.shortDuration
|
||||||
|
easing.type: theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
Widgets/SystemTrayWidget.qml
Normal file
111
Widgets/SystemTrayWidget.qml
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.SystemTray
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property var theme
|
||||||
|
property var root
|
||||||
|
|
||||||
|
width: Math.max(40, systemTrayRow.implicitWidth + theme.spacingS * 2)
|
||||||
|
height: 32
|
||||||
|
radius: theme.cornerRadius
|
||||||
|
color: Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: systemTrayRow.children.length > 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: systemTrayRow
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: theme.spacingXS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: SystemTray.items
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: 24
|
||||||
|
height: 24
|
||||||
|
radius: theme.cornerRadiusSmall
|
||||||
|
color: trayItemArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
property var trayItem: modelData
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 18
|
||||||
|
height: 18
|
||||||
|
source: {
|
||||||
|
let icon = trayItem?.icon || "";
|
||||||
|
if (!icon) return "";
|
||||||
|
|
||||||
|
if (icon.includes("?path=")) {
|
||||||
|
const [name, path] = icon.split("?path=");
|
||||||
|
const fileName = name.substring(name.lastIndexOf("/") + 1);
|
||||||
|
return `file://${path}/${fileName}`;
|
||||||
|
}
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
asynchronous: true
|
||||||
|
smooth: true
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: trayItemArea
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (!trayItem) return;
|
||||||
|
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
if (!trayItem.onlyMenu) {
|
||||||
|
trayItem.activate()
|
||||||
|
}
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
if (trayItem.hasMenu) {
|
||||||
|
console.log("Right-click detected, showing menu for:", trayItem.title || "Unknown")
|
||||||
|
customTrayMenu.showMenu(mouse.x, mouse.y)
|
||||||
|
} else {
|
||||||
|
console.log("No menu available for:", trayItem.title || "Unknown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: customTrayMenu
|
||||||
|
|
||||||
|
property bool menuVisible: false
|
||||||
|
|
||||||
|
function showMenu(x, y) {
|
||||||
|
root.currentTrayMenu = customTrayMenu
|
||||||
|
root.currentTrayItem = trayItem
|
||||||
|
|
||||||
|
root.trayMenuX = parent.parent.parent.parent.x + parent.parent.parent.parent.width - 180 - theme.spacingL
|
||||||
|
root.trayMenuY = theme.barHeight + theme.spacingS
|
||||||
|
|
||||||
|
console.log("Showing menu at:", root.trayMenuX, root.trayMenuY)
|
||||||
|
menuVisible = true
|
||||||
|
root.showTrayMenu = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideMenu() {
|
||||||
|
menuVisible = false
|
||||||
|
root.showTrayMenu = false
|
||||||
|
root.currentTrayMenu = null
|
||||||
|
root.currentTrayItem = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: theme.shortDuration
|
||||||
|
easing.type: theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
Widgets/TopBar.qml
Normal file
117
Widgets/TopBar.qml
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Services.SystemTray
|
||||||
|
import "../Services"
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: topBar
|
||||||
|
|
||||||
|
property var theme
|
||||||
|
property var root
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: theme.barHeight
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(theme.surfaceContainer.r, theme.surfaceContainer.g, theme.surfaceContainer.b, 0.95)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "transparent"
|
||||||
|
border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.12)
|
||||||
|
border.width: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(theme.surfaceTint.r, theme.surfaceTint.g, theme.surfaceTint.b, 0.08)
|
||||||
|
|
||||||
|
SequentialAnimation on opacity {
|
||||||
|
running: true
|
||||||
|
loops: Animation.Infinite
|
||||||
|
NumberAnimation {
|
||||||
|
to: 0.12
|
||||||
|
duration: theme.extraLongDuration
|
||||||
|
easing.type: theme.standardEasing
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
to: 0.06
|
||||||
|
duration: theme.extraLongDuration
|
||||||
|
easing.type: theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: theme.spacingL
|
||||||
|
anchors.rightMargin: theme.spacingL
|
||||||
|
|
||||||
|
// Left section - Apps and Workspace Switcher
|
||||||
|
Row {
|
||||||
|
id: leftSection
|
||||||
|
height: parent.height
|
||||||
|
spacing: theme.spacingL
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
AppLauncherButton {
|
||||||
|
theme: topBar.theme
|
||||||
|
root: topBar.root
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceSwitcher {
|
||||||
|
theme: topBar.theme
|
||||||
|
root: topBar.root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center section - Clock/Media Player
|
||||||
|
ClockWidget {
|
||||||
|
id: clockWidget
|
||||||
|
theme: topBar.theme
|
||||||
|
root: topBar.root
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right section - System controls
|
||||||
|
Row {
|
||||||
|
id: rightSection
|
||||||
|
height: parent.height
|
||||||
|
spacing: theme.spacingXS
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
SystemTrayWidget {
|
||||||
|
theme: topBar.theme
|
||||||
|
root: topBar.root
|
||||||
|
}
|
||||||
|
|
||||||
|
ClipboardButton {
|
||||||
|
theme: topBar.theme
|
||||||
|
root: topBar.root
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorPickerButton {
|
||||||
|
theme: topBar.theme
|
||||||
|
root: topBar.root
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationButton {
|
||||||
|
theme: topBar.theme
|
||||||
|
root: topBar.root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
Widgets/TopBarSimple.qml
Normal file
111
Widgets/TopBarSimple.qml
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Services.SystemTray
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: topBar
|
||||||
|
|
||||||
|
property var theme
|
||||||
|
property var root
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: theme.barHeight
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(theme.surfaceContainer.r, theme.surfaceContainer.g, theme.surfaceContainer.b, 0.95)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "transparent"
|
||||||
|
border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.12)
|
||||||
|
border.width: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(theme.surfaceTint.r, theme.surfaceTint.g, theme.surfaceTint.b, 0.08)
|
||||||
|
|
||||||
|
SequentialAnimation on opacity {
|
||||||
|
running: true
|
||||||
|
loops: Animation.Infinite
|
||||||
|
NumberAnimation {
|
||||||
|
to: 0.12
|
||||||
|
duration: theme.extraLongDuration
|
||||||
|
easing.type: theme.standardEasing
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
to: 0.06
|
||||||
|
duration: theme.extraLongDuration
|
||||||
|
easing.type: theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: theme.spacingL
|
||||||
|
anchors.rightMargin: theme.spacingL
|
||||||
|
|
||||||
|
// Left section - Apps and Workspace Switcher
|
||||||
|
Row {
|
||||||
|
id: leftSection
|
||||||
|
height: parent.height
|
||||||
|
spacing: theme.spacingL
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
AppLauncherButton {
|
||||||
|
theme: topBar.theme
|
||||||
|
root: topBar.root
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceSwitcher {
|
||||||
|
theme: topBar.theme
|
||||||
|
root: topBar.root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center section - Clock/Media Player
|
||||||
|
ClockWidget {
|
||||||
|
id: clockWidget
|
||||||
|
theme: topBar.theme
|
||||||
|
root: topBar.root
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right section - System controls
|
||||||
|
Row {
|
||||||
|
id: rightSection
|
||||||
|
height: parent.height
|
||||||
|
spacing: theme.spacingXS
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
ClipboardButton {
|
||||||
|
theme: topBar.theme
|
||||||
|
root: topBar.root
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorPickerButton {
|
||||||
|
theme: topBar.theme
|
||||||
|
root: topBar.root
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationButton {
|
||||||
|
theme: topBar.theme
|
||||||
|
root: topBar.root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
145
Widgets/WorkspaceSwitcher.qml
Normal file
145
Widgets/WorkspaceSwitcher.qml
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: workspaceSwitcher
|
||||||
|
|
||||||
|
property var theme
|
||||||
|
property var root
|
||||||
|
|
||||||
|
width: Math.max(120, workspaceRow.implicitWidth + theme.spacingL * 2)
|
||||||
|
height: 32
|
||||||
|
radius: theme.cornerRadiusLarge
|
||||||
|
color: Qt.rgba(theme.surfaceContainerHigh.r, theme.surfaceContainerHigh.g, theme.surfaceContainerHigh.b, 0.8)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
property int currentWorkspace: 1
|
||||||
|
property var workspaceList: []
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: workspaceQuery
|
||||||
|
command: ["niri", "msg", "workspaces"]
|
||||||
|
running: true
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: "\n"
|
||||||
|
onRead: (data) => {
|
||||||
|
if (data.trim()) {
|
||||||
|
workspaceSwitcher.parseWorkspaceOutput(data.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWorkspaceOutput(data) {
|
||||||
|
const lines = data.split('\n')
|
||||||
|
let currentOutputName = ""
|
||||||
|
let focusedOutput = ""
|
||||||
|
let focusedWorkspace = 1
|
||||||
|
let outputWorkspaces = {}
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('Output "')) {
|
||||||
|
const outputMatch = line.match(/Output "(.+)"/)
|
||||||
|
if (outputMatch) {
|
||||||
|
currentOutputName = outputMatch[1]
|
||||||
|
outputWorkspaces[currentOutputName] = []
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.trim() && line.match(/^\s*\*?\s*(\d+)$/)) {
|
||||||
|
const wsMatch = line.match(/^\s*(\*?)\s*(\d+)$/)
|
||||||
|
if (wsMatch) {
|
||||||
|
const isActive = wsMatch[1] === '*'
|
||||||
|
const wsNum = parseInt(wsMatch[2])
|
||||||
|
|
||||||
|
if (currentOutputName && outputWorkspaces[currentOutputName]) {
|
||||||
|
outputWorkspaces[currentOutputName].push(wsNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
focusedOutput = currentOutputName
|
||||||
|
focusedWorkspace = wsNum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWorkspace = focusedWorkspace
|
||||||
|
|
||||||
|
if (focusedOutput && outputWorkspaces[focusedOutput]) {
|
||||||
|
workspaceList = outputWorkspaces[focusedOutput]
|
||||||
|
} else {
|
||||||
|
workspaceList = [1, 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 500
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
workspaceQuery.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: workspaceRow
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: workspaceSwitcher.workspaceList
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property bool isActive: modelData === workspaceSwitcher.currentWorkspace
|
||||||
|
property bool isHovered: mouseArea.containsMouse
|
||||||
|
|
||||||
|
width: isActive ? theme.spacingXL + theme.spacingS : theme.spacingL
|
||||||
|
height: theme.spacingS
|
||||||
|
radius: height / 2
|
||||||
|
color: isActive ? theme.primary :
|
||||||
|
isHovered ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.5) :
|
||||||
|
Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.3)
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: theme.mediumDuration
|
||||||
|
easing.type: theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: theme.mediumDuration
|
||||||
|
easing.type: theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
switchProcess.command = ["niri", "msg", "action", "focus-workspace", modelData.toString()]
|
||||||
|
switchProcess.running = true
|
||||||
|
workspaceSwitcher.currentWorkspace = modelData
|
||||||
|
Qt.callLater(() => {
|
||||||
|
workspaceQuery.running = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: switchProcess
|
||||||
|
running: false
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Widgets/qmldir
Normal file
9
Widgets/qmldir
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
TopBar 1.0 TopBar.qml
|
||||||
|
TopBarSimple 1.0 TopBarSimple.qml
|
||||||
|
AppLauncherButton 1.0 AppLauncherButton.qml
|
||||||
|
WorkspaceSwitcher 1.0 WorkspaceSwitcher.qml
|
||||||
|
ClockWidget 1.0 ClockWidget.qml
|
||||||
|
SystemTrayWidget 1.0 SystemTrayWidget.qml
|
||||||
|
ClipboardButton 1.0 ClipboardButton.qml
|
||||||
|
ColorPickerButton 1.0 ColorPickerButton.qml
|
||||||
|
NotificationButton 1.0 NotificationButton.qml
|
||||||
274
shell-refactored.qml
Normal file
274
shell-refactored.qml
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
//@ pragma UseQApplication
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Services.SystemTray
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import "Services"
|
||||||
|
import "Widgets"
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool calendarVisible: false
|
||||||
|
property bool showTrayMenu: false
|
||||||
|
property real trayMenuX: 0
|
||||||
|
property real trayMenuY: 0
|
||||||
|
property var currentTrayMenu: null
|
||||||
|
property var currentTrayItem: null
|
||||||
|
property bool notificationHistoryVisible: false
|
||||||
|
property var activeNotification: null
|
||||||
|
property bool showNotificationPopup: false
|
||||||
|
property bool mediaPlayerVisible: false
|
||||||
|
property MprisPlayer activePlayer: MprisController.activePlayer
|
||||||
|
property bool hasActiveMedia: MprisController.isPlaying && (activePlayer?.trackTitle || activePlayer?.trackArtist)
|
||||||
|
|
||||||
|
property bool useFahrenheit: true
|
||||||
|
property var weather: WeatherService.weather
|
||||||
|
property string osLogo: OSDetectionService.osLogo
|
||||||
|
property string osName: OSDetectionService.osName
|
||||||
|
|
||||||
|
property var notificationHistory: notificationHistoryModel
|
||||||
|
property var appLauncher: appLauncherPopup
|
||||||
|
property var clipboardHistoryPopup: clipboardHistoryPopupInstance
|
||||||
|
property var colorPickerProcess: colorPickerProcessInstance
|
||||||
|
|
||||||
|
property var weatherIcons: ({
|
||||||
|
"113": "clear_day",
|
||||||
|
"116": "partly_cloudy_day",
|
||||||
|
"119": "cloud",
|
||||||
|
"122": "cloud",
|
||||||
|
"143": "foggy",
|
||||||
|
"176": "rainy",
|
||||||
|
"179": "rainy",
|
||||||
|
"182": "rainy",
|
||||||
|
"185": "rainy",
|
||||||
|
"200": "thunderstorm",
|
||||||
|
"227": "cloudy_snowing",
|
||||||
|
"230": "snowing_heavy",
|
||||||
|
"248": "foggy",
|
||||||
|
"260": "foggy",
|
||||||
|
"263": "rainy",
|
||||||
|
"266": "rainy",
|
||||||
|
"281": "rainy",
|
||||||
|
"284": "rainy",
|
||||||
|
"293": "rainy",
|
||||||
|
"296": "rainy",
|
||||||
|
"299": "rainy",
|
||||||
|
"302": "weather_hail",
|
||||||
|
"305": "rainy",
|
||||||
|
"308": "weather_hail",
|
||||||
|
"311": "rainy",
|
||||||
|
"314": "rainy",
|
||||||
|
"317": "rainy",
|
||||||
|
"320": "cloudy_snowing",
|
||||||
|
"323": "cloudy_snowing",
|
||||||
|
"326": "cloudy_snowing",
|
||||||
|
"329": "snowing_heavy",
|
||||||
|
"332": "snowing_heavy",
|
||||||
|
"335": "snowing",
|
||||||
|
"338": "snowing_heavy",
|
||||||
|
"350": "rainy",
|
||||||
|
"353": "rainy",
|
||||||
|
"356": "rainy",
|
||||||
|
"359": "weather_hail",
|
||||||
|
"362": "rainy",
|
||||||
|
"365": "rainy",
|
||||||
|
"368": "cloudy_snowing",
|
||||||
|
"371": "snowing",
|
||||||
|
"374": "rainy",
|
||||||
|
"377": "rainy",
|
||||||
|
"386": "thunderstorm",
|
||||||
|
"389": "thunderstorm",
|
||||||
|
"392": "thunderstorm",
|
||||||
|
"395": "snowing"
|
||||||
|
})
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: theme
|
||||||
|
|
||||||
|
property color primary: "#D0BCFF"
|
||||||
|
property color primaryText: "#381E72"
|
||||||
|
property color primaryContainer: "#4F378B"
|
||||||
|
property color secondary: "#CCC2DC"
|
||||||
|
property color surface: "#10121E"
|
||||||
|
property color surfaceText: "#E6E0E9"
|
||||||
|
property color surfaceVariant: "#49454F"
|
||||||
|
property color surfaceVariantText: "#CAC4D0"
|
||||||
|
property color surfaceTint: "#D0BCFF"
|
||||||
|
property color background: "#10121E"
|
||||||
|
property color backgroundText: "#E6E0E9"
|
||||||
|
property color outline: "#938F99"
|
||||||
|
property color surfaceContainer: "#1D1B20"
|
||||||
|
property color surfaceContainerHigh: "#2B2930"
|
||||||
|
property color archBlue: "#1793D1"
|
||||||
|
property color success: "#4CAF50"
|
||||||
|
property color warning: "#FF9800"
|
||||||
|
property color info: "#2196F3"
|
||||||
|
property color error: "#F2B8B5"
|
||||||
|
|
||||||
|
property int shortDuration: 150
|
||||||
|
property int mediumDuration: 300
|
||||||
|
property int longDuration: 500
|
||||||
|
property int extraLongDuration: 1000
|
||||||
|
|
||||||
|
property int standardEasing: Easing.OutCubic
|
||||||
|
property int emphasizedEasing: Easing.OutQuart
|
||||||
|
|
||||||
|
property real cornerRadius: 12
|
||||||
|
property real cornerRadiusSmall: 8
|
||||||
|
property real cornerRadiusLarge: 16
|
||||||
|
property real cornerRadiusXLarge: 24
|
||||||
|
|
||||||
|
property real spacingXS: 4
|
||||||
|
property real spacingS: 8
|
||||||
|
property real spacingM: 12
|
||||||
|
property real spacingL: 16
|
||||||
|
property real spacingXL: 24
|
||||||
|
|
||||||
|
property real fontSizeSmall: 12
|
||||||
|
property real fontSizeMedium: 14
|
||||||
|
property real fontSizeLarge: 16
|
||||||
|
property real fontSizeXLarge: 20
|
||||||
|
|
||||||
|
property real barHeight: 48
|
||||||
|
property real iconSize: 24
|
||||||
|
property real iconSizeSmall: 16
|
||||||
|
property real iconSizeLarge: 32
|
||||||
|
|
||||||
|
property real opacityDisabled: 0.38
|
||||||
|
property real opacityMedium: 0.60
|
||||||
|
property real opacityHigh: 0.87
|
||||||
|
property real opacityFull: 1.0
|
||||||
|
|
||||||
|
property string iconFont: "Material Symbols Rounded"
|
||||||
|
property string iconFontFilled: "Material Symbols Rounded"
|
||||||
|
property int iconFontWeight: Font.Normal
|
||||||
|
property int iconFontFilledWeight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
TopBar {
|
||||||
|
id: topBar
|
||||||
|
theme: root.theme
|
||||||
|
root: root
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLauncher {
|
||||||
|
id: appLauncherPopup
|
||||||
|
theme: root.theme
|
||||||
|
}
|
||||||
|
|
||||||
|
ClipboardHistory {
|
||||||
|
id: clipboardHistoryPopupInstance
|
||||||
|
theme: root.theme
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaPlayer {
|
||||||
|
id: mediaPlayer
|
||||||
|
theme: root.theme
|
||||||
|
isVisible: root.mediaPlayerVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: colorPickerProcessInstance
|
||||||
|
command: ["hyprpicker", "-a"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: (exitCode) => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
console.warn("Color picker failed. Make sure hyprpicker is installed: yay -S hyprpicker")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationServer {
|
||||||
|
id: notificationServer
|
||||||
|
actionsSupported: true
|
||||||
|
bodyMarkupSupported: true
|
||||||
|
imageSupported: true
|
||||||
|
keepOnReload: false
|
||||||
|
persistenceSupported: true
|
||||||
|
|
||||||
|
onNotification: (notification) => {
|
||||||
|
if (!notification || !notification.id) return
|
||||||
|
|
||||||
|
if (!notification.appName && !notification.summary && !notification.body) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("New notification from:", notification.appName || "Unknown", "Summary:", notification.summary || "No summary")
|
||||||
|
|
||||||
|
var notifObj = {
|
||||||
|
"id": notification.id,
|
||||||
|
"appName": notification.appName || "App",
|
||||||
|
"summary": notification.summary || "",
|
||||||
|
"body": notification.body || "",
|
||||||
|
"timestamp": new Date(),
|
||||||
|
"appIcon": notification.appIcon || notification.icon || "",
|
||||||
|
"icon": notification.icon || "",
|
||||||
|
"image": notification.image || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationHistoryModel.insert(0, notifObj)
|
||||||
|
|
||||||
|
while (notificationHistoryModel.count > 50) {
|
||||||
|
notificationHistoryModel.remove(notificationHistoryModel.count - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
root.activeNotification = notifObj
|
||||||
|
root.showNotificationPopup = true
|
||||||
|
notificationTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: notificationHistoryModel
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: notificationTimer
|
||||||
|
interval: 5000
|
||||||
|
repeat: false
|
||||||
|
onTriggered: hideNotificationPopup()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: clearNotificationTimer
|
||||||
|
interval: theme.mediumDuration + 50
|
||||||
|
repeat: false
|
||||||
|
onTriggered: root.activeNotification = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotificationPopup(notification) {
|
||||||
|
root.activeNotification = notification
|
||||||
|
root.showNotificationPopup = true
|
||||||
|
notificationTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideNotificationPopup() {
|
||||||
|
root.showNotificationPopup = false
|
||||||
|
notificationTimer.stop()
|
||||||
|
clearNotificationTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
running: root.activePlayer?.playbackState === MprisPlaybackState.Playing
|
||||||
|
interval: 1000
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
if (root.activePlayer) {
|
||||||
|
root.activePlayer.positionChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
console.log("DankMaterialDark shell loaded successfully!")
|
||||||
|
}
|
||||||
|
}
|
||||||
269
shell-test.qml
Normal file
269
shell-test.qml
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
//@ pragma UseQApplication
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Services.SystemTray
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import "Services"
|
||||||
|
import "Widgets"
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool calendarVisible: false
|
||||||
|
property bool showTrayMenu: false
|
||||||
|
property real trayMenuX: 0
|
||||||
|
property real trayMenuY: 0
|
||||||
|
property var currentTrayMenu: null
|
||||||
|
property var currentTrayItem: null
|
||||||
|
property bool notificationHistoryVisible: false
|
||||||
|
property var activeNotification: null
|
||||||
|
property bool showNotificationPopup: false
|
||||||
|
property bool mediaPlayerVisible: false
|
||||||
|
property MprisPlayer activePlayer: MprisController.activePlayer
|
||||||
|
property bool hasActiveMedia: MprisController.isPlaying && (activePlayer?.trackTitle || activePlayer?.trackArtist)
|
||||||
|
|
||||||
|
property bool useFahrenheit: true
|
||||||
|
property var weather: WeatherService.weather
|
||||||
|
property string osLogo: OSDetectionService.osLogo
|
||||||
|
property string osName: OSDetectionService.osName
|
||||||
|
|
||||||
|
property var notificationHistory: notificationHistoryModel
|
||||||
|
property var appLauncher: appLauncherPopup
|
||||||
|
property var clipboardHistoryPopup: clipboardHistoryPopupInstance
|
||||||
|
property var colorPickerProcess: colorPickerProcessInstance
|
||||||
|
|
||||||
|
property var weatherIcons: ({
|
||||||
|
"113": "clear_day",
|
||||||
|
"116": "partly_cloudy_day",
|
||||||
|
"119": "cloud",
|
||||||
|
"122": "cloud",
|
||||||
|
"143": "foggy",
|
||||||
|
"176": "rainy",
|
||||||
|
"179": "rainy",
|
||||||
|
"182": "rainy",
|
||||||
|
"185": "rainy",
|
||||||
|
"200": "thunderstorm",
|
||||||
|
"227": "cloudy_snowing",
|
||||||
|
"230": "snowing_heavy",
|
||||||
|
"248": "foggy",
|
||||||
|
"260": "foggy",
|
||||||
|
"263": "rainy",
|
||||||
|
"266": "rainy",
|
||||||
|
"281": "rainy",
|
||||||
|
"284": "rainy",
|
||||||
|
"293": "rainy",
|
||||||
|
"296": "rainy",
|
||||||
|
"299": "rainy",
|
||||||
|
"302": "weather_hail",
|
||||||
|
"305": "rainy",
|
||||||
|
"308": "weather_hail",
|
||||||
|
"311": "rainy",
|
||||||
|
"314": "rainy",
|
||||||
|
"317": "rainy",
|
||||||
|
"320": "cloudy_snowing",
|
||||||
|
"323": "cloudy_snowing",
|
||||||
|
"326": "cloudy_snowing",
|
||||||
|
"329": "snowing_heavy",
|
||||||
|
"332": "snowing_heavy",
|
||||||
|
"335": "snowing",
|
||||||
|
"338": "snowing_heavy",
|
||||||
|
"350": "rainy",
|
||||||
|
"353": "rainy",
|
||||||
|
"356": "rainy",
|
||||||
|
"359": "weather_hail",
|
||||||
|
"362": "rainy",
|
||||||
|
"365": "rainy",
|
||||||
|
"368": "cloudy_snowing",
|
||||||
|
"371": "snowing",
|
||||||
|
"374": "rainy",
|
||||||
|
"377": "rainy",
|
||||||
|
"386": "thunderstorm",
|
||||||
|
"389": "thunderstorm",
|
||||||
|
"392": "thunderstorm",
|
||||||
|
"395": "snowing"
|
||||||
|
})
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: theme
|
||||||
|
|
||||||
|
property color primary: "#D0BCFF"
|
||||||
|
property color primaryText: "#381E72"
|
||||||
|
property color primaryContainer: "#4F378B"
|
||||||
|
property color secondary: "#CCC2DC"
|
||||||
|
property color surface: "#10121E"
|
||||||
|
property color surfaceText: "#E6E0E9"
|
||||||
|
property color surfaceVariant: "#49454F"
|
||||||
|
property color surfaceVariantText: "#CAC4D0"
|
||||||
|
property color surfaceTint: "#D0BCFF"
|
||||||
|
property color background: "#10121E"
|
||||||
|
property color backgroundText: "#E6E0E9"
|
||||||
|
property color outline: "#938F99"
|
||||||
|
property color surfaceContainer: "#1D1B20"
|
||||||
|
property color surfaceContainerHigh: "#2B2930"
|
||||||
|
property color archBlue: "#1793D1"
|
||||||
|
property color success: "#4CAF50"
|
||||||
|
property color warning: "#FF9800"
|
||||||
|
property color info: "#2196F3"
|
||||||
|
property color error: "#F2B8B5"
|
||||||
|
|
||||||
|
property int shortDuration: 150
|
||||||
|
property int mediumDuration: 300
|
||||||
|
property int longDuration: 500
|
||||||
|
property int extraLongDuration: 1000
|
||||||
|
|
||||||
|
property int standardEasing: Easing.OutCubic
|
||||||
|
property int emphasizedEasing: Easing.OutQuart
|
||||||
|
|
||||||
|
property real cornerRadius: 12
|
||||||
|
property real cornerRadiusSmall: 8
|
||||||
|
property real cornerRadiusLarge: 16
|
||||||
|
property real cornerRadiusXLarge: 24
|
||||||
|
|
||||||
|
property real spacingXS: 4
|
||||||
|
property real spacingS: 8
|
||||||
|
property real spacingM: 12
|
||||||
|
property real spacingL: 16
|
||||||
|
property real spacingXL: 24
|
||||||
|
|
||||||
|
property real fontSizeSmall: 12
|
||||||
|
property real fontSizeMedium: 14
|
||||||
|
property real fontSizeLarge: 16
|
||||||
|
property real fontSizeXLarge: 20
|
||||||
|
|
||||||
|
property real barHeight: 48
|
||||||
|
property real iconSize: 24
|
||||||
|
property real iconSizeSmall: 16
|
||||||
|
property real iconSizeLarge: 32
|
||||||
|
|
||||||
|
property real opacityDisabled: 0.38
|
||||||
|
property real opacityMedium: 0.60
|
||||||
|
property real opacityHigh: 0.87
|
||||||
|
property real opacityFull: 1.0
|
||||||
|
|
||||||
|
property string iconFont: "Material Symbols Rounded"
|
||||||
|
property string iconFontFilled: "Material Symbols Rounded"
|
||||||
|
property int iconFontWeight: Font.Normal
|
||||||
|
property int iconFontFilledWeight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
TopBarSimple {
|
||||||
|
id: topBar
|
||||||
|
theme: root.theme
|
||||||
|
root: root
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLauncher {
|
||||||
|
id: appLauncherPopup
|
||||||
|
theme: root.theme
|
||||||
|
}
|
||||||
|
|
||||||
|
ClipboardHistory {
|
||||||
|
id: clipboardHistoryPopupInstance
|
||||||
|
theme: root.theme
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: colorPickerProcessInstance
|
||||||
|
command: ["hyprpicker", "-a"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: (exitCode) => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
console.warn("Color picker failed. Make sure hyprpicker is installed: yay -S hyprpicker")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationServer {
|
||||||
|
id: notificationServer
|
||||||
|
actionsSupported: true
|
||||||
|
bodyMarkupSupported: true
|
||||||
|
imageSupported: true
|
||||||
|
keepOnReload: false
|
||||||
|
persistenceSupported: true
|
||||||
|
|
||||||
|
onNotification: (notification) => {
|
||||||
|
if (!notification || !notification.id) return
|
||||||
|
|
||||||
|
if (!notification.appName && !notification.summary && !notification.body) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("New notification from:", notification.appName || "Unknown", "Summary:", notification.summary || "No summary")
|
||||||
|
|
||||||
|
var notifObj = {
|
||||||
|
"id": notification.id,
|
||||||
|
"appName": notification.appName || "App",
|
||||||
|
"summary": notification.summary || "",
|
||||||
|
"body": notification.body || "",
|
||||||
|
"timestamp": new Date(),
|
||||||
|
"appIcon": notification.appIcon || notification.icon || "",
|
||||||
|
"icon": notification.icon || "",
|
||||||
|
"image": notification.image || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationHistoryModel.insert(0, notifObj)
|
||||||
|
|
||||||
|
while (notificationHistoryModel.count > 50) {
|
||||||
|
notificationHistoryModel.remove(notificationHistoryModel.count - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
root.activeNotification = notifObj
|
||||||
|
root.showNotificationPopup = true
|
||||||
|
notificationTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: notificationHistoryModel
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: notificationTimer
|
||||||
|
interval: 5000
|
||||||
|
repeat: false
|
||||||
|
onTriggered: hideNotificationPopup()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: clearNotificationTimer
|
||||||
|
interval: theme.mediumDuration + 50
|
||||||
|
repeat: false
|
||||||
|
onTriggered: root.activeNotification = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotificationPopup(notification) {
|
||||||
|
root.activeNotification = notification
|
||||||
|
root.showNotificationPopup = true
|
||||||
|
notificationTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideNotificationPopup() {
|
||||||
|
root.showNotificationPopup = false
|
||||||
|
notificationTimer.stop()
|
||||||
|
clearNotificationTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
running: root.activePlayer?.playbackState === MprisPlaybackState.Playing
|
||||||
|
interval: 1000
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
if (root.activePlayer) {
|
||||||
|
root.activePlayer.positionChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
console.log("DankMaterialDark shell loaded successfully!")
|
||||||
|
}
|
||||||
|
}
|
||||||
2246
shell-working.qml
Normal file
2246
shell-working.qml
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user