Two of the panels this desktop still handed to GNOME Settings. Users manages the account through accountsservice -- the same daemon GNOME's panel drives, so a name or picture set here is what the login screen and lock screen read. Name, picture, account type, password, automatic login, and adding or removing other accounts. Every change is authorized by polkit through the agent this session already runs; a dismissed prompt is a normal outcome and says so. A new password is read from the helper's stdin, hashed by openssl reading its own stdin, and handed over D-Bus from inside that process. It is never an argument: argv is world-readable through /proc, so a password passed that way is published to every process on the machine. Removing an account takes two presses and says it destroys their files; the last administrator cannot be removed or demoted, because a machine nobody can administer is not a state to offer. Sharing reports what is actually true, including "the software for this is not installed" -- the honest answer for Samba here, and the case the panel it replaces shows as a switch that does nothing. Password sign-in is reported from sshd's configuration rather than assumed: claiming "keys only" when the file is silent would state a security property that cannot be backed up. The Control Center now draws the account's real picture and name. A generic glyph sat there while a real avatar was already set, which made the desktop look like it did not know whose it was. Also here: the KDE Connect contract no longer requires a phone to be awake. kdeconnectd drops its device objects for a phone it has not seen recently while the pairing survives in its config, so demanding one failed whenever the phone was off. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
380 lines
14 KiB
QML
380 lines
14 KiB
QML
// Your account, and anyone else who signs in to this machine.
|
|
//
|
|
// The layout puts your own account first because that is what someone opens
|
|
// this page for, and the avatar leads because it is the thing that shows up
|
|
// elsewhere in the desktop -- the Control Center draws it, and the lock screen
|
|
// and login screen read the same file.
|
|
//
|
|
// Everything privileged here prompts through polkit. A prompt that is dismissed
|
|
// is a normal outcome and says so plainly rather than reporting a failure.
|
|
|
|
import Quickshell
|
|
import Quickshell.Widgets
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
|
|
SettingsPage {
|
|
id: root
|
|
|
|
objectName: "users"
|
|
title: "Users"
|
|
lede: "Your account, and anyone else who signs in to this machine."
|
|
|
|
// One panel open at a time: changing a password and adding an account are
|
|
// both multi-field, and two open at once reads as a form with no shape.
|
|
property string openPanel: ""
|
|
property string newPassword: ""
|
|
property string confirmPassword: ""
|
|
property string newUserName: ""
|
|
property string newRealName: ""
|
|
property bool newUserIsAdministrator: false
|
|
property string confirmingRemoval: ""
|
|
|
|
readonly property var me: UserAccounts.me
|
|
|
|
readonly property string passwordProblem: {
|
|
if (root.newPassword === "")
|
|
return "";
|
|
if (root.newPassword.length < 6)
|
|
return "Use at least six characters.";
|
|
if (root.confirmPassword !== "" && root.newPassword !== root.confirmPassword)
|
|
return "The two entries do not match.";
|
|
return "";
|
|
}
|
|
|
|
readonly property bool passwordReady: root.newPassword.length >= 6
|
|
&& root.newPassword === root.confirmPassword
|
|
|
|
function closePanels(): void {
|
|
root.openPanel = "";
|
|
root.newPassword = "";
|
|
root.confirmPassword = "";
|
|
root.newUserName = "";
|
|
root.newRealName = "";
|
|
root.newUserIsAdministrator = false;
|
|
}
|
|
|
|
Component.onCompleted: UserAccounts.refresh()
|
|
|
|
TextRow {
|
|
visible: UserAccounts.lastError !== ""
|
|
label: "Accounts need attention"
|
|
detail: UserAccounts.lastError
|
|
value: ""
|
|
divider: false
|
|
}
|
|
|
|
// ── You ──────────────────────────────────────────────────────────────────
|
|
|
|
SettingsCard {
|
|
visible: root.me !== null
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: 20
|
|
|
|
Item {
|
|
width: 96
|
|
height: 96
|
|
|
|
ClippingRectangle {
|
|
anchors.fill: parent
|
|
radius: width / 2
|
|
color: Theme.alpha(Theme.fg, 0.08)
|
|
|
|
Image {
|
|
anchors.fill: parent
|
|
source: UserAccounts.avatarUrl
|
|
visible: UserAccounts.avatarUrl !== ""
|
|
fillMode: Image.PreserveAspectCrop
|
|
// The file is replaced in place when the picture
|
|
// changes, so the cache has to be told to let go.
|
|
cache: false
|
|
asynchronous: true
|
|
sourceSize.width: 192
|
|
sourceSize.height: 192
|
|
}
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
visible: UserAccounts.avatarUrl === ""
|
|
text: UserAccounts.displayName(root.me).slice(0, 1).toUpperCase()
|
|
color: Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: 38
|
|
font.weight: Font.DemiBold
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width - 116
|
|
spacing: 4
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
Text {
|
|
text: UserAccounts.displayName(root.me)
|
|
color: Theme.fg
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeTitle
|
|
font.weight: Font.DemiBold
|
|
}
|
|
|
|
Text {
|
|
text: String(root.me?.userName ?? "") + " · "
|
|
+ (root.me?.administrator ? "Administrator" : "Standard account")
|
|
color: Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
}
|
|
|
|
Item { width: 1; height: 6 }
|
|
|
|
SettingsButton {
|
|
text: "Change picture…"
|
|
enabled: !UserAccounts.busy
|
|
onClicked: avatarPicker.open()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Account"
|
|
visible: root.me !== null
|
|
|
|
TextFieldRow {
|
|
label: "Full name"
|
|
detail: "Shown on the lock screen and in the Control Center"
|
|
text: String(root.me?.realName ?? "")
|
|
placeholder: "Your name"
|
|
enabled: !UserAccounts.busy
|
|
onAccepted: value => UserAccounts.setRealName(String(root.me?.userName ?? ""), value)
|
|
}
|
|
|
|
TextRow {
|
|
label: "Username"
|
|
detail: "Fixed when the account was created, because files and permissions are keyed to it"
|
|
value: String(root.me?.userName ?? "")
|
|
}
|
|
|
|
SegmentRow {
|
|
label: "Account type"
|
|
detail: root.me?.administrator && UserAccounts.administratorCount <= 1
|
|
? "This is the only administrator, so it cannot be changed"
|
|
: "Administrators can install software and manage other accounts"
|
|
options: [
|
|
{ value: "standard", label: "Standard" },
|
|
{ value: "administrator", label: "Administrator" }
|
|
]
|
|
value: root.me?.administrator ? "administrator" : "standard"
|
|
enabled: !UserAccounts.busy
|
|
&& !(root.me?.administrator && UserAccounts.administratorCount <= 1)
|
|
onSelected: value => UserAccounts.setAccountType(String(root.me?.userName ?? ""), value)
|
|
}
|
|
|
|
ActionRow {
|
|
label: "Password"
|
|
detail: root.openPanel === "password"
|
|
? "Changing it asks for authorization first"
|
|
: "Change the password used to sign in and to unlock the screen"
|
|
action: root.openPanel === "password" ? "Cancel" : "Change…"
|
|
enabled: !UserAccounts.busy
|
|
divider: root.openPanel === "password"
|
|
onTriggered: {
|
|
if (root.openPanel === "password")
|
|
root.closePanels();
|
|
else {
|
|
root.closePanels();
|
|
root.openPanel = "password";
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
visible: root.openPanel === "password"
|
|
|
|
PasswordRow {
|
|
width: parent.width
|
|
label: "New password"
|
|
detail: "At least six characters"
|
|
onChanged: value => root.newPassword = value
|
|
}
|
|
|
|
PasswordRow {
|
|
width: parent.width
|
|
label: "Confirm"
|
|
detail: root.passwordProblem !== ""
|
|
? root.passwordProblem
|
|
: "Type it a second time"
|
|
onChanged: value => root.confirmPassword = value
|
|
}
|
|
|
|
ActionRow {
|
|
width: parent.width
|
|
label: "Set this password"
|
|
detail: "You will be asked to authorize the change"
|
|
action: "Set password"
|
|
enabled: root.passwordReady && !UserAccounts.busy
|
|
divider: false
|
|
onTriggered: {
|
|
UserAccounts.setPassword(String(root.me?.userName ?? ""), root.newPassword);
|
|
root.closePanels();
|
|
}
|
|
}
|
|
}
|
|
|
|
SwitchRow {
|
|
label: "Automatic login"
|
|
detail: "Sign in without typing a password. The login keyring stays locked when this is on, so stored passwords are unavailable until something asks for them."
|
|
checked: root.me?.automaticLogin === true
|
|
enabled: !UserAccounts.busy
|
|
divider: false
|
|
onToggled: value => UserAccounts.setAutomaticLogin(String(root.me?.userName ?? ""), value)
|
|
}
|
|
}
|
|
|
|
// ── Everyone else ────────────────────────────────────────────────────────
|
|
|
|
SettingsCard {
|
|
title: "Other accounts"
|
|
subtitle: UserAccounts.others.length === 0
|
|
? "Only your account exists on this machine."
|
|
: UserAccounts.others.length + " other account"
|
|
+ (UserAccounts.others.length === 1 ? "" : "s")
|
|
|
|
Repeater {
|
|
model: UserAccounts.others
|
|
|
|
delegate: Column {
|
|
id: otherBlock
|
|
|
|
required property var modelData
|
|
|
|
width: parent.width
|
|
|
|
readonly property string userName: String(otherBlock.modelData.userName ?? "")
|
|
readonly property bool confirming: root.confirmingRemoval === otherBlock.userName
|
|
|
|
SettingRow {
|
|
width: otherBlock.width
|
|
label: UserAccounts.displayName(otherBlock.modelData)
|
|
detail: otherBlock.userName + " · "
|
|
+ (otherBlock.modelData.administrator ? "Administrator" : "Standard account")
|
|
controlWidth: 210
|
|
divider: false
|
|
|
|
Row {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 8
|
|
|
|
SettingsButton {
|
|
text: otherBlock.confirming ? "Keep" : "Remove…"
|
|
enabled: !UserAccounts.busy
|
|
onClicked: root.confirmingRemoval = otherBlock.confirming
|
|
? "" : otherBlock.userName
|
|
}
|
|
|
|
SettingsButton {
|
|
visible: otherBlock.confirming
|
|
text: "Delete account and files"
|
|
tone: "danger"
|
|
enabled: !UserAccounts.busy
|
|
onClicked: {
|
|
root.confirmingRemoval = "";
|
|
UserAccounts.deleteUser(otherBlock.userName, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TextRow {
|
|
width: otherBlock.width
|
|
visible: otherBlock.confirming
|
|
label: "This cannot be undone"
|
|
detail: "Their home directory and everything in it is deleted."
|
|
value: ""
|
|
divider: false
|
|
}
|
|
|
|
Item { width: 1; height: 6 }
|
|
}
|
|
}
|
|
|
|
ActionRow {
|
|
label: "Add an account"
|
|
detail: "Creating an account asks for authorization first"
|
|
action: root.openPanel === "newUser" ? "Cancel" : "Add…"
|
|
enabled: !UserAccounts.busy
|
|
divider: root.openPanel === "newUser"
|
|
onTriggered: {
|
|
if (root.openPanel === "newUser")
|
|
root.closePanels();
|
|
else {
|
|
root.closePanels();
|
|
root.openPanel = "newUser";
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
visible: root.openPanel === "newUser"
|
|
|
|
TextFieldRow {
|
|
width: parent.width
|
|
label: "Full name"
|
|
placeholder: "Their name"
|
|
detail: "Shown on the login screen"
|
|
text: root.newRealName
|
|
onAccepted: value => root.newRealName = value
|
|
}
|
|
|
|
TextFieldRow {
|
|
width: parent.width
|
|
label: "Username"
|
|
placeholder: "lowercase, no spaces"
|
|
detail: "Their home directory is named after this and cannot be changed later"
|
|
text: root.newUserName
|
|
onAccepted: value => root.newUserName = value
|
|
}
|
|
|
|
SegmentRow {
|
|
width: parent.width
|
|
label: "Account type"
|
|
detail: "Standard accounts cannot install software or manage other accounts"
|
|
options: [
|
|
{ value: "standard", label: "Standard" },
|
|
{ value: "administrator", label: "Administrator" }
|
|
]
|
|
value: root.newUserIsAdministrator ? "administrator" : "standard"
|
|
onSelected: value => root.newUserIsAdministrator = value === "administrator"
|
|
}
|
|
|
|
ActionRow {
|
|
width: parent.width
|
|
label: "Create the account"
|
|
detail: "They set their own password the first time they sign in"
|
|
action: "Create"
|
|
enabled: !UserAccounts.busy && /^[a-z_][a-z0-9_-]*$/.test(root.newUserName)
|
|
divider: false
|
|
onTriggered: {
|
|
UserAccounts.createUser(root.newUserName, root.newRealName,
|
|
root.newUserIsAdministrator ? "administrator" : "standard");
|
|
root.closePanels();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Picking a picture goes through the desktop portal, which is the same
|
|
// chooser every other application gets and needs no privilege of its own.
|
|
AvatarPicker {
|
|
id: avatarPicker
|
|
onPicked: path => UserAccounts.setIcon(String(root.me?.userName ?? ""), path)
|
|
}
|
|
}
|