mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
Modularlize the shell
This commit is contained in:
944
Widgets/AppLauncher.qml
Normal file
944
Widgets/AppLauncher.qml
Normal file
@@ -0,0 +1,944 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
import "../Common"
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
562
Widgets/CalendarPopup.qml
Normal file
562
Widgets/CalendarPopup.qml
Normal file
@@ -0,0 +1,562 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Mpris
|
||||
import "../Common"
|
||||
|
||||
PanelWindow {
|
||||
id: calendarPopup
|
||||
|
||||
visible: root.calendarVisible
|
||||
|
||||
implicitWidth: 320
|
||||
implicitHeight: 400
|
||||
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
property date displayDate: new Date()
|
||||
property date selectedDate: new Date()
|
||||
|
||||
Rectangle {
|
||||
width: 400
|
||||
height: root.hasActiveMedia ? 580 : (root.weather.available ? 480 : 400)
|
||||
x: (parent.width - width) / 2
|
||||
y: Theme.barHeight + Theme.spacingS
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 1
|
||||
|
||||
opacity: root.calendarVisible ? 1.0 : 0.0
|
||||
scale: root.calendarVisible ? 1.0 : 0.85
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Media Player (when active)
|
||||
Rectangle {
|
||||
visible: root.hasActiveMedia
|
||||
width: parent.width
|
||||
height: 180
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 100
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 100
|
||||
radius: Theme.cornerRadius
|
||||
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: "album"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 48
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - 100 - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: root.activePlayer?.trackTitle || "Unknown Track"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.activePlayer?.trackArtist || "Unknown Artist"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.activePlayer?.trackAlbum || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 6
|
||||
radius: 3
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
|
||||
Rectangle {
|
||||
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 ratio = mouse.x / width
|
||||
const newPosition = ratio * root.activePlayer.length
|
||||
console.log("Seeking to position:", newPosition, "ratio:", ratio, "canSeek:", root.activePlayer.canSeek)
|
||||
if (root.activePlayer.canSeek) {
|
||||
root.activePlayer.position = newPosition
|
||||
} else {
|
||||
console.log("Player does not support seeking")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Control buttons
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: 18
|
||||
color: prevBtnAreaCal.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: 20
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: prevBtnAreaCal
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.activePlayer?.previous()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: 20
|
||||
color: Theme.primary
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 24
|
||||
color: Theme.background
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.activePlayer?.togglePlaying()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: 18
|
||||
color: nextBtnAreaCal.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: 20
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nextBtnAreaCal
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.activePlayer?.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Weather header (when available and no media)
|
||||
Rectangle {
|
||||
visible: root.weather.available && !root.hasActiveMedia
|
||||
width: parent.width
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Weather icon and temp
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: root.weatherIcons[root.weather.wCode] || "clear_day"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize + 4
|
||||
color: Theme.primary
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: (root.useFahrenheit ? root.weather.tempF : root.weather.temp) + "°" + (root.useFahrenheit ? "F" : "C")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Bold
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.useFahrenheit = !root.useFahrenheit
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.weather.city
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Weather details grid
|
||||
Grid {
|
||||
columns: 2
|
||||
spacing: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
Text {
|
||||
text: "humidity_low"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
text: root.weather.humidity + "%"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
Text {
|
||||
text: "air"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
text: root.weather.wind
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
Text {
|
||||
text: "wb_twilight"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
text: root.weather.sunrise
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
Text {
|
||||
text: "bedtime"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
text: root.weather.sunset
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "chevron_left"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.primary
|
||||
font.weight: Theme.iconFontWeight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: prevMonthArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
let newDate = new Date(calendarPopup.displayDate)
|
||||
newDate.setMonth(newDate.getMonth() - 1)
|
||||
calendarPopup.displayDate = newDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width - 80
|
||||
height: 40
|
||||
text: Qt.formatDate(calendarPopup.displayDate, "MMMM yyyy")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "chevron_right"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.primary
|
||||
font.weight: Theme.iconFontWeight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nextMonthArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
let newDate = new Date(calendarPopup.displayDate)
|
||||
newDate.setMonth(newDate.getMonth() + 1)
|
||||
calendarPopup.displayDate = newDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 32
|
||||
|
||||
Repeater {
|
||||
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||
|
||||
Rectangle {
|
||||
width: parent.width / 7
|
||||
height: 32
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Grid {
|
||||
width: parent.width
|
||||
height: root.hasActiveMedia ? parent.height - 300 : (root.weather.available ? parent.height - 200 : parent.height - 120)
|
||||
columns: 7
|
||||
rows: 6
|
||||
|
||||
property date firstDay: {
|
||||
let date = new Date(calendarPopup.displayDate.getFullYear(), calendarPopup.displayDate.getMonth(), 1)
|
||||
let dayOfWeek = date.getDay()
|
||||
date.setDate(date.getDate() - dayOfWeek)
|
||||
return date
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: 42
|
||||
|
||||
Rectangle {
|
||||
width: parent.width / 7
|
||||
height: parent.height / 6
|
||||
|
||||
property date dayDate: {
|
||||
let date = new Date(parent.firstDay)
|
||||
date.setDate(date.getDate() + index)
|
||||
return date
|
||||
}
|
||||
|
||||
property bool isCurrentMonth: dayDate.getMonth() === calendarPopup.displayDate.getMonth()
|
||||
property bool isToday: dayDate.toDateString() === new Date().toDateString()
|
||||
property bool isSelected: dayDate.toDateString() === calendarPopup.selectedDate.toDateString()
|
||||
|
||||
color: isSelected ? Theme.primary :
|
||||
isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
|
||||
dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||
|
||||
radius: Theme.cornerRadiusSmall
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: dayDate.getDate()
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: isSelected ? Theme.surface :
|
||||
isToday ? Theme.primary :
|
||||
isCurrentMonth ? Theme.surfaceText :
|
||||
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
||||
font.weight: isToday || isSelected ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dayArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
calendarPopup.selectedDate = dayDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
onClicked: {
|
||||
root.calendarVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
626
Widgets/ClipboardHistory.qml
Normal file
626
Widgets/ClipboardHistory.qml
Normal file
@@ -0,0 +1,626 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
import "../Common"
|
||||
|
||||
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})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onStarted: {
|
||||
clipboardHistory.clipboardEntries = []
|
||||
clipboardModel.clear()
|
||||
console.log("ClipboardHistory: Starting cliphist process...")
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
1366
Widgets/ControlCenterPopup.qml
Normal file
1366
Widgets/ControlCenterPopup.qml
Normal file
File diff suppressed because it is too large
Load Diff
150
Widgets/CustomSlider.qml
Normal file
150
Widgets/CustomSlider.qml
Normal file
@@ -0,0 +1,150 @@
|
||||
import QtQuick
|
||||
import "../Common"
|
||||
|
||||
Item {
|
||||
id: slider
|
||||
|
||||
property int value: 50
|
||||
property int minimum: 0
|
||||
property int maximum: 100
|
||||
property string leftIcon: ""
|
||||
property string rightIcon: ""
|
||||
property bool enabled: true
|
||||
property string unit: "%"
|
||||
property bool showValue: true
|
||||
|
||||
signal sliderValueChanged(int newValue)
|
||||
|
||||
height: 80
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Value display
|
||||
Text {
|
||||
text: slider.value + slider.unit
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: slider.enabled ? Theme.surfaceText : Theme.surfaceVariantText
|
||||
font.weight: Font.Medium
|
||||
visible: slider.showValue
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
// Slider row
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Left icon
|
||||
Text {
|
||||
text: slider.leftIcon
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: slider.enabled ? Theme.surfaceText : Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: slider.leftIcon.length > 0
|
||||
}
|
||||
|
||||
// Slider track
|
||||
Rectangle {
|
||||
id: sliderTrack
|
||||
width: parent.width - (leftIconWidth + rightIconWidth + (slider.leftIcon.length > 0 ? Theme.spacingM : 0) + (slider.rightIcon.length > 0 ? Theme.spacingM : 0))
|
||||
height: 6
|
||||
radius: 3
|
||||
color: slider.enabled ?
|
||||
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) :
|
||||
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
property int leftIconWidth: slider.leftIcon.length > 0 ? Theme.iconSize : 0
|
||||
property int rightIconWidth: slider.rightIcon.length > 0 ? Theme.iconSize : 0
|
||||
|
||||
// Fill
|
||||
Rectangle {
|
||||
id: sliderFill
|
||||
width: parent.width * ((slider.value - slider.minimum) / (slider.maximum - slider.minimum))
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: slider.enabled ? Theme.primary : Theme.surfaceVariantText
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
// Draggable handle
|
||||
Rectangle {
|
||||
id: sliderHandle
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
color: slider.enabled ? Theme.primary : Theme.surfaceVariantText
|
||||
border.color: slider.enabled ? Qt.lighter(Theme.primary, 1.3) : Qt.lighter(Theme.surfaceVariantText, 1.3)
|
||||
border.width: 2
|
||||
|
||||
x: Math.max(0, Math.min(parent.width - width, sliderFill.width - width/2))
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
scale: sliderMouseArea.containsMouse || sliderMouseArea.pressed ? 1.2 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150 }
|
||||
}
|
||||
|
||||
// Handle glow effect when active
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width + 4
|
||||
height: parent.height + 4
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||
border.width: 2
|
||||
visible: sliderMouseArea.containsMouse && slider.enabled
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 150 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: sliderMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: slider.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: slider.enabled
|
||||
|
||||
onClicked: (mouse) => {
|
||||
if (slider.enabled) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
|
||||
slider.value = newValue
|
||||
slider.sliderValueChanged(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed && slider.enabled) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
|
||||
slider.value = newValue
|
||||
slider.sliderValueChanged(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Right icon
|
||||
Text {
|
||||
text: slider.rightIcon
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: slider.enabled ? Theme.surfaceText : Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: slider.rightIcon.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
354
Widgets/NotificationHistoryPopup.qml
Normal file
354
Widgets/NotificationHistoryPopup.qml
Normal file
@@ -0,0 +1,354 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
import "../Common"
|
||||
|
||||
PanelWindow {
|
||||
id: notificationHistoryPopup
|
||||
|
||||
visible: root.notificationHistoryVisible
|
||||
|
||||
implicitWidth: 400
|
||||
implicitHeight: 500
|
||||
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 400
|
||||
height: 500
|
||||
x: parent.width - width - Theme.spacingL
|
||||
y: Theme.barHeight + Theme.spacingS
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 1
|
||||
|
||||
opacity: root.notificationHistoryVisible ? 1.0 : 0.0
|
||||
scale: root.notificationHistoryVisible ? 1.0 : 0.85
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Header
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 32
|
||||
|
||||
Text {
|
||||
text: "Notifications"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item { width: parent.width - 200; height: 1 }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: clearArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.16) : Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
||||
border.color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.5)
|
||||
border.width: 1
|
||||
visible: notificationHistory.count > 0
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "delete_sweep"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSizeSmall + 2
|
||||
color: Theme.error
|
||||
font.weight: Theme.iconFontWeight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Clear All Notifications"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.error
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clearArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
notificationHistory.clear()
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notification List
|
||||
ScrollView {
|
||||
width: parent.width
|
||||
height: parent.height - 120
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
id: notificationListView
|
||||
model: notificationHistory
|
||||
spacing: Theme.spacingS
|
||||
|
||||
delegate: Rectangle {
|
||||
width: notificationListView.width
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: notifArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Notification icon using reference pattern
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Fallback material icon when no app icon
|
||||
Loader {
|
||||
active: !model.appIcon || model.appIcon === ""
|
||||
anchors.fill: parent
|
||||
sourceComponent: Text {
|
||||
anchors.centerIn: parent
|
||||
text: model.appName ? model.appName.charAt(0).toUpperCase() : "notifications"
|
||||
font.family: model.appName ? "Roboto" : Theme.iconFont
|
||||
font.pixelSize: model.appName ? Theme.fontSizeMedium : 16
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// App icon when no notification image
|
||||
Loader {
|
||||
active: model.appIcon && model.appIcon !== "" && (!model.image || model.image === "")
|
||||
anchors.fill: parent
|
||||
anchors.margins: 3
|
||||
sourceComponent: IconImage {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
asynchronous: true
|
||||
source: {
|
||||
if (!model.appIcon) return ""
|
||||
// Skip file:// URLs as they're usually screenshots/images, not icons
|
||||
if (model.appIcon.startsWith("file://")) return ""
|
||||
return Quickshell.iconPath(model.appIcon, "image-missing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notification image with rounded corners
|
||||
Loader {
|
||||
active: model.image && model.image !== ""
|
||||
anchors.fill: parent
|
||||
sourceComponent: Item {
|
||||
anchors.fill: parent
|
||||
Image {
|
||||
id: historyNotifImage
|
||||
anchors.fill: parent
|
||||
source: model.image || ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: false
|
||||
antialiasing: true
|
||||
asynchronous: true
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: historyNotifImage.width
|
||||
height: historyNotifImage.height
|
||||
radius: Theme.cornerRadius
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Small app icon overlay when showing notification image
|
||||
Loader {
|
||||
active: model.appIcon && model.appIcon !== ""
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 2
|
||||
sourceComponent: IconImage {
|
||||
width: 12
|
||||
height: 12
|
||||
asynchronous: true
|
||||
source: model.appIcon ? Quickshell.iconPath(model.appIcon, "image-missing") : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 80
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: model.appName || "App"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: model.summary || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
Text {
|
||||
text: model.body || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: notifArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
notificationHistory.remove(index)
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state - properly centered
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: notificationHistory.count === 0
|
||||
color: "transparent"
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
width: parent.width * 0.8
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "notifications_none"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSizeLarge + 16
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
|
||||
font.weight: Theme.iconFontWeight
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "No notifications"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
font.weight: Font.Medium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Notifications will appear here"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Click outside to close
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
onClicked: {
|
||||
root.notificationHistoryVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
203
Widgets/NotificationPopup.qml
Normal file
203
Widgets/NotificationPopup.qml
Normal file
@@ -0,0 +1,203 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
import "../Common"
|
||||
|
||||
PanelWindow {
|
||||
id: notificationPopup
|
||||
|
||||
visible: root.showNotificationPopup && root.activeNotification
|
||||
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
implicitWidth: 400
|
||||
|
||||
Rectangle {
|
||||
id: popupContainer
|
||||
width: 380
|
||||
height: 100
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Theme.barHeight + 16
|
||||
anchors.rightMargin: 16
|
||||
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 1
|
||||
|
||||
opacity: root.showNotificationPopup ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.OutQuad }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: Utils.hideNotificationPopup()
|
||||
}
|
||||
|
||||
// Close button with cursor pointer
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 8
|
||||
text: "×"
|
||||
font.pixelSize: 16
|
||||
color: Theme.surfaceText
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -4
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: Utils.hideNotificationPopup()
|
||||
}
|
||||
}
|
||||
|
||||
// Content layout
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
anchors.rightMargin: 32
|
||||
spacing: 12
|
||||
|
||||
// Notification icon using reference pattern
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: 8
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Fallback material icon when no app icon
|
||||
Loader {
|
||||
active: !root.activeNotification || root.activeNotification.appIcon === ""
|
||||
anchors.fill: parent
|
||||
sourceComponent: Text {
|
||||
anchors.centerIn: parent
|
||||
text: "notifications"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 20
|
||||
color: Theme.primary
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// App icon when no notification image
|
||||
Loader {
|
||||
active: root.activeNotification && root.activeNotification.appIcon !== "" && (root.activeNotification.image === "" || !root.activeNotification.image)
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
sourceComponent: IconImage {
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
source: {
|
||||
if (!root.activeNotification) return ""
|
||||
let iconPath = root.activeNotification.appIcon
|
||||
// Skip file:// URLs as they're usually screenshots/images, not icons
|
||||
if (iconPath && iconPath.startsWith("file://")) return ""
|
||||
return iconPath ? Quickshell.iconPath(iconPath, "image-missing") : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notification image with rounded corners
|
||||
Loader {
|
||||
active: root.activeNotification && root.activeNotification.image !== ""
|
||||
anchors.fill: parent
|
||||
sourceComponent: Item {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 8
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
Image {
|
||||
id: notifImage
|
||||
anchors.fill: parent
|
||||
source: root.activeNotification ? root.activeNotification.image : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: false
|
||||
antialiasing: true
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
|
||||
// Ensure minimum size and proper scaling
|
||||
sourceSize.width: 64
|
||||
sourceSize.height: 64
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error) {
|
||||
console.warn("Failed to load notification image:", source)
|
||||
} else if (status === Image.Ready) {
|
||||
console.log("Notification image loaded:", source, "size:", sourceSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Small app icon overlay when showing notification image
|
||||
Loader {
|
||||
active: root.activeNotification && root.activeNotification.appIcon !== ""
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 2
|
||||
sourceComponent: IconImage {
|
||||
width: 16
|
||||
height: 16
|
||||
asynchronous: true
|
||||
source: root.activeNotification ? Quickshell.iconPath(root.activeNotification.appIcon, "image-missing") : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Text content
|
||||
Column {
|
||||
width: parent.width - 52
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: root.activeNotification ? (root.activeNotification.summary || "") : ""
|
||||
font.pixelSize: 14
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.activeNotification ? (root.activeNotification.body || "") : ""
|
||||
font.pixelSize: 12
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,25 @@
|
||||
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 "../Common"
|
||||
import "../Services"
|
||||
|
||||
PanelWindow {
|
||||
id: topBar
|
||||
|
||||
property var theme
|
||||
property var root
|
||||
// modelData contains the screen from Quickshell.screens
|
||||
property var modelData
|
||||
screen: modelData
|
||||
|
||||
// Get the screen name (e.g., "DP-1", "DP-2")
|
||||
property string screenName: modelData.name
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
@@ -18,100 +27,779 @@ PanelWindow {
|
||||
right: true
|
||||
}
|
||||
|
||||
implicitHeight: theme.barHeight
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(theme.surfaceTint.r, theme.surfaceTint.g, theme.surfaceTint.b, 0.08)
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: true
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 0.12
|
||||
duration: theme.extraLongDuration
|
||||
easing.type: theme.standardEasing
|
||||
Row {
|
||||
id: leftSection
|
||||
height: parent.height
|
||||
spacing: Theme.spacingL
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: archLauncher
|
||||
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" // Use OS logo if detected, fallback to apps icon
|
||||
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: root.isSmallScreen ? "Apps" : "Applications"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
visible: !root.isSmallScreen || width > 60
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: launcherArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
appLauncher.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0.06
|
||||
duration: theme.extraLongDuration
|
||||
easing.type: theme.standardEasing
|
||||
|
||||
Rectangle {
|
||||
id: workspaceSwitcher
|
||||
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: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text && text.trim()) {
|
||||
workspaceSwitcher.parseWorkspaceOutput(text.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show workspaces for THIS screen only
|
||||
if (topBar.screenName && outputWorkspaces[topBar.screenName]) {
|
||||
workspaceList = outputWorkspaces[topBar.screenName]
|
||||
|
||||
// Always track the active workspace for this display
|
||||
// Parse all lines to find which workspace is active on this display
|
||||
let thisDisplayActiveWorkspace = 1
|
||||
let inThisOutput = false
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('Output "')) {
|
||||
const outputMatch = line.match(/Output "(.+)"/)
|
||||
inThisOutput = outputMatch && outputMatch[1] === topBar.screenName
|
||||
continue
|
||||
}
|
||||
|
||||
if (inThisOutput && line.trim() && line.match(/^\s*\*\s*(\d+)$/)) {
|
||||
const wsMatch = line.match(/^\s*\*\s*(\d+)$/)
|
||||
if (wsMatch) {
|
||||
thisDisplayActiveWorkspace = parseInt(wsMatch[1])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentWorkspace = thisDisplayActiveWorkspace
|
||||
// console.log("Monitor", topBar.screenName, "active workspace:", thisDisplayActiveWorkspace)
|
||||
} else {
|
||||
// Fallback if screen name not found
|
||||
workspaceList = [1, 2]
|
||||
currentWorkspace = 1
|
||||
}
|
||||
}
|
||||
|
||||
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: {
|
||||
// Set target workspace and focus monitor first
|
||||
console.log("Clicking workspace", modelData, "on monitor", topBar.screenName)
|
||||
workspaceSwitcher.targetWorkspace = modelData
|
||||
focusMonitorProcess.command = ["niri", "msg", "action", "focus-monitor", topBar.screenName]
|
||||
focusMonitorProcess.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: switchProcess
|
||||
running: false
|
||||
|
||||
onExited: {
|
||||
// Update current workspace and refresh query
|
||||
workspaceSwitcher.currentWorkspace = workspaceSwitcher.targetWorkspace
|
||||
Qt.callLater(() => {
|
||||
workspaceQuery.running = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: focusMonitorProcess
|
||||
running: false
|
||||
|
||||
onExited: {
|
||||
// After focusing the monitor, switch to the workspace
|
||||
Qt.callLater(() => {
|
||||
switchProcess.command = ["niri", "msg", "action", "focus-workspace", workspaceSwitcher.targetWorkspace.toString()]
|
||||
switchProcess.running = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
property int targetWorkspace: 1
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: clockContainer
|
||||
width: {
|
||||
let baseWidth = 200
|
||||
if (root.hasActiveMedia) {
|
||||
// Calculate width needed for media info + time/date + spacing + padding
|
||||
let mediaWidth = 24 + Theme.spacingXS + mediaTitleText.implicitWidth + Theme.spacingM + 180
|
||||
return Math.min(Math.max(mediaWidth, 300), parent.width - Theme.spacingL * 2)
|
||||
} else if (root.weather.available) {
|
||||
return Math.min(280, parent.width - Theme.spacingL * 2)
|
||||
} else {
|
||||
return Math.min(baseWidth, parent.width - Theme.spacingL * 2)
|
||||
}
|
||||
}
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: clockMouseArea.containsMouse ?
|
||||
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)
|
||||
anchors.centerIn: parent
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
property date currentDate: new Date()
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Media info or Weather info
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
visible: root.hasActiveMedia || root.weather.available
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Music icon when media is playing
|
||||
Text {
|
||||
text: "music_note"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 2
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.hasActiveMedia
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: root.activePlayer?.playbackState === MprisPlaybackState.Playing
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { to: 1.1; duration: 500 }
|
||||
NumberAnimation { to: 1.0; duration: 500 }
|
||||
}
|
||||
}
|
||||
|
||||
// Song title when media is playing
|
||||
Text {
|
||||
id: mediaTitleText
|
||||
text: root.activePlayer?.trackTitle || "Unknown Track"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.hasActiveMedia
|
||||
width: Math.min(implicitWidth, clockContainer.width - 100)
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Weather icon when no media but weather available
|
||||
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
|
||||
visible: !root.hasActiveMedia && root.weather.available
|
||||
}
|
||||
|
||||
// Weather temp when no media but weather available
|
||||
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
|
||||
visible: !root.hasActiveMedia && root.weather.available
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
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.hasActiveMedia || 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: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
root.calendarVisible = !root.calendarVisible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: rightSection
|
||||
height: parent.height
|
||||
spacing: Theme.spacingXS
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Material 3 styled menu
|
||||
QtObject {
|
||||
id: customTrayMenu
|
||||
|
||||
property bool menuVisible: false
|
||||
|
||||
function showMenu(x, y) {
|
||||
root.currentTrayMenu = customTrayMenu
|
||||
root.currentTrayItem = trayItem
|
||||
|
||||
// Simple positioning: right side of screen, below the panel
|
||||
root.trayMenuX = rightSection.x + rightSection.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clipboard History Button
|
||||
Rectangle {
|
||||
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" // Material icon for clipboard
|
||||
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: {
|
||||
clipboardHistoryPopup.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Color Picker Button
|
||||
Rectangle {
|
||||
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" // Material icon for color picker
|
||||
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: {
|
||||
ColorPickerService.pickColor()
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notification Center Button
|
||||
Rectangle {
|
||||
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: notificationHistory.count > 0
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "notifications" // Material icon for notifications
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 6
|
||||
font.weight: Theme.iconFontWeight
|
||||
color: notificationArea.containsMouse || root.notificationHistoryVisible ?
|
||||
Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
// Notification dot indicator
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Control Center Indicators
|
||||
Rectangle {
|
||||
width: Math.max(80, controlIndicators.implicitWidth + Theme.spacingS * 2)
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: controlCenterArea.containsMouse || root.controlCenterVisible ?
|
||||
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
|
||||
|
||||
Row {
|
||||
id: controlIndicators
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
// Network Status Icon
|
||||
Text {
|
||||
text: {
|
||||
if (root.networkStatus === "ethernet") return "lan"
|
||||
else if (root.networkStatus === "wifi") {
|
||||
switch (root.wifiSignalStrength) {
|
||||
case "excellent": return "wifi"
|
||||
case "good": return "wifi_2_bar"
|
||||
case "fair": return "wifi_1_bar"
|
||||
case "poor": return "wifi_calling_3"
|
||||
default: return "wifi"
|
||||
}
|
||||
}
|
||||
else return "wifi_off"
|
||||
}
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 8
|
||||
font.weight: Theme.iconFontWeight
|
||||
color: root.networkStatus !== "disconnected" ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: true
|
||||
}
|
||||
|
||||
// Audio Icon
|
||||
Text {
|
||||
text: root.volumeLevel === 0 ? "volume_off" :
|
||||
root.volumeLevel < 33 ? "volume_down" : "volume_up"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 8
|
||||
font.weight: Theme.iconFontWeight
|
||||
color: controlCenterArea.containsMouse || root.controlCenterVisible ?
|
||||
Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Microphone Icon (when active)
|
||||
Text {
|
||||
text: "mic"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 8
|
||||
font.weight: Theme.iconFontWeight
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: false // TODO: Add mic detection
|
||||
}
|
||||
|
||||
// Bluetooth Icon (when available and enabled)
|
||||
Text {
|
||||
text: "bluetooth"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 8
|
||||
font.weight: Theme.iconFontWeight
|
||||
color: root.bluetoothEnabled ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.bluetoothAvailable && root.bluetoothEnabled
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: controlCenterArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
root.controlCenterVisible = !root.controlCenterVisible
|
||||
if (root.controlCenterVisible) {
|
||||
// Refresh data when opening control center
|
||||
WifiService.scanWifi()
|
||||
BluetoothService.scanDevices()
|
||||
// Audio sink info is automatically refreshed by AudioService
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
154
Widgets/TrayMenuPopup.qml
Normal file
154
Widgets/TrayMenuPopup.qml
Normal file
@@ -0,0 +1,154 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
import "../Common"
|
||||
|
||||
PanelWindow {
|
||||
id: trayMenuPopup
|
||||
|
||||
visible: root.showTrayMenu
|
||||
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: menuContainer
|
||||
x: root.trayMenuX
|
||||
y: root.trayMenuY
|
||||
width: 180
|
||||
height: Math.max(60, menuList.contentHeight + Theme.spacingS * 2)
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 1
|
||||
|
||||
// Material 3 drop shadow
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: parent.z - 1
|
||||
}
|
||||
|
||||
// Material 3 animations
|
||||
opacity: root.showTrayMenu ? 1.0 : 0.0
|
||||
scale: root.showTrayMenu ? 1.0 : 0.85
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
|
||||
QsMenuOpener {
|
||||
id: menuOpener
|
||||
menu: root.currentTrayItem?.menu
|
||||
}
|
||||
|
||||
// Custom menu styling using ListView
|
||||
ListView {
|
||||
id: menuList
|
||||
anchors.fill: parent
|
||||
spacing: 1
|
||||
model: ScriptModel {
|
||||
values: menuOpener.children ? [...menuOpener.children.values].filter(item => {
|
||||
// Filter out empty items and separators
|
||||
return item && item.text && item.text.trim().length > 0 && !item.isSeparator
|
||||
}) : []
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: ListView.view.width
|
||||
height: modelData.isSeparator ? 5 : 28
|
||||
radius: modelData.isSeparator ? 0 : Theme.cornerRadiusSmall
|
||||
color: modelData.isSeparator ? "transparent" :
|
||||
(menuItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent")
|
||||
|
||||
// Separator line
|
||||
Rectangle {
|
||||
visible: modelData.isSeparator
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
|
||||
// Menu item content
|
||||
Row {
|
||||
visible: !modelData.isSeparator
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: modelData.text || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: menuItemArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: modelData.isSeparator ? Qt.ArrowCursor : Qt.PointingHandCursor
|
||||
enabled: !modelData.isSeparator
|
||||
|
||||
onClicked: {
|
||||
if (modelData.triggered) {
|
||||
modelData.triggered()
|
||||
}
|
||||
root.showTrayMenu = false
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Click outside to close
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
onClicked: {
|
||||
root.showTrayMenu = false
|
||||
}
|
||||
}
|
||||
}
|
||||
308
Widgets/WifiPasswordDialog.qml
Normal file
308
Widgets/WifiPasswordDialog.qml
Normal file
@@ -0,0 +1,308 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
import "../Common"
|
||||
import "../Services"
|
||||
|
||||
PanelWindow {
|
||||
id: wifiPasswordDialog
|
||||
|
||||
visible: root.wifiPasswordDialogVisible
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: root.wifiPasswordDialogVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||
|
||||
color: "transparent"
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.5)
|
||||
opacity: root.wifiPasswordDialogVisible ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
root.wifiPasswordDialogVisible = false
|
||||
root.wifiPasswordInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.min(400, parent.width - Theme.spacingL * 2)
|
||||
height: Math.min(250, parent.height - Theme.spacingL * 2)
|
||||
anchors.centerIn: parent
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 1
|
||||
|
||||
opacity: root.wifiPasswordDialogVisible ? 1.0 : 0.0
|
||||
scale: root.wifiPasswordDialogVisible ? 1.0 : 0.9
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Header
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Column {
|
||||
width: parent.width - 40
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: "Connect to Wi-Fi"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Enter password for \"" + root.wifiPasswordSSID + "\""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: closeDialogArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "close"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 4
|
||||
color: closeDialogArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeDialogArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.wifiPasswordDialogVisible = false
|
||||
root.wifiPasswordInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Password input
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
border.color: passwordInput.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: passwordInput.activeFocus ? 2 : 1
|
||||
|
||||
TextInput {
|
||||
id: passwordInput
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
cursorVisible: activeFocus
|
||||
selectByMouse: true
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
text: "Enter password"
|
||||
font: parent.font
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: parent.text.length === 0
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
root.wifiPasswordInput = text
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
WifiService.connectToWifiWithPassword(root.wifiPasswordSSID, root.wifiPasswordInput)
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.wifiPasswordDialogVisible) {
|
||||
forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.IBeamCursor
|
||||
onClicked: {
|
||||
passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show password checkbox
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
id: showPasswordCheckbox
|
||||
property bool checked: false
|
||||
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 4
|
||||
color: checked ? Theme.primary : "transparent"
|
||||
border.color: checked ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.5)
|
||||
border.width: 2
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "check"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 12
|
||||
color: Theme.background
|
||||
visible: parent.checked
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Show password"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
id: cancelText
|
||||
anchors.centerIn: parent
|
||||
text: "Cancel"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.wifiPasswordDialogVisible = false
|
||||
root.wifiPasswordInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
enabled: root.wifiPasswordInput.length > 0
|
||||
opacity: enabled ? 1.0 : 0.5
|
||||
|
||||
Text {
|
||||
id: connectText
|
||||
anchors.centerIn: parent
|
||||
text: "Connect"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: connectArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: {
|
||||
WifiService.connectToWifiWithPassword(root.wifiPasswordSSID, root.wifiPasswordInput)
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,13 @@ 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
|
||||
NotificationButton 1.0 NotificationButton.qml
|
||||
CalendarPopup 1.0 CalendarPopup.qml
|
||||
TrayMenuPopup 1.0 TrayMenuPopup.qml
|
||||
NotificationPopup 1.0 NotificationPopup.qml
|
||||
NotificationHistoryPopup 1.0 NotificationHistoryPopup.qml
|
||||
ControlCenterPopup 1.0 ControlCenterPopup.qml
|
||||
WifiPasswordDialog 1.0 WifiPasswordDialog.qml
|
||||
AppLauncher 1.0 AppLauncher.qml
|
||||
ClipboardHistory 1.0 ClipboardHistory.qml
|
||||
CustomSlider 1.0 CustomSlider.qml
|
||||
Reference in New Issue
Block a user