mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-06 05:25:41 -05:00
Add an ArchUpdater widget (#201)
This commit is contained in:
353
Modules/ArchUpdaterPopout.qml
Normal file
353
Modules/ArchUpdaterPopout.qml
Normal file
@@ -0,0 +1,353 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankPopout {
|
||||
id: archUpdaterPopout
|
||||
|
||||
property var parentWidget: null
|
||||
property string triggerSection: "right"
|
||||
property var triggerScreen: null
|
||||
|
||||
function setTriggerPosition(x, y, width, section, screen) {
|
||||
triggerX = x;
|
||||
triggerY = y;
|
||||
triggerWidth = width;
|
||||
triggerSection = section;
|
||||
triggerScreen = screen;
|
||||
}
|
||||
|
||||
popupWidth: 400
|
||||
popupHeight: 500
|
||||
triggerX: Screen.width - 600 - Theme.spacingL
|
||||
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
|
||||
triggerWidth: 55
|
||||
positioning: "center"
|
||||
screen: triggerScreen
|
||||
visible: shouldBeVisible
|
||||
shouldBeVisible: false
|
||||
|
||||
onShouldBeVisibleChanged: {
|
||||
if (shouldBeVisible) {
|
||||
if (ArchUpdaterService.updateCount === 0 && !ArchUpdaterService.isChecking) {
|
||||
ArchUpdaterService.checkForUpdates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Rectangle {
|
||||
id: updaterPanel
|
||||
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadius
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
|
||||
Repeater {
|
||||
model: [{
|
||||
"margin": -3,
|
||||
"color": Qt.rgba(0, 0, 0, 0.05),
|
||||
"z": -3
|
||||
}, {
|
||||
"margin": -2,
|
||||
"color": Qt.rgba(0, 0, 0, 0.08),
|
||||
"z": -2
|
||||
}, {
|
||||
"margin": 0,
|
||||
"color": Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12),
|
||||
"z": -1
|
||||
}]
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: modelData.margin
|
||||
color: "transparent"
|
||||
radius: parent.radius + Math.abs(modelData.margin)
|
||||
border.color: modelData.color
|
||||
border.width: 1
|
||||
z: modelData.z
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.spacingL * 2
|
||||
height: parent.height - Theme.spacingL * 2
|
||||
x: Theme.spacingL
|
||||
y: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
StyledText {
|
||||
text: "System Updates"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: {
|
||||
if (ArchUpdaterService.isChecking) return "Checking...";
|
||||
if (ArchUpdaterService.hasError) return "Error";
|
||||
if (ArchUpdaterService.updateCount === 0) return "Up to date";
|
||||
return ArchUpdaterService.updateCount + " updates";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: {
|
||||
if (ArchUpdaterService.hasError) return Theme.error;
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: checkForUpdatesButton
|
||||
buttonSize: 28
|
||||
iconName: "refresh"
|
||||
iconSize: 18
|
||||
z: 15
|
||||
iconColor: Theme.surfaceText
|
||||
enabled: !ArchUpdaterService.isChecking
|
||||
opacity: enabled ? 1.0 : 0.5
|
||||
onClicked: {
|
||||
ArchUpdaterService.checkForUpdates()
|
||||
}
|
||||
|
||||
RotationAnimation {
|
||||
target: checkForUpdatesButton
|
||||
property: "rotation"
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
running: ArchUpdaterService.isChecking
|
||||
loops: Animation.Infinite
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
checkForUpdatesButton.rotation = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: {
|
||||
let usedHeight = 40 + Theme.spacingL
|
||||
usedHeight += 48 + Theme.spacingL
|
||||
return parent.height - usedHeight
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
anchors.rightMargin: 0
|
||||
|
||||
StyledText {
|
||||
id: statusText
|
||||
width: parent.width
|
||||
text: {
|
||||
if (ArchUpdaterService.hasError) {
|
||||
return "Failed to check for updates:\n" + ArchUpdaterService.errorMessage;
|
||||
}
|
||||
if (!ArchUpdaterService.helperAvailable) {
|
||||
return "No AUR helper found. Please install 'paru' or 'yay' to check for updates.";
|
||||
}
|
||||
if (ArchUpdaterService.isChecking) {
|
||||
return "Checking for updates...";
|
||||
}
|
||||
if (ArchUpdaterService.updateCount === 0) {
|
||||
return "Your system is up to date!";
|
||||
}
|
||||
return `Found ${ArchUpdaterService.updateCount} packages to update:`;
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: {
|
||||
if (ArchUpdaterService.hasError) return Theme.errorText;
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
wrapMode: Text.WordWrap
|
||||
visible: ArchUpdaterService.updateCount === 0 || ArchUpdaterService.hasError || ArchUpdaterService.isChecking
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: packagesList
|
||||
|
||||
width: parent.width
|
||||
height: parent.height - (ArchUpdaterService.updateCount === 0 || ArchUpdaterService.hasError || ArchUpdaterService.isChecking ? statusText.height + Theme.spacingM : 0)
|
||||
visible: ArchUpdaterService.updateCount > 0 && !ArchUpdaterService.isChecking && !ArchUpdaterService.hasError
|
||||
clip: true
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
model: ArchUpdaterService.availableUpdates
|
||||
|
||||
delegate: Rectangle {
|
||||
width: ListView.view.width - Theme.spacingM
|
||||
height: 48
|
||||
radius: Theme.cornerRadius
|
||||
color: packageMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent"
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 24 - Theme.spacingM
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: modelData.name || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: `${modelData.currentVersion} → ${modelData.newVersion}`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: packageMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 48
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: parent.height
|
||||
radius: Theme.cornerRadius
|
||||
color: updateMouseArea.containsMouse ? Theme.primaryHover : Theme.secondaryHover
|
||||
opacity: ArchUpdaterService.updateCount > 0 ? 1.0 : 0.5
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "system_update_alt"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Update All"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: updateMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: ArchUpdaterService.updateCount > 0
|
||||
onClicked: {
|
||||
ArchUpdaterService.runUpdates()
|
||||
archUpdaterPopout.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: parent.height
|
||||
radius: Theme.cornerRadius
|
||||
color: closeMouseArea.containsMouse ? Theme.errorPressed : Theme.secondaryHover
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "close"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Close"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
archUpdaterPopout.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,6 +163,12 @@ Item {
|
||||
"description": "Quick access to color picker",
|
||||
"icon": "palette",
|
||||
"enabled": true
|
||||
}, {
|
||||
"id": "archUpdater",
|
||||
"text": "Arch Updater",
|
||||
"description": "Check for updates in Arch-based systems",
|
||||
"icon": "update",
|
||||
"enabled": true
|
||||
}]
|
||||
property var defaultLeftWidgets: [{
|
||||
"id": "launcherButton",
|
||||
|
||||
110
Modules/TopBar/ArchUpdater.qml
Normal file
110
Modules/TopBar/ArchUpdater.qml
Normal file
@@ -0,0 +1,110 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool isActive: false
|
||||
property string section: "right"
|
||||
property var popupTarget: null
|
||||
property var parentScreen: null
|
||||
property real widgetHeight: 30
|
||||
property real barHeight: 48
|
||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
||||
readonly property bool hasUpdates: ArchUpdaterService.updateCount > 0
|
||||
readonly property bool isChecking: ArchUpdaterService.isChecking
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: updaterIcon.width + horizontalPadding * 2
|
||||
height: widgetHeight
|
||||
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
|
||||
color: {
|
||||
if (SettingsData.topBarNoBackground) {
|
||||
return "transparent";
|
||||
}
|
||||
|
||||
const baseColor = updaterArea.containsMouse || root.isActive ? Theme.primaryPressed : Theme.secondaryHover;
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
||||
}
|
||||
|
||||
Row {
|
||||
id: updaterIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
id: statusIcon
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: {
|
||||
if (isChecking) return "refresh";
|
||||
if (ArchUpdaterService.hasError) return "error";
|
||||
if (hasUpdates) return "system_update_alt";
|
||||
return "check_circle";
|
||||
}
|
||||
size: Theme.iconSize - 6
|
||||
color: {
|
||||
if (ArchUpdaterService.hasError) return Theme.error;
|
||||
if (hasUpdates) return Theme.primary;
|
||||
return (updaterArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText);
|
||||
}
|
||||
|
||||
RotationAnimation {
|
||||
id: rotationAnimation
|
||||
target: statusIcon
|
||||
property: "rotation"
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
running: isChecking
|
||||
loops: Animation.Infinite
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
statusIcon.rotation = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: countText
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: ArchUpdaterService.updateCount.toString()
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
visible: hasUpdates && !isChecking
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: updaterArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
if (popupTarget && popupTarget.setTriggerPosition) {
|
||||
const globalPos = mapToGlobal(0, 0);
|
||||
const currentScreen = parentScreen || Screen;
|
||||
const screenX = currentScreen.x || 0;
|
||||
const relativeX = globalPos.x - screenX;
|
||||
popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, width, section, currentScreen);
|
||||
}
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,6 +183,9 @@ PanelWindow {
|
||||
}, {
|
||||
"loader": clipboardHistoryModalPopup,
|
||||
"prop": "visible"
|
||||
}, {
|
||||
"loader": archUpdaterLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}]
|
||||
return notepadInstanceVisible || loaders.some(item => {
|
||||
if (item.loader) {
|
||||
@@ -373,7 +376,8 @@ PanelWindow {
|
||||
"keyboard_layout_name": keyboardLayoutNameComponent,
|
||||
"vpn": vpnComponent,
|
||||
"notepadButton": notepadButtonComponent,
|
||||
"colorPicker": colorPickerComponent
|
||||
"colorPicker": colorPickerComponent,
|
||||
"archUpdater": archUpdaterComponent
|
||||
})
|
||||
|
||||
function getWidgetComponent(widgetId) {
|
||||
@@ -1016,6 +1020,26 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: archUpdaterComponent
|
||||
|
||||
ArchUpdater {
|
||||
isActive: archUpdaterLoader.item ? archUpdaterLoader.item.shouldBeVisible : false
|
||||
widgetHeight: root.widgetHeight
|
||||
barHeight: root.effectiveBarHeight
|
||||
section: topBarContent.getWidgetSection(parent) || "right"
|
||||
popupTarget: {
|
||||
archUpdaterLoader.active = true
|
||||
return archUpdaterLoader.item
|
||||
}
|
||||
parentScreen: root.screen
|
||||
onClicked: {
|
||||
archUpdaterLoader.active = true
|
||||
archUpdaterLoader.item?.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
111
Services/ArchUpdaterService.qml
Normal file
111
Services/ArchUpdaterService.qml
Normal file
@@ -0,0 +1,111 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property var availableUpdates: []
|
||||
property bool isChecking: false
|
||||
property bool hasError: false
|
||||
property string errorMessage: ""
|
||||
property string aurHelper: ""
|
||||
|
||||
readonly property int updateCount: availableUpdates.length
|
||||
readonly property bool helperAvailable: aurHelper !== ""
|
||||
|
||||
Process {
|
||||
id: helperDetection
|
||||
command: ["sh", "-c", "which paru || which yay"]
|
||||
running: true
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
const helperPath = stdout.text.trim()
|
||||
aurHelper = helperPath.split('/').pop()
|
||||
checkForUpdates()
|
||||
} else {
|
||||
console.warn("ArchUpdater: No AUR helper found")
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: updateChecker
|
||||
|
||||
onExited: (exitCode) => {
|
||||
isChecking = false
|
||||
if (exitCode === 0 || exitCode === 1) {
|
||||
// Exit code 0 = updates available, 1 = no updates
|
||||
parseUpdates(stdout.text)
|
||||
hasError = false
|
||||
errorMessage = ""
|
||||
} else {
|
||||
hasError = true
|
||||
errorMessage = "Failed to check for updates"
|
||||
console.warn("ArchUpdater: Update check failed with code:", exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: updater
|
||||
onExited: (exitCode) => {
|
||||
checkForUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
function checkForUpdates() {
|
||||
if (!helperAvailable || isChecking) return
|
||||
|
||||
isChecking = true
|
||||
hasError = false
|
||||
updateChecker.command = [aurHelper, "-Qu"]
|
||||
updateChecker.running = true
|
||||
}
|
||||
|
||||
function parseUpdates(output) {
|
||||
const lines = output.trim().split('\n').filter(line => line.trim())
|
||||
const updates = []
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/)
|
||||
if (match) {
|
||||
updates.push({
|
||||
name: match[1],
|
||||
currentVersion: match[2],
|
||||
newVersion: match[3],
|
||||
description: `${match[1]} ${match[2]} → ${match[3]}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
availableUpdates = updates
|
||||
}
|
||||
|
||||
function runUpdates() {
|
||||
if (!helperAvailable || updateCount === 0) return
|
||||
|
||||
const terminal = Quickshell.env("TERMINAL") || "xterm"
|
||||
const updateCommand = `${aurHelper} -Syu && echo "Updates complete! Press Enter to close..." && read`
|
||||
|
||||
updater.command = [terminal, "-e", "sh", "-c", updateCommand]
|
||||
updater.running = true
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 30 * 60 * 1000
|
||||
repeat: true
|
||||
running: helperAvailable
|
||||
onTriggered: checkForUpdates()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user