mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
iSome extra widgets and adjustments
This commit is contained in:
@@ -7,18 +7,19 @@ pragma ComponentBehavior: Bound
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Battery properties
|
||||
property bool batteryAvailable: false
|
||||
property int batteryLevel: 0
|
||||
property string batteryStatus: "Unknown" // "Charging", "Discharging", "Full", "Not charging", "Unknown"
|
||||
property int timeRemaining: 0 // minutes
|
||||
property string batteryStatus: "Unknown"
|
||||
property int timeRemaining: 0
|
||||
property bool isCharging: false
|
||||
property bool isLowBattery: false
|
||||
property int batteryHealth: 100 // percentage
|
||||
property int batteryHealth: 100
|
||||
property string batteryTechnology: "Unknown"
|
||||
property int cycleCount: 0
|
||||
property int batteryCapacity: 0 // mAh
|
||||
property int batteryCapacity: 0
|
||||
property var powerProfiles: []
|
||||
property string activePowerProfile: "balanced"
|
||||
property string activePowerProfile: ""
|
||||
|
||||
// Check if battery is available
|
||||
Process {
|
||||
@@ -238,12 +239,13 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Update battery status every 30 seconds
|
||||
|
||||
// Update timer
|
||||
Timer {
|
||||
interval: 30000
|
||||
running: root.batteryAvailable
|
||||
repeat: true
|
||||
triggeredOnStart: false
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
batteryStatusChecker.running = true
|
||||
powerProfilesChecker.running = true
|
||||
|
||||
@@ -145,7 +145,6 @@ Singleton {
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
console.log(`[MprisController] Players: ${Mpris.players.length}, Active: ${activePlayer?.identity || 'none'}, Playing: ${isPlaying}`)
|
||||
if (activePlayer) {
|
||||
console.log(` Track: ${activePlayer.trackTitle || 'Unknown'} by ${activePlayer.trackArtist || 'Unknown'}`)
|
||||
console.log(` State: ${activePlayer.playbackState}`)
|
||||
|
||||
@@ -79,38 +79,50 @@ Singleton {
|
||||
|
||||
Process {
|
||||
id: weatherFetcher
|
||||
command: ["bash", "-c", "curl -s 'wttr.in/?format=j1' | jq '{current: .current_condition[0], location: .nearest_area[0], astronomy: .weather[0].astronomy[0]}'"]
|
||||
command: ["bash", "-c", "curl -s 'wttr.in/?format=j1'"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim() && text.trim().startsWith("{")) {
|
||||
const raw = text.trim()
|
||||
if (!raw || raw[0] !== "{") {
|
||||
console.warn("No valid weather data received")
|
||||
root.weather.available = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
let parsedData = JSON.parse(text.trim())
|
||||
if (parsedData.current && parsedData.location) {
|
||||
const data = JSON.parse(raw)
|
||||
|
||||
const current = data.current_condition?.[0] || {}
|
||||
const location = data.nearest_area?.[0] || {}
|
||||
const astronomy = data.weather?.[0]?.astronomy?.[0] || {}
|
||||
|
||||
if (!Object.keys(current).length || !Object.keys(location).length) {
|
||||
throw new Error("Required fields missing")
|
||||
}
|
||||
|
||||
root.weather = {
|
||||
available: true,
|
||||
temp: parseInt(parsedData.current.temp_C || 0),
|
||||
tempF: parseInt(parsedData.current.temp_F || 0),
|
||||
city: parsedData.location.areaName[0]?.value || "Unknown",
|
||||
wCode: parsedData.current.weatherCode || "113",
|
||||
humidity: parseInt(parsedData.current.humidity || 0),
|
||||
wind: (parsedData.current.windspeedKmph || 0) + " km/h",
|
||||
sunrise: parsedData.astronomy?.sunrise || "06:00",
|
||||
sunset: parsedData.astronomy?.sunset || "18:00",
|
||||
uv: parseInt(parsedData.current.uvIndex || 0),
|
||||
pressure: parseInt(parsedData.current.pressure || 0)
|
||||
}
|
||||
console.log("Weather updated:", root.weather.city, root.weather.temp + "°C")
|
||||
temp: Number(current.temp_C) || 0,
|
||||
tempF: Number(current.temp_F) || 0,
|
||||
city: location.areaName?.[0]?.value || "Unknown",
|
||||
wCode: current.weatherCode || "113",
|
||||
humidity: Number(current.humidity) || 0,
|
||||
wind: `${current.windspeedKmph || 0} km/h`,
|
||||
sunrise: astronomy.sunrise || "06:00",
|
||||
sunset: astronomy.sunset || "18:00",
|
||||
uv: Number(current.uvIndex) || 0,
|
||||
pressure: Number(current.pressure) || 0
|
||||
}
|
||||
|
||||
console.log("Weather updated:", root.weather.city,
|
||||
`${root.weather.temp}°C`)
|
||||
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse weather data:", e.message)
|
||||
root.weather.available = false
|
||||
}
|
||||
} else {
|
||||
console.warn("No valid weather data received")
|
||||
root.weather.available = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
@@ -600,69 +601,6 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -715,32 +653,41 @@ PanelWindow {
|
||||
height: 56
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
IconImage {
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
source: Quickshell.iconPath(model.icon, "application-x-executable")
|
||||
sourceComponent: model.icon ? iconComponent : fallbackComponent
|
||||
|
||||
Component {
|
||||
id: iconComponent
|
||||
IconImage {
|
||||
source: model.icon ? Quickshell.iconPath(model.icon) : ""
|
||||
smooth: true
|
||||
visible: status === Image.Ready
|
||||
asynchronous: true
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error && model.name.includes("Avahi")) {
|
||||
console.log("Avahi icon failed to load:", model.icon, "->", source)
|
||||
if (status === Image.Error || status === Image.Null) {
|
||||
parent.sourceComponent = fallbackComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for missing icons
|
||||
Component {
|
||||
id: fallbackComponent
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !parent.children[0].visible
|
||||
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
|
||||
color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.1)
|
||||
radius: activeTheme.cornerRadiusLarge
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.2)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: model.name ? model.name.charAt(0).toUpperCase() : "A"
|
||||
font.pixelSize: activeTheme.iconSizeLarge
|
||||
color: activeTheme.surfaceVariantText
|
||||
font.weight: Font.Medium
|
||||
font.pixelSize: 28
|
||||
color: activeTheme.primary
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -801,8 +748,8 @@ PanelWindow {
|
||||
// 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
|
||||
leftMargin: Math.max(activeTheme.spacingS, remainingSpace / 2)
|
||||
rightMargin: leftMargin
|
||||
|
||||
model: filteredModel
|
||||
|
||||
@@ -832,32 +779,41 @@ PanelWindow {
|
||||
height: iconSize
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
IconImage {
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
source: Quickshell.iconPath(model.icon, "application-x-executable")
|
||||
sourceComponent: model.icon ? gridIconComponent : gridFallbackComponent
|
||||
|
||||
Component {
|
||||
id: gridIconComponent
|
||||
IconImage {
|
||||
source: model.icon ? Quickshell.iconPath(model.icon) : ""
|
||||
smooth: true
|
||||
visible: status === Image.Ready
|
||||
asynchronous: true
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error && model.name.includes("Avahi")) {
|
||||
console.log("Avahi grid icon failed to load:", model.icon, "->", source)
|
||||
if (status === Image.Error || status === Image.Null) {
|
||||
parent.sourceComponent = gridFallbackComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for missing icons
|
||||
Component {
|
||||
id: gridFallbackComponent
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !parent.children[0].visible
|
||||
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
|
||||
color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.1)
|
||||
radius: activeTheme.cornerRadiusLarge
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.2)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: model.name ? model.name.charAt(0).toUpperCase() : "A"
|
||||
font.pixelSize: activeTheme.iconSizeLarge
|
||||
color: activeTheme.surfaceVariantText
|
||||
font.weight: Font.Medium
|
||||
font.pixelSize: parent.parent.parent.iconSize / 2
|
||||
color: activeTheme.primary
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -892,6 +848,80 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Category dropdown overlay - now positioned absolutely
|
||||
Rectangle {
|
||||
id: categoryDropdown
|
||||
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: 1000
|
||||
|
||||
// Position it below the category button
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 140 + (searchField.text.length === 0 ? 0 : -40)
|
||||
anchors.left: parent.left
|
||||
|
||||
// Drop shadow
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -4
|
||||
color: "transparent"
|
||||
radius: parent.radius + 4
|
||||
z: -1
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: DropShadow {
|
||||
radius: 8
|
||||
samples: 16
|
||||
color: Qt.rgba(0, 0, 0, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,14 @@ PanelWindow {
|
||||
bottom: true
|
||||
}
|
||||
|
||||
// Click outside to dismiss overlay
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
root.batteryPopupVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.min(380, parent.width - Theme.spacingL * 2)
|
||||
height: Math.min(450, parent.height - Theme.barHeight - Theme.spacingS * 2)
|
||||
@@ -40,6 +48,14 @@ PanelWindow {
|
||||
opacity: root.batteryPopupVisible ? 1.0 : 0.0
|
||||
scale: root.batteryPopupVisible ? 1.0 : 0.85
|
||||
|
||||
// Prevent click-through to background
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
// Consume the click to prevent it from reaching the background
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
|
||||
@@ -8,17 +8,15 @@ Rectangle {
|
||||
|
||||
property bool batteryPopupVisible: false
|
||||
|
||||
width: Theme.barHeight - Theme.spacingS
|
||||
height: Theme.barHeight - Theme.spacingS
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: batteryArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||
width: 48
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: batteryArea.containsMouse || batteryPopupVisible ?
|
||||
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)
|
||||
visible: BatteryService.batteryAvailable
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
// Battery icon
|
||||
// Battery icon - Material Design icons already show level visually
|
||||
Text {
|
||||
text: BatteryService.getBatteryIcon()
|
||||
font.family: Theme.iconFont
|
||||
@@ -29,32 +27,14 @@ Rectangle {
|
||||
if (BatteryService.isCharging) return Theme.primary
|
||||
return Theme.surfaceText
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.centerIn: parent
|
||||
|
||||
// Subtle animation for charging
|
||||
RotationAnimation on rotation {
|
||||
// Subtle pulse animation for charging
|
||||
SequentialAnimation on opacity {
|
||||
running: BatteryService.isCharging
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 8000
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
}
|
||||
|
||||
// Battery percentage
|
||||
Text {
|
||||
text: BatteryService.batteryLevel + "%"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: {
|
||||
if (!BatteryService.batteryAvailable) return Theme.surfaceText
|
||||
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
|
||||
if (BatteryService.isCharging) return Theme.primary
|
||||
return Theme.surfaceText
|
||||
}
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: BatteryService.batteryAvailable
|
||||
NumberAnimation { to: 0.6; duration: 1000; easing.type: Easing.InOutQuad }
|
||||
NumberAnimation { to: 1.0; duration: 1000; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
74
Widgets/PowerButton.qml
Normal file
74
Widgets/PowerButton.qml
Normal file
@@ -0,0 +1,74 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import "../Common"
|
||||
|
||||
Rectangle {
|
||||
id: powerButton
|
||||
|
||||
width: 48
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: powerArea.containsMouse || root.powerMenuVisible ?
|
||||
Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.16) :
|
||||
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
|
||||
|
||||
// Power icon
|
||||
Text {
|
||||
text: "power_settings_new"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: powerArea.containsMouse || root.powerMenuVisible ? Theme.error : Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: powerArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
root.powerMenuVisible = !root.powerMenuVisible
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip on hover
|
||||
Rectangle {
|
||||
id: powerTooltip
|
||||
width: tooltipText.contentWidth + Theme.spacingM * 2
|
||||
height: tooltipText.contentHeight + Theme.spacingS * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 1
|
||||
visible: powerArea.containsMouse && !root.powerMenuVisible
|
||||
|
||||
anchors.bottom: parent.top
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
opacity: powerArea.containsMouse ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: tooltipText
|
||||
text: "Power Menu"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
203
Widgets/PowerConfirmDialog.qml
Normal file
203
Widgets/PowerConfirmDialog.qml
Normal file
@@ -0,0 +1,203 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
import "../Common"
|
||||
|
||||
PanelWindow {
|
||||
id: powerConfirmDialog
|
||||
|
||||
visible: root.powerConfirmVisible
|
||||
|
||||
implicitWidth: 400
|
||||
implicitHeight: 300
|
||||
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
// Darkened background
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: 0.5
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.min(400, parent.width - Theme.spacingL * 2)
|
||||
height: Math.min(200, 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.powerConfirmVisible ? 1.0 : 0.0
|
||||
scale: root.powerConfirmVisible ? 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
|
||||
|
||||
// Title
|
||||
Text {
|
||||
text: root.powerConfirmTitle
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: {
|
||||
switch(root.powerConfirmAction) {
|
||||
case "poweroff": return Theme.error
|
||||
case "reboot": return Theme.warning
|
||||
default: return Theme.surfaceText
|
||||
}
|
||||
}
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
// Message
|
||||
Text {
|
||||
text: root.powerConfirmMessage
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Item { height: Theme.spacingL }
|
||||
|
||||
// Buttons
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Cancel button
|
||||
Rectangle {
|
||||
width: 120
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelButton.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
|
||||
Text {
|
||||
text: "Cancel"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelButton
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.powerConfirmVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm button
|
||||
Rectangle {
|
||||
width: 120
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
let baseColor
|
||||
switch(root.powerConfirmAction) {
|
||||
case "poweroff": baseColor = Theme.error; break
|
||||
case "reboot": baseColor = Theme.warning; break
|
||||
default: baseColor = Theme.primary; break
|
||||
}
|
||||
return confirmButton.containsMouse ?
|
||||
Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9) :
|
||||
baseColor
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Confirm"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.onPrimary
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: confirmButton
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.powerConfirmVisible = false
|
||||
executePowerAction(root.powerConfirmAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function executePowerAction(action) {
|
||||
console.log("Executing power action:", action)
|
||||
|
||||
let command = []
|
||||
switch(action) {
|
||||
case "logout":
|
||||
// Try multiple logout commands for different environments
|
||||
command = ["bash", "-c", "loginctl terminate-user $USER || pkill -KILL -u $USER || gnome-session-quit --force || xfce4-session-logout --logout || i3-msg exit || swaymsg exit || niri msg quit"]
|
||||
break
|
||||
case "suspend":
|
||||
command = ["systemctl", "suspend"]
|
||||
break
|
||||
case "reboot":
|
||||
command = ["systemctl", "reboot"]
|
||||
break
|
||||
case "poweroff":
|
||||
command = ["systemctl", "poweroff"]
|
||||
break
|
||||
}
|
||||
|
||||
if (command.length > 0) {
|
||||
powerActionProcess.command = command
|
||||
powerActionProcess.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: powerActionProcess
|
||||
running: false
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.error("Power action failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
305
Widgets/PowerMenuPopup.qml
Normal file
305
Widgets/PowerMenuPopup.qml
Normal file
@@ -0,0 +1,305 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
import "../Common"
|
||||
|
||||
PanelWindow {
|
||||
id: powerMenuPopup
|
||||
|
||||
visible: root.powerMenuVisible
|
||||
|
||||
implicitWidth: 400
|
||||
implicitHeight: 320
|
||||
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
// Click outside to dismiss overlay
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
root.powerMenuVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.min(320, parent.width - Theme.spacingL * 2)
|
||||
height: 320 // Fixed height to prevent cropping
|
||||
x: Math.max(Theme.spacingL, 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.powerMenuVisible ? 1.0 : 0.0
|
||||
scale: root.powerMenuVisible ? 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
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent click-through to background
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
// Consume the click to prevent it from reaching the background
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Header
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Text {
|
||||
text: "Power Options"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item { width: parent.width - 150; height: 1 }
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: closePowerArea.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: closePowerArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closePowerArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.powerMenuVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Power options
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Log Out
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: logoutArea.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.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "logout"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Log Out"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: logoutArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.powerMenuVisible = false
|
||||
root.powerConfirmAction = "logout"
|
||||
root.powerConfirmTitle = "Log Out"
|
||||
root.powerConfirmMessage = "Are you sure you want to log out?"
|
||||
root.powerConfirmVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Suspend
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: suspendArea.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.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "bedtime"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Suspend"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: suspendArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.powerMenuVisible = false
|
||||
root.powerConfirmAction = "suspend"
|
||||
root.powerConfirmTitle = "Suspend"
|
||||
root.powerConfirmMessage = "Are you sure you want to suspend the system?"
|
||||
root.powerConfirmVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reboot
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "restart_alt"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Reboot"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rebootArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.powerMenuVisible = false
|
||||
root.powerConfirmAction = "reboot"
|
||||
root.powerConfirmTitle = "Reboot"
|
||||
root.powerConfirmMessage = "Are you sure you want to reboot the system?"
|
||||
root.powerConfirmVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Power Off
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "power_settings_new"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Power Off"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: powerOffArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.powerMenuVisible = false
|
||||
root.powerConfirmAction = "poweroff"
|
||||
root.powerConfirmTitle = "Power Off"
|
||||
root.powerConfirmMessage = "Are you sure you want to power off the system?"
|
||||
root.powerConfirmVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,51 +2,37 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Widgets
|
||||
|
||||
Rectangle {
|
||||
Item {
|
||||
property var theme
|
||||
property var root
|
||||
|
||||
width: Math.max(40, systemTrayRow.implicitWidth + theme.spacingS * 2)
|
||||
height: 32
|
||||
radius: theme.cornerRadius
|
||||
color: Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: systemTrayRow.children.length > 0
|
||||
implicitWidth: trayRow.implicitWidth
|
||||
visible: trayRow.children.length > 0
|
||||
|
||||
Row {
|
||||
id: systemTrayRow
|
||||
id: trayRow
|
||||
anchors.centerIn: parent
|
||||
spacing: theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: SystemTray.items
|
||||
|
||||
delegate: Rectangle {
|
||||
required property SystemTrayItem modelData
|
||||
|
||||
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 {
|
||||
IconImage {
|
||||
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
|
||||
source: parent.modelData.icon
|
||||
smooth: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -57,45 +43,23 @@ Rectangle {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: (mouse) => {
|
||||
if (!trayItem) return;
|
||||
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (!trayItem.onlyMenu) {
|
||||
trayItem.activate()
|
||||
}
|
||||
parent.modelData.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")
|
||||
}
|
||||
menuHandler.showMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simple menu handling for now
|
||||
QtObject {
|
||||
id: customTrayMenu
|
||||
id: menuHandler
|
||||
|
||||
property bool menuVisible: false
|
||||
|
||||
function showMenu(x, y) {
|
||||
root.currentTrayMenu = customTrayMenu
|
||||
root.currentTrayItem = trayItem
|
||||
|
||||
root.trayMenuX = parent.parent.parent.parent.x + parent.parent.parent.parent.width - 180 - theme.spacingL
|
||||
root.trayMenuY = theme.barHeight + theme.spacingS
|
||||
|
||||
console.log("Showing menu at:", root.trayMenuX, root.trayMenuY)
|
||||
menuVisible = true
|
||||
root.showTrayMenu = true
|
||||
function showMenu() {
|
||||
if (parent.modelData.hasMenu) {
|
||||
console.log("Right-click menu for:", parent.modelData.title || "Unknown")
|
||||
// TODO: Implement proper menu positioning
|
||||
}
|
||||
|
||||
function hideMenu() {
|
||||
menuVisible = false
|
||||
root.showTrayMenu = false
|
||||
root.currentTrayMenu = null
|
||||
root.currentTrayItem = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -804,7 +804,11 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Power Button
|
||||
PowerButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,3 +19,6 @@ CustomSlider 1.0 CustomSlider.qml
|
||||
InputDialog 1.0 InputDialog.qml
|
||||
BatteryWidget 1.0 BatteryWidget.qml
|
||||
BatteryControlPopup 1.0 BatteryControlPopup.qml
|
||||
PowerButton 1.0 PowerButton.qml
|
||||
PowerMenuPopup 1.0 PowerMenuPopup.qml
|
||||
PowerConfirmDialog 1.0 PowerConfirmDialog.qml
|
||||
@@ -34,6 +34,11 @@ ShellRoot {
|
||||
property bool hasActiveMedia: activePlayer && (activePlayer.trackTitle || activePlayer.trackArtist)
|
||||
property bool controlCenterVisible: false
|
||||
property bool batteryPopupVisible: false
|
||||
property bool powerMenuVisible: false
|
||||
property bool powerConfirmVisible: false
|
||||
property string powerConfirmAction: ""
|
||||
property string powerConfirmTitle: ""
|
||||
property string powerConfirmMessage: ""
|
||||
|
||||
// Network properties from NetworkService
|
||||
property string networkStatus: NetworkService.networkStatus
|
||||
@@ -259,6 +264,8 @@ ShellRoot {
|
||||
id: globalInputDialog
|
||||
}
|
||||
BatteryControlPopup {}
|
||||
PowerMenuPopup {}
|
||||
PowerConfirmDialog {}
|
||||
|
||||
// Application and clipboard components
|
||||
AppLauncher {
|
||||
|
||||
Reference in New Issue
Block a user