1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-23 11:35:25 -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:
purian23
2026-05-25 22:41:23 -04:00
parent d9525908f1
commit 078180fe42
18 changed files with 1577 additions and 127 deletions
+209 -60
View File
@@ -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()
}
}
}