mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-08 12:13:31 -04:00
feat(Greeter): improved multi-user UI and per-user theme sync
- Introduce multi-account greeter login with per-user theme previews - Add `dms greeter sync --profile` for secondary users with or without sudo - Add Manage greeter group membership from Settings UI → Users Tab
This commit is contained in:
@@ -12,16 +12,24 @@ Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("GreetdSettings")
|
||||
|
||||
readonly property string configPath: {
|
||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||
return greetCfgDir + "/settings.json";
|
||||
readonly property string _greeterCacheDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
||||
|
||||
property string configBaseDir: root._greeterCacheDir
|
||||
readonly property string configPath: root.configBaseDir ? (root.configBaseDir + "/settings.json") : ""
|
||||
readonly property string greeterWallpaperOverridePath: root.configBaseDir ? (root.configBaseDir + "/greeter_wallpaper_override.jpg") : ""
|
||||
|
||||
function setConfigBaseDir(dir) {
|
||||
const next = dir || root._greeterCacheDir;
|
||||
if (configBaseDir === next)
|
||||
return;
|
||||
configBaseDir = next;
|
||||
settingsLoaded = false;
|
||||
settingsFile.reload();
|
||||
}
|
||||
|
||||
readonly property string _greeterCacheDir: {
|
||||
const i = root.configPath.lastIndexOf("/");
|
||||
return i >= 0 ? root.configPath.substring(0, i) : "";
|
||||
function resetConfigBaseDir() {
|
||||
setConfigBaseDir(root._greeterCacheDir);
|
||||
}
|
||||
readonly property string greeterWallpaperOverridePath: root._greeterCacheDir ? (root._greeterCacheDir + "/greeter_wallpaper_override.jpg") : ""
|
||||
|
||||
property string currentThemeName: "purple"
|
||||
property bool settingsLoaded: false
|
||||
|
||||
@@ -62,6 +62,11 @@ Item {
|
||||
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
|
||||
readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f)
|
||||
readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f
|
||||
readonly property bool multipleUsersAvailable: GreeterUsersService.loaded && GreeterUsersService.users.length > 1
|
||||
readonly property bool showUserPicker: multipleUsersAvailable && !GreeterState.showPasswordInput
|
||||
property bool userListOpen: false
|
||||
property bool skipAutoSelectUser: false
|
||||
property string pickerThemeUsername: ""
|
||||
|
||||
function initWeatherService() {
|
||||
if (weatherInitialized)
|
||||
@@ -428,20 +433,61 @@ Item {
|
||||
fprintdDeviceProbe.running = true;
|
||||
}
|
||||
|
||||
function applyPickerPreviewTheme() {
|
||||
let previewUser = (pickerThemeUsername || "").trim();
|
||||
if (!previewUser && GreetdSettings.rememberLastUser)
|
||||
previewUser = (GreetdMemory.lastSuccessfulUser || "").trim();
|
||||
if (previewUser)
|
||||
GreeterUserTheme.applyForUser(previewUser);
|
||||
else
|
||||
GreeterUserTheme.applyDefault();
|
||||
}
|
||||
|
||||
function applyLastSuccessfulUser() {
|
||||
if (root.skipAutoSelectUser)
|
||||
return;
|
||||
if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
|
||||
return;
|
||||
const lastUser = GreetdMemory.lastSuccessfulUser;
|
||||
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
|
||||
GreeterState.username = lastUser;
|
||||
GreeterState.usernameInput = lastUser;
|
||||
GreeterState.showPasswordInput = true;
|
||||
PortalService.getGreeterUserProfileImage(lastUser);
|
||||
maybeAutoStartExternalAuth();
|
||||
selectUser(lastUser, true);
|
||||
}
|
||||
}
|
||||
|
||||
function submitUsername(rawValue) {
|
||||
function returnToUserPicker() {
|
||||
if (!root.multipleUsersAvailable || GreeterState.unlocking)
|
||||
return;
|
||||
root.skipAutoSelectUser = true;
|
||||
awaitingExternalAuth = false;
|
||||
pendingPasswordResponse = false;
|
||||
passwordSubmitRequested = false;
|
||||
resetPasswordSessionTransition(true);
|
||||
authTimeout.interval = defaultAuthTimeoutMs;
|
||||
authTimeout.stop();
|
||||
clearAuthFeedback();
|
||||
passwordFailureCount = 0;
|
||||
externalAuthAutoStartedForUser = "";
|
||||
if (Greetd.state !== GreetdState.Inactive)
|
||||
Greetd.cancelSession();
|
||||
const previousUser = GreeterState.username;
|
||||
GreeterState.reset();
|
||||
inputField.text = "";
|
||||
PortalService.profileImage = "";
|
||||
if (previousUser)
|
||||
root.pickerThemeUsername = previousUser;
|
||||
root.applyPickerPreviewTheme();
|
||||
root.userListOpen = true;
|
||||
}
|
||||
|
||||
function selectUser(rawValue, skipDropdownUpdate) {
|
||||
const user = (rawValue || "").trim();
|
||||
if (!user)
|
||||
return;
|
||||
root.skipAutoSelectUser = false;
|
||||
submitUsername(user, skipDropdownUpdate === true);
|
||||
}
|
||||
|
||||
function submitUsername(rawValue, skipDropdownUpdate) {
|
||||
const user = (rawValue || "").trim();
|
||||
if (!user)
|
||||
return;
|
||||
@@ -450,8 +496,15 @@ Item {
|
||||
clearAuthFeedback();
|
||||
externalAuthAutoStartedForUser = "";
|
||||
}
|
||||
root.pickerThemeUsername = user;
|
||||
GreeterState.username = user;
|
||||
GreeterState.usernameInput = user;
|
||||
GreeterState.showPasswordInput = true;
|
||||
if (!skipDropdownUpdate && typeof GreeterUsersService !== "undefined") {
|
||||
const idx = GreeterUsersService.usernames.indexOf(user);
|
||||
GreeterState.selectedUserIndex = idx;
|
||||
}
|
||||
root.userListOpen = false;
|
||||
PortalService.getGreeterUserProfileImage(user);
|
||||
GreeterState.passwordBuffer = "";
|
||||
pendingPasswordResponse = false;
|
||||
@@ -637,13 +690,44 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GreeterUsersService
|
||||
function onLoadedChanged() {
|
||||
if (GreeterUsersService.loaded && isPrimaryScreen)
|
||||
applyPickerPreviewTheme();
|
||||
}
|
||||
function onSyncedThemePathsChanged() {
|
||||
if (!isPrimaryScreen)
|
||||
return;
|
||||
if (GreeterState.username)
|
||||
GreeterUserTheme.applyForUser(GreeterState.username);
|
||||
else if (root.showUserPicker || root.userListOpen)
|
||||
applyPickerPreviewTheme();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GreeterState
|
||||
function onUsernameChanged() {
|
||||
if (GreeterState.username) {
|
||||
root.pickerThemeUsername = GreeterState.username;
|
||||
GreeterUserTheme.applyForUser(GreeterState.username);
|
||||
PortalService.getGreeterUserProfileImage(GreeterState.username);
|
||||
} else if (root.showUserPicker || root.userListOpen) {
|
||||
applyPickerPreviewTheme();
|
||||
}
|
||||
}
|
||||
function onShowPasswordInputChanged() {
|
||||
if (GreeterState.showPasswordInput)
|
||||
root.userListOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
onShowUserPickerChanged: {
|
||||
if (showUserPicker && !GreeterState.username)
|
||||
applyPickerPreviewTheme();
|
||||
if (!showUserPicker)
|
||||
userListOpen = false;
|
||||
}
|
||||
|
||||
FileView {
|
||||
@@ -736,19 +820,26 @@ Item {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
|
||||
Item {
|
||||
id: clockContainer
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.verticalCenter
|
||||
anchors.bottomMargin: 60
|
||||
width: parent.width
|
||||
height: clockText.implicitHeight
|
||||
Column {
|
||||
id: greeterMainColumn
|
||||
|
||||
Row {
|
||||
id: clockText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
spacing: 0
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
width: 380
|
||||
|
||||
Item {
|
||||
id: clockContainer
|
||||
|
||||
width: parent.width
|
||||
height: clockText.implicitHeight
|
||||
|
||||
Row {
|
||||
id: clockText
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
spacing: 0
|
||||
|
||||
property string fullTimeStr: {
|
||||
const format = GreetdSettings.getEffectiveTimeFormat();
|
||||
@@ -853,60 +944,118 @@ Item {
|
||||
visible: clockText.ampm !== ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: dateText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: clockContainer.bottom
|
||||
anchors.topMargin: 4
|
||||
text: {
|
||||
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat());
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: "white"
|
||||
opacity: 0.9
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: dateText.bottom
|
||||
anchors.topMargin: Theme.spacingL
|
||||
width: 380
|
||||
height: 140
|
||||
StyledText {
|
||||
id: dateText
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat())
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: "white"
|
||||
opacity: 0.9
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: userPickerHint
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: root.showUserPicker && !GreeterState.showPasswordInput && !GreeterState.username && !root.userListOpen
|
||||
text: I18n.tr("Select user...", "greeter user picker placeholder")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: "white"
|
||||
opacity: 0.85
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
id: authColumn
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
RowLayout {
|
||||
spacing: Theme.spacingL
|
||||
Layout.fillWidth: true
|
||||
|
||||
DankCircularImage {
|
||||
Item {
|
||||
Layout.preferredWidth: 60
|
||||
Layout.preferredHeight: 60
|
||||
imageSource: {
|
||||
if (PortalService.profileImage === "")
|
||||
return "";
|
||||
if (PortalService.profileImage.startsWith("/"))
|
||||
return encodeFileUrl(PortalService.profileImage);
|
||||
return PortalService.profileImage;
|
||||
visible: GreetdSettings.lockScreenShowProfileImage || root.multipleUsersAvailable
|
||||
|
||||
DankCircularImage {
|
||||
anchors.fill: parent
|
||||
imageSource: {
|
||||
const displayUser = GreeterState.username || root.pickerThemeUsername;
|
||||
if (displayUser) {
|
||||
const cachedPath = GreeterUsersService.profileImagePath(displayUser);
|
||||
if (cachedPath)
|
||||
return encodeFileUrl(cachedPath);
|
||||
}
|
||||
if (PortalService.profileImage === "")
|
||||
return "";
|
||||
if (PortalService.profileImage.startsWith("/"))
|
||||
return encodeFileUrl(PortalService.profileImage);
|
||||
return PortalService.profileImage;
|
||||
}
|
||||
fallbackIcon: "person"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.color: Theme.primary
|
||||
border.width: avatarPickerArea.containsMouse || root.userListOpen ? 2 : 0
|
||||
visible: root.multipleUsersAvailable
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: avatarPickerArea
|
||||
|
||||
anchors.fill: parent
|
||||
visible: root.multipleUsersAvailable
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (GreeterState.showPasswordInput)
|
||||
root.returnToUserPicker();
|
||||
else
|
||||
root.userListOpen = !root.userListOpen;
|
||||
}
|
||||
}
|
||||
fallbackIcon: "person"
|
||||
visible: GreetdSettings.lockScreenShowProfileImage
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
property bool showPassword: false
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 60
|
||||
Layout.preferredHeight: root.showUserPicker && root.userListOpen ? Math.max(60, userPicker.implicitHeight + Theme.spacingM * 2) : 60
|
||||
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9)
|
||||
border.color: inputField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3)
|
||||
border.width: inputField.activeFocus ? 2 : 1
|
||||
|
||||
GreeterUserPicker {
|
||||
id: userPicker
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: root.userListOpen ? undefined : parent.verticalCenter
|
||||
anchors.top: root.userListOpen ? parent.top : undefined
|
||||
anchors.margins: Theme.spacingM
|
||||
visible: root.showUserPicker && !GreeterState.showPasswordInput
|
||||
expanded: root.userListOpen
|
||||
onUserSelected: username => root.selectUser(username, false)
|
||||
onToggleRequested: root.userListOpen = !root.userListOpen
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: lockIcon
|
||||
|
||||
@@ -916,6 +1065,7 @@ Item {
|
||||
name: GreeterState.showPasswordInput ? "lock" : "person"
|
||||
size: 20
|
||||
color: inputField.activeFocus ? Theme.primary : Theme.surfaceVariantText
|
||||
visible: !root.showUserPicker
|
||||
}
|
||||
|
||||
TextInput {
|
||||
@@ -941,8 +1091,9 @@ Item {
|
||||
}
|
||||
return margin;
|
||||
}
|
||||
enabled: !root.showUserPicker || GreeterState.showPasswordInput
|
||||
opacity: 0
|
||||
focus: true
|
||||
focus: !root.showUserPicker || GreeterState.showPasswordInput
|
||||
echoMode: GreeterState.showPasswordInput ? (parent.showPassword ? TextInput.Normal : TextInput.Password) : TextInput.Normal
|
||||
onTextChanged: {
|
||||
if (syncingFromState)
|
||||
@@ -1005,11 +1156,14 @@ Item {
|
||||
if (GreeterState.showPasswordInput) {
|
||||
return I18n.tr("Password...");
|
||||
}
|
||||
if (root.showUserPicker) {
|
||||
return "";
|
||||
}
|
||||
return I18n.tr("Username...");
|
||||
}
|
||||
color: (GreeterState.unlocking || (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse)) ? Theme.primary : Theme.outline
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
|
||||
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : (root.showUserPicker ? false : GreeterState.usernameInput.length === 0)) ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
@@ -1043,7 +1197,7 @@ Item {
|
||||
}
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: (GreeterState.showPasswordInput && !parent.showPassword) ? Theme.fontSizeLarge : Theme.fontSizeMedium
|
||||
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length > 0 : GreeterState.usernameInput.length > 0) ? 1 : 0
|
||||
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length > 0 : (root.showUserPicker ? false : GreeterState.usernameInput.length > 0)) ? 1 : 0
|
||||
clip: true
|
||||
elide: Text.ElideNone
|
||||
horizontalAlignment: implicitWidth > width ? Text.AlignRight : Text.AlignLeft
|
||||
@@ -1088,7 +1242,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "keyboard"
|
||||
buttonSize: 32
|
||||
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking && (!root.showUserPicker || GreeterState.showPasswordInput)
|
||||
enabled: visible
|
||||
onClicked: {
|
||||
if (keyboard_controller.isKeyboardActive) {
|
||||
@@ -1107,7 +1261,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "keyboard_return"
|
||||
buttonSize: 36
|
||||
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking && (!root.showUserPicker || GreeterState.showPasswordInput)
|
||||
enabled: true
|
||||
onClicked: {
|
||||
if (GreeterState.showPasswordInput) {
|
||||
@@ -1198,13 +1352,8 @@ Item {
|
||||
StateLayer {
|
||||
stateColor: Theme.primary
|
||||
cornerRadius: parent.radius
|
||||
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
|
||||
onClicked: {
|
||||
GreeterState.reset();
|
||||
root.externalAuthAutoStartedForUser = "";
|
||||
inputField.text = "";
|
||||
PortalService.profileImage = "";
|
||||
}
|
||||
enabled: !GreeterState.unlocking && GreeterState.showPasswordInput
|
||||
onClicked: root.returnToUserPicker()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ Singleton {
|
||||
property var sessionExecs: []
|
||||
property var sessionPaths: []
|
||||
property int currentSessionIndex: 0
|
||||
property var availableUsers: []
|
||||
property int selectedUserIndex: -1
|
||||
|
||||
function reset() {
|
||||
showPasswordInput = false;
|
||||
@@ -26,5 +28,6 @@ Singleton {
|
||||
usernameInput = "";
|
||||
passwordBuffer = "";
|
||||
pamState = "";
|
||||
selectedUserIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool expanded: false
|
||||
|
||||
signal userSelected(string username)
|
||||
signal toggleRequested()
|
||||
|
||||
function encodeFileUrl(path) {
|
||||
if (!path)
|
||||
return "";
|
||||
return "file://" + path.split("/").map(s => encodeURIComponent(s)).join("/");
|
||||
}
|
||||
|
||||
function profileImageSource(username) {
|
||||
const path = GreeterUsersService.profileImagePath(username);
|
||||
if (path)
|
||||
return encodeFileUrl(path);
|
||||
return "";
|
||||
}
|
||||
|
||||
implicitHeight: column.implicitHeight
|
||||
implicitWidth: parent ? parent.width : 320
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.spacingS
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingM
|
||||
visible: !root.expanded && !!GreeterState.username
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: GreeterUsersService.optionLabel(GreeterState.username)
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "expand_more"
|
||||
size: 20
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.toggleRequested()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 36
|
||||
visible: !root.expanded && !GreeterState.username
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "expand_more"
|
||||
size: 20
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.toggleRequested()
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingXS
|
||||
visible: root.expanded
|
||||
|
||||
Repeater {
|
||||
model: GreeterUsersService.users
|
||||
|
||||
delegate: Rectangle {
|
||||
id: userRow
|
||||
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 52
|
||||
radius: Theme.cornerRadius
|
||||
color: userRowMouse.containsMouse ? Theme.surfacePressed : "transparent"
|
||||
border.color: GreeterState.username === userRow.modelData.username ? Theme.primary : "transparent"
|
||||
border.width: GreeterState.username === userRow.modelData.username ? 1 : 0
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
|
||||
DankCircularImage {
|
||||
anchors.fill: parent
|
||||
imageSource: root.profileImageSource(userRow.modelData.username)
|
||||
fallbackIcon: "person"
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: GreeterUsersService.optionLabel(userRow.modelData.username)
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: userRowMouse
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.userSelected(userRow.modelData.username)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var log: Log.scoped("GreeterUserTheme")
|
||||
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
||||
|
||||
property string activeUsername: ""
|
||||
|
||||
function userCacheDir(username) {
|
||||
if (!username)
|
||||
return "";
|
||||
return greetCfgDir + "/users/" + username;
|
||||
}
|
||||
|
||||
function applyForUser(username) {
|
||||
const name = (username || "").trim();
|
||||
activeUsername = name;
|
||||
if (!name) {
|
||||
applyDefault();
|
||||
return;
|
||||
}
|
||||
const dir = userCacheDir(name);
|
||||
if (typeof GreeterUsersService !== "undefined" && GreeterUsersService.hasSyncedTheme(name)) {
|
||||
Theme.setGreeterColorsBaseDir(dir);
|
||||
SessionData.setGreeterSessionBaseDir(dir);
|
||||
GreetdSettings.setConfigBaseDir(dir);
|
||||
return;
|
||||
}
|
||||
applyDefault();
|
||||
}
|
||||
|
||||
function applyDefault() {
|
||||
activeUsername = "";
|
||||
Theme.resetGreeterColorsBaseDir();
|
||||
SessionData.resetGreeterSessionBaseDir();
|
||||
GreetdSettings.resetConfigBaseDir();
|
||||
}
|
||||
|
||||
readonly property string activeWallpaperOverridePath: {
|
||||
const base = activeUsername && typeof GreeterUsersService !== "undefined" && GreeterUsersService.hasSyncedTheme(activeUsername) ? userCacheDir(activeUsername) : greetCfgDir;
|
||||
return base ? base + "/greeter_wallpaper_override.jpg" : "";
|
||||
}
|
||||
}
|
||||
@@ -250,7 +250,17 @@ Only niri currently has a generated greeter config path managed by `dms greeter
|
||||
|
||||
The greeter can be personalized with wallpapers, themes, weather, clock formats, and more - configured exactly the same as dms.
|
||||
|
||||
**Easiest method:** Run `dms greeter sync` to automatically sync your DMS theme with the greeter.
|
||||
**Easiest method (single user):** Run `dms greeter sync` to automatically sync your DMS theme with the greeter.
|
||||
|
||||
**Multi-user systems:** One **main admin** runs full sync once to set up greetd and the shared cache (`dms greeter sync`, or `dms greeter sync --local` when developing from a checkout). **Every other account**—including other admins—should only run:
|
||||
|
||||
```bash
|
||||
dms greeter sync --profile
|
||||
```
|
||||
|
||||
Before that, an administrator must add each user to the `greeter` group in **Settings → Users** (greeter toggle) or with `sudo usermod -aG greeter <username>`. Each added user must log out and back in before `--profile` will work.
|
||||
|
||||
Per-user settings are stored under `/var/cache/dms-greeter/users/<username>/` for the login picker; the root cache remains the default fallback and is owned by whoever ran full sync.
|
||||
|
||||
**Manual method:** You can manually synchronize configurations if you want greeter settings to always mirror your shell:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user