1
0
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:
Aziz Hasanain
2025-09-15 15:54:39 +03:00
committed by GitHub
parent 8ee43de145
commit e4f86abda9
6 changed files with 615 additions and 1 deletions

View 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()
}
}
}
}
}
}
}
}

View File

@@ -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",

View 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
}
}
}

View File

@@ -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()
}
}
}
}
}
}

View 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()
}
}

View File

@@ -273,6 +273,16 @@ ShellRoot {
}
}
LazyLoader {
id: archUpdaterLoader
active: false
ArchUpdaterPopout {
id: archUpdaterPopout
}
}
Variants {
id: notepadSlideoutVariants
model: SettingsData.getFilteredScreens("notepad")