mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-11 23:09:42 -04:00
quickshell: drop support for 0.2, require 0.3+
- Remove all compat code - Rewire LegacyNetworkService to use Quickshell.Networking - Add parentWindow to settings child windows
This commit is contained in:
@@ -1,40 +1,29 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Clear all image cache
|
||||
function clearImageCache() {
|
||||
Quickshell.execDetached(["rm", "-rf", Paths.stringify(Paths.imagecache)]);
|
||||
Paths.mkdir(Paths.imagecache);
|
||||
}
|
||||
|
||||
// Clear cache older than specified minutes
|
||||
function clearOldCache(ageInMinutes) {
|
||||
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"]);
|
||||
}
|
||||
|
||||
// Clear cache for specific size
|
||||
function clearCacheForSize(size) {
|
||||
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"]);
|
||||
}
|
||||
|
||||
// Get cache size in MB
|
||||
function getCacheSize(callback) {
|
||||
var process = Qt.createQmlObject(`
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
command: ["du", "-sm", "${Paths.stringify(Paths.imagecache)}"]
|
||||
running: true
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var sizeMB = parseInt(text.split("\\t")[0]) || 0
|
||||
callback(sizeMB)
|
||||
}
|
||||
}
|
||||
}
|
||||
`, root);
|
||||
Proc.runCommand("cache_size", ["du", "-sm", Paths.stringify(Paths.imagecache)], function (output, exitCode) {
|
||||
const sizeMB = parseInt(output.split("\t")[0]) || 0;
|
||||
callback(sizeMB);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
@@ -21,7 +22,7 @@ Singleton {
|
||||
const isRandomId = !id;
|
||||
|
||||
if (!_procDebouncers[procId]) {
|
||||
const t = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root);
|
||||
const t = debounceTimerComp.createObject(root);
|
||||
t.triggered.connect(function () {
|
||||
_launchProc(procId, isRandomId);
|
||||
});
|
||||
@@ -49,14 +50,10 @@ Singleton {
|
||||
const entry = _procDebouncers[id];
|
||||
if (!entry)
|
||||
return;
|
||||
const proc = Qt.createQmlObject('import Quickshell.Io; Process { running: false }', root);
|
||||
const out = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc);
|
||||
const err = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc);
|
||||
const timeoutTimer = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root);
|
||||
|
||||
proc.stdout = out;
|
||||
proc.stderr = err;
|
||||
proc.command = entry.command;
|
||||
const proc = procComp.createObject(root, {
|
||||
command: entry.command
|
||||
});
|
||||
const timeoutTimer = debounceTimerComp.createObject(root);
|
||||
|
||||
let capturedOut = "";
|
||||
let capturedErr = "";
|
||||
@@ -77,9 +74,9 @@ Singleton {
|
||||
}
|
||||
});
|
||||
|
||||
out.streamFinished.connect(function () {
|
||||
proc.stdout.streamFinished.connect(function () {
|
||||
try {
|
||||
capturedOut = out.text || "";
|
||||
capturedOut = proc.stdout.text || "";
|
||||
} catch (e) {
|
||||
capturedOut = "";
|
||||
}
|
||||
@@ -87,9 +84,9 @@ Singleton {
|
||||
maybeComplete();
|
||||
});
|
||||
|
||||
err.streamFinished.connect(function () {
|
||||
proc.stderr.streamFinished.connect(function () {
|
||||
try {
|
||||
capturedErr = err.text || "";
|
||||
capturedErr = proc.stderr.text || "";
|
||||
} catch (e) {
|
||||
capturedErr = "";
|
||||
}
|
||||
@@ -140,4 +137,20 @@ Singleton {
|
||||
if (entry.timeoutMs !== noTimeout)
|
||||
timeoutTimer.start();
|
||||
}
|
||||
|
||||
Component {
|
||||
id: debounceTimerComp
|
||||
Timer {
|
||||
repeat: false
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: procComp
|
||||
Process {
|
||||
running: false
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,6 +523,8 @@ Item {
|
||||
enabled: PolkitService.polkitAvailable
|
||||
|
||||
function onAuthenticationRequestStarted() {
|
||||
if (PopoutService.systemUpdatePopout?.shouldBeVisible)
|
||||
return;
|
||||
polkitAuthModalLoader.active = true;
|
||||
if (polkitAuthModalLoader.item)
|
||||
polkitAuthModalLoader.item.show();
|
||||
|
||||
@@ -67,7 +67,7 @@ FloatingWindow {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
visible: windowControls.supported && windowControls.canMaximize
|
||||
visible: windowControls.canMaximize
|
||||
iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
|
||||
@@ -16,6 +16,7 @@ FloatingWindow {
|
||||
property bool saveMode: false
|
||||
property string defaultFileName: ""
|
||||
property var parentModal: null
|
||||
parentWindow: parentModal
|
||||
property bool shouldHaveFocus: visible
|
||||
property bool allowFocusOverride: false
|
||||
property bool shouldBeVisible: visible
|
||||
|
||||
@@ -215,7 +215,7 @@ FloatingWindow {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
visible: windowControls.supported && windowControls.canMaximize
|
||||
visible: windowControls.canMaximize
|
||||
iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
|
||||
378
quickshell/Modals/PolkitAuthContent.qml
Normal file
378
quickshell/Modals/PolkitAuthContent.qml
Normal file
@@ -0,0 +1,378 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
property var currentFlow: PolkitService.agent?.flow
|
||||
property string passwordInput: ""
|
||||
property bool isLoading: false
|
||||
property bool awaitingFprintForPassword: false
|
||||
property var windowControls: null
|
||||
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
||||
|
||||
property string polkitEtcPamText: ""
|
||||
property string polkitLibPamText: ""
|
||||
property string systemAuthPamText: ""
|
||||
property string commonAuthPamText: ""
|
||||
property string passwordAuthPamText: ""
|
||||
readonly property bool polkitPamHasFprint: {
|
||||
const polkitText = polkitEtcPamText !== "" ? polkitEtcPamText : polkitLibPamText;
|
||||
if (!polkitText)
|
||||
return false;
|
||||
return pamModuleEnabled(polkitText, "pam_fprintd") || (polkitText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (polkitText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (polkitText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"));
|
||||
}
|
||||
|
||||
signal closeRequested
|
||||
signal authenticationSucceeded
|
||||
|
||||
focus: true
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
cancelAuth();
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
function stripPamComment(line) {
|
||||
if (!line)
|
||||
return "";
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#"))
|
||||
return "";
|
||||
const hashIdx = trimmed.indexOf("#");
|
||||
if (hashIdx >= 0)
|
||||
return trimmed.substring(0, hashIdx).trim();
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function pamModuleEnabled(pamText, moduleName) {
|
||||
if (!pamText || !moduleName)
|
||||
return false;
|
||||
const lines = pamText.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = stripPamComment(lines[i]);
|
||||
if (line && line.includes(moduleName))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function focusPasswordField() {
|
||||
passwordField.forceActiveFocus();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
passwordInput = "";
|
||||
isLoading = false;
|
||||
awaitingFprintForPassword = false;
|
||||
}
|
||||
|
||||
function _commitSubmit() {
|
||||
isLoading = true;
|
||||
awaitingFprintForPassword = false;
|
||||
currentFlow.submit(passwordInput);
|
||||
passwordInput = "";
|
||||
}
|
||||
|
||||
function submitAuth() {
|
||||
if (!currentFlow || isLoading)
|
||||
return;
|
||||
if (!currentFlow.isResponseRequired) {
|
||||
awaitingFprintForPassword = true;
|
||||
return;
|
||||
}
|
||||
_commitSubmit();
|
||||
}
|
||||
|
||||
function cancelAuth() {
|
||||
if (isLoading)
|
||||
return;
|
||||
awaitingFprintForPassword = false;
|
||||
if (currentFlow) {
|
||||
currentFlow.cancelAuthenticationRequest();
|
||||
return;
|
||||
}
|
||||
closeRequested();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.currentFlow
|
||||
enabled: root.currentFlow !== null
|
||||
|
||||
function onIsResponseRequiredChanged() {
|
||||
if (!root.currentFlow.isResponseRequired)
|
||||
return;
|
||||
if (root.awaitingFprintForPassword && root.passwordInput !== "") {
|
||||
root._commitSubmit();
|
||||
return;
|
||||
}
|
||||
root.awaitingFprintForPassword = false;
|
||||
root.isLoading = false;
|
||||
root.passwordInput = "";
|
||||
passwordField.forceActiveFocus();
|
||||
}
|
||||
|
||||
function onAuthenticationSucceeded() {
|
||||
root.authenticationSucceeded();
|
||||
root.closeRequested();
|
||||
}
|
||||
|
||||
function onAuthenticationFailed() {
|
||||
root.isLoading = false;
|
||||
}
|
||||
|
||||
function onAuthenticationRequestCancelled() {
|
||||
root.closeRequested();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/etc/pam.d/polkit-1"
|
||||
printErrors: false
|
||||
onLoaded: root.polkitEtcPamText = text()
|
||||
onLoadFailed: root.polkitEtcPamText = ""
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/usr/lib/pam.d/polkit-1"
|
||||
printErrors: false
|
||||
onLoaded: root.polkitLibPamText = text()
|
||||
onLoadFailed: root.polkitLibPamText = ""
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/etc/pam.d/system-auth"
|
||||
printErrors: false
|
||||
onLoaded: root.systemAuthPamText = text()
|
||||
onLoadFailed: root.systemAuthPamText = ""
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/etc/pam.d/common-auth"
|
||||
printErrors: false
|
||||
onLoaded: root.commonAuthPamText = text()
|
||||
onLoadFailed: root.commonAuthPamText = ""
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/etc/pam.d/password-auth"
|
||||
printErrors: false
|
||||
onLoaded: root.passwordAuthPamText = text()
|
||||
onLoadFailed: root.passwordAuthPamText = ""
|
||||
}
|
||||
|
||||
Item {
|
||||
id: headerSection
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingM
|
||||
height: Math.max(titleColumn.implicitHeight, windowButtonRow.implicitHeight)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.windowControls !== null
|
||||
onPressed: {
|
||||
if (root.windowControls)
|
||||
root.windowControls.tryStartMove();
|
||||
}
|
||||
onDoubleClicked: {
|
||||
if (root.windowControls)
|
||||
root.windowControls.tryToggleMaximize();
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: titleColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: windowButtonRow.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Authentication Required")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.currentFlow?.message ?? ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
visible: text !== ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.currentFlow?.supplementaryMessage ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: (root.currentFlow?.supplementaryIsError ?? false) ? Theme.error : Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
opacity: (root.currentFlow?.supplementaryIsError ?? false) ? 1 : 0.8
|
||||
visible: text !== ""
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: windowButtonRow
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
visible: root.windowControls?.supported === true && root.windowControls?.canMaximize === true
|
||||
iconName: (root.windowControls?.targetWindow?.maximized ?? false) ? "fullscreen_exit" : "fullscreen"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: {
|
||||
if (root.windowControls)
|
||||
root.windowControls.tryToggleMaximize();
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
enabled: !root.isLoading
|
||||
opacity: enabled ? 1 : 0.5
|
||||
onClicked: root.cancelAuth()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: bottomSection
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: root.currentFlow?.inputPrompt ?? ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
visible: text !== ""
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: passwordField
|
||||
|
||||
width: parent.width
|
||||
height: root.inputFieldHeight
|
||||
backgroundColor: Theme.surfaceHover
|
||||
normalBorderColor: Theme.outlineStrong
|
||||
focusedBorderColor: Theme.primary
|
||||
borderWidth: 1
|
||||
focusedBorderWidth: 2
|
||||
leftIconName: root.polkitPamHasFprint ? "fingerprint" : ""
|
||||
leftIconSize: 20
|
||||
leftIconColor: Theme.primary
|
||||
leftIconFocusedColor: Theme.primary
|
||||
opacity: root.isLoading ? 0.5 : 1
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: root.passwordInput
|
||||
showPasswordToggle: !(root.currentFlow?.responseVisible ?? false)
|
||||
echoMode: (root.currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
|
||||
placeholderText: ""
|
||||
enabled: !root.isLoading
|
||||
onTextEdited: root.passwordInput = text
|
||||
onAccepted: root.submitAuth()
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Authentication failed, please try again")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
width: parent.width
|
||||
visible: root.currentFlow?.failed ?? false
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 36
|
||||
|
||||
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 ? Theme.surfaceTextHover : "transparent"
|
||||
border.color: Theme.surfaceVariantAlpha
|
||||
border.width: 1
|
||||
enabled: !root.isLoading
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
StyledText {
|
||||
id: cancelText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Cancel")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: root.cancelAuth()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, authText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: authArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
enabled: !root.isLoading
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
StyledText {
|
||||
id: authText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Authenticate")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: authArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: root.submitAuth()
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,427 +1,68 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
FloatingWindow {
|
||||
id: root
|
||||
|
||||
property bool disablePopupTransparency: true
|
||||
property string passwordInput: ""
|
||||
property var currentFlow: PolkitService.agent?.flow
|
||||
property bool isLoading: false
|
||||
property bool awaitingFprintForPassword: false
|
||||
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
||||
|
||||
property string polkitEtcPamText: ""
|
||||
property string polkitLibPamText: ""
|
||||
property string systemAuthPamText: ""
|
||||
property string commonAuthPamText: ""
|
||||
property string passwordAuthPamText: ""
|
||||
readonly property bool polkitPamHasFprint: {
|
||||
const polkitText = polkitEtcPamText !== "" ? polkitEtcPamText : polkitLibPamText;
|
||||
if (!polkitText)
|
||||
return false;
|
||||
return pamModuleEnabled(polkitText, "pam_fprintd") || (polkitText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (polkitText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (polkitText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"));
|
||||
}
|
||||
|
||||
function stripPamComment(line) {
|
||||
if (!line)
|
||||
return "";
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#"))
|
||||
return "";
|
||||
const hashIdx = trimmed.indexOf("#");
|
||||
if (hashIdx >= 0)
|
||||
return trimmed.substring(0, hashIdx).trim();
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function pamModuleEnabled(pamText, moduleName) {
|
||||
if (!pamText || !moduleName)
|
||||
return false;
|
||||
const lines = pamText.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = stripPamComment(lines[i]);
|
||||
if (line && line.includes(moduleName))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function focusPasswordField() {
|
||||
passwordField.forceActiveFocus();
|
||||
}
|
||||
|
||||
function show(targetScreen) {
|
||||
if (targetScreen)
|
||||
screen = targetScreen;
|
||||
passwordInput = "";
|
||||
isLoading = false;
|
||||
awaitingFprintForPassword = false;
|
||||
function show() {
|
||||
if (contentLoader.item)
|
||||
contentLoader.item.reset();
|
||||
visible = true;
|
||||
Qt.callLater(focusPasswordField);
|
||||
Qt.callLater(focusContent);
|
||||
}
|
||||
|
||||
function hide() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
function _commitSubmit() {
|
||||
isLoading = true;
|
||||
awaitingFprintForPassword = false;
|
||||
currentFlow.submit(passwordInput);
|
||||
passwordInput = "";
|
||||
}
|
||||
|
||||
function submitAuth() {
|
||||
if (!currentFlow || isLoading)
|
||||
return;
|
||||
if (!currentFlow.isResponseRequired) {
|
||||
awaitingFprintForPassword = true;
|
||||
return;
|
||||
}
|
||||
_commitSubmit();
|
||||
}
|
||||
|
||||
function cancelAuth() {
|
||||
if (isLoading)
|
||||
return;
|
||||
awaitingFprintForPassword = false;
|
||||
if (currentFlow) {
|
||||
currentFlow.cancelAuthenticationRequest();
|
||||
return;
|
||||
}
|
||||
hide();
|
||||
function focusContent() {
|
||||
if (contentLoader.item)
|
||||
contentLoader.item.focusPasswordField();
|
||||
}
|
||||
|
||||
objectName: "polkitAuthModal"
|
||||
|
||||
screen: Quickshell.screens[0]
|
||||
color: "transparent"
|
||||
title: I18n.tr("Authentication")
|
||||
minimumSize: Qt.size(460, 220)
|
||||
maximumSize: Qt.size(460, 220)
|
||||
color: Theme.surfaceContainer
|
||||
visible: false
|
||||
|
||||
WlrLayershell.namespace: "dms:polkit-auth"
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
Qt.callLater(focusPasswordField);
|
||||
Qt.callLater(focusContent);
|
||||
return;
|
||||
}
|
||||
passwordInput = "";
|
||||
isLoading = false;
|
||||
awaitingFprintForPassword = false;
|
||||
if (contentLoader.item)
|
||||
contentLoader.item.reset();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PolkitService.agent
|
||||
enabled: PolkitService.polkitAvailable
|
||||
|
||||
function onAuthenticationRequestStarted() {
|
||||
show();
|
||||
}
|
||||
|
||||
function onIsActiveChanged() {
|
||||
if (!(PolkitService.agent?.isActive ?? false))
|
||||
hide();
|
||||
root.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: currentFlow
|
||||
enabled: currentFlow !== null
|
||||
|
||||
function onIsResponseRequiredChanged() {
|
||||
if (!currentFlow.isResponseRequired)
|
||||
return;
|
||||
if (awaitingFprintForPassword && passwordInput !== "") {
|
||||
_commitSubmit();
|
||||
return;
|
||||
}
|
||||
awaitingFprintForPassword = false;
|
||||
isLoading = false;
|
||||
passwordInput = "";
|
||||
passwordField.forceActiveFocus();
|
||||
}
|
||||
|
||||
function onAuthenticationSucceeded() {
|
||||
hide();
|
||||
}
|
||||
|
||||
function onAuthenticationFailed() {
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
function onAuthenticationRequestCancelled() {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/etc/pam.d/polkit-1"
|
||||
printErrors: false
|
||||
onLoaded: root.polkitEtcPamText = text()
|
||||
onLoadFailed: root.polkitEtcPamText = ""
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/usr/lib/pam.d/polkit-1"
|
||||
printErrors: false
|
||||
onLoaded: root.polkitLibPamText = text()
|
||||
onLoadFailed: root.polkitLibPamText = ""
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/etc/pam.d/system-auth"
|
||||
printErrors: false
|
||||
onLoaded: root.systemAuthPamText = text()
|
||||
onLoadFailed: root.systemAuthPamText = ""
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/etc/pam.d/common-auth"
|
||||
printErrors: false
|
||||
onLoaded: root.commonAuthPamText = text()
|
||||
onLoadFailed: root.commonAuthPamText = ""
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/etc/pam.d/password-auth"
|
||||
printErrors: false
|
||||
onLoaded: root.passwordAuthPamText = text()
|
||||
onLoadFailed: root.passwordAuthPamText = ""
|
||||
}
|
||||
|
||||
// Dim overlay — clicking outside cancels authentication
|
||||
Rectangle {
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.45)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: cancelAuth()
|
||||
active: root.visible
|
||||
sourceComponent: PolkitAuthContent {
|
||||
windowControls: windowControls
|
||||
onCloseRequested: root.hide()
|
||||
}
|
||||
}
|
||||
|
||||
// Centered dialog box
|
||||
Rectangle {
|
||||
id: dialogBox
|
||||
width: 460
|
||||
height: 220
|
||||
anchors.centerIn: parent
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
FocusScope {
|
||||
id: contentFocusScope
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
cancelAuth();
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Item {
|
||||
id: headerSection
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingM
|
||||
height: Math.max(titleColumn.implicitHeight, windowButtonRow.implicitHeight)
|
||||
|
||||
Column {
|
||||
id: titleColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: windowButtonRow.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Authentication Required")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFlow?.message ?? ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
visible: text !== ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFlow?.supplementaryMessage ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: (currentFlow?.supplementaryIsError ?? false) ? Theme.error : Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
opacity: (currentFlow?.supplementaryIsError ?? false) ? 1 : 0.8
|
||||
visible: text !== ""
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: windowButtonRow
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
enabled: !isLoading
|
||||
opacity: enabled ? 1 : 0.5
|
||||
onClicked: cancelAuth()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: bottomSection
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: currentFlow?.inputPrompt ?? ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
visible: text !== ""
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: passwordField
|
||||
|
||||
width: parent.width
|
||||
height: inputFieldHeight
|
||||
backgroundColor: Theme.surfaceHover
|
||||
normalBorderColor: Theme.outlineStrong
|
||||
focusedBorderColor: Theme.primary
|
||||
borderWidth: 1
|
||||
focusedBorderWidth: 2
|
||||
leftIconName: polkitPamHasFprint ? "fingerprint" : ""
|
||||
leftIconSize: 20
|
||||
leftIconColor: Theme.primary
|
||||
leftIconFocusedColor: Theme.primary
|
||||
opacity: isLoading ? 0.5 : 1
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: passwordInput
|
||||
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
|
||||
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
|
||||
placeholderText: ""
|
||||
enabled: !isLoading
|
||||
onTextEdited: passwordInput = text
|
||||
onAccepted: submitAuth()
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Authentication failed, please try again")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
width: parent.width
|
||||
visible: currentFlow?.failed ?? false
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 36
|
||||
|
||||
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 ? Theme.surfaceTextHover : "transparent"
|
||||
border.color: Theme.surfaceVariantAlpha
|
||||
border.width: 1
|
||||
enabled: !isLoading
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
StyledText {
|
||||
id: cancelText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Cancel")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: cancelAuth()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, authText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: authArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
enabled: !isLoading
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
StyledText {
|
||||
id: authText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Authenticate")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: authArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: submitAuth()
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FloatingWindowControls {
|
||||
id: windowControls
|
||||
targetWindow: root
|
||||
}
|
||||
}
|
||||
|
||||
51
quickshell/Modals/PolkitAuthSurfaceModal.qml
Normal file
51
quickshell/Modals/PolkitAuthSurfaceModal.qml
Normal file
@@ -0,0 +1,51 @@
|
||||
import QtQuick
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
property var parentPopout: null
|
||||
|
||||
layerNamespace: "dms:polkit-auth-surface"
|
||||
modalWidth: 460
|
||||
modalHeight: 220
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
closeOnEscapeKey: true
|
||||
closeOnBackgroundClick: false
|
||||
allowStacking: true
|
||||
keepPopoutsOpen: true
|
||||
|
||||
onOpened: {
|
||||
if (parentPopout)
|
||||
parentPopout.customKeyboardFocus = WlrKeyboardFocus.None;
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item) {
|
||||
contentLoader.item.reset();
|
||||
contentLoader.item.focusPasswordField();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDialogClosed: {
|
||||
if (parentPopout)
|
||||
parentPopout.customKeyboardFocus = null;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PolkitService.agent
|
||||
enabled: PolkitService.polkitAvailable
|
||||
|
||||
function onIsActiveChanged() {
|
||||
if (!(PolkitService.agent?.isActive ?? false))
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
content: PolkitAuthContent {
|
||||
focus: true
|
||||
onCloseRequested: root.close()
|
||||
}
|
||||
}
|
||||
@@ -274,7 +274,7 @@ FloatingWindow {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
visible: windowControls.supported
|
||||
visible: windowControls.canMaximize
|
||||
circular: false
|
||||
iconName: processListModal.maximized ? "fullscreen_exit" : "fullscreen"
|
||||
iconSize: Theme.iconSize - 4
|
||||
|
||||
@@ -235,7 +235,7 @@ FloatingWindow {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
visible: windowControls.supported
|
||||
visible: windowControls.canMaximize
|
||||
circular: false
|
||||
iconName: settingsModal.maximized ? "fullscreen_exit" : "fullscreen"
|
||||
iconSize: Theme.iconSize - 4
|
||||
|
||||
@@ -381,7 +381,7 @@ FloatingWindow {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
visible: windowControls.supported && windowControls.canMaximize
|
||||
visible: windowControls.canMaximize
|
||||
iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -97,7 +96,7 @@ FloatingWindow {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
visible: windowControls.supported && windowControls.canMaximize
|
||||
visible: windowControls.canMaximize
|
||||
iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
@@ -213,5 +212,4 @@ FloatingWindow {
|
||||
id: windowControls
|
||||
targetWindow: root
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -85,8 +85,7 @@ Variants {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (typeof blurWallpaperWindow.updatesEnabled !== "undefined")
|
||||
blurWallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
|
||||
blurWallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -152,6 +152,20 @@ PanelWindow {
|
||||
onTriggered: barBlur.rebuild()
|
||||
}
|
||||
|
||||
Component {
|
||||
id: blurRegionComp
|
||||
Region {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: blurSubRegionComp
|
||||
Region {
|
||||
property Item w
|
||||
item: w
|
||||
radius: Theme.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: barBlur
|
||||
visible: false
|
||||
@@ -173,33 +187,32 @@ PanelWindow {
|
||||
if (!hasBar && widgets.length === 0)
|
||||
return;
|
||||
|
||||
const cr = Theme.cornerRadius;
|
||||
let qml = 'import QtQuick; import Quickshell; Region {';
|
||||
const region = blurRegionComp.createObject(barWindow);
|
||||
if (!region) {
|
||||
log.warn("BarBlur: Failed to create blur region");
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasBar) {
|
||||
region.x = Qt.binding(() => topBarMouseArea.x + barUnitInset.x + topBarSlide.x);
|
||||
region.y = Qt.binding(() => topBarMouseArea.y + barUnitInset.y + topBarSlide.y);
|
||||
region.width = Qt.binding(() => barUnitInset.width);
|
||||
region.height = Qt.binding(() => barUnitInset.height);
|
||||
region.radius = Qt.binding(() => barBackground.rt);
|
||||
}
|
||||
|
||||
const subRegions = [];
|
||||
for (let i = 0; i < widgets.length; i++) {
|
||||
qml += ` property Item w${i}; Region { item: w${i}; radius: ${cr} }`;
|
||||
const sub = blurSubRegionComp.createObject(region, {
|
||||
w: widgets[i]
|
||||
});
|
||||
if (sub)
|
||||
subRegions.push(sub);
|
||||
}
|
||||
qml += '}';
|
||||
region.regions = subRegions;
|
||||
|
||||
try {
|
||||
const region = Qt.createQmlObject(qml, barWindow, "BarBlurRegion");
|
||||
|
||||
if (hasBar) {
|
||||
region.x = Qt.binding(() => topBarMouseArea.x + barUnitInset.x + topBarSlide.x);
|
||||
region.y = Qt.binding(() => topBarMouseArea.y + barUnitInset.y + topBarSlide.y);
|
||||
region.width = Qt.binding(() => barUnitInset.width);
|
||||
region.height = Qt.binding(() => barUnitInset.height);
|
||||
region.radius = Qt.binding(() => barBackground.rt);
|
||||
}
|
||||
|
||||
for (let i = 0; i < widgets.length; i++) {
|
||||
region[`w${i}`] = widgets[i];
|
||||
}
|
||||
|
||||
barWindow.BackgroundEffect.blurRegion = region;
|
||||
barWindow.blurRegion = region;
|
||||
} catch (e) {
|
||||
log.warn("BarBlur: Failed to create blur region:", e);
|
||||
}
|
||||
barWindow.BackgroundEffect.blurRegion = region;
|
||||
barWindow.blurRegion = region;
|
||||
}
|
||||
|
||||
function teardown() {
|
||||
@@ -529,27 +542,17 @@ PanelWindow {
|
||||
implicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0
|
||||
color: "transparent"
|
||||
|
||||
property var nativeInhibitor: null
|
||||
|
||||
Component.onCompleted: {
|
||||
updateGpuTempConfig();
|
||||
_updateBackgroundAlpha();
|
||||
_updateHasMaximizedToplevel();
|
||||
_updateHasFullscreenToplevel();
|
||||
_updateShouldHideForWindows();
|
||||
|
||||
inhibitorInitTimer.start();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: inhibitorInitTimer
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (SessionService.nativeInhibitorAvailable) {
|
||||
createNativeInhibitor();
|
||||
}
|
||||
}
|
||||
IdleInhibitor {
|
||||
window: barWindow
|
||||
enabled: SessionService.idleInhibited
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -581,35 +584,6 @@ PanelWindow {
|
||||
DgopService.nonNvidiaGpuTempEnabled = hasGpuTempWidget || SessionData.nonNvidiaGpuTempEnabled;
|
||||
}
|
||||
|
||||
function createNativeInhibitor() {
|
||||
if (!SessionService.nativeInhibitorAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const qmlString = `
|
||||
import QtQuick
|
||||
import Quickshell.Wayland
|
||||
|
||||
IdleInhibitor {
|
||||
enabled: false
|
||||
}
|
||||
`;
|
||||
|
||||
nativeInhibitor = Qt.createQmlObject(qmlString, barWindow, "DankBar.NativeInhibitor");
|
||||
nativeInhibitor.window = barWindow;
|
||||
nativeInhibitor.enabled = Qt.binding(() => SessionService.idleInhibited);
|
||||
nativeInhibitor.enabledChanged.connect(function () {
|
||||
if (SessionService.idleInhibited !== nativeInhibitor.enabled) {
|
||||
SessionService.idleInhibited = nativeInhibitor.enabled;
|
||||
SessionService.inhibitorChanged();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
nativeInhibitor = null;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onBarConfigChanged() {
|
||||
barWindow.updateGpuTempConfig();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Modals
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
@@ -18,9 +19,23 @@ DankPopout {
|
||||
|
||||
property bool _reopenAfterUpgrade: false
|
||||
|
||||
readonly property bool polkitModalOpen: PopoutService.polkitAuthModal?.visible ?? false
|
||||
readonly property bool polkitModalOpen: polkitAuthSurfaceModal.shouldBeVisible
|
||||
readonly property bool anyModalOpen: polkitModalOpen
|
||||
|
||||
Connections {
|
||||
target: PolkitService.agent
|
||||
enabled: PolkitService.polkitAvailable && systemUpdatePopout.shouldBeVisible
|
||||
|
||||
function onAuthenticationRequestStarted() {
|
||||
polkitAuthSurfaceModal.open();
|
||||
}
|
||||
}
|
||||
|
||||
PolkitAuthSurfaceModal {
|
||||
id: polkitAuthSurfaceModal
|
||||
parentPopout: systemUpdatePopout
|
||||
}
|
||||
|
||||
backgroundInteractive: !anyModalOpen
|
||||
|
||||
customKeyboardFocus: {
|
||||
@@ -33,16 +48,6 @@ DankPopout {
|
||||
return WlrKeyboardFocus.Exclusive;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PolkitService.agent
|
||||
enabled: PolkitService.polkitAvailable && triggerScreen !== null
|
||||
|
||||
function onAuthenticationRequestStarted() {
|
||||
if (PopoutService.polkitAuthModal && triggerScreen)
|
||||
PopoutService.polkitAuthModal.screen = triggerScreen;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SystemUpdateService
|
||||
function onIsUpgradingChanged() {
|
||||
|
||||
@@ -1397,6 +1397,13 @@ BasePill {
|
||||
close();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: pendingActionCloseTimer
|
||||
interval: 80
|
||||
repeat: false
|
||||
onTriggered: menuRoot.closeWithAction()
|
||||
}
|
||||
|
||||
function showSubMenu(entry) {
|
||||
if (!entry || !entry.hasChildren)
|
||||
return;
|
||||
@@ -1853,7 +1860,7 @@ BasePill {
|
||||
} else if (typeof menuEntry.triggered === "function") {
|
||||
menuEntry.triggered();
|
||||
}
|
||||
Qt.createQmlObject('import QtQuick; Timer { interval: 80; running: true; repeat: false; onTriggered: menuRoot.closeWithAction() }', menuRoot);
|
||||
pendingActionCloseTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ PanelWindow {
|
||||
|
||||
screen: targetScreen
|
||||
visible: _frameActive
|
||||
updatesEnabled: _connectedActive
|
||||
|
||||
WlrLayershell.namespace: "dms:frame"
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
|
||||
@@ -4,11 +4,13 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property bool active: false
|
||||
property bool _completed: false
|
||||
|
||||
signal fadeCompleted
|
||||
signal fadeCancelled
|
||||
@@ -35,7 +37,8 @@ PanelWindow {
|
||||
opacity: 0
|
||||
|
||||
onOpacityChanged: {
|
||||
if (opacity >= 0.99 && root.active) {
|
||||
if (opacity >= 0.99 && root.active && !root._completed) {
|
||||
root._completed = true;
|
||||
root.fadeCompleted();
|
||||
}
|
||||
}
|
||||
@@ -58,6 +61,7 @@ PanelWindow {
|
||||
function startFade() {
|
||||
if (!SettingsData.fadeToLockEnabled)
|
||||
return;
|
||||
_completed = false;
|
||||
active = true;
|
||||
fadeOverlay.opacity = 0.0;
|
||||
fadeSeq.stop();
|
||||
@@ -65,12 +69,29 @@ PanelWindow {
|
||||
}
|
||||
|
||||
function cancelFade() {
|
||||
if (_completed)
|
||||
return;
|
||||
fadeSeq.stop();
|
||||
fadeOverlay.opacity = 0.0;
|
||||
active = false;
|
||||
fadeCancelled();
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
fadeSeq.stop();
|
||||
fadeOverlay.opacity = 0.0;
|
||||
active = false;
|
||||
_completed = false;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: IdleService
|
||||
function onIsShellLockedChanged() {
|
||||
if (!IdleService.isShellLocked && root._completed)
|
||||
root.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.active
|
||||
|
||||
@@ -15,7 +15,7 @@ Item {
|
||||
property bool inputEnabled: false
|
||||
property point lastMousePos: Qt.point(-1, -1)
|
||||
property bool mouseInitialized: false
|
||||
property var videoPlayer: null
|
||||
readonly property var videoPlayer: playerLoader.item
|
||||
|
||||
signal dismissed
|
||||
|
||||
@@ -27,6 +27,24 @@ Item {
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
visible: root.active
|
||||
|
||||
Loader {
|
||||
id: playerLoader
|
||||
anchors.fill: parent
|
||||
active: false
|
||||
source: "VideoScreensaverPlayer.qml"
|
||||
onLoaded: {
|
||||
item.errorOccurred.connect((error, errorString) => {
|
||||
log.warn("playback error:", errorString);
|
||||
ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("Playback error: ") + errorString);
|
||||
root.dismiss();
|
||||
});
|
||||
if (root.videoSource) {
|
||||
item.source = root.videoSource;
|
||||
item.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
@@ -82,43 +100,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function createVideoPlayer() {
|
||||
if (videoPlayer)
|
||||
return true;
|
||||
|
||||
try {
|
||||
videoPlayer = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
Video {
|
||||
anchors.fill: parent
|
||||
fillMode: VideoOutput.PreserveAspectCrop
|
||||
loops: MediaPlayer.Infinite
|
||||
volume: 0
|
||||
}
|
||||
`, background, "VideoScreensaver.VideoPlayer");
|
||||
|
||||
videoPlayer.errorOccurred.connect((error, errorString) => {
|
||||
log.warn("playback error:", errorString);
|
||||
ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("Playback error: ") + errorString);
|
||||
root.dismiss();
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
log.warn("Failed to create video player:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function destroyVideoPlayer() {
|
||||
if (videoPlayer) {
|
||||
videoPlayer.stop();
|
||||
videoPlayer.destroy();
|
||||
videoPlayer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (!SettingsData.lockScreenVideoEnabled || !SettingsData.lockScreenVideoPath)
|
||||
return;
|
||||
@@ -128,8 +109,12 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!createVideoPlayer())
|
||||
playerLoader.active = true;
|
||||
if (playerLoader.status === Loader.Error) {
|
||||
log.warn("Failed to load video player");
|
||||
playerLoader.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
videoPicker.result = "";
|
||||
videoPicker.folder = "";
|
||||
@@ -144,7 +129,9 @@ Item {
|
||||
function dismiss() {
|
||||
if (!active)
|
||||
return;
|
||||
destroyVideoPlayer();
|
||||
if (videoPlayer)
|
||||
videoPlayer.stop();
|
||||
playerLoader.active = false;
|
||||
inputEnabled = false;
|
||||
active = false;
|
||||
videoSource = "";
|
||||
|
||||
8
quickshell/Modules/Lock/VideoScreensaverPlayer.qml
Normal file
8
quickshell/Modules/Lock/VideoScreensaverPlayer.qml
Normal file
@@ -0,0 +1,8 @@
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
|
||||
Video {
|
||||
fillMode: VideoOutput.PreserveAspectCrop
|
||||
loops: MediaPlayer.Infinite
|
||||
volume: 0
|
||||
}
|
||||
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -230,44 +231,41 @@ Column {
|
||||
if (!inlinePreviewVisible || !textArea.text)
|
||||
return;
|
||||
const content = textArea.text;
|
||||
if (content.length > 0) {
|
||||
const proc = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
property string content: ""
|
||||
command: ["sh", "-c", "printf '%s' \\"$CONTENT\\" | dms clipboard copy"]
|
||||
environment: { "CONTENT": content }
|
||||
running: false
|
||||
}`, root, "copyProc");
|
||||
proc.content = content;
|
||||
proc.running = true;
|
||||
proc.exited.connect(() => {
|
||||
ToastService.showInfo(I18n.tr("Copied to clipboard"));
|
||||
proc.destroy();
|
||||
});
|
||||
}
|
||||
if (content.length === 0)
|
||||
return;
|
||||
const proc = clipboardCopyProcComp.createObject(root, {
|
||||
content: content,
|
||||
running: true
|
||||
});
|
||||
proc.exited.connect(() => {
|
||||
ToastService.showInfo(I18n.tr("Copied to clipboard"));
|
||||
proc.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
function copyHtmlToClipboard() {
|
||||
if (!inlinePreviewVisible || !pluginHighlightedHtml)
|
||||
return;
|
||||
if (pluginHighlightedHtml.length > 0) {
|
||||
const proc = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
property string content: ""
|
||||
command: ["sh", "-c", "printf '%s' \\"$CONTENT\\" | dms clipboard copy"]
|
||||
environment: { "CONTENT": content }
|
||||
running: false
|
||||
}`, root, "copyProcHtml");
|
||||
proc.content = pluginHighlightedHtml;
|
||||
proc.running = true;
|
||||
proc.exited.connect(() => {
|
||||
ToastService.showInfo(I18n.tr("HTML copied to clipboard"));
|
||||
proc.destroy();
|
||||
});
|
||||
if (pluginHighlightedHtml.length === 0)
|
||||
return;
|
||||
const proc = clipboardCopyProcComp.createObject(root, {
|
||||
content: pluginHighlightedHtml,
|
||||
running: true
|
||||
});
|
||||
proc.exited.connect(() => {
|
||||
ToastService.showInfo(I18n.tr("HTML copied to clipboard"));
|
||||
proc.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
Component {
|
||||
id: clipboardCopyProcComp
|
||||
Process {
|
||||
property string content: ""
|
||||
command: ["sh", "-c", "printf '%s' \"$CONTENT\" | dms clipboard copy"]
|
||||
environment: ({
|
||||
"CONTENT": content
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -586,10 +586,11 @@ PanelWindow {
|
||||
width: alignedWidth
|
||||
height: alignedHeight
|
||||
visible: !win._finalized && !chromeOnlyExit
|
||||
scale: (!win.inlineHeightAnimating && cardHoverHandler.hovered) ? 1.01 : 1.0
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Behavior on scale {
|
||||
property real chromeScale: (!win.inlineHeightAnimating && cardHoverHandler.hovered) ? 1.01 : 1.0
|
||||
|
||||
Behavior on chromeScale {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
@@ -650,6 +651,8 @@ PanelWindow {
|
||||
id: bgShadowLayer
|
||||
anchors.fill: parent
|
||||
anchors.margins: -content.shadowRenderPadding
|
||||
scale: content.chromeScale
|
||||
transformOrigin: Item.Center
|
||||
level: content.elevLevel
|
||||
fallbackOffset: 6
|
||||
shadowBlurPx: content.shadowBlurPx
|
||||
@@ -684,6 +687,8 @@ PanelWindow {
|
||||
visible: win.notificationData && win.notificationData.urgency === NotificationUrgency.Critical
|
||||
opacity: 1
|
||||
clip: true
|
||||
scale: content.chromeScale
|
||||
transformOrigin: Item.Center
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
@@ -713,6 +718,8 @@ PanelWindow {
|
||||
border.color: win.connectedFrameMode ? "transparent" : BlurService.borderColor
|
||||
border.width: win.connectedFrameMode ? 0 : BlurService.borderWidth
|
||||
z: 100
|
||||
scale: content.chromeScale
|
||||
transformOrigin: Item.Center
|
||||
}
|
||||
|
||||
Item {
|
||||
|
||||
@@ -15,6 +15,7 @@ FloatingWindow {
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
property var parentModal: null
|
||||
parentWindow: parentModal
|
||||
|
||||
signal widgetAdded(string widgetType)
|
||||
|
||||
@@ -233,7 +234,7 @@ FloatingWindow {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
visible: windowControls.supported
|
||||
visible: windowControls.canMaximize
|
||||
circular: false
|
||||
iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
|
||||
iconSize: Theme.iconSize - 4
|
||||
|
||||
@@ -17,6 +17,7 @@ FloatingWindow {
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool isLoading: false
|
||||
property var parentModal: null
|
||||
parentWindow: parentModal
|
||||
property bool pendingInstallHandled: false
|
||||
property string typeFilter: ""
|
||||
|
||||
@@ -295,7 +296,7 @@ FloatingWindow {
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
visible: windowControls.supported
|
||||
visible: windowControls.canMaximize
|
||||
iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: Theme.outline
|
||||
@@ -723,6 +724,7 @@ FloatingWindow {
|
||||
id: thirdPartyConfirmModal
|
||||
|
||||
property bool disablePopupTransparency: true
|
||||
parentWindow: root
|
||||
|
||||
function show() {
|
||||
visible = true;
|
||||
|
||||
@@ -370,14 +370,6 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Idle monitoring not supported - requires newer Quickshell version")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: !IdleService.idleMonitorAvailable
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
|
||||
@@ -17,6 +17,7 @@ FloatingWindow {
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool isLoading: false
|
||||
property var parentModal: null
|
||||
parentWindow: parentModal
|
||||
property bool pendingInstallHandled: false
|
||||
property string pendingApplyThemeId: ""
|
||||
|
||||
@@ -264,7 +265,7 @@ FloatingWindow {
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
visible: windowControls.supported
|
||||
visible: windowControls.canMaximize
|
||||
iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: Theme.outline
|
||||
|
||||
@@ -3064,6 +3064,7 @@ Item {
|
||||
|
||||
ThemeBrowser {
|
||||
id: themeBrowserItem
|
||||
parentModal: themeColorsTab.parentModal
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ FloatingWindow {
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
property var parentModal: null
|
||||
parentWindow: parentModal
|
||||
readonly property bool blurActive: Theme.blurForegroundLayers || Theme.transparentBlurLayers
|
||||
readonly property real surfaceAlpha: blurActive ? Math.min(Theme.popupTransparency, Theme.transparentBlurLayers ? 0.36 : 0.78) : 1.0
|
||||
readonly property real fieldAlpha: blurActive ? Math.min(Theme.popupTransparency, Theme.transparentBlurLayers ? 0.18 : 0.62) : 1.0
|
||||
@@ -238,7 +239,7 @@ FloatingWindow {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
visible: windowControls.supported
|
||||
visible: windowControls.canMaximize
|
||||
circular: false
|
||||
iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
|
||||
iconSize: Theme.iconSize - 4
|
||||
|
||||
@@ -222,8 +222,7 @@ Variants {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (typeof wallpaperWindow.updatesEnabled !== "undefined")
|
||||
wallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || root.overviewBlurActive || root._overviewBlurSettling || root.pendingWallpaper !== "" || root._deferredSource !== "" || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
|
||||
wallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || root.overviewBlurActive || root._overviewBlurSettling || root.pendingWallpaper !== "" || root._deferredSource !== "" || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
|
||||
|
||||
if (!source) {
|
||||
root._renderSettling = false;
|
||||
|
||||
@@ -22,17 +22,30 @@ Singleton {
|
||||
property string currentSoundTheme: ""
|
||||
property var soundFilePaths: ({})
|
||||
|
||||
property var volumeChangeSound: null
|
||||
property var powerPlugSound: null
|
||||
property var powerUnplugSound: null
|
||||
property var normalNotificationSound: null
|
||||
property var criticalNotificationSound: null
|
||||
property var loginSound: null
|
||||
readonly property var volumeChangeSound: soundsLoader.item?.volumeChangeSound ?? null
|
||||
readonly property var powerPlugSound: soundsLoader.item?.powerPlugSound ?? null
|
||||
readonly property var powerUnplugSound: soundsLoader.item?.powerUnplugSound ?? null
|
||||
readonly property var normalNotificationSound: soundsLoader.item?.normalNotificationSound ?? null
|
||||
readonly property var criticalNotificationSound: soundsLoader.item?.criticalNotificationSound ?? null
|
||||
readonly property var loginSound: soundsLoader.item?.loginSound ?? null
|
||||
readonly property var mediaDevices: soundsLoader.item?.mediaDevices ?? null
|
||||
property real notificationsVolume: 1.0
|
||||
property bool notificationsAudioMuted: false
|
||||
|
||||
property var mediaDevices: null
|
||||
property var mediaDevicesConnections: null
|
||||
Loader {
|
||||
id: soundsLoader
|
||||
active: root.soundsAvailable
|
||||
source: "AudioSoundPlayers.qml"
|
||||
onLoaded: {
|
||||
item.volume = Qt.binding(() => root.notificationsVolume);
|
||||
item.volumeChangeSource = Qt.binding(() => root.getSoundPath("audio-volume-change"));
|
||||
item.powerPlugSource = Qt.binding(() => root.getSoundPath("power-plug"));
|
||||
item.powerUnplugSource = Qt.binding(() => root.getSoundPath("power-unplug"));
|
||||
item.normalNotificationSource = Qt.binding(() => root.getSoundPath("message"));
|
||||
item.criticalNotificationSource = Qt.binding(() => root.getSoundPath("message-new-instant"));
|
||||
item.loginSource = Qt.binding(() => root.getSoundPath("desktop-login"));
|
||||
}
|
||||
}
|
||||
|
||||
property var deviceAliases: ({})
|
||||
property string wireplumberConfigPath: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/wireplumber/wireplumber.conf.d/51-dms-audio-aliases.conf"
|
||||
@@ -452,10 +465,6 @@ EOFCONFIG
|
||||
function discoverSoundFiles(themeName) {
|
||||
if (!themeName) {
|
||||
soundFilePaths = {};
|
||||
if (soundsAvailable) {
|
||||
destroySoundPlayers();
|
||||
createSoundPlayers();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -514,11 +523,6 @@ EOFCONFIG
|
||||
}
|
||||
}
|
||||
soundFilePaths = paths;
|
||||
|
||||
if (soundsAvailable) {
|
||||
destroySoundPlayers();
|
||||
createSoundPlayers();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@@ -559,159 +563,6 @@ EOFCONFIG
|
||||
discoverSoundFiles(currentSoundTheme);
|
||||
} else {
|
||||
soundFilePaths = {};
|
||||
if (soundsAvailable) {
|
||||
destroySoundPlayers();
|
||||
createSoundPlayers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupMediaDevices() {
|
||||
if (!soundsAvailable || mediaDevices) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mediaDevices = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
MediaDevices {
|
||||
id: devices
|
||||
Component.onCompleted: {
|
||||
log.debug("MediaDevices initialized, default output:", defaultAudioOutput?.description)
|
||||
}
|
||||
}
|
||||
`, root, "AudioService.MediaDevices");
|
||||
|
||||
if (mediaDevices) {
|
||||
mediaDevicesConnections = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
Connections {
|
||||
target: root.mediaDevices
|
||||
function onDefaultAudioOutputChanged() {
|
||||
log.debug("Default audio output changed, recreating sound players")
|
||||
root.destroySoundPlayers()
|
||||
root.createSoundPlayers()
|
||||
}
|
||||
}
|
||||
`, root, "AudioService.MediaDevicesConnections");
|
||||
}
|
||||
} catch (e) {
|
||||
log.debug("MediaDevices not available, using default audio output");
|
||||
mediaDevices = null;
|
||||
}
|
||||
}
|
||||
|
||||
function destroySoundPlayers() {
|
||||
if (volumeChangeSound) {
|
||||
volumeChangeSound.destroy();
|
||||
volumeChangeSound = null;
|
||||
}
|
||||
if (powerPlugSound) {
|
||||
powerPlugSound.destroy();
|
||||
powerPlugSound = null;
|
||||
}
|
||||
if (powerUnplugSound) {
|
||||
powerUnplugSound.destroy();
|
||||
powerUnplugSound = null;
|
||||
}
|
||||
if (normalNotificationSound) {
|
||||
normalNotificationSound.destroy();
|
||||
normalNotificationSound = null;
|
||||
}
|
||||
if (criticalNotificationSound) {
|
||||
criticalNotificationSound.destroy();
|
||||
criticalNotificationSound = null;
|
||||
}
|
||||
if (loginSound) {
|
||||
loginSound.destroy();
|
||||
loginSound = null;
|
||||
}
|
||||
}
|
||||
|
||||
function createSoundPlayers() {
|
||||
if (!soundsAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
setupMediaDevices();
|
||||
|
||||
try {
|
||||
const deviceProperty = mediaDevices ? `device: root.mediaDevices.defaultAudioOutput\n ` : "";
|
||||
|
||||
const volumeChangePath = getSoundPath("audio-volume-change");
|
||||
volumeChangeSound = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
MediaPlayer {
|
||||
source: "${volumeChangePath}"
|
||||
audioOutput: AudioOutput {
|
||||
${deviceProperty}volume: notificationsVolume
|
||||
}
|
||||
}
|
||||
`, root, "AudioService.VolumeChangeSound");
|
||||
|
||||
const powerPlugPath = getSoundPath("power-plug");
|
||||
powerPlugSound = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
MediaPlayer {
|
||||
source: "${powerPlugPath}"
|
||||
audioOutput: AudioOutput {
|
||||
${deviceProperty}volume: notificationsVolume
|
||||
}
|
||||
}
|
||||
`, root, "AudioService.PowerPlugSound");
|
||||
|
||||
const powerUnplugPath = getSoundPath("power-unplug");
|
||||
powerUnplugSound = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
MediaPlayer {
|
||||
source: "${powerUnplugPath}"
|
||||
audioOutput: AudioOutput {
|
||||
${deviceProperty}volume: notificationsVolume
|
||||
}
|
||||
}
|
||||
`, root, "AudioService.PowerUnplugSound");
|
||||
|
||||
const messagePath = getSoundPath("message");
|
||||
normalNotificationSound = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
MediaPlayer {
|
||||
source: "${messagePath}"
|
||||
audioOutput: AudioOutput {
|
||||
${deviceProperty}volume: notificationsVolume
|
||||
}
|
||||
}
|
||||
`, root, "AudioService.NormalNotificationSound");
|
||||
|
||||
const messageNewInstantPath = getSoundPath("message-new-instant");
|
||||
criticalNotificationSound = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
MediaPlayer {
|
||||
source: "${messageNewInstantPath}"
|
||||
audioOutput: AudioOutput {
|
||||
${deviceProperty}volume: notificationsVolume
|
||||
}
|
||||
}
|
||||
`, root, "AudioService.CriticalNotificationSound");
|
||||
|
||||
const loginPath = getSoundPath("desktop-login");
|
||||
loginSound = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
MediaPlayer {
|
||||
source: "${loginPath}"
|
||||
audioOutput: AudioOutput {
|
||||
${deviceProperty}volume: notificationsVolume
|
||||
}
|
||||
}
|
||||
`, root, "AudioService.LoginSound");
|
||||
} catch (e) {
|
||||
log.warn("Error creating sound players:", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -955,16 +806,6 @@ EOFCONFIG
|
||||
objects: Pipewire.nodes.values.filter(node => node.audio && !node.isStream)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Pipewire
|
||||
function onDefaultAudioSinkChanged() {
|
||||
if (soundsAvailable) {
|
||||
Qt.callLater(root.destroySoundPlayers);
|
||||
Qt.callLater(root.createSoundPlayers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setVolume(percentage) {
|
||||
if (!root.sink?.audio)
|
||||
return "No audio sink available";
|
||||
@@ -1127,10 +968,8 @@ EOFCONFIG
|
||||
Component.onCompleted: {
|
||||
rebuildTypedNodeLists();
|
||||
|
||||
if (soundsAvailable) {
|
||||
if (soundsAvailable)
|
||||
checkGsettings();
|
||||
Qt.callLater(createSoundPlayers);
|
||||
}
|
||||
|
||||
loadDeviceAliases();
|
||||
}
|
||||
|
||||
80
quickshell/Services/AudioSoundPlayers.qml
Normal file
80
quickshell/Services/AudioSoundPlayers.qml
Normal file
@@ -0,0 +1,80 @@
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real volume: 1.0
|
||||
property url volumeChangeSource
|
||||
property url powerPlugSource
|
||||
property url powerUnplugSource
|
||||
property url normalNotificationSource
|
||||
property url criticalNotificationSource
|
||||
property url loginSource
|
||||
|
||||
readonly property alias mediaDevices: devices
|
||||
readonly property alias volumeChangeSound: volumeChangePlayer
|
||||
readonly property alias powerPlugSound: powerPlugPlayer
|
||||
readonly property alias powerUnplugSound: powerUnplugPlayer
|
||||
readonly property alias normalNotificationSound: normalNotificationPlayer
|
||||
readonly property alias criticalNotificationSound: criticalNotificationPlayer
|
||||
readonly property alias loginSound: loginPlayer
|
||||
|
||||
MediaDevices {
|
||||
id: devices
|
||||
}
|
||||
|
||||
MediaPlayer {
|
||||
id: volumeChangePlayer
|
||||
source: root.volumeChangeSource
|
||||
audioOutput: AudioOutput {
|
||||
device: devices.defaultAudioOutput
|
||||
volume: root.volume
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer {
|
||||
id: powerPlugPlayer
|
||||
source: root.powerPlugSource
|
||||
audioOutput: AudioOutput {
|
||||
device: devices.defaultAudioOutput
|
||||
volume: root.volume
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer {
|
||||
id: powerUnplugPlayer
|
||||
source: root.powerUnplugSource
|
||||
audioOutput: AudioOutput {
|
||||
device: devices.defaultAudioOutput
|
||||
volume: root.volume
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer {
|
||||
id: normalNotificationPlayer
|
||||
source: root.normalNotificationSource
|
||||
audioOutput: AudioOutput {
|
||||
device: devices.defaultAudioOutput
|
||||
volume: root.volume
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer {
|
||||
id: criticalNotificationPlayer
|
||||
source: root.criticalNotificationSource
|
||||
audioOutput: AudioOutput {
|
||||
device: devices.defaultAudioOutput
|
||||
volume: root.volume
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer {
|
||||
id: loginPlayer
|
||||
source: root.loginSource
|
||||
audioOutput: AudioOutput {
|
||||
device: devices.defaultAudioOutput
|
||||
volume: root.volume
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland // ! Import is needed despite what qmlls says
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
@@ -12,9 +11,8 @@ Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("BlurService")
|
||||
|
||||
property bool quickshellSupported: false
|
||||
property bool compositorSupported: false
|
||||
property bool available: quickshellSupported && compositorSupported
|
||||
readonly property bool available: compositorSupported
|
||||
readonly property bool enabled: available && (SettingsData.blurEnabled ?? false)
|
||||
|
||||
readonly property color borderColor: {
|
||||
@@ -42,41 +40,6 @@ Singleton {
|
||||
return Theme.withAlpha(baseColor, hoverAlpha ?? 0.15);
|
||||
}
|
||||
|
||||
function createBlurRegion(targetWindow) {
|
||||
if (!available)
|
||||
return null;
|
||||
|
||||
try {
|
||||
const region = Qt.createQmlObject(`
|
||||
import Quickshell
|
||||
Region {}
|
||||
`, targetWindow, "BlurRegion");
|
||||
targetWindow.BackgroundEffect.blurRegion = region;
|
||||
return region;
|
||||
} catch (e) {
|
||||
log.warn("Failed to create blur region:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function reapplyBlurRegion(targetWindow, region) {
|
||||
if (!region || !available)
|
||||
return;
|
||||
try {
|
||||
targetWindow.BackgroundEffect.blurRegion = region;
|
||||
region.changed();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function destroyBlurRegion(targetWindow, region) {
|
||||
if (!region)
|
||||
return;
|
||||
try {
|
||||
targetWindow.BackgroundEffect.blurRegion = null;
|
||||
} catch (e) {}
|
||||
region.destroy();
|
||||
}
|
||||
|
||||
Process {
|
||||
id: blurProbe
|
||||
running: false
|
||||
@@ -98,18 +61,5 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
try {
|
||||
const test = Qt.createQmlObject(`
|
||||
import Quickshell
|
||||
Region { radius: 0 }
|
||||
`, root, "BlurAvailabilityTest");
|
||||
test.destroy();
|
||||
quickshellSupported = true;
|
||||
log.info("Quickshell blur support available");
|
||||
blurProbe.running = true;
|
||||
} catch (e) {
|
||||
log.info("BackgroundEffect not available - blur disabled. Requires a newer version of Quickshell.");
|
||||
}
|
||||
}
|
||||
Component.onCompleted: blurProbe.running = true
|
||||
}
|
||||
|
||||
@@ -11,25 +11,8 @@ Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("IdleService")
|
||||
|
||||
readonly property bool idleMonitorAvailable: {
|
||||
try {
|
||||
return typeof IdleMonitor !== "undefined";
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool idleInhibitorAvailable: {
|
||||
try {
|
||||
return typeof IdleInhibitor !== "undefined";
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
property bool enabled: true
|
||||
property bool respectInhibitors: true
|
||||
property bool _enableGate: true
|
||||
|
||||
readonly property bool externalInhibitActive: DMSService.screensaverInhibited
|
||||
|
||||
@@ -43,17 +26,28 @@ Singleton {
|
||||
|
||||
readonly property bool mediaPlaying: MprisController.activePlayer !== null && MprisController.activePlayer.isPlaying
|
||||
|
||||
onEnabledChanged: _applyMonitorEnableds()
|
||||
onPostLockMonitorActiveChanged: _applyMonitorEnableds()
|
||||
onMonitorTimeoutChanged: _rearmIdleMonitors()
|
||||
onLockTimeoutChanged: _rearmIdleMonitors()
|
||||
onSuspendTimeoutChanged: _rearmIdleMonitors()
|
||||
onPostLockMonitorTimeoutChanged: _rearmIdleMonitors()
|
||||
onIsShellLockedChanged: _rearmIdleMonitors()
|
||||
|
||||
function _applyMonitorEnableds() {
|
||||
const base = enabled;
|
||||
monitorOffMonitor.enabled = base && monitorTimeout > 0 && !postLockMonitorActive;
|
||||
postLockMonitorOffMonitor.enabled = base && postLockMonitorActive;
|
||||
lockMonitor.enabled = base && lockTimeout > 0;
|
||||
suspendMonitor.enabled = base && suspendTimeout > 0;
|
||||
}
|
||||
|
||||
function _rearmIdleMonitors() {
|
||||
_enableGate = false;
|
||||
Qt.callLater(() => {
|
||||
_enableGate = true;
|
||||
});
|
||||
monitorOffMonitor.enabled = false;
|
||||
postLockMonitorOffMonitor.enabled = false;
|
||||
lockMonitor.enabled = false;
|
||||
suspendMonitor.enabled = false;
|
||||
Qt.callLater(_applyMonitorEnableds);
|
||||
}
|
||||
|
||||
signal lockRequested
|
||||
@@ -65,10 +59,6 @@ Singleton {
|
||||
signal requestMonitorOn
|
||||
signal requestSuspend
|
||||
|
||||
property var monitorOffMonitor: null
|
||||
property var postLockMonitorOffMonitor: null
|
||||
property var lockMonitor: null
|
||||
property var suspendMonitor: null
|
||||
property var lockComponent: null
|
||||
property bool monitorsOff: false
|
||||
property bool isShellLocked: false
|
||||
@@ -82,84 +72,69 @@ Singleton {
|
||||
CompositorService.powerOffMonitors();
|
||||
}
|
||||
|
||||
function createIdleMonitors() {
|
||||
if (!idleMonitorAvailable) {
|
||||
log.info("IdleMonitor not available, skipping creation");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const qmlString = `
|
||||
import QtQuick
|
||||
import Quickshell.Wayland
|
||||
|
||||
IdleMonitor {
|
||||
enabled: false
|
||||
respectInhibitors: true
|
||||
timeout: 0
|
||||
}
|
||||
`;
|
||||
|
||||
monitorOffMonitor = Qt.createQmlObject(qmlString, root, "IdleService.MonitorOffMonitor");
|
||||
monitorOffMonitor.timeout = Qt.binding(() => root.monitorTimeout > 0 ? root.monitorTimeout : 86400);
|
||||
monitorOffMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors);
|
||||
monitorOffMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.monitorTimeout > 0 && !root.postLockMonitorActive);
|
||||
monitorOffMonitor.isIdleChanged.connect(function () {
|
||||
if (monitorOffMonitor.isIdle) {
|
||||
if (SettingsData.fadeToDpmsEnabled) {
|
||||
root.fadeToDpmsRequested();
|
||||
} else {
|
||||
root.requestMonitorOff();
|
||||
}
|
||||
IdleMonitor {
|
||||
id: monitorOffMonitor
|
||||
timeout: root.monitorTimeout > 0 ? root.monitorTimeout : 86400
|
||||
respectInhibitors: root.respectInhibitors
|
||||
enabled: false
|
||||
onIsIdleChanged: {
|
||||
if (isIdle) {
|
||||
if (SettingsData.fadeToDpmsEnabled) {
|
||||
root.fadeToDpmsRequested();
|
||||
} else {
|
||||
if (SettingsData.fadeToDpmsEnabled) {
|
||||
root.cancelFadeToDpms();
|
||||
}
|
||||
root.requestMonitorOn();
|
||||
}
|
||||
});
|
||||
|
||||
postLockMonitorOffMonitor = Qt.createQmlObject(qmlString, root, "IdleService.PostLockMonitorOffMonitor");
|
||||
postLockMonitorOffMonitor.timeout = Qt.binding(() => root.postLockMonitorTimeout > 0 ? root.postLockMonitorTimeout : 86400);
|
||||
postLockMonitorOffMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors);
|
||||
postLockMonitorOffMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.postLockMonitorActive);
|
||||
postLockMonitorOffMonitor.isIdleChanged.connect(function () {
|
||||
if (postLockMonitorOffMonitor.isIdle) {
|
||||
root.requestMonitorOff();
|
||||
} else {
|
||||
root.requestMonitorOn();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (SettingsData.fadeToDpmsEnabled) {
|
||||
root.cancelFadeToDpms();
|
||||
}
|
||||
root.requestMonitorOn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lockMonitor = Qt.createQmlObject(qmlString, root, "IdleService.LockMonitor");
|
||||
lockMonitor.timeout = Qt.binding(() => root.lockTimeout > 0 ? root.lockTimeout : 86400);
|
||||
lockMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors);
|
||||
lockMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.lockTimeout > 0);
|
||||
lockMonitor.isIdleChanged.connect(function () {
|
||||
if (lockMonitor.isIdle) {
|
||||
if (SettingsData.fadeToLockEnabled) {
|
||||
root.fadeToLockRequested();
|
||||
} else {
|
||||
root.lockRequested();
|
||||
}
|
||||
} else {
|
||||
if (SettingsData.fadeToLockEnabled) {
|
||||
root.cancelFadeToLock();
|
||||
}
|
||||
}
|
||||
});
|
||||
IdleMonitor {
|
||||
id: postLockMonitorOffMonitor
|
||||
timeout: root.postLockMonitorTimeout > 0 ? root.postLockMonitorTimeout : 86400
|
||||
respectInhibitors: root.respectInhibitors
|
||||
enabled: false
|
||||
onIsIdleChanged: {
|
||||
if (isIdle) {
|
||||
root.requestMonitorOff();
|
||||
} else {
|
||||
root.requestMonitorOn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspendMonitor = Qt.createQmlObject(qmlString, root, "IdleService.SuspendMonitor");
|
||||
suspendMonitor.timeout = Qt.binding(() => root.suspendTimeout > 0 ? root.suspendTimeout : 86400);
|
||||
suspendMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors);
|
||||
suspendMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.suspendTimeout > 0);
|
||||
suspendMonitor.isIdleChanged.connect(function () {
|
||||
if (suspendMonitor.isIdle) {
|
||||
root.requestSuspend();
|
||||
IdleMonitor {
|
||||
id: lockMonitor
|
||||
timeout: root.lockTimeout > 0 ? root.lockTimeout : 86400
|
||||
respectInhibitors: root.respectInhibitors
|
||||
enabled: false
|
||||
onIsIdleChanged: {
|
||||
if (isIdle) {
|
||||
if (SettingsData.fadeToLockEnabled) {
|
||||
root.fadeToLockRequested();
|
||||
} else {
|
||||
root.lockRequested();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
log.warn("Error creating IdleMonitors:", e);
|
||||
} else {
|
||||
if (SettingsData.fadeToLockEnabled) {
|
||||
root.cancelFadeToLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IdleMonitor {
|
||||
id: suspendMonitor
|
||||
timeout: root.suspendTimeout > 0 ? root.suspendTimeout : 86400
|
||||
respectInhibitors: root.respectInhibitors
|
||||
enabled: false
|
||||
onIsIdleChanged: {
|
||||
if (isIdle)
|
||||
root.requestSuspend();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,13 +169,7 @@ Singleton {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!idleMonitorAvailable) {
|
||||
log.warn("IdleMonitor not available - power management disabled. This requires a newer version of Quickshell.");
|
||||
} else {
|
||||
log.info("Initialized with idle monitoring support");
|
||||
createIdleMonitors();
|
||||
}
|
||||
|
||||
_applyMonitorEnableds();
|
||||
if (externalInhibitActive) {
|
||||
const apps = DMSService.screensaverInhibitors.map(i => i.appName).join(", ");
|
||||
SessionService.idleInhibited = true;
|
||||
|
||||
@@ -5,8 +5,6 @@ import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
// ! Even though qmlls says this is unused, it is wrong
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import "../Common/KeybindActions.js" as Actions
|
||||
@@ -15,21 +13,7 @@ Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("KeybindsService")
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!shortcutInhibitorAvailable) {
|
||||
log.warn("ShortcutInhibitor is not available in this environment, keybinds editor disabled.");
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool shortcutInhibitorAvailable: {
|
||||
try {
|
||||
return typeof ShortcutInhibitor !== "undefined";
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
property bool available: (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl) && shortcutInhibitorAvailable
|
||||
property bool available: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl
|
||||
property string currentProvider: {
|
||||
if (CompositorService.isNiri)
|
||||
return "niri";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
7
quickshell/Services/MultimediaProbe.qml
Normal file
7
quickshell/Services/MultimediaProbe.qml
Normal file
@@ -0,0 +1,7 @@
|
||||
import QtQuick
|
||||
// qmllint disable unused-imports
|
||||
import QtMultimedia
|
||||
|
||||
// qmllint enable unused-imports
|
||||
|
||||
Item {}
|
||||
@@ -8,30 +8,15 @@ Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("MultimediaService")
|
||||
|
||||
property bool available: false
|
||||
readonly property bool available: probeLoader.status === Loader.Ready
|
||||
|
||||
function detectAvailability() {
|
||||
try {
|
||||
const testObj = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
import qs.Services
|
||||
Item {}
|
||||
`, root, "MultimediaService.TestComponent");
|
||||
if (testObj) {
|
||||
testObj.destroy();
|
||||
}
|
||||
available = true;
|
||||
return true;
|
||||
} catch (e) {
|
||||
available = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!detectAvailability()) {
|
||||
log.warn("QtMultimedia not available");
|
||||
Loader {
|
||||
id: probeLoader
|
||||
source: "MultimediaProbe.qml"
|
||||
active: true
|
||||
onStatusChanged: {
|
||||
if (status === Loader.Error)
|
||||
log.warn("QtMultimedia not available");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import QtCore
|
||||
import QtQuick
|
||||
import Qt.labs.folderlistmodel
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
@@ -154,35 +155,43 @@ Singleton {
|
||||
}
|
||||
|
||||
function loadPluginManifestFile(manifestPathNoScheme, sourceTag, mtimeEpochMs) {
|
||||
const manifestId = "m_" + Math.random().toString(36).slice(2);
|
||||
const qml = `
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
FileView {
|
||||
id: fv
|
||||
property string absPath: ""
|
||||
onLoaded: {
|
||||
try {
|
||||
let raw = text()
|
||||
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1)
|
||||
const manifest = JSON.parse(raw)
|
||||
root._onManifestParsed(absPath, manifest, "${sourceTag}", ${mtimeEpochMs})
|
||||
} catch (e) {
|
||||
log.error("bad manifest", absPath, e.message)
|
||||
knownManifests[absPath] = { mtime: ${mtimeEpochMs}, source: "${sourceTag}", bad: true }
|
||||
}
|
||||
fv.destroy()
|
||||
}
|
||||
onLoadFailed: (err) => {
|
||||
log.warn("manifest load failed", absPath, err)
|
||||
fv.destroy()
|
||||
}
|
||||
}
|
||||
`;
|
||||
const loader = manifestFvComp.createObject(root, {
|
||||
absPath: manifestPathNoScheme,
|
||||
path: manifestPathNoScheme,
|
||||
sourceTag: sourceTag,
|
||||
mtimeEpochMs: mtimeEpochMs
|
||||
});
|
||||
}
|
||||
|
||||
const loader = Qt.createQmlObject(qml, root, "mf_" + manifestId);
|
||||
loader.absPath = manifestPathNoScheme;
|
||||
loader.path = manifestPathNoScheme;
|
||||
Component {
|
||||
id: manifestFvComp
|
||||
FileView {
|
||||
id: fv
|
||||
property string absPath: ""
|
||||
property string sourceTag: ""
|
||||
property double mtimeEpochMs: 0
|
||||
onLoaded: {
|
||||
try {
|
||||
let raw = text();
|
||||
if (raw.charCodeAt(0) === 0xFEFF)
|
||||
raw = raw.slice(1);
|
||||
const manifest = JSON.parse(raw);
|
||||
root._onManifestParsed(absPath, manifest, sourceTag, mtimeEpochMs);
|
||||
} catch (e) {
|
||||
root.log.error("bad manifest", absPath, e.message);
|
||||
root.knownManifests[absPath] = {
|
||||
mtime: mtimeEpochMs,
|
||||
source: sourceTag,
|
||||
bad: true
|
||||
};
|
||||
}
|
||||
fv.destroy();
|
||||
}
|
||||
onLoadFailed: err => {
|
||||
root.log.warn("manifest load failed", absPath, err);
|
||||
fv.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _onManifestParsed(absPath, manifest, sourceTag, mtimeEpochMs) {
|
||||
@@ -670,10 +679,10 @@ Singleton {
|
||||
_stateLoaded[pluginId] = true;
|
||||
_ensureStateDir();
|
||||
const path = getPluginStatePath(pluginId);
|
||||
const escapedPath = path.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
try {
|
||||
const qml = 'import QtQuick; import Quickshell.Io; FileView { path: "' + escapedPath + '"; blockLoading: true; blockWrites: true; atomicWrites: true }';
|
||||
const fv = Qt.createQmlObject(qml, root, "sf_" + pluginId);
|
||||
const fv = stateLoadFvComp.createObject(root, {
|
||||
path: path
|
||||
});
|
||||
const raw = fv.text();
|
||||
if (raw && raw.trim()) {
|
||||
_stateCache[pluginId] = JSON.parse(raw);
|
||||
@@ -694,10 +703,10 @@ Singleton {
|
||||
return;
|
||||
}
|
||||
const path = getPluginStatePath(pluginId);
|
||||
const escapedPath = path.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
try {
|
||||
const qml = 'import QtQuick; import Quickshell.Io; FileView { path: "' + escapedPath + '"; blockWrites: true; atomicWrites: true }';
|
||||
const fv = Qt.createQmlObject(qml, root, "sw_" + pluginId);
|
||||
const fv = stateSaveFvComp.createObject(root, {
|
||||
path: path
|
||||
});
|
||||
_stateWriters[pluginId] = fv;
|
||||
fv.loaded.connect(function () {
|
||||
fv.setText(content);
|
||||
@@ -710,6 +719,23 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: stateLoadFvComp
|
||||
FileView {
|
||||
blockLoading: true
|
||||
blockWrites: true
|
||||
atomicWrites: true
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: stateSaveFvComp
|
||||
FileView {
|
||||
blockWrites: true
|
||||
atomicWrites: true
|
||||
}
|
||||
}
|
||||
|
||||
function _flushDirtyStates() {
|
||||
const dirty = _stateDirtyPlugins;
|
||||
_stateDirtyPlugins = {};
|
||||
@@ -748,22 +774,8 @@ Singleton {
|
||||
}
|
||||
|
||||
function createPluginDirectory() {
|
||||
const mkdirProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { }");
|
||||
if (mkdirProcess.status === Component.Ready) {
|
||||
const process = mkdirProcess.createObject(root);
|
||||
process.command = ["mkdir", "-p", pluginDirectory];
|
||||
process.exited.connect(function (exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
log.error("Failed to create plugin directory, exit code:", exitCode);
|
||||
}
|
||||
process.destroy();
|
||||
});
|
||||
process.running = true;
|
||||
return true;
|
||||
} else {
|
||||
log.error("Failed to create mkdir process");
|
||||
return false;
|
||||
}
|
||||
Quickshell.execDetached(["mkdir", "-p", pluginDirectory]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Launcher plugin helper functions
|
||||
|
||||
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Polkit
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -10,33 +11,15 @@ Singleton {
|
||||
|
||||
readonly property bool disablePolkitIntegration: Quickshell.env("DMS_DISABLE_POLKIT") === "1"
|
||||
|
||||
property bool polkitAvailable: false
|
||||
property var agent: null
|
||||
readonly property bool polkitAvailable: !disablePolkitIntegration
|
||||
readonly property alias agent: polkitAgentInstance
|
||||
|
||||
function createPolkitAgent() {
|
||||
try {
|
||||
const qmlString = `
|
||||
import QtQuick
|
||||
import Quickshell.Services.Polkit
|
||||
import qs.Services
|
||||
|
||||
PolkitAgent {
|
||||
}
|
||||
`
|
||||
|
||||
agent = Qt.createQmlObject(qmlString, root, "PolkitService.Agent")
|
||||
polkitAvailable = true
|
||||
log.info("Initialized successfully")
|
||||
} catch (e) {
|
||||
polkitAvailable = false
|
||||
log.warn("Polkit not available - authentication prompts disabled. This requires a newer version of Quickshell.")
|
||||
}
|
||||
PolkitAgent {
|
||||
id: polkitAgentInstance
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (disablePolkitIntegration) {
|
||||
return
|
||||
}
|
||||
createPolkitAgent()
|
||||
if (!disablePolkitIntegration)
|
||||
log.info("Initialized successfully");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.I3
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
@@ -22,14 +21,6 @@ Singleton {
|
||||
property string inhibitReason: "Keep system awake"
|
||||
property string nvidiaCommand: ""
|
||||
|
||||
readonly property bool nativeInhibitorAvailable: {
|
||||
try {
|
||||
return typeof IdleInhibitor !== "undefined";
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
property bool loginctlAvailable: false
|
||||
property bool wtypeAvailable: false
|
||||
property string sessionId: ""
|
||||
@@ -66,8 +57,6 @@ Singleton {
|
||||
detectHibernateProcess.running = true;
|
||||
detectPrimeRunProcess.running = true;
|
||||
detectWtypeProcess.running = true;
|
||||
cleanupOrphanedInhibitors();
|
||||
log.info("Native inhibitor available:", nativeInhibitorAvailable);
|
||||
if (!SettingsData.loginctlLockIntegration) {
|
||||
log.debug("loginctl lock integration disabled by user");
|
||||
return;
|
||||
@@ -396,19 +385,15 @@ Singleton {
|
||||
signal inhibitorChanged
|
||||
|
||||
function enableIdleInhibit() {
|
||||
if (idleInhibited) {
|
||||
if (idleInhibited)
|
||||
return;
|
||||
}
|
||||
log.debug("Enabling idle inhibit (native:", nativeInhibitorAvailable, ")");
|
||||
idleInhibited = true;
|
||||
inhibitorChanged();
|
||||
}
|
||||
|
||||
function disableIdleInhibit() {
|
||||
if (!idleInhibited) {
|
||||
if (!idleInhibited)
|
||||
return;
|
||||
}
|
||||
log.debug("Disabling idle inhibit (native:", nativeInhibitorAvailable, ")");
|
||||
idleInhibited = false;
|
||||
inhibitorChanged();
|
||||
}
|
||||
@@ -423,64 +408,6 @@ Singleton {
|
||||
|
||||
function setInhibitReason(reason) {
|
||||
inhibitReason = reason;
|
||||
|
||||
if (idleInhibited && !nativeInhibitorAvailable) {
|
||||
const wasActive = idleInhibited;
|
||||
idleInhibited = false;
|
||||
|
||||
Qt.callLater(() => {
|
||||
if (wasActive) {
|
||||
idleInhibited = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: idleInhibitProcess
|
||||
|
||||
command: {
|
||||
if (!idleInhibited || nativeInhibitorAvailable) {
|
||||
return ["true"];
|
||||
}
|
||||
|
||||
log.debug("Starting systemd/elogind inhibit process");
|
||||
return [isElogind ? "elogind-inhibit" : "systemd-inhibit", "--what=idle", "--who=quickshell", `--why=${inhibitReason}`, "--mode=block", "sleep", "infinity"];
|
||||
}
|
||||
|
||||
running: idleInhibited && !nativeInhibitorAvailable
|
||||
|
||||
onRunningChanged: {
|
||||
log.debug("Inhibit process running:", running, "(native:", nativeInhibitorAvailable, ")");
|
||||
}
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (idleInhibited && exitCode !== 0 && !nativeInhibitorAvailable) {
|
||||
log.warn("Inhibitor process crashed with exit code:", exitCode);
|
||||
idleInhibited = false;
|
||||
ToastService.showWarning("Idle inhibitor failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kill orphaned idle inhibitor processes left behind by previous quickshell sessions.
|
||||
// When quickshell crashes or is force-killed, the child systemd-inhibit process gets
|
||||
// reparented to PID 1 and continues to block idle indefinitely.
|
||||
function cleanupOrphanedInhibitors() {
|
||||
if (nativeInhibitorAvailable) return;
|
||||
orphanCleanupProcess.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: orphanCleanupProcess
|
||||
running: false
|
||||
command: ["pkill", "-f", "systemd-inhibit --what=idle --who=quickshell.*sleep infinity"]
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode === 0) {
|
||||
log.info("Cleaned up orphaned idle inhibitor process(es) from a previous session");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
@@ -71,8 +71,13 @@ Item {
|
||||
PathCubic {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pathMoveComp
|
||||
PathMove {}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
shapePath.pathElements.push(Qt.createQmlObject('import QtQuick; import QtQuick.Shapes; PathMove {}', shapePath));
|
||||
shapePath.pathElements.push(pathMoveComp.createObject(shapePath));
|
||||
|
||||
for (let i = 0; i < segments; i++) {
|
||||
const seg = cubicSegment.createObject(shapePath);
|
||||
|
||||
@@ -182,14 +182,19 @@ Item {
|
||||
setBarContext(pos, bottomGap);
|
||||
}
|
||||
|
||||
// Briefly forces backgroundWindow.updatesEnabled true while the surface
|
||||
// body changes, so the contentHoleRect mask carve-out commits to the
|
||||
// compositor — otherwise the input region stays stuck at the popup's
|
||||
// initial size and clicks in any newly-grown area dismiss the popup.
|
||||
// Cleared by the frameSwapped Connections below as soon as the dirty
|
||||
// frame ships, so the bg window goes back to skipping buffer updates.
|
||||
// Holds backgroundWindow.updatesEnabled true while the surface body is
|
||||
// changing so the contentHoleRect mask carve-out tracks the popup body —
|
||||
// otherwise clicks in newly-grown areas hit the bg window and dismiss.
|
||||
// Debounced off ~250ms after the last change so a stable popup doesn't
|
||||
// keep the bg window in active-update mode.
|
||||
property bool _bgCommitWindow: false
|
||||
|
||||
Timer {
|
||||
id: bgCommitSettleTimer
|
||||
interval: 250
|
||||
onTriggered: root._bgCommitWindow = false
|
||||
}
|
||||
|
||||
function _setSurfaceGeometry(bodyX, bodyY, bodyW, bodyH) {
|
||||
const newX = Theme.snap(bodyX, dpr);
|
||||
const newY = Theme.snap(bodyY, dpr);
|
||||
@@ -206,15 +211,7 @@ Item {
|
||||
_surfaceH = _surfaceBodyH + shadowBuffer * 2;
|
||||
if (changed && backgroundWindow.visible) {
|
||||
_bgCommitWindow = true;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: backgroundWindow
|
||||
ignoreUnknownSignals: true
|
||||
function onFrameSwapped() {
|
||||
if (root._bgCommitWindow)
|
||||
root._bgCommitWindow = false;
|
||||
bgCommitSettleTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,31 +5,28 @@ Item {
|
||||
|
||||
readonly property real edgeSize: 8
|
||||
required property var targetWindow
|
||||
property bool supported: typeof targetWindow.startSystemMove === "function"
|
||||
readonly property bool canMaximize: targetWindow.minimumSize.width !== targetWindow.maximumSize.width || targetWindow.minimumSize.height !== targetWindow.maximumSize.height
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
function tryStartMove() {
|
||||
if (!supported)
|
||||
return;
|
||||
targetWindow.startSystemMove();
|
||||
}
|
||||
|
||||
function tryStartResize(edges) {
|
||||
if (!supported || !canMaximize)
|
||||
if (!canMaximize)
|
||||
return;
|
||||
targetWindow.startSystemResize(edges);
|
||||
}
|
||||
|
||||
function tryToggleMaximize() {
|
||||
if (!supported || !canMaximize)
|
||||
if (!canMaximize)
|
||||
return;
|
||||
targetWindow.maximized = !targetWindow.maximized;
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: root.supported && root.canMaximize
|
||||
visible: root.canMaximize
|
||||
height: root.edgeSize
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
@@ -41,7 +38,7 @@ Item {
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: root.supported && root.canMaximize
|
||||
visible: root.canMaximize
|
||||
width: root.edgeSize
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
@@ -53,7 +50,7 @@ Item {
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: root.supported && root.canMaximize
|
||||
visible: root.canMaximize
|
||||
width: root.edgeSize
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
@@ -65,7 +62,7 @@ Item {
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: root.supported && root.canMaximize
|
||||
visible: root.canMaximize
|
||||
width: root.edgeSize
|
||||
height: root.edgeSize
|
||||
anchors.left: parent.left
|
||||
@@ -75,7 +72,7 @@ Item {
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: root.supported && root.canMaximize
|
||||
visible: root.canMaximize
|
||||
width: root.edgeSize
|
||||
height: root.edgeSize
|
||||
anchors.right: parent.right
|
||||
@@ -85,7 +82,7 @@ Item {
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: root.supported && root.canMaximize
|
||||
visible: root.canMaximize
|
||||
height: root.edgeSize
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
@@ -97,7 +94,7 @@ Item {
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: root.supported && root.canMaximize
|
||||
visible: root.canMaximize
|
||||
width: root.edgeSize
|
||||
height: root.edgeSize
|
||||
anchors.left: parent.left
|
||||
@@ -107,7 +104,7 @@ Item {
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: root.supported && root.canMaximize
|
||||
visible: root.canMaximize
|
||||
width: root.edgeSize
|
||||
height: root.edgeSize
|
||||
anchors.right: parent.right
|
||||
|
||||
@@ -41,17 +41,8 @@ Item {
|
||||
property string _actionType: ""
|
||||
property bool addingNewKey: false
|
||||
property bool useCustomCompositor: false
|
||||
property var _shortcutInhibitor: null
|
||||
property bool _altShiftGhost: false
|
||||
|
||||
readonly property bool _shortcutInhibitorAvailable: {
|
||||
try {
|
||||
return typeof ShortcutInhibitor !== "undefined";
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var keys: bindData.keys || []
|
||||
readonly property bool hasOverride: {
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
@@ -251,41 +242,17 @@ Item {
|
||||
addingNewKey = false;
|
||||
}
|
||||
|
||||
function _createShortcutInhibitor() {
|
||||
if (!_shortcutInhibitorAvailable || _shortcutInhibitor)
|
||||
return;
|
||||
const qmlString = `
|
||||
import QtQuick
|
||||
import Quickshell.Wayland
|
||||
|
||||
ShortcutInhibitor {
|
||||
enabled: false
|
||||
window: null
|
||||
}
|
||||
`;
|
||||
|
||||
_shortcutInhibitor = Qt.createQmlObject(qmlString, root, "KeybindItem.ShortcutInhibitor");
|
||||
_shortcutInhibitor.enabled = Qt.binding(() => root.recording);
|
||||
_shortcutInhibitor.window = Qt.binding(() => root.panelWindow);
|
||||
}
|
||||
|
||||
function _destroyShortcutInhibitor() {
|
||||
if (_shortcutInhibitor) {
|
||||
_shortcutInhibitor.enabled = false;
|
||||
_shortcutInhibitor.destroy();
|
||||
_shortcutInhibitor = null;
|
||||
}
|
||||
ShortcutInhibitor {
|
||||
window: root.panelWindow
|
||||
enabled: root.recording
|
||||
}
|
||||
|
||||
function startRecording() {
|
||||
_destroyShortcutInhibitor();
|
||||
_createShortcutInhibitor();
|
||||
recording = true;
|
||||
}
|
||||
|
||||
function stopRecording() {
|
||||
recording = false;
|
||||
_destroyShortcutInhibitor();
|
||||
}
|
||||
|
||||
Column {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
@@ -8,7 +10,6 @@ Item {
|
||||
visible: false
|
||||
|
||||
required property var targetWindow
|
||||
property var blurItem: null
|
||||
property bool blurEnabled: Theme.connectedSurfaceBlurEnabled
|
||||
property real blurX: 0
|
||||
property real blurY: 0
|
||||
@@ -16,54 +17,38 @@ Item {
|
||||
property real blurHeight: 0
|
||||
property real blurRadius: 0
|
||||
|
||||
property var _region: null
|
||||
readonly property bool _active: blurEnabled && BlurService.enabled && !!targetWindow
|
||||
|
||||
Region {
|
||||
id: blurRegion
|
||||
x: root.blurX
|
||||
y: root.blurY
|
||||
width: root.blurWidth
|
||||
height: root.blurHeight
|
||||
radius: root.blurRadius
|
||||
}
|
||||
|
||||
function _apply() {
|
||||
if (!blurEnabled || !BlurService.enabled || !targetWindow) {
|
||||
_cleanup();
|
||||
if (!targetWindow)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_region)
|
||||
_region = BlurService.createBlurRegion(targetWindow);
|
||||
|
||||
if (!_region)
|
||||
return;
|
||||
|
||||
_region.item = Qt.binding(() => root.blurItem);
|
||||
_region.x = Qt.binding(() => root.blurX);
|
||||
_region.y = Qt.binding(() => root.blurY);
|
||||
_region.width = Qt.binding(() => root.blurWidth);
|
||||
_region.height = Qt.binding(() => root.blurHeight);
|
||||
_region.radius = Qt.binding(() => root.blurRadius);
|
||||
targetWindow.BackgroundEffect.blurRegion = _active ? blurRegion : null;
|
||||
}
|
||||
|
||||
function _cleanup() {
|
||||
if (!_region)
|
||||
return;
|
||||
BlurService.destroyBlurRegion(targetWindow, _region);
|
||||
_region = null;
|
||||
}
|
||||
|
||||
onBlurEnabledChanged: _apply()
|
||||
|
||||
Connections {
|
||||
target: BlurService
|
||||
function onEnabledChanged() {
|
||||
root._apply();
|
||||
}
|
||||
}
|
||||
on_ActiveChanged: _apply()
|
||||
onTargetWindowChanged: _apply()
|
||||
|
||||
Connections {
|
||||
target: root.targetWindow ?? null
|
||||
ignoreUnknownSignals: true
|
||||
function onVisibleChanged() {
|
||||
if (root.targetWindow && root.targetWindow.visible) {
|
||||
root._region = null;
|
||||
if (root.targetWindow && root.targetWindow.visible)
|
||||
root._apply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: _apply()
|
||||
Component.onDestruction: _cleanup()
|
||||
Component.onDestruction: {
|
||||
if (targetWindow)
|
||||
targetWindow.BackgroundEffect.blurRegion = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//@ pragma Env QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
||||
//@ pragma Env QT_QUICK_CONTROLS_STYLE=Material
|
||||
//@ pragma UseQApplication
|
||||
// ! TODO - replace pragma AppId when next QS releases, remove from GO launch injection.
|
||||
//@ pragma AppId com.danklinux.dms
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Reference in New Issue
Block a user