Own user accounts and sharing
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
This commit is contained in:
@@ -304,14 +304,44 @@ Item {
|
||||
width: content.width
|
||||
height: 36
|
||||
|
||||
ThemedIcon {
|
||||
// The account's own picture, the same file the lock screen and the
|
||||
// login screen read. A generic glyph sat here while a real avatar
|
||||
// was already set, which made the desktop look like it did not know
|
||||
// whose it was.
|
||||
Item {
|
||||
id: avatar
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
size: 20
|
||||
icon: "avatar-default-symbolic"
|
||||
iconFallback: "user-info-symbolic"
|
||||
width: 22
|
||||
height: 22
|
||||
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
visible: UserAccounts.avatarUrl !== ""
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: UserAccounts.avatarUrl
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
// Replaced in place when the picture changes, so the
|
||||
// cache has to be told to let go of the old one.
|
||||
cache: false
|
||||
asynchronous: true
|
||||
sourceSize.width: 44
|
||||
sourceSize.height: 44
|
||||
}
|
||||
}
|
||||
|
||||
ThemedIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: UserAccounts.avatarUrl === ""
|
||||
size: 20
|
||||
icon: "avatar-default-symbolic"
|
||||
iconFallback: "user-info-symbolic"
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
@@ -320,7 +350,9 @@ Item {
|
||||
anchors.right: actions.left
|
||||
anchors.rightMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Quickshell.env("USER") || "user"
|
||||
text: UserAccounts.me
|
||||
? UserAccounts.displayName(UserAccounts.me)
|
||||
: (Quickshell.env("USER") || "user")
|
||||
color: Theme.fg
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontFamily
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Choosing a profile picture.
|
||||
//
|
||||
// A thin wrapper on the same FileDialog the phone controls use, so picking a
|
||||
// picture is the ordinary file chooser rather than something this desktop
|
||||
// invented. Nothing privileged happens here -- reading a file the user selected
|
||||
// needs no authorization; only setting it on the account does.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Dialogs
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
signal picked(path: string)
|
||||
|
||||
function open(): void {
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: dialog
|
||||
title: "Choose a profile picture"
|
||||
fileMode: FileDialog.OpenFile
|
||||
nameFilters: ["Pictures (*.png *.jpg *.jpeg *.webp *.gif *.bmp)", "All files (*)"]
|
||||
onAccepted: {
|
||||
const value = String(dialog.selectedFile);
|
||||
if (!value.startsWith("file://"))
|
||||
return;
|
||||
root.picked(decodeURIComponent(value.slice(7)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// A row whose trailing control is a masked password field.
|
||||
//
|
||||
// Reports every keystroke rather than committing on Enter, because the page it
|
||||
// serves has to compare two fields as they are typed and say whether they match
|
||||
// before offering to set anything.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
SettingRow {
|
||||
id: root
|
||||
|
||||
property string placeholder: "Password"
|
||||
|
||||
signal changed(value: string)
|
||||
|
||||
controlWidth: 220
|
||||
|
||||
PasswordField {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: root.controlWidth
|
||||
placeholder: root.placeholder
|
||||
onTextChanged: root.changed(this.text)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// A segmented choice that is NOT schema-bound.
|
||||
//
|
||||
// ChoiceRow reads its options from the preference schema. This one is handed
|
||||
// them, for choices that describe system state rather than a Panama setting.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
SettingRow {
|
||||
id: root
|
||||
|
||||
// [{ value, label }]
|
||||
property var options: []
|
||||
property var value: null
|
||||
property bool enabled: true
|
||||
|
||||
signal selected(value: var)
|
||||
|
||||
controlWidth: Math.max(150, root.options.length * 92)
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 6
|
||||
opacity: root.enabled ? 1 : 0.45
|
||||
|
||||
Repeater {
|
||||
model: root.options
|
||||
|
||||
delegate: Rectangle {
|
||||
id: segment
|
||||
|
||||
required property var modelData
|
||||
|
||||
readonly property bool current: root.value === segment.modelData.value
|
||||
|
||||
width: Math.max(84, segmentLabel.implicitWidth + 22)
|
||||
height: 30
|
||||
radius: 9
|
||||
color: segment.current
|
||||
? Theme.alpha(Theme.accent, 0.22)
|
||||
: Theme.alpha(Theme.fg, segmentMouse.containsMouse ? 0.12 : 0.06)
|
||||
border.width: 1
|
||||
border.color: segment.current
|
||||
? Theme.alpha(Theme.accent, 0.5)
|
||||
: Theme.alpha(Theme.fg, 0.08)
|
||||
|
||||
Text {
|
||||
id: segmentLabel
|
||||
anchors.centerIn: parent
|
||||
text: String(segment.modelData.label ?? "")
|
||||
color: segment.current ? Theme.fg : Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: segment.current ? Font.DemiBold : Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: segmentMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: root.enabled && !segment.current
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.selected(segment.modelData.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,8 @@ Rectangle {
|
||||
case "datetime": return dateTimePage;
|
||||
case "applications": return applicationsPage;
|
||||
case "storage": return storagePage;
|
||||
case "users": return usersPage;
|
||||
case "sharing": return sharingPage;
|
||||
case "services": return healthPage;
|
||||
case "about": return aboutPage;
|
||||
default: return homePage;
|
||||
@@ -160,6 +162,8 @@ Rectangle {
|
||||
Component { id: homePage; HomePage {} }
|
||||
Component { id: applicationsPage; ApplicationsPage {} }
|
||||
Component { id: storagePage; StoragePage {} }
|
||||
Component { id: usersPage; UsersPage {} }
|
||||
Component { id: sharingPage; SharingPage {} }
|
||||
Component { id: accessibilityPage; AccessibilityPage {} }
|
||||
Component { id: powerPage; PowerPage {} }
|
||||
Component { id: dateTimePage; DateTimePage {} }
|
||||
|
||||
@@ -26,6 +26,7 @@ Rectangle {
|
||||
{ page: "appearance", label: "Appearance", icon: "\u{F0E0D}" },
|
||||
{ page: "displays", label: "Displays", icon: "\u{F0379}" },
|
||||
{ page: "connectivity", label: "Network & Devices", icon: "\u{F08D4}" },
|
||||
{ page: "sharing", label: "Sharing", icon: "\u{F04E6}" },
|
||||
{ page: "home-phone", label: "Home & Phone", icon: "\u{F02DC}" },
|
||||
{ page: "desktop", label: "Desktop & Dock", icon: "\u{F04A4}" },
|
||||
{ page: "sound", label: "Sound", icon: "\u{F057E}" },
|
||||
@@ -41,6 +42,7 @@ Rectangle {
|
||||
{ page: "datetime", label: "Date & Time", icon: "\u{F0954}" },
|
||||
{ page: "applications", label: "Applications", icon: "\u{F003B}" },
|
||||
{ page: "storage", label: "Storage", icon: "\u{F02CA}" },
|
||||
{ page: "users", label: "Users", icon: "\u{F0004}" },
|
||||
{ page: "services", label: "System Health", icon: "\u{F0493}" },
|
||||
{ page: "about", label: "About", icon: "\u{F02FD}" }
|
||||
]
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
// What this machine offers to other machines on the network.
|
||||
//
|
||||
// Every row states what is true right now, including "the software for this is
|
||||
// not installed" -- which is the honest answer for file sharing here, and is
|
||||
// what the panel this replaces hides behind a switch that silently does
|
||||
// nothing.
|
||||
//
|
||||
// Turning remote login on changes the whole system and prompts. Remote desktop
|
||||
// is a user service and does not.
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
SettingsPage {
|
||||
id: root
|
||||
|
||||
objectName: "sharing"
|
||||
title: "Sharing"
|
||||
lede: "What this machine offers to other machines on the network."
|
||||
|
||||
Component.onCompleted: Sharing.refresh()
|
||||
|
||||
TextRow {
|
||||
visible: Sharing.lastError !== ""
|
||||
label: "Sharing needs attention"
|
||||
detail: Sharing.lastError
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "This machine"
|
||||
subtitle: "The name other machines see."
|
||||
|
||||
TextFieldRow {
|
||||
label: "Network name"
|
||||
detail: "Used for ssh and for anything else that finds this machine by name"
|
||||
text: Sharing.hostname
|
||||
placeholder: "desktop"
|
||||
enabled: !Sharing.busy
|
||||
divider: false
|
||||
onAccepted: value => Sharing.setHostname(value)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Remote login"
|
||||
subtitle: "Sign in to a terminal on this machine over SSH."
|
||||
|
||||
SwitchRow {
|
||||
label: "Allow remote login"
|
||||
detail: Sharing.remoteLogin?.installed === true
|
||||
? (Sharing.remoteLoginOn
|
||||
? "Running, and starts automatically at boot"
|
||||
: "Not running")
|
||||
: "OpenSSH server is not installed"
|
||||
checked: Sharing.remoteLoginOn
|
||||
enabled: !Sharing.busy && Sharing.remoteLogin?.installed === true
|
||||
onToggled: value => Sharing.setRemoteLogin(value)
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Sharing.remoteLoginOn
|
||||
label: "Connect with"
|
||||
detail: "From another machine on your network"
|
||||
value: "ssh " + Sharing.networkName
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Sharing.remoteLoginOn
|
||||
label: "Port"
|
||||
detail: "Where the SSH server is listening"
|
||||
value: String(Sharing.remoteLogin?.port ?? "22")
|
||||
}
|
||||
|
||||
// Reported from the configuration rather than assumed. Saying "keys
|
||||
// only" on a machine that actually accepts passwords would be a
|
||||
// security claim this page cannot back up.
|
||||
TextRow {
|
||||
visible: Sharing.remoteLoginOn
|
||||
label: "Password sign-in"
|
||||
detail: Sharing.passwordLoginSummary()
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Remote desktop"
|
||||
subtitle: "See and control this desktop from another machine."
|
||||
|
||||
SwitchRow {
|
||||
label: "Allow remote desktop"
|
||||
detail: Sharing.remoteDesktop?.available === true
|
||||
? (Sharing.remoteDesktopOn
|
||||
? "Running for your session"
|
||||
: (Sharing.remoteDesktop?.hasCredentials === true
|
||||
? "Not running"
|
||||
: "Set a username and password before turning this on"))
|
||||
: "Remote desktop support is not installed"
|
||||
checked: Sharing.remoteDesktopOn
|
||||
enabled: !Sharing.busy
|
||||
&& Sharing.remoteDesktop?.available === true
|
||||
&& Sharing.remoteDesktop?.hasCredentials === true
|
||||
onToggled: value => Sharing.setRemoteDesktop(value)
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Sharing.remoteDesktop?.available === true
|
||||
label: "Port"
|
||||
detail: "The RDP port other machines connect to"
|
||||
value: String(Sharing.remoteDesktop?.port ?? "")
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Sharing.remoteDesktop?.available === true
|
||||
label: "Credentials"
|
||||
detail: Sharing.remoteDesktop?.hasCredentials === true
|
||||
? "Stored in the login keyring, where Privacy & Security can manage them"
|
||||
: "None stored yet"
|
||||
value: Sharing.remoteDesktop?.hasCredentials === true ? "Stored" : "Not set"
|
||||
divider: false
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "File and media sharing"
|
||||
subtitle: "Sharing folders and media needs software this machine does not necessarily have."
|
||||
|
||||
TextRow {
|
||||
label: "Share folders on the network"
|
||||
detail: Sharing.fileSharing?.installed === true
|
||||
? "Samba is installed"
|
||||
: "Needs Samba, which is not installed. Settings does not install software."
|
||||
value: Sharing.fileSharing?.installed === true ? "Available" : "Not installed"
|
||||
}
|
||||
|
||||
TextRow {
|
||||
label: "Share music and video to devices"
|
||||
detail: Sharing.mediaSharing?.installed === true
|
||||
? "Rygel is installed"
|
||||
: "Needs Rygel, which is not installed."
|
||||
value: Sharing.mediaSharing?.installed === true ? "Available" : "Not installed"
|
||||
divider: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// A toggle that is NOT schema-bound.
|
||||
//
|
||||
// ToggleRow reads and writes a preference key. Some switches govern system
|
||||
// state instead -- an account flag, a systemd unit -- which lives outside
|
||||
// Panama's settings file and is read back from the system that owns it.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
SettingRow {
|
||||
id: root
|
||||
|
||||
property bool checked: false
|
||||
property bool enabled: true
|
||||
|
||||
signal toggled(value: bool)
|
||||
|
||||
controlWidth: 54
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 44
|
||||
height: 26
|
||||
radius: height / 2
|
||||
color: root.checked ? Theme.accent : Theme.alpha(Theme.fg, 0.14)
|
||||
opacity: root.enabled ? 1 : 0.45
|
||||
|
||||
Rectangle {
|
||||
x: root.checked ? parent.width - width - 3 : 3
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 20
|
||||
height: 20
|
||||
radius: height / 2
|
||||
color: root.checked ? Theme.bgDark : Theme.fg
|
||||
|
||||
// Short and non-repeating: this animates only when someone acts.
|
||||
Behavior on x {
|
||||
NumberAnimation { duration: 120; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.enabled
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.toggled(!root.checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// A row whose trailing control is a free-text field, NOT bound to a schema key.
|
||||
//
|
||||
// TextFieldRow {
|
||||
// label: "Full name"
|
||||
// text: Accounts.me.realName
|
||||
// onAccepted: value => Accounts.setRealName("gib", value)
|
||||
// }
|
||||
//
|
||||
// TextEntryRow is the schema-bound one, and is the right choice for anything
|
||||
// that is a Panama setting. This is for values that live in the system rather
|
||||
// than in settings.json -- an account's real name, the machine's hostname --
|
||||
// where the label, the validation, and the write all belong to whoever owns
|
||||
// that value.
|
||||
//
|
||||
// Committed on Enter or when focus leaves, never per keystroke: these drive
|
||||
// privileged calls that prompt, and prompting once per typed character would be
|
||||
// unusable.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
SettingRow {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
property string placeholder: ""
|
||||
property bool enabled: true
|
||||
|
||||
signal accepted(value: string)
|
||||
|
||||
controlWidth: 220
|
||||
|
||||
// An external change replaces what is shown, unless it would yank the field
|
||||
// out from under someone mid-edit.
|
||||
onTextChanged: if (!input.activeFocus) input.text = root.text
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: root.controlWidth
|
||||
height: 32
|
||||
radius: 9
|
||||
color: Theme.alpha(Theme.fg, root.enabled ? 0.06 : 0.03)
|
||||
border.width: input.activeFocus ? 2 : 1
|
||||
border.color: input.activeFocus
|
||||
? Theme.alpha(Theme.accent, 0.55)
|
||||
: Theme.alpha(Theme.fg, 0.1)
|
||||
opacity: root.enabled ? 1 : 0.5
|
||||
|
||||
TextInput {
|
||||
id: input
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 11
|
||||
anchors.rightMargin: 11
|
||||
enabled: root.enabled
|
||||
activeFocusOnTab: true
|
||||
text: root.text
|
||||
color: Theme.fg
|
||||
selectionColor: Theme.alpha(Theme.accent, 0.5)
|
||||
selectedTextColor: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
clip: true
|
||||
|
||||
onAccepted: if (input.text !== root.text) root.accepted(input.text)
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (input.activeFocus)
|
||||
return;
|
||||
if (input.text !== root.text)
|
||||
root.accepted(input.text);
|
||||
else
|
||||
input.text = root.text;
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
visible: input.text === ""
|
||||
text: root.placeholder
|
||||
color: Theme.fgMuted
|
||||
font: input.font
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
module qs.modules.settings
|
||||
AboutPage 1.0 AboutPage.qml
|
||||
AppearancePage 1.0 AppearancePage.qml
|
||||
AvatarPicker 1.0 AvatarPicker.qml
|
||||
ConnectivityPage 1.0 ConnectivityPage.qml
|
||||
HomePhonePage 1.0 HomePhonePage.qml
|
||||
HomeFavoriteCard 1.0 HomeFavoriteCard.qml
|
||||
@@ -9,10 +10,12 @@ DesktopPage 1.0 DesktopPage.qml
|
||||
DisplaysPage 1.0 DisplaysPage.qml
|
||||
HomePage 1.0 HomePage.qml
|
||||
NotificationsPage 1.0 NotificationsPage.qml
|
||||
PasswordRow 1.0 PasswordRow.qml
|
||||
ScreenIntelligencePage 1.0 ScreenIntelligencePage.qml
|
||||
HealthPage 1.0 HealthPage.qml
|
||||
HealthSummary 1.0 HealthSummary.qml
|
||||
HealthCheckRow 1.0 HealthCheckRow.qml
|
||||
SegmentRow 1.0 SegmentRow.qml
|
||||
SettingRow 1.0 SettingRow.qml
|
||||
SettingsCard 1.0 SettingsCard.qml
|
||||
SettingsButton 1.0 SettingsButton.qml
|
||||
@@ -20,10 +23,12 @@ SettingsShell 1.0 SettingsShell.qml
|
||||
SettingsSidebar 1.0 SettingsSidebar.qml
|
||||
SettingsToggle 1.0 SettingsToggle.qml
|
||||
SettingsWindow 1.0 SettingsWindow.qml
|
||||
SharingPage 1.0 SharingPage.qml
|
||||
ShortcutsPage 1.0 ShortcutsPage.qml
|
||||
SoundPage 1.0 SoundPage.qml
|
||||
SettingsPage 1.0 SettingsPage.qml
|
||||
StoragePage 1.0 StoragePage.qml
|
||||
SwitchRow 1.0 SwitchRow.qml
|
||||
ToggleRow 1.0 ToggleRow.qml
|
||||
SliderRow 1.0 SliderRow.qml
|
||||
ChoiceRow 1.0 ChoiceRow.qml
|
||||
@@ -34,6 +39,7 @@ LockScreenPreview 1.0 LockScreenPreview.qml
|
||||
PowerPage 1.0 PowerPage.qml
|
||||
DateTimePage 1.0 DateTimePage.qml
|
||||
AccessibilityPage 1.0 AccessibilityPage.qml
|
||||
UsersPage 1.0 UsersPage.qml
|
||||
WallpaperPicker 1.0 WallpaperPicker.qml
|
||||
WallpaperControls 1.0 WallpaperControls.qml
|
||||
ApplicationsPage 1.0 ApplicationsPage.qml
|
||||
@@ -63,3 +69,4 @@ RegionPage 1.0 RegionPage.qml
|
||||
SearchPicker 1.0 SearchPicker.qml
|
||||
OnlineAccountsPage 1.0 OnlineAccountsPage.qml
|
||||
AccentPicker 1.0 AccentPicker.qml
|
||||
TextFieldRow 1.0 TextFieldRow.qml
|
||||
|
||||
Reference in New Issue
Block a user