greeter: own widget list, state-driven sync page, clock typography fix

This commit is contained in:
bbedward
2026-10-05 13:39:49 -04:00
parent d11075d154
commit 9b7c0eddd7
30 changed files with 827 additions and 508 deletions
Generated
+3 -3
View File
@@ -3,11 +3,11 @@
"dank-qml-common": {
"flake": false,
"locked": {
"lastModified": 1791213890,
"narHash": "sha256-zVQmB7Bv4r7jeoWOOoMsOziqQTO9Owab0xD4pacRXQQ=",
"lastModified": 1791221960,
"narHash": "sha256-TWWR8Gv0ZBT/+byucBMNKdcjotsymgIBysE2qn2sces=",
"owner": "AvengeMedia",
"repo": "dank-qml-common",
"rev": "acc16579a112fd1f0c65e7ad297962682cd6b22a",
"rev": "44058322a4d3261293fa0f8d34933b1256cd5ba0",
"type": "github"
},
"original": {
+46
View File
@@ -294,6 +294,7 @@ Singleton {
property var lockScreenWidgetGridSettings: ({})
property var desktopWidgetInstancePositions: ({})
property var lockScreenAutoPositions: ({})
property var greeterAutoPositions: ({})
property var islandFreePositions: ({})
property var builtInPluginState: ({})
property bool greeterSyncPending: false
@@ -657,6 +658,14 @@ Singleton {
saveSettings();
}
function setGreeterAutoPositions(screenKey, placement) {
if (JSON.stringify(greeterAutoPositions[screenKey] ?? null) === JSON.stringify(placement))
return;
const updated = Object.assign({}, greeterAutoPositions);
updated[screenKey] = placement;
greeterAutoPositions = updated;
saveSettings();
}
function setLockScreenAutoPositions(screenKey, placement) {
if (JSON.stringify(lockScreenAutoPositions[screenKey] ?? null) === JSON.stringify(placement))
return;
@@ -666,6 +675,43 @@ Singleton {
saveSettings();
}
function copyDesktopWidgetInstancePositions(fromId, toId) {
const source = desktopWidgetInstancePositions[fromId];
if (!source)
return;
const updated = JSON.parse(JSON.stringify(desktopWidgetInstancePositions));
updated[toId] = JSON.parse(JSON.stringify(source));
desktopWidgetInstancePositions = updated;
saveSettings();
}
// Detached greeter clocks keep the spot the lock screen picked for them.
function pinPublishedLockPosition(fromId, toId, synced) {
const updated = JSON.parse(JSON.stringify(desktopWidgetInstancePositions));
let pinned = false;
for (const screenKey in lockScreenAutoPositions) {
const entry = lockScreenAutoPositions[screenKey];
const position = entry?.positions?.[fromId];
if (!position || !(entry.width > 0) || !(entry.height > 0))
continue;
const key = synced ? "_synced" : screenKey;
if (!updated[toId])
updated[toId] = {};
updated[toId][key] = Object.assign({}, updated[toId][key] || {}, synced ? {
x: position.x / entry.width,
y: position.y / entry.height
} : {
x: position.x,
y: position.y
});
pinned = true;
if (synced)
break;
}
if (!pinned)
return;
desktopWidgetInstancePositions = updated;
saveSettings();
}
function removeDesktopWidgetInstancePositions(instanceId) {
if (!(instanceId in desktopWidgetInstancePositions))
return;
+91 -8
View File
@@ -1113,13 +1113,15 @@ Singleton {
property var desktopWidgetInstances: Spec.SPEC.desktopWidgetInstances.def
property var desktopWidgetGroups: Spec.SPEC.desktopWidgetGroups.def
property var lockScreenWidgetInstances: Spec.SPEC.lockScreenWidgetInstances.def
readonly property var widgetInstanceListKeys: ["desktopWidgetInstances", "lockScreenWidgetInstances"]
property var greeterWidgetInstances: Spec.SPEC.greeterWidgetInstances.def
property bool greeterFollowLockScreen: Spec.SPEC.greeterFollowLockScreen.def
readonly property var widgetInstanceListKeys: ["desktopWidgetInstances", "lockScreenWidgetInstances", "greeterWidgetInstances"]
// The greeter still reads these three shared keys, so they follow the lock widgets.
// Released greeters still read these three shared keys, so they follow the lock widgets.
onLockScreenWidgetInstancesChanged: {
const status = lockWidgetInstance("lockStatus");
const auth = lockWidgetInstance("lockAuth");
const power = lockWidgetInstance("lockPower");
const status = widgetInstanceOfType("lockScreenWidgetInstances", "lockStatus");
const auth = widgetInstanceOfType("lockScreenWidgetInstances", "lockAuth");
const power = widgetInstanceOfType("lockScreenWidgetInstances", "lockPower");
const mirror = (key, value) => {
if (root[key] !== value)
set(key, value);
@@ -1127,10 +1129,52 @@ Singleton {
mirror("lockScreenShowWeather", !!status && status.enabled !== false && (status.config?.showWeather ?? true));
mirror("lockScreenShowProfileImage", !!auth && (auth.config?.showProfileImage ?? true));
mirror("lockScreenShowPowerActions", !!power && power.enabled !== false);
syncGreeterWidgets();
}
function widgetInstanceOfType(listKey, widgetType) {
return (root[listKey] || []).find(inst => inst.widgetType === widgetType) ?? null;
}
function lockWidgetInstance(widgetType) {
return (lockScreenWidgetInstances || []).find(inst => inst.widgetType === widgetType) ?? null;
return widgetInstanceOfType("lockScreenWidgetInstances", widgetType);
}
function syncGreeterWidgets() {
if (!greeterFollowLockScreen)
return;
const next = Spec.greeterWidgetsFromLock(lockScreenWidgetInstances, greeterWidgetInstances);
if (JSON.stringify(next) === JSON.stringify(greeterWidgetInstances))
return;
set("greeterWidgetInstances", next);
}
function setGreeterFollowLockScreen(follow) {
if (follow === greeterFollowLockScreen)
return;
if (follow) {
for (const inst of greeterWidgetInstances || []) {
if (inst.id.startsWith("gw_"))
SessionData.removeDesktopWidgetInstancePositions(inst.id);
}
set("greeterFollowLockScreen", true);
syncGreeterWidgets();
return;
}
const detached = (greeterWidgetInstances || []).map(inst => {
if (inst.widgetType === "greeterSession")
return inst;
const copy = JSON.parse(JSON.stringify(inst));
copy.id = "gw_" + inst.id;
SessionData.copyDesktopWidgetInstancePositions(inst.id, copy.id);
if (inst.widgetType !== "desktopClock" || inst.config?.autoPosition === false)
return copy;
copy.config.autoPosition = false;
SessionData.pinPublishedLockPosition(inst.id, copy.id, inst.config?.syncPositionAcrossScreens ?? false);
return copy;
});
set("greeterWidgetInstances", detached);
set("greeterFollowLockScreen", false);
}
function resetLockScreenWidgets() {
@@ -1141,6 +1185,22 @@ Singleton {
resetToDefault(["lockScreenWidgetInstances"]);
}
// Following means the lock layout is the greeter layout, so that is what resets.
function resetGreeterWidgets() {
for (const inst of greeterWidgetInstances || []) {
if (!greeterFollowLockScreen || inst.widgetType === "greeterSession")
SessionData.removeDesktopWidgetInstancePositions(inst.id);
}
if (greeterFollowLockScreen) {
resetLockScreenWidgets();
set("greeterWidgetInstances", Spec.greeterWidgetsFromLock(lockScreenWidgetInstances, []));
return;
}
set("greeterWidgetInstances", Spec.greeterWidgetDefaults().map(inst => inst.widgetType === "greeterSession" ? inst : Object.assign(inst, {
id: "gw_" + inst.id
})));
}
function getDefaultSystemMonitorConfig() {
return {
showHeader: true,
@@ -1174,10 +1234,16 @@ Singleton {
return widgetInstanceListKeys.find(key => (root[key] || []).some(inst => inst.id === instanceId)) ?? "desktopWidgetInstances";
}
readonly property var widgetInstanceIdPrefixes: ({
desktopWidgetInstances: "dw_",
lockScreenWidgetInstances: "lw_",
greeterWidgetInstances: "gw_"
})
function createDesktopWidgetInstance(widgetType, name, config, listKey = "desktopWidgetInstances") {
const lockScreen = listKey === "lockScreenWidgetInstances";
const lockScreen = listKey !== "desktopWidgetInstances";
const instance = {
id: (lockScreen ? "lw_" : "dw_") + Date.now() + "_" + Math.random().toString(36).substr(2, 9),
id: widgetInstanceIdPrefixes[listKey] + Date.now() + "_" + Math.random().toString(36).substr(2, 9),
widgetType: widgetType,
name: name || widgetType,
enabled: true,
@@ -1566,6 +1632,22 @@ Singleton {
SessionData.saveSettings();
}
// Older builds flagged keys the linked slot now serves live; drop them so Apply does not nag forever.
function pruneGreeterSyncPending() {
const baseline = SessionData.greeterSyncBaseline || {};
const keys = Object.keys(baseline);
const live = keys.filter(key => Spec.SPEC[key]?.onChange === "markGreeterSyncPending");
if (live.length === keys.length)
return;
const pruned = {};
for (const key of live)
pruned[key] = baseline[key];
SessionData.greeterSyncBaseline = pruned;
if (live.length === 0)
SessionData.greeterSyncPending = false;
SessionData.saveSettings();
}
function clearGreeterSyncPending() {
SessionData.greeterSyncBaseline = {};
SessionData.greeterSyncPending = false;
@@ -1778,6 +1860,7 @@ Singleton {
function _mergeSessionState() {
if (!_hasLoaded || !SessionData._hasLoaded)
return;
pruneGreeterSyncPending();
const pluginState = SessionData.builtInPluginState || {};
if (Object.keys(pluginState).length > 0) {
+1 -1
View File
@@ -369,7 +369,7 @@ Singleton {
function deferGreeterAutoLoginSyncToPill(details) {
toastCategoryDismissed("greeter-autologin-sync");
SessionData.set("greeterSyncPending", true);
toastRequested(1, I18n.tr("Auto-login change needs a sync"), I18n.tr("Administrator access is required. Use the Sync button in Settings → Greeter to apply.") + (details ? "\n\n" + details : ""), "dms-greeter sync --autologin", "greeter-autologin-sync");
toastRequested(1, I18n.tr("Auto-login change needs a sync"), I18n.tr("Administrator access is required. Press Apply changes in Settings → Greeter.") + (details ? "\n\n" + details : ""), "dms-greeter sync --autologin", "greeter-autologin-sync");
finishGreeterAutoLoginSync();
}
@@ -7,6 +7,18 @@ function lockWidgetDefaults() {
return Shared.lockWidgetDefaults();
}
function greeterWidgetsFromLock(lockInstances, previous) {
return Shared.greeterWidgetsFromLock(lockInstances, previous);
}
function greeterSessionDefault() {
return Shared.greeterSessionDefault();
}
function greeterWidgetDefaults() {
return Shared.greeterWidgetDefaults();
}
var LOCAL_SPEC = {
dockConfigs: {
def: [DockConfig.create("dock", "Dock")]
@@ -645,6 +657,9 @@ var LOCAL_SPEC = {
def: false,
onChange: "scheduleGreeterAutoLoginSync"
},
greeterFollowLockScreen: {
def: true
},
greeterPamExternallyManaged: {
def: false,
onChange: "markGreeterSyncPending"
@@ -3,150 +3,25 @@ import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import "../../DankCommon/Common/Hct.js" as Hct
Item {
DankClockWidget {
id: root
property real widgetWidth: 280
property real widgetHeight: 200
property string instanceId: ""
property var instanceData: null
property bool lockScreen: false
property var lockHost: null
readonly property var cfg: instanceData?.config ?? ({})
readonly property string face: {
switch (cfg.style ?? "analog") {
case "analog":
case "stacked":
case "expressive":
case "overlap":
return cfg.style;
}
return "digital";
}
readonly property bool analog: face === "analog"
readonly property bool resizable: true
property bool forceSquare: analog
readonly property real defaultScale: lockScreen ? 1.75 : 1
function baseWidth() {
switch (face) {
case "analog":
return 200;
case "stacked":
return 100;
case "overlap":
return 160;
case "expressive":
return 240;
}
return 160;
}
function baseHeight() {
switch (face) {
case "analog":
case "overlap":
return 200;
case "stacked":
return 160;
case "expressive":
return 120;
}
return 70;
}
property real defaultWidth: baseWidth() * defaultScale
property real defaultHeight: baseHeight() * defaultScale
property real minWidth: {
switch (face) {
case "analog":
return 120;
case "stacked":
return 70;
case "overlap":
return 80;
}
return 100;
}
property real minHeight: {
switch (face) {
case "analog":
return 120;
case "stacked":
case "overlap":
return 100;
}
return 45;
}
cfg: instanceData?.config ?? ({})
enabled: instanceData?.enabled ?? true
readonly property real transparency: cfg.transparency ?? (lockScreen ? 0 : 0.8)
readonly property string colorMode: cfg.colorMode ?? (lockScreen ? "default" : "primary")
readonly property color customColor: cfg.customColor ?? "#ffffff"
readonly property bool showDate: cfg.showDate ?? !lockScreen
readonly property bool twoTone: cfg.twoTone ?? lockScreen
readonly property bool showAnalogNumbers: cfg.showAnalogNumbers ?? false
readonly property bool showAnalogSeconds: cfg.showAnalogSeconds ?? true
readonly property bool showDigitalSeconds: cfg.showDigitalSeconds ?? false
readonly property bool italic: face === "digital" && (cfg.italic ?? false)
readonly property string fontFamily: cfg.fontFamily || (lockScreen ? SettingsData.lockScreenFontFamily : "")
readonly property int weight: cfg.weight ?? 0
readonly property int digitWeight: weight > 0 ? weight : Theme.fontWeightMedium
readonly property color accentColor: {
if (lockHost)
return lockHost.contentColor(colorMode, customColor);
switch (colorMode) {
case "secondary":
return Theme.secondary;
case "custom":
return customColor;
}
return Theme.primary;
}
readonly property color minutesColor: twoTone ? Theme.primary : accentColor
readonly property color dimColor: Theme.withAlpha(accentColor, 0.65)
readonly property color backgroundColor: Theme.withAlpha(Theme.hostSurface, transparency)
readonly property bool themedOverlap: lockScreen && colorMode === "default"
readonly property var primaryHct: Hct.toHct(Theme.primary)
readonly property color overlapFaceColor: {
if (!themedOverlap)
return accentColor;
return Theme.isLightMode ? Hct.fromHct(primaryHct.hue, primaryHct.chroma, 65) : Theme.accentOnPrimaryContainer;
}
readonly property color overlapShadowColor: {
if (themedOverlap)
return Theme.isLightMode ? Hct.fromHct(primaryHct.hue, primaryHct.chroma, 30) : Theme.primaryContainer;
const source = Hct.toHct(accentColor);
return Hct.fromHct(source.hue, source.chroma, source.tone > 50 ? 30 : 80);
}
readonly property var contrastColors: {
switch (face) {
case "overlap":
return [overlapFaceColor, overlapShadowColor];
case "digital":
case "stacked":
case "expressive":
return [accentColor, minutesColor];
}
return [accentColor];
}
readonly property bool needsSeconds: analog ? showAnalogSeconds : showDigitalSeconds
readonly property string hoursText: {
const hours = systemClock.date?.getHours() ?? 0;
const display = SettingsData.use24HourClock ? hours : (hours % 12 || 12);
return SettingsData.use24HourClock || SettingsData.padHours12Hour ? String(display).padStart(2, "0") : String(display);
}
readonly property string minutesText: String(systemClock.date?.getMinutes() ?? 0).padStart(2, "0")
readonly property string secondsText: String(systemClock.date?.getSeconds() ?? 0).padStart(2, "0")
readonly property string meridiem: (systemClock.date?.getHours() ?? 0) >= 12 ? "PM" : "AM"
readonly property string dateFormat: lockScreen ? SettingsData.lockDateFormat : SettingsData.clockDateFormat
readonly property string formattedDate: systemClock.date?.toLocaleDateString(I18n.locale(), dateFormat || "ddd, MMM d") ?? ""
now: systemClock.date
locale: I18n.locale()
use24HourClock: SettingsData.use24HourClock
padHours12Hour: SettingsData.padHours12Hour
dateFormat: lockScreen ? SettingsData.lockDateFormat : SettingsData.clockDateFormat
fallbackFontFamily: SettingsData.lockScreenFontFamily
accentColor: lockHost ? lockHost.contentColor(colorMode, customColor) : themeAccent
SystemClock {
id: systemClock
@@ -161,277 +36,4 @@ Item {
systemClock.enabled = true;
}
}
component DigitText: StyledText {
font.weight: root.digitWeight
font.italic: root.italic
font.family: root.fontFamily !== "" ? root.fontFamily : resolvedFontFamily
font.features: ({
"tnum": 1
})
color: root.accentColor
horizontalAlignment: Text.AlignHCenter
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: root.backgroundColor
visible: !root.analog
}
Loader {
anchors.fill: parent
anchors.margins: root.analog ? 0 : Theme.spacingM
sourceComponent: {
switch (root.face) {
case "analog":
return analogClock;
case "stacked":
return stackedClock;
case "expressive":
return expressiveClock;
case "overlap":
return overlapClock;
}
return digitalClock;
}
}
Component {
id: analogClock
DankAnalogClock {
hours: systemClock.date?.getHours() ?? 0
minutes: systemClock.date?.getMinutes() ?? 0
seconds: systemClock.date?.getSeconds() ?? 0
showSeconds: root.showAnalogSeconds
showNumbers: root.showAnalogNumbers
dateText: root.showDate ? root.formattedDate : ""
color: root.accentColor
backgroundColor: root.backgroundColor
}
}
Component {
id: expressiveClock
DankClockFace {
readonly property bool tallBox: height > width * 0.6
hours: root.hoursText
minutes: root.minutesText
seconds: root.showDigitalSeconds && !tallBox ? root.secondsText : ""
dateText: root.showDate ? root.formattedDate : ""
stacked: tallBox
color: root.accentColor
minutesColor: root.minutesColor
supportingColor: root.dimColor
}
}
Component {
id: overlapClock
Item {
DankOverlapClockFace {
anchors.centerIn: parent
width: implicitWidth
height: implicitHeight
hours: root.hoursText
minutes: root.minutesText
color: root.overlapFaceColor
shadowColor: root.overlapShadowColor
fontFamily: root.fontFamily
weight: root.weight > 0 ? root.weight : 1000
scale: implicitWidth > 0 && implicitHeight > 0 ? Math.min(parent.width / implicitWidth, parent.height / implicitHeight) : 1
renderScale: scale
}
}
}
Component {
id: digitalClock
Item {
id: digitalRoot
readonly property bool hasAmPm: !SettingsData.use24HourClock
readonly property real verticalScale: root.showDate && hasAmPm ? 0.55 : (root.showDate || hasAmPm ? 0.65 : 0.8)
readonly property real baseSize: Math.min(height * verticalScale, width * 0.22)
readonly property real digitWidth: baseSize * 0.62
readonly property real smallSize: baseSize * 0.35
Column {
anchors.centerIn: parent
spacing: 0
DigitText {
visible: root.showDate
anchors.horizontalCenter: parent.horizontalCenter
text: root.formattedDate
font.pixelSize: digitalRoot.smallSize
color: root.dimColor
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 0
Repeater {
model: root.hoursText.split("")
DigitText {
required property string modelData
width: digitalRoot.digitWidth
text: modelData
font.pixelSize: digitalRoot.baseSize
}
}
DigitText {
text: ":"
font.pixelSize: digitalRoot.baseSize
}
Repeater {
model: root.minutesText.split("")
DigitText {
required property string modelData
width: digitalRoot.digitWidth
text: modelData
font.pixelSize: digitalRoot.baseSize
color: root.minutesColor
}
}
DigitText {
visible: root.showDigitalSeconds
text: ":"
font.pixelSize: digitalRoot.baseSize
color: root.dimColor
}
Repeater {
model: root.showDigitalSeconds ? root.secondsText.split("") : []
DigitText {
required property string modelData
width: digitalRoot.digitWidth
text: modelData
font.pixelSize: digitalRoot.baseSize
color: root.dimColor
}
}
}
DigitText {
visible: digitalRoot.hasAmPm
anchors.horizontalCenter: parent.horizontalCenter
text: root.meridiem
font.pixelSize: digitalRoot.smallSize
color: root.dimColor
}
}
}
}
Component {
id: stackedClock
Item {
id: stackedRoot
readonly property bool hasAmPm: !SettingsData.use24HourClock
readonly property real extraContent: (root.showDigitalSeconds ? 0.12 : 0) + (root.showDate ? 0.08 : 0) + (hasAmPm ? 0.08 : 0)
readonly property real baseSize: height * (0.42 - extraContent * 0.5)
readonly property real digitWidth: baseSize * 0.58
readonly property real smallSize: baseSize * 0.5
readonly property real rowSpacing: -baseSize * 0.17
Column {
anchors.centerIn: parent
spacing: 0
Column {
spacing: stackedRoot.rowSpacing
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: root.hoursText.padStart(2, "0").split("")
DigitText {
required property string modelData
width: stackedRoot.digitWidth
text: modelData
font.pixelSize: stackedRoot.baseSize
}
}
}
Row {
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: root.minutesText.split("")
DigitText {
required property string modelData
width: stackedRoot.digitWidth
text: modelData
font.pixelSize: stackedRoot.baseSize
color: root.minutesColor
}
}
}
}
Row {
visible: root.showDigitalSeconds
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: root.showDigitalSeconds ? root.secondsText.split("") : []
DigitText {
required property string modelData
width: stackedRoot.smallSize * 0.58
text: modelData
font.pixelSize: stackedRoot.smallSize
color: root.dimColor
}
}
}
Item {
width: 1
height: stackedRoot.baseSize * 0.1
visible: root.showDate
}
DigitText {
visible: root.showDate
anchors.horizontalCenter: parent.horizontalCenter
text: systemClock.date?.toLocaleDateString(I18n.locale(), "MMM dd") ?? ""
font.pixelSize: stackedRoot.smallSize * 0.7
color: root.dimColor
}
DigitText {
visible: stackedRoot.hasAmPm
anchors.horizontalCenter: parent.horizontalCenter
text: root.meridiem
font.pixelSize: stackedRoot.smallSize * 0.7
color: root.dimColor
}
}
}
}
}
+6
View File
@@ -242,6 +242,12 @@ Scope {
demoWindow.showDemo();
}
function onGreeterEditorRequested() {
if (shouldLock)
return;
demoWindow.showDemo(true);
}
function onLoginctlStateChanged() {
if (SessionService.active && pendingLock) {
pendingLock = false;
@@ -21,6 +21,7 @@ FocusScope {
inputRevealRequested(revealed);
}
property bool demoMode: false
property bool greeterPreview: false
property var pam: demoPam
property string screenName: ""
property bool unlocking: false
@@ -155,6 +156,7 @@ FocusScope {
focus: true
screenName: root.screenName
lockScreen: true
greeter: root.greeterPreview
lockHost: root
editMode: root.demoMode
bottomInset: editorLoader.item?.fabReserved ?? 0
+5 -2
View File
@@ -10,9 +10,11 @@ Scope {
readonly property var log: Log.scoped("LockScreenDemo")
property bool demoActive: false
property bool greeterPreview: false
function showDemo(): void {
log.debug("Showing lock screen demo");
function showDemo(greeter = false): void {
log.debug("Showing lock screen demo", greeter ? "(greeter)" : "");
greeterPreview = greeter;
demoActive = true;
}
@@ -49,6 +51,7 @@ Scope {
LockScreenContent {
anchors.fill: parent
demoMode: true
greeterPreview: root.greeterPreview
screenName: demoWindow.screen?.name ?? ""
onUnlockRequested: root.hideDemo()
}
@@ -0,0 +1,39 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.DankCommon.Session
// Settings preview of the greeter session picker. The greeter renders the real dropdown.
Item {
id: root
property var instanceData: null
property var lockHost: null
readonly property bool resizable: false
readonly property real minWidth: implicitWidth
readonly property real minHeight: implicitHeight
readonly property string sessionName: CompositorService.displayName || I18n.tr("Session", "greeter session picker widget")
implicitWidth: Math.max(Theme.fieldDefaultWidth, sessionMetrics.width + Theme.buttonHeightM + Theme.spacingXL)
implicitHeight: LockMetrics.fieldHeight
StyledTextMetrics {
id: sessionMetrics
text: root.sessionName
}
DankDropdown {
anchors.fill: parent
text: ""
description: ""
backgroundColor: Theme.cardSurface
hoverBackgroundColor: Theme.blend(Theme.cardSurface, Theme.onSurface, Theme.stateLayerHover)
normalBorderColor: Theme.outlineMedium
currentValue: root.sessionName
options: [root.sessionName]
popupWidthOffset: 0
openUpwards: true
alignPopupRight: true
}
}
@@ -32,7 +32,7 @@ Item {
anchors.right: parent.right
anchors.margins: root.pad
interactive: !(root.lockHost?.demoMode ?? true)
showMediaPlayer: root.cfg.showMediaPlayer ?? true
showMediaPlayer: (root.cfg.showMediaPlayer ?? true) && !(root.lockHost?.greeterPreview ?? false)
showWeather: root.cfg.showWeather ?? true
useFahrenheit: SettingsData.useFahrenheit
}
@@ -33,6 +33,8 @@ Loader {
return lockStatusComponent;
case "lockPower":
return lockPowerComponent;
case "greeterSession":
return greeterSessionComponent;
}
return PluginService.pluginDesktopComponents[pluginId] ?? null;
}
@@ -72,6 +74,10 @@ Loader {
LockPowerWidget {}
}
property Component greeterSessionComponent: Component {
GreeterSessionWidget {}
}
onInstanceDataChanged: {
if (!item || item.instanceData === undefined)
return;
@@ -14,7 +14,7 @@ Item {
readonly property string instanceId: instanceData?.id ?? ""
readonly property string widgetType: instanceData?.widgetType ?? ""
readonly property bool lockScreen: hostLayer.lockScreen
readonly property bool removable: widgetType !== "lockAuth"
readonly property bool removable: widgetType !== "lockAuth" && widgetType !== "greeterSession"
readonly property bool selected: hostLayer.selectedInstanceId === instanceId
readonly property bool interacting: editChrome.item?.interacting ?? false
readonly property var contrastColors: content.item?.contrastColors ?? []
+19 -2
View File
@@ -12,13 +12,14 @@ FocusScope {
required property string screenName
property bool lockScreen: false
property bool greeter: false
property var lockHost: null
property bool editMode: false
property string selectedInstanceId: ""
readonly property var screen: Quickshell.screens.find(s => s.name === screenName) ?? null
readonly property string listKey: lockScreen ? "lockScreenWidgetInstances" : "desktopWidgetInstances"
readonly property var instances: (lockScreen ? SettingsData.lockScreenWidgetInstances : SettingsData.desktopWidgetInstances) || []
readonly property string listKey: greeter ? "greeterWidgetInstances" : (lockScreen ? "lockScreenWidgetInstances" : "desktopWidgetInstances")
readonly property var instances: (greeter ? SettingsData.greeterWidgetInstances : (lockScreen ? SettingsData.lockScreenWidgetInstances : SettingsData.desktopWidgetInstances)) || []
readonly property string screenKey: SettingsData.getScreenDisplayName(screen)
property var _gridSettingsTrigger: lockScreen ? SessionData.lockScreenWidgetGridSettings : SessionData.desktopWidgetGridSettings
readonly property int gridSize: {
@@ -133,6 +134,17 @@ FocusScope {
return LockPlacementService.layoutFor(screenName, LockPlacementService.sampleFor(screenName, backgroundKey), placementKey, placeClocks);
}
// The greeter list never renders outside the editor, so the editor publishes its auto placement.
onAutoPositionsChanged: {
if (!greeter || !editMode || !completed || interactingItem || width <= 0 || height <= 0)
return;
SessionData.setGreeterAutoPositions(screenKey, {
width: width,
height: height,
positions: autoPositions
});
}
function refreshPlacement() {
if (!lockScreen || !completed || editMode || width <= 0 || height <= 0)
return;
@@ -204,6 +216,11 @@ FocusScope {
x: Theme.spacingXL,
y: height - Theme.spacingXL - item.height
};
case "greeterSession":
return {
x: width - Theme.spacingXL - item.width,
y: height - Theme.spacingXL - item.height
};
}
return null;
}
@@ -14,12 +14,14 @@ FocusScope {
required property WidgetEditLayer editLayer
readonly property bool lockScreen: editLayer.lockScreen
readonly property bool greeter: editLayer.greeter
property bool libraryOpen: false
readonly property real fabReserved: fabBar.reservedHeight
readonly property var libraryWidgets: DesktopWidgetRegistry.registeredWidgetsList.filter(widget => {
readonly property var singletonTypes: ["lockAuth", "greeterSession"]
readonly property var libraryWidgets: DesktopWidgetRegistry.getListWidgets(editLayer.listKey).filter(widget => {
if (!lockScreen)
return !widget.lockOnly;
return widget.id !== "lockAuth" || !SettingsData.lockWidgetInstance("lockAuth");
return true;
return !singletonTypes.includes(widget.id) || !SettingsData.widgetInstanceOfType(editLayer.listKey, widget.id);
}).map(widget => ({
id: widget.id,
text: widget.name,
@@ -27,6 +29,14 @@ FocusScope {
description: widget.description
}))
readonly property string surfaceLabel: {
if (greeter)
return SettingsData.greeterFollowLockScreen ? I18n.tr("Editing the login screen, shared with the lock screen", "widget editor banner") : I18n.tr("Editing the login screen", "widget editor banner");
if (!lockScreen || !GreeterService.available)
return "";
return SettingsData.greeterFollowLockScreen && GreeterService.slotState !== "" ? I18n.tr("Editing the lock screen, the login screen follows it", "widget editor banner") : I18n.tr("Editing the lock screen", "widget editor banner");
}
signal finished
function closeLibrary() {
@@ -46,6 +56,10 @@ FocusScope {
}
function resetLayout() {
if (greeter) {
SettingsData.resetGreeterWidgets();
return;
}
if (lockScreen) {
SettingsData.resetLockScreenWidgets();
return;
@@ -119,7 +133,42 @@ FocusScope {
}
}
Rectangle {
visible: root.surfaceLabel !== "" && fabBar.shown
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: fabBar.reservedHeight + Theme.spacingL + (gridHint.visible ? gridHint.height + Theme.spacingS : 0)
width: surfaceRow.implicitWidth + Theme.spacingL * 2
height: Theme.buttonHeightM
radius: Theme.fullRadius(width, height)
color: Theme.readableSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Row {
id: surfaceRow
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: root.greeter ? "login" : "lock"
size: Theme.iconSizeMedium
color: Theme.primary
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: root.surfaceLabel
font.pixelSize: Theme.fontSizeMedium
font.weight: Theme.fontWeightMedium
color: Theme.surfaceText
}
}
}
DesktopWidgetGridHint {
id: gridHint
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: fabBar.reservedHeight + Theme.spacingL
@@ -23,7 +23,7 @@ WidgetPickerWindow {
objectName: "desktopWidgetBrowser"
title: I18n.tr("Add Desktop Widget")
widgets: (DesktopWidgetRegistry.registeredWidgetsList || []).filter(widget => !widget.lockOnly)
widgets: DesktopWidgetRegistry.getListWidgets(listKey).filter(widget => listKey === "desktopWidgetInstances" ? true : (widget.id !== "lockAuth" && widget.id !== "greeterSession"))
featuredFirst: true
showEmptyState: true
widgetDelegate: tileDelegate
@@ -41,6 +41,13 @@ DesktopWidgetInstanceSettings {
onSliderValueChanged: newValue => root.updateConfig("weight", newValue)
}
SettingsToggleRow {
visible: root.style === "expressive"
text: I18n.tr("Rounded")
checked: root.cfg.rounded ?? true
onToggled: checked => root.updateConfig("rounded", checked)
}
SettingsToggleRow {
visible: root.style === "digital"
text: I18n.tr("Italic", "clock font style toggle")
@@ -10,7 +10,9 @@ Column {
property bool showAppearance: true
property bool showPlacement: true
readonly property var cfg: instanceData?.config ?? {}
readonly property bool lockScreenInstance: SettingsData.widgetInstanceListKey(instanceId) === "lockScreenWidgetInstances"
readonly property string listKey: SettingsData.widgetInstanceListKey(instanceId)
readonly property bool lockScreenInstance: listKey !== "desktopWidgetInstances"
readonly property bool greeterInstance: listKey === "greeterWidgetInstances"
default property alias rows: optionsCard.content
function updateConfig(key, value) {
@@ -27,6 +27,8 @@ Loader {
return lockDateSettings;
case "lockPower":
return lockPowerSettings;
case "greeterSession":
return null;
default:
return pluginSettings;
}
@@ -14,7 +14,9 @@ Item {
property var parentModal: null
readonly property string instanceId: SettingsUiState.selectedDesktopWidgetId
readonly property bool lockScreenInstance: SettingsData.widgetInstanceListKey(instanceId) === "lockScreenWidgetInstances"
readonly property string listKey: SettingsData.widgetInstanceListKey(instanceId)
readonly property bool lockScreenInstance: listKey !== "desktopWidgetInstances"
readonly property bool greeterInstance: listKey === "greeterWidgetInstances"
readonly property var instanceData: SettingsData.getDesktopWidgetInstance(instanceId)
readonly property string widgetType: instanceData?.widgetType ?? ""
readonly property var widgetDef: DesktopWidgetRegistry.getWidget(widgetType)
@@ -20,7 +20,7 @@ Item {
switch (SettingsData.greeterFingerprintReason) {
case "ready":
return I18n.tr("Applies on the next greeter sync", "greeter auth setting description");
return I18n.tr("Press Apply changes to update the login screen", "greeter auth setting description");
case "missing_enrollment":
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.", "greeter fingerprint login setting");
case "missing_reader":
@@ -40,7 +40,7 @@ Item {
switch (SettingsData.greeterU2fReason) {
case "ready":
return I18n.tr("Applies on the next greeter sync", "greeter auth setting description");
return I18n.tr("Press Apply changes to update the login screen", "greeter auth setting description");
case "missing_key_registration":
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.", "security key setting status");
case "missing_pam_support":
@@ -12,15 +12,16 @@ SettingsFabBar {
shown: SessionData.greeterSyncPending && GreeterService.binaryExists
DankFab {
iconName: "close"
text: I18n.tr("Revert")
iconName: "undo"
colorRole: "secondaryContainer"
Accessible.name: I18n.tr("Dismiss")
enabled: !GreeterService.syncing
onClicked: SettingsData.revertGreeterSyncPending()
}
DankFab {
text: GreeterService.syncing ? I18n.tr("Syncing...", "greeter settings status while sync is running") : I18n.tr("Sync to apply")
iconName: "sync"
text: GreeterService.syncing ? I18n.tr("Syncing...", "greeter settings status while sync is running") : I18n.tr("Apply changes")
iconName: "check"
colorRole: "primary"
busy: GreeterService.syncing
enabled: !GreeterService.syncing && !root.blocked
+265 -61
View File
@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Modals.Common
@@ -20,6 +21,38 @@ Item {
id: greeterActionConfirm
}
ConfirmModal {
id: unlinkConfirm
}
ConfirmModal {
id: unlinkFinalConfirm
}
function promptUnlink() {
unlinkConfirm.showWithOptions({
"title": I18n.tr("Unlink login screen", "greeter advanced action"),
"message": I18n.tr("The login screen stops following your DMS theme, wallpaper, widgets and settings. greetd, authentication and other users are not affected.", "greeter unlink confirmation"),
"confirmText": I18n.tr("Unlink", "verb, greeter unlink button"),
"cancelText": I18n.tr("Cancel"),
"confirmColor": Theme.error,
"onConfirm": () => Qt.callLater(root.promptUnlinkFinal),
"onCancel": () => {}
});
}
function promptUnlinkFinal() {
unlinkFinalConfirm.showWithOptions({
"title": I18n.tr("Unlink now?", "greeter unlink second confirmation"),
"message": I18n.tr("Your greeter slot and any cache links to your home are removed. Administrator access may be required. Set up links them again.", "greeter unlink second confirmation"),
"confirmText": I18n.tr("Unlink", "verb, greeter unlink button"),
"cancelText": I18n.tr("Cancel"),
"confirmColor": Theme.error,
"onConfirm": () => GreeterService.unlink(),
"onCancel": () => {}
});
}
property string greeterStatusText: ""
property bool greeterStatusRunning: false
readonly property bool greeterSyncRunning: GreeterService.syncing
@@ -28,31 +61,93 @@ Item {
property string greeterStatusStdout: ""
property string greeterStatusStderr: ""
readonly property bool greeterBinaryExists: GreeterService.binaryExists
property bool greeterEnabled: false
readonly property bool greeterEnabled: GreeterService.enabled
property bool embeddedGreeterConfigured: false
readonly property bool embeddedGreeterOnly: embeddedGreeterConfigured && !greeterBinaryExists
readonly property string greeterAction: greeterBinaryExists && !greeterEnabled ? "activate" : ""
readonly property bool greeterActionAvailable: greeterAction !== ""
readonly property bool busy: greeterSyncRunning || greeterInstallActionRunning
readonly property string greeterActionLabel: greeterAction === "activate" ? I18n.tr("Activate") : ""
readonly property string greeterActionIcon: greeterAction === "activate" ? "login" : ""
readonly property var greeterActionCommand: greeterAction === "activate" ? ["dms-greeter", "enable", "--terminal"] : []
readonly property string greeterStatusOutput: {
if (greeterStatusRunning)
return I18n.tr("Checking...", "greeter status loading");
if (greeterStatusText !== "")
return greeterStatusText;
if (embeddedGreeterOnly)
readonly property string greeterState: {
if (!greeterBinaryExists)
return embeddedGreeterOnly ? "embedded" : "missing";
if (!greeterEnabled)
return "inactive";
switch (GreeterService.slotState) {
case "live":
return "linked";
case "snapshot":
return "snapshot";
}
return "unlinked";
}
readonly property bool greeterStateWarn: greeterState !== "linked"
readonly property string greeterStateIcon: {
switch (greeterState) {
case "linked":
return "link";
case "snapshot":
return "history";
case "inactive":
return "login";
case "unlinked":
return "link_off";
}
return "info";
}
readonly property string greeterStateTitle: {
switch (greeterState) {
case "linked":
return I18n.tr("Linked", "greeter status, login screen follows DMS settings live");
case "snapshot":
return I18n.tr("Linked as a snapshot", "greeter status, login screen reads a copy of DMS settings");
case "inactive":
return I18n.tr("Not active", "greeter status, greetd does not run the DMS greeter");
case "unlinked":
return I18n.tr("Not linked", "greeter status, no greeter slot for this user");
case "embedded":
return I18n.tr("Bundled greeter active", "greeter status");
}
return I18n.tr("Not installed", "greeter status");
}
readonly property string greeterStateSubtitle: {
switch (greeterState) {
case "linked":
return I18n.tr("The login screen follows your theme, wallpaper, widgets and behavior live. Only authentication changes need applying.", "greeter status");
case "snapshot":
return I18n.tr("This system cannot link settings live. Sync again after changing them.", "greeter status");
case "inactive":
return I18n.tr("greetd is not using the DMS greeter yet.", "greeter status");
case "unlinked":
return GreeterService.profileSyncSufficient ? I18n.tr("Link your DMS settings so the login screen follows them.", "greeter status") : I18n.tr("Link your DMS settings so the login screen follows them. Administrator access is required.", "greeter status");
case "embedded":
return I18n.tr("The greeter bundled with DMS is active (archinstall setup). It keeps working as is, but syncing theme and settings needs the standalone greeter. Install greetd-dms-greeter-bin from the AUR, then run Sync to migrate the login screen.", "embedded greeter status");
if (!greeterBinaryExists && greeterEnabled)
return I18n.tr("dms-greeter is not installed. Install the dms-greeter package to manage the greeter.", "greeter status placeholder");
}
return I18n.tr("dms-greeter is not installed. Install the dms-greeter package to manage the greeter.", "greeter status placeholder");
}
readonly property string greeterStateAction: {
switch (greeterState) {
case "inactive":
return I18n.tr("Activate");
case "unlinked":
return I18n.tr("Set up", "greeter setup button, links DMS settings to the login screen");
case "snapshot":
return I18n.tr("Sync", "verb, button that copies settings to the login greeter");
}
return "";
}
function runStateAction() {
if (greeterState === "inactive") {
promptGreeterActionConfirm();
return;
}
GreeterService.link();
}
readonly property string greeterStatusOutput: greeterStatusRunning ? I18n.tr("Checking...", "greeter status loading") : greeterStatusText
onGreeterSyncStatusChanged: greeterStatusText = greeterSyncStatus
function checkGreeterInstallState() {
greetdEnabledCheckProcess.running = true;
GreeterService.refresh();
embeddedGreeterCheckProcess.running = true;
}
@@ -66,15 +161,12 @@ Item {
}
function runGreeterInstallAction() {
greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + "...";
greeterStatusText = I18n.tr("Opening terminal: ") + I18n.tr("Activate") + "...";
greeterInstallActionRunning = true;
greeterInstallActionProcess.running = true;
}
function promptGreeterActionConfirm() {
if (!root.greeterActionAvailable)
return;
greeterActionConfirm.showWithOptions({
"title": I18n.tr("Activate Greeter", "greeter action confirmation"),
"message": I18n.tr("Activate the DMS greeter? A terminal will open for sudo authentication. Run Sync after activation to apply your settings."),
@@ -90,13 +182,20 @@ Item {
Qt.callLater(checkGreeterInstallState);
}
Process {
id: greetdEnabledCheckProcess
command: ["systemctl", "is-enabled", "greetd"]
running: false
function showWidgetBrowser() {
greeterWidgetBrowserLoader.active = true;
greeterWidgetBrowserLoader.item?.show();
}
stdout: StdioCollector {
onStreamFinished: root.greeterEnabled = text.trim() === "enabled"
LazyLoader {
id: greeterWidgetBrowserLoader
active: false
DesktopWidgetBrowser {
parentModal: root.parentModal
listKey: "greeterWidgetInstances"
title: I18n.tr("Add widget")
onWidgetAdded: ToastService.showInfo(I18n.tr("Widget added"))
}
}
@@ -155,7 +254,7 @@ Item {
Process {
id: greeterInstallActionProcess
command: root.greeterActionCommand
command: ["dms-greeter", "enable", "--terminal"]
running: false
onExited: exitCode => {
@@ -177,42 +276,21 @@ Item {
iconName: "info"
title: I18n.tr("Status")
settingKey: "greeterStatus"
tags: ["greeter", "login", "sync", "status", "link"]
SettingsRow {
subtitle: I18n.tr("Sync applies your theme and settings to the login screen. Shared users should run dms-greeter sync --profile instead of a primary user sync.")
iconName: root.greeterStateIcon
iconColor: root.greeterStateWarn ? Theme.warning : Theme.primary
title: root.greeterStateTitle
subtitle: root.greeterStateSubtitle
body: Flow {
width: parent.width
spacing: Theme.spacingS
layoutDirection: Qt.RightToLeft
DankButton {
text: I18n.tr("Sync", "verb, button that copies settings to the login greeter")
iconName: "sync"
busy: root.greeterSyncRunning
enabled: root.greeterBinaryExists && !root.greeterSyncRunning && !root.greeterInstallActionRunning
onClicked: GreeterService.sync()
}
DankButton {
text: I18n.tr("Check status", "greeter settings button, runs dms-greeter status")
iconName: "fact_check"
backgroundColor: Theme.secondaryContainer
textColor: Theme.onSecondaryContainer
busy: root.greeterStatusRunning
enabled: !root.greeterStatusRunning
onClicked: root.runGreeterStatus()
}
DankButton {
visible: root.greeterActionAvailable
text: root.greeterActionLabel
iconName: root.greeterActionIcon
backgroundColor: Theme.secondaryContainer
textColor: Theme.onSecondaryContainer
enabled: !root.greeterInstallActionRunning && !root.greeterSyncRunning
onClicked: root.promptGreeterActionConfirm()
}
DankButton {
visible: root.greeterStateAction !== ""
anchors.verticalCenter: parent.verticalCenter
text: root.greeterStateAction
busy: root.greeterSyncRunning
enabled: !root.busy
onClicked: root.runStateAction()
}
}
@@ -220,6 +298,7 @@ Item {
visible: root.greeterStatusOutput !== ""
noteIconName: ""
monospace: true
maxHeight: SettingsMetrics.noteMaxHeight
text: root.greeterStatusOutput
tint: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText
tintBackground: SettingsMetrics.controlColor
@@ -236,6 +315,57 @@ Item {
}
}
SettingsCard {
width: parent.width
iconName: "widgets"
title: I18n.tr("Widgets")
settingKey: "greeterWidgets"
tags: ["greeter", "login", "widgets", "clock", "session", "layout"]
SettingsToggleRow {
settingKey: "greeterFollowLockScreen"
tags: ["greeter", "login", "lock", "widgets", "layout", "follow"]
text: I18n.tr("Follow lock screen", "greeter widgets toggle")
description: I18n.tr("The login screen shows the lock screen widgets it supports and its own session picker. Turn off to arrange it separately.", "greeter widgets toggle")
checked: SettingsData.greeterFollowLockScreen
onToggled: checked => SettingsData.setGreeterFollowLockScreen(checked)
}
SettingsReorderList {
id: greeterWidgetList
model: SettingsData.greeterWidgetInstances || []
delegate: DesktopWidgetInstanceCard {
required property var modelData
reorderList: greeterWidgetList
reorderEnabled: false
instanceData: modelData
fixed: SettingsData.greeterFollowLockScreen || modelData.widgetType === "lockAuth" || modelData.widgetType === "greeterSession"
onConfigureRequested: {
SettingsUiState.selectedDesktopWidgetId = instanceId;
SettingsUiState.selectedWidgetTitle = widgetName;
root.parentModal?.navigateTo("desktop_widget");
}
onDuplicateRequested: SettingsData.duplicateDesktopWidgetInstance(instanceId)
onDeleteRequested: {
SettingsData.removeDesktopWidgetInstance(instanceId);
ToastService.showInfo(I18n.tr("Widget removed"));
}
}
}
SettingsRow {
iconName: "restart_alt"
title: I18n.tr("Reset to default")
visible: !SettingsData.greeterFollowLockScreen
clickable: true
onClicked: SettingsData.resetGreeterWidgets()
}
}
SettingsCard {
title: I18n.tr("Appearance")
settingKey: "greeterAppearance"
@@ -297,6 +427,44 @@ Item {
}
}
SettingsCard {
width: parent.width
iconName: "tune"
title: I18n.tr("Advanced")
settingKey: "greeterAdvanced"
tags: ["greeter", "login", "sync", "status", "repair"]
collapsible: true
expanded: false
SettingsRow {
iconName: "sync"
title: I18n.tr("Run full sync", "greeter advanced action")
subtitle: I18n.tr("Re-applies the greetd command, permissions and PAM authentication. Needs administrator access.", "greeter advanced action")
clickable: true
enabled: root.greeterBinaryExists && !root.busy
onClicked: GreeterService.sync()
}
SettingsRow {
iconName: "fact_check"
title: I18n.tr("Check status", "greeter settings button, runs dms-greeter status")
clickable: true
enabled: root.greeterBinaryExists && !root.greeterStatusRunning
onClicked: root.runGreeterStatus()
}
SettingsRow {
iconName: "link_off"
iconColor: Theme.error
title: I18n.tr("Unlink login screen", "greeter advanced action")
subtitle: I18n.tr("Stops syncing your DMS settings to the greeter. Set up links them again.", "greeter advanced action")
visible: GreeterService.slotState !== ""
clickable: true
enabled: root.greeterBinaryExists && !root.busy
onClicked: root.promptUnlink()
}
}
SettingsCard {
width: parent.width
iconName: "extension"
@@ -336,8 +504,44 @@ Item {
}
}
GreeterSyncFabBar {
blocked: root.greeterInstallActionRunning
SettingsFabBar {
id: greeterFabs
readonly property bool syncPending: SessionData.greeterSyncPending && GreeterService.binaryExists
DankFab {
visible: greeterFabs.syncPending
text: I18n.tr("Revert")
iconName: "undo"
colorRole: "secondaryContainer"
enabled: !GreeterService.syncing
onClicked: SettingsData.revertGreeterSyncPending()
}
DankFab {
visible: greeterFabs.syncPending
text: GreeterService.syncing ? I18n.tr("Syncing...", "greeter settings status while sync is running") : I18n.tr("Apply changes")
iconName: "check"
colorRole: "primary"
busy: GreeterService.syncing
enabled: !root.busy
onClicked: GreeterService.sync()
}
DankFab {
visible: !greeterFabs.syncPending
text: I18n.tr("Edit widgets")
iconName: "edit"
colorRole: SettingsData.greeterFollowLockScreen ? "primary" : "secondaryContainer"
onClicked: SessionService.greeterEditorRequested()
}
DankFab {
visible: !greeterFabs.syncPending && !SettingsData.greeterFollowLockScreen
text: I18n.tr("Add widget")
iconName: "add"
onClicked: root.showWidgetBrowser()
}
}
}
}
@@ -17,6 +17,7 @@ DesktopWidgetInstanceSettings {
}
SettingsToggleRow {
visible: !root.greeterInstance
text: I18n.tr("Show media player")
checked: root.cfg.showMediaPlayer ?? true
onToggled: checked => root.updateConfig("showMediaPlayer", checked)
+41 -6
View File
@@ -44,6 +44,7 @@ Singleton {
type: "builtin",
component: "qs.Modules.BuiltinDesktopPlugins.DesktopClockWidget",
settingsComponent: "qs.Modules.Settings.DesktopWidgetSettings.ClockSettings",
greeter: true,
defaultConfig: getDefaultClockConfig(),
defaultSize: {
width: 280,
@@ -68,24 +69,40 @@ Singleton {
}
function registerLockBuiltins() {
const lockWidget = (id, name, icon) => registerWidget({
const lockWidget = (id, name, icon, greeter) => registerWidget({
id: id,
name: name,
icon: icon,
description: "",
type: "builtin",
lockOnly: true,
greeter: greeter,
defaultConfig: Spec.SPEC.lockScreenWidgetInstances.def.find(inst => inst.widgetType === id)?.config ?? {},
defaultSize: {
width: 200,
height: 200
}
});
lockWidget("lockDate", I18n.tr("Date"), "event");
lockWidget("lockAuth", I18n.tr("Password"), "lock");
lockWidget("lockNotifications", I18n.tr("Notifications"), "notifications");
lockWidget("lockStatus", I18n.tr("Status"), "wifi");
lockWidget("lockPower", I18n.tr("Power"), "power_settings_new");
lockWidget("lockDate", I18n.tr("Date"), "event", true);
lockWidget("lockAuth", I18n.tr("Password"), "lock", true);
lockWidget("lockNotifications", I18n.tr("Notifications"), "notifications", false);
lockWidget("lockStatus", I18n.tr("Status"), "wifi", true);
lockWidget("lockPower", I18n.tr("Power"), "power_settings_new", true);
registerWidget({
id: "greeterSession",
name: I18n.tr("Session", "greeter session picker widget"),
icon: "login",
description: "",
type: "builtin",
lockOnly: true,
greeterOnly: true,
greeter: true,
defaultConfig: Spec.greeterSessionDefault().config,
defaultSize: {
width: 200,
height: 48
}
});
}
function getDefaultClockConfig() {
@@ -255,6 +272,24 @@ Singleton {
return registeredWidgetsList.filter(w => !w.lockOnly);
}
function getLockWidgets() {
return registeredWidgetsList.filter(w => !w.greeterOnly);
}
function getGreeterWidgets() {
return registeredWidgetsList.filter(w => w.greeter);
}
function getListWidgets(listKey) {
switch (listKey) {
case "lockScreenWidgetInstances":
return getLockWidgets();
case "greeterWidgetInstances":
return getGreeterWidgets();
}
return getDesktopWidgets();
}
function getPluginWidgets() {
return registeredWidgetsList.filter(w => w.type === "plugin");
}
+110 -2
View File
@@ -12,10 +12,18 @@ Singleton {
property bool available: false
property bool binaryExists: false
property bool configured: false
property bool enabled: false
property bool inGroup: false
// "live" when the greeter slot symlinks this user's DMS files, "snapshot" for a copied slot, "" when absent
property string slotState: ""
property bool syncing: false
property string syncStatus: ""
property bool fallbackFromPrecheck: false
readonly property bool linked: slotState === "live"
readonly property bool profileSyncSufficient: configured && inGroup
signal syncFinished
function refresh() {
@@ -31,6 +39,19 @@ Singleton {
sudoProbeProcess.running = true;
}
// Links this user's slot without touching system config when the greeter is already installed.
function link() {
if (!profileSyncSufficient) {
sync();
return;
}
if (!binaryExists || syncing)
return;
syncStatus = "";
syncing = true;
profileSyncProcess.running = true;
}
function launchTerminalFallback(fromPrecheck, statusText) {
fallbackFromPrecheck = fromPrecheck;
if (statusText)
@@ -38,6 +59,14 @@ Singleton {
terminalFallbackProcess.running = true;
}
function unlink() {
if (!binaryExists || syncing)
return;
syncStatus = "";
syncing = true;
unlinkProcess.running = true;
}
function withOutput(message, out, err) {
let text = message;
if (out !== "")
@@ -49,24 +78,65 @@ Singleton {
function finishSync() {
syncing = false;
refresh();
syncFinished();
}
Process {
id: detectProcess
command: ["sh", "-c", "command -v dms-greeter >/dev/null 2>&1 && echo binary; grep -qs dms-greeter /etc/greetd/config.toml && echo config"]
command: ["sh", "-c", `command -v dms-greeter >/dev/null 2>&1 && echo binary
grep -qs dms-greeter /etc/greetd/config.toml && echo config
[ "$(systemctl is-enabled greetd 2>/dev/null)" = enabled ] && echo enabled
id -nG | tr ' ' '\n' | grep -qxE 'greeter|greetd|_greeter' && echo group
own=$(readlink -f "$HOME/.config/DankMaterialShell/settings.json")
for f in "/var/cache/dms-greeter/users/$USER/settings.json" /var/cache/dms-greeter/settings.json; do
[ -L "$f" ] && [ "$(readlink -f "$f")" = "$own" ] && echo live && break
done
[ -f "/var/cache/dms-greeter/users/$USER/settings.json" ] && echo slot
exit 0`]
running: true
stdout: StdioCollector {
onStreamFinished: {
const found = text.split("\n");
root.binaryExists = found.includes("binary");
root.available = root.binaryExists || found.includes("config");
root.configured = found.includes("config");
root.enabled = found.includes("enabled");
root.inGroup = found.includes("group");
root.slotState = found.includes("live") ? "live" : (found.includes("slot") ? "snapshot" : "");
root.available = root.binaryExists || root.configured;
}
}
}
Process {
id: profileSyncProcess
command: ["dms-greeter", "sync", "--profile", "--yes"]
stdout: StdioCollector {
id: profileOut
}
stderr: StdioCollector {
id: profileErr
}
onExited: exitCode => {
const out = profileOut.text.trim();
const err = profileErr.text.trim();
if (exitCode !== 0) {
root.syncStatus = root.withOutput(I18n.tr("Profile sync failed.") + " (exit " + exitCode + ")", out, err);
root.finishSync();
return;
}
root.syncStatus = root.withOutput(I18n.tr("Sync completed successfully."), out, err);
ToastService.showInfo(I18n.tr("Greeter sync complete"));
root.finishSync();
}
}
Process {
id: sudoProbeProcess
@@ -118,6 +188,44 @@ Singleton {
}
}
Process {
id: unlinkProcess
command: ["dms-greeter", "unlink", "--yes"]
stdout: StdioCollector {
id: unlinkOut
}
stderr: StdioCollector {
id: unlinkErr
}
onExited: exitCode => {
const out = unlinkOut.text.trim();
const err = unlinkErr.text.trim();
if (exitCode === 0) {
root.syncStatus = root.withOutput(I18n.tr("Login screen unlinked.", "greeter unlink status"), out, err);
root.finishSync();
return;
}
root.syncStatus = root.withOutput(I18n.tr("Unlink needs sudo authentication. Opening terminal so you can use password or fingerprint.", "greeter unlink status"), out, err);
unlinkTerminalProcess.running = true;
}
}
Process {
id: unlinkTerminalProcess
command: ["dms-greeter", "unlink", "--terminal", "--yes"]
onExited: exitCode => {
if (exitCode !== 0)
root.syncStatus += "\n\n" + I18n.tr("Terminal fallback failed. Install one of the supported terminal emulators or run 'dms-greeter unlink' manually.", "greeter unlink status") + " (exit " + exitCode + ")";
root.finishSync();
}
}
Process {
id: terminalFallbackProcess
+1
View File
@@ -44,6 +44,7 @@ Singleton {
signal sessionUnlocked
signal sessionResumed
signal lockEditorRequested
signal greeterEditorRequested
signal lidOpened
signal loginctlStateChanged
+3
View File
@@ -0,0 +1,3 @@
import qs.DankCommon.Widgets as DankCommon
DankCommon.DankClockWidget {}
@@ -11502,6 +11502,26 @@
],
"parentLabel": "Date, time & region"
},
{
"section": "greeterAdvanced",
"label": "Advanced",
"tabIndex": 31,
"category": "Greeter",
"keywords": [
"advanced",
"display manager",
"greetd",
"greeter",
"login",
"repair",
"security",
"status",
"sync"
],
"parentLabel": "Security",
"icon": "tune",
"conditionKey": "greeterAvailable"
},
{
"section": "greeterAppearance",
"label": "Appearance",
@@ -11611,6 +11631,37 @@
"icon": "extension",
"conditionKey": "greeterAvailable"
},
{
"section": "greeterFollowLockScreen",
"label": "Follow lock screen",
"tabIndex": 31,
"category": "Greeter",
"keywords": [
"arrange",
"components",
"display manager",
"follow",
"greetd",
"greeter",
"layout",
"lock",
"lockscreen",
"login",
"modules",
"picker",
"screen",
"security",
"separately",
"session",
"shows",
"supports",
"turn",
"widgets"
],
"parentLabel": "Security",
"description": "The login screen shows the lock screen widgets it supports and its own session picker. Turn off to arrange it separately.",
"conditionKey": "greeterAvailable"
},
{
"section": "_tab_31",
"label": "Greeter",
@@ -11677,14 +11728,48 @@
"display manager",
"greetd",
"greeter",
"link",
"login",
"security",
"status"
"status",
"sync"
],
"parentLabel": "Security",
"icon": "info",
"conditionKey": "greeterAvailable"
},
{
"section": "greeterWidgets",
"label": "Widgets",
"tabIndex": 31,
"category": "Greeter",
"keywords": [
"arrange",
"clock",
"components",
"display manager",
"greetd",
"greeter",
"layout",
"lock",
"lockscreen",
"login",
"modules",
"picker",
"screen",
"security",
"separately",
"session",
"shows",
"supports",
"turn",
"widgets"
],
"parentLabel": "Security",
"icon": "widgets",
"description": "The login screen shows the lock screen widgets it supports and its own session picker. Turn off to arrange it separately.",
"conditionKey": "greeterAvailable"
},
{
"section": "muxType",
"label": "General",