mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-14 23:34:01 -04:00
* lock: lock before sleep when the command skips logind Custom hibernate never emits PrepareForSleep, so the session stayed unlocked. Wait for the compositor lock on every sleep path, and release the delay inhibitor if that lock finished first. * lock: release the sleep inhibitor only after lockerReady * lock: read the sleep inhibitor under its mutex in the race test
927 lines
41 KiB
QML
927 lines
41 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import qs.Common
|
|
import qs.Modals.FileBrowser
|
|
import qs.Services
|
|
import qs.Widgets
|
|
import qs.Modules.Settings.Widgets
|
|
import "../../Common/KeyUtils.js" as KeyUtils
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property var parentModal: null
|
|
|
|
readonly property bool lockFprintToggleAvailable: SettingsData.lockFingerprintCanEnable || SettingsData.enableFprint
|
|
readonly property bool lockU2fToggleAvailable: SettingsData.lockU2fCanEnable || SettingsData.enableU2f
|
|
readonly property bool primaryPamHasFprint: SettingsData.lockPamPath !== "" && SettingsData.lockPamInlineFprint
|
|
readonly property bool primaryPamHasU2f: SettingsData.lockPamPath !== "" && SettingsData.lockPamInlineU2f
|
|
readonly property bool lockFprintControlledByPrimary: SettingsData.lockPamExternallyManaged || primaryPamHasFprint
|
|
readonly property bool lockU2fControlledByPrimary: SettingsData.lockPamExternallyManaged || primaryPamHasU2f
|
|
|
|
property var authServices: []
|
|
property bool authValidateRunning: false
|
|
property bool authValidateOk: false
|
|
property bool authValidateWarn: false
|
|
property string authValidateMessage: ""
|
|
property string authPendingApplyPath: ""
|
|
property bool authShowCustom: false
|
|
property bool u2fValidateRunning: false
|
|
property bool u2fValidateOk: false
|
|
property bool u2fValidateWarn: false
|
|
property string u2fValidateMessage: ""
|
|
property string u2fPendingApplyPath: ""
|
|
property bool u2fShowCustom: false
|
|
|
|
readonly property string authAutoLabel: I18n.tr("Auto", "automatic PAM authentication source option")
|
|
readonly property string authCustomLabel: I18n.tr("Custom", "custom PAM authentication source option") + "…"
|
|
|
|
function authServiceLabel(service) {
|
|
const label = service.name.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join("-");
|
|
return service.dir === "/etc/pam.d" ? label : label + " (" + service.dir + ")";
|
|
}
|
|
|
|
readonly property var authOptions: [authAutoLabel, ...authServices.map(s => authServiceLabel(s)), authCustomLabel]
|
|
|
|
readonly property string authCurrentValue: {
|
|
if (SettingsData.lockPamPath === "")
|
|
return authAutoLabel;
|
|
const svc = authServices.find(s => s.path === SettingsData.lockPamPath);
|
|
return svc ? authServiceLabel(svc) : authCustomLabel;
|
|
}
|
|
|
|
readonly property string u2fAuthCurrentValue: SettingsData.lockU2fPamPath === "" ? authAutoLabel : authCustomLabel
|
|
|
|
function refreshAuthServices() {
|
|
authListServicesProcess.running = true;
|
|
}
|
|
|
|
function applyAutoAuthSource() {
|
|
SettingsData.set("lockPamPath", "");
|
|
SettingsData.set("lockPamInlineFprint", false);
|
|
SettingsData.set("lockPamInlineU2f", false);
|
|
root.authValidateOk = false;
|
|
root.authValidateWarn = false;
|
|
root.authValidateMessage = "";
|
|
}
|
|
|
|
function validateAndApplyAuthSource(path) {
|
|
if (!path)
|
|
return;
|
|
root.authPendingApplyPath = path;
|
|
root.authValidateMessage = "";
|
|
root.authValidateOk = false;
|
|
root.authValidateWarn = false;
|
|
root.authValidateRunning = true;
|
|
authValidateProcess.command = ["dms", "auth", "validate", "--path", path, "--json"];
|
|
authValidateProcess.running = true;
|
|
}
|
|
|
|
function applyAutoU2fSource() {
|
|
SettingsData.set("lockU2fPamPath", "");
|
|
root.u2fValidateOk = false;
|
|
root.u2fValidateWarn = false;
|
|
root.u2fValidateMessage = "";
|
|
}
|
|
|
|
function validateAndApplyU2fSource(path) {
|
|
if (!path)
|
|
return;
|
|
root.u2fPendingApplyPath = path;
|
|
root.u2fValidateMessage = "";
|
|
root.u2fValidateOk = false;
|
|
root.u2fValidateWarn = false;
|
|
root.u2fValidateRunning = true;
|
|
u2fValidateProcess.command = ["dms", "auth", "validate", "--purpose", "u2f", "--path", path, "--json"];
|
|
u2fValidateProcess.running = true;
|
|
}
|
|
|
|
function lockFingerprintDescription() {
|
|
switch (SettingsData.lockFingerprintReason) {
|
|
case "ready":
|
|
return I18n.tr("Use fingerprint authentication for the lock screen", "lock screen fingerprint setting");
|
|
case "missing_enrollment":
|
|
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.", "lock screen fingerprint setting");
|
|
case "missing_reader":
|
|
return I18n.tr("No fingerprint reader detected", "fingerprint setting status");
|
|
case "missing_pam_support":
|
|
return I18n.tr("Not available - install fprintd and pam_fprintd", "lock screen fingerprint setting");
|
|
default:
|
|
return I18n.tr("Fingerprint availability could not be confirmed", "fingerprint setting status");
|
|
}
|
|
}
|
|
|
|
function lockU2fDescription() {
|
|
switch (SettingsData.lockU2fReason) {
|
|
case "ready":
|
|
return I18n.tr("Use a security key for lock screen authentication", "lock screen U2F security key setting");
|
|
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":
|
|
return I18n.tr("Not available - install or configure pam_u2f", "lock screen security key setting");
|
|
default:
|
|
return I18n.tr("Security-key availability could not be confirmed", "security key setting status");
|
|
}
|
|
}
|
|
|
|
function refreshAuthDetection() {
|
|
SettingsData.refreshAuthAvailability();
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
MultimediaService.ensureProbed();
|
|
refreshAuthDetection();
|
|
refreshAuthServices();
|
|
}
|
|
onVisibleChanged: {
|
|
if (visible) {
|
|
refreshAuthDetection();
|
|
refreshAuthServices();
|
|
}
|
|
}
|
|
|
|
LazyLoader {
|
|
id: wallpaperBrowserLoader
|
|
active: false
|
|
|
|
FileBrowserModal {
|
|
parentModal: root.parentModal
|
|
browserTitle: I18n.tr("Select lock screen background image")
|
|
bucket: "wallpaper"
|
|
showHiddenFiles: true
|
|
startPath: SettingsData.lockScreenWallpaperPath || Theme.wallpaperPath
|
|
filters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.svg"]
|
|
onAccepted: paths => SettingsData.set("lockScreenWallpaperPath", paths[0])
|
|
}
|
|
}
|
|
|
|
FileBrowserModal {
|
|
id: videoBrowserModal
|
|
browserTitle: I18n.tr("Select Video or Folder")
|
|
bucket: "video"
|
|
filters: ["*.mp4", "*.mkv", "*.webm", "*.mov", "*.avi", "*.m4v"]
|
|
onAccepted: paths => SettingsData.set("lockScreenVideoPath", paths[0])
|
|
}
|
|
|
|
Process {
|
|
id: authListServicesProcess
|
|
command: ["dms", "auth", "list-services", "--json"]
|
|
running: false
|
|
|
|
property string collected: ""
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: authListServicesProcess.collected = text || ""
|
|
}
|
|
|
|
onExited: exitCode => {
|
|
if (exitCode !== 0) {
|
|
root.authServices = [];
|
|
return;
|
|
}
|
|
try {
|
|
const data = JSON.parse(authListServicesProcess.collected);
|
|
root.authServices = (data && Array.isArray(data.services)) ? data.services.filter(s => s.hasAuth) : [];
|
|
} catch (e) {
|
|
root.authServices = [];
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: authValidateProcess
|
|
running: false
|
|
|
|
property string collected: ""
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: authValidateProcess.collected = text || ""
|
|
}
|
|
|
|
onExited: exitCode => {
|
|
root.authValidateRunning = false;
|
|
root.authValidateOk = false;
|
|
root.authValidateWarn = false;
|
|
|
|
let data = null;
|
|
try {
|
|
data = JSON.parse(authValidateProcess.collected);
|
|
} catch (e) {}
|
|
|
|
if (!data) {
|
|
root.authValidateMessage = I18n.tr("Config validation failed");
|
|
return;
|
|
}
|
|
if (!data.valid) {
|
|
const errs = Array.isArray(data.errors) ? data.errors : [];
|
|
root.authValidateMessage = [I18n.tr("Config validation failed"), ...errs].join("\n");
|
|
return;
|
|
}
|
|
|
|
SettingsData.set("lockPamPath", root.authPendingApplyPath);
|
|
SettingsData.set("lockPamInlineFprint", data.inlineFingerprint === true);
|
|
SettingsData.set("lockPamInlineU2f", data.inlineU2f === true);
|
|
const warns = Array.isArray(data.warnings) ? data.warnings : [];
|
|
root.authValidateOk = true;
|
|
root.authValidateWarn = warns.length > 0;
|
|
root.authValidateMessage = [I18n.tr("Authentication changes applied"), ...warns].join("\n");
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: u2fValidateProcess
|
|
running: false
|
|
|
|
property string collected: ""
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: u2fValidateProcess.collected = text || ""
|
|
}
|
|
|
|
onExited: exitCode => {
|
|
root.u2fValidateRunning = false;
|
|
root.u2fValidateOk = false;
|
|
root.u2fValidateWarn = false;
|
|
|
|
let data = null;
|
|
try {
|
|
data = JSON.parse(u2fValidateProcess.collected);
|
|
} catch (e) {}
|
|
|
|
if (!data) {
|
|
root.u2fValidateMessage = I18n.tr("Config validation failed");
|
|
return;
|
|
}
|
|
if (!data.valid) {
|
|
const errs = Array.isArray(data.errors) ? data.errors : [];
|
|
root.u2fValidateMessage = [I18n.tr("Config validation failed"), ...errs].join("\n");
|
|
return;
|
|
}
|
|
|
|
SettingsData.set("lockU2fPamPath", root.u2fPendingApplyPath);
|
|
const warns = Array.isArray(data.warnings) ? data.warnings : [];
|
|
root.u2fValidateOk = true;
|
|
root.u2fValidateWarn = warns.length > 0;
|
|
root.u2fValidateMessage = [I18n.tr("Authentication changes applied"), ...warns].join("\n");
|
|
root.refreshAuthDetection();
|
|
}
|
|
}
|
|
|
|
SettingsPage {
|
|
id: mainColumn
|
|
|
|
SettingsCard {
|
|
width: parent.width
|
|
iconName: "lock"
|
|
title: I18n.tr("Layout")
|
|
settingKey: "lockLayout"
|
|
|
|
SettingsToggleRow {
|
|
settingKey: "lockScreenShowPowerActions"
|
|
tags: ["lock", "screen", "power", "actions", "shutdown", "reboot"]
|
|
text: I18n.tr("Show power actions")
|
|
checked: SettingsData.lockScreenShowPowerActions
|
|
onToggled: checked => SettingsData.set("lockScreenShowPowerActions", checked)
|
|
}
|
|
|
|
SettingsToggleRow {
|
|
settingKey: "lockScreenShowSystemIcons"
|
|
tags: ["lock", "screen", "system", "icons", "status"]
|
|
text: I18n.tr("Show system icons")
|
|
checked: SettingsData.lockScreenShowSystemIcons
|
|
onToggled: checked => SettingsData.set("lockScreenShowSystemIcons", checked)
|
|
}
|
|
|
|
SettingsToggleRow {
|
|
settingKey: "lockScreenShowTime"
|
|
tags: ["lock", "screen", "time", "clock", "display"]
|
|
text: I18n.tr("Show time")
|
|
checked: SettingsData.lockScreenShowTime
|
|
onToggled: checked => SettingsData.set("lockScreenShowTime", checked)
|
|
}
|
|
|
|
SettingsButtonGroupRow {
|
|
settingKey: "lockScreenClockStyle"
|
|
tags: ["lock", "screen", "time", "clock", "style", "vertical"]
|
|
text: I18n.tr("Clock style")
|
|
visible: SettingsData.lockScreenShowTime
|
|
model: [I18n.tr("Horizontal", "lock screen clock style option"), I18n.tr("Vertical", "lock screen clock style option")]
|
|
currentIndex: SettingsData.lockScreenClockStyle === "vertical" ? 1 : 0
|
|
onSelectionChanged: (index, selected) => {
|
|
if (!selected)
|
|
return;
|
|
SettingsData.set("lockScreenClockStyle", index === 1 ? "vertical" : "horizontal");
|
|
}
|
|
}
|
|
|
|
SettingsToggleRow {
|
|
settingKey: "lockScreenShowDate"
|
|
tags: ["lock", "screen", "date", "calendar", "display"]
|
|
text: I18n.tr("Show date")
|
|
checked: SettingsData.lockScreenShowDate
|
|
onToggled: checked => SettingsData.set("lockScreenShowDate", checked)
|
|
}
|
|
|
|
SettingsToggleRow {
|
|
settingKey: "lockScreenShowProfileImage"
|
|
tags: ["lock", "screen", "profile", "image", "avatar", "picture"]
|
|
text: I18n.tr("Show profile image")
|
|
checked: SettingsData.lockScreenShowProfileImage
|
|
onToggled: checked => SettingsData.set("lockScreenShowProfileImage", checked)
|
|
}
|
|
|
|
SettingsToggleRow {
|
|
settingKey: "lockScreenShowPasswordField"
|
|
tags: ["lock", "screen", "password", "field", "input", "visible"]
|
|
text: I18n.tr("Show password field")
|
|
description: I18n.tr("A hidden field appears as soon as a key is pressed")
|
|
checked: SettingsData.lockScreenShowPasswordField
|
|
onToggled: checked => SettingsData.set("lockScreenShowPasswordField", checked)
|
|
}
|
|
|
|
SettingsToggleRow {
|
|
settingKey: "lockScreenShowMediaPlayer"
|
|
tags: ["lock", "screen", "media", "player", "music", "mpris"]
|
|
text: I18n.tr("Show media player")
|
|
checked: SettingsData.lockScreenShowMediaPlayer
|
|
onToggled: checked => SettingsData.set("lockScreenShowMediaPlayer", checked)
|
|
}
|
|
|
|
SettingsSplitRow {
|
|
settingKey: "lockScreenShowWeather"
|
|
tab: "lock_screen"
|
|
tags: ["weather", "temperature"]
|
|
title: I18n.tr("Weather")
|
|
checked: SettingsData.lockScreenShowWeather
|
|
onToggled: checked => SettingsData.set("lockScreenShowWeather", checked)
|
|
onNavigated: root.parentModal?.navigateTo("weather")
|
|
}
|
|
|
|
SettingsDropdownRow {
|
|
settingKey: "lockScreenNotificationMode"
|
|
tags: ["lock", "screen", "notification", "notifications", "privacy"]
|
|
text: I18n.tr("Notifications")
|
|
options: [I18n.tr("Disabled", "lock screen notification mode option"), I18n.tr("Count only", "lock screen notification mode option"), I18n.tr("App names", "lock screen notification mode option"), I18n.tr("Full content", "lock screen notification mode option")]
|
|
currentValue: options[SettingsData.lockScreenNotificationMode] || options[0]
|
|
onValueChanged: value => {
|
|
const idx = options.indexOf(value);
|
|
if (idx >= 0) {
|
|
SettingsData.set("lockScreenNotificationMode", idx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
width: parent.width
|
|
iconName: "palette"
|
|
title: I18n.tr("Appearance")
|
|
settingKey: "lockAppearance"
|
|
|
|
SettingsFontDropdownRow {
|
|
settingKey: "lockScreenFontFamily"
|
|
tags: ["lock", "screen", "font", "typography"]
|
|
text: I18n.tr("Font")
|
|
currentFont: SettingsData.lockScreenFontFamily || ""
|
|
onFontSelected: family => SettingsData.set("lockScreenFontFamily", family)
|
|
}
|
|
|
|
SettingsRow {
|
|
settingKey: "lockScreenWallpaperPath"
|
|
tags: ["lock", "screen", "wallpaper", "background", "image"]
|
|
title: I18n.tr("Background")
|
|
resetKeys: ["lockScreenWallpaperPath"]
|
|
body: SettingsWallpaperThumb {
|
|
width: (parent.width - Theme.spacingL) / 2
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
placeholderIcon: "lock"
|
|
emptyText: I18n.tr("Use desktop wallpaper")
|
|
allowColor: false
|
|
path: SettingsData.lockScreenWallpaperPath
|
|
onBrowse: {
|
|
wallpaperBrowserLoader.active = true;
|
|
if (wallpaperBrowserLoader.item)
|
|
wallpaperBrowserLoader.item.open();
|
|
}
|
|
onClear: SettingsData.set("lockScreenWallpaperPath", "")
|
|
}
|
|
}
|
|
|
|
SettingsDropdownRow {
|
|
readonly property var fillModes: ["Stretch", "Fit", "Fill", "Tile", "TileVertically", "TileHorizontally", "Pad"]
|
|
readonly property var fillModeLabels: fillModes.map(mode => I18n.tr(mode, "wallpaper fill mode"))
|
|
|
|
settingKey: "lockScreenWallpaperFillMode"
|
|
tags: ["lock", "screen", "wallpaper", "background", "fill"]
|
|
text: I18n.tr("Wallpaper fill mode")
|
|
options: fillModeLabels
|
|
currentValue: {
|
|
const idx = fillModes.indexOf(SettingsData.lockScreenWallpaperFillMode || "Fill");
|
|
return fillModeLabels[idx >= 0 ? idx : fillModes.indexOf("Fill")];
|
|
}
|
|
onValueChanged: value => {
|
|
const idx = fillModeLabels.indexOf(value);
|
|
if (idx >= 0)
|
|
SettingsData.set("lockScreenWallpaperFillMode", fillModes[idx]);
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
width: parent.width
|
|
iconName: "lock"
|
|
title: I18n.tr("Behavior")
|
|
settingKey: "lockBehavior"
|
|
SettingsToggleRow {
|
|
settingKey: "lockScreenPowerOffMonitorsOnLock"
|
|
tags: ["lock", "screen", "monitor", "display", "dpms", "power"]
|
|
text: I18n.tr("Power off displays on lock")
|
|
checked: SettingsData.lockScreenPowerOffMonitorsOnLock
|
|
onToggled: checked => SettingsData.set("lockScreenPowerOffMonitorsOnLock", checked)
|
|
}
|
|
|
|
SettingsToggleRow {
|
|
settingKey: "lockAtStartup"
|
|
tags: ["lock", "screen", "startup", "start", "boot", "login", "automatic"]
|
|
text: I18n.tr("Lock at startup")
|
|
checked: SettingsData.lockAtStartup
|
|
onToggled: checked => SettingsData.set("lockAtStartup", checked)
|
|
}
|
|
|
|
SettingsToggleRow {
|
|
settingKey: "lockBeforeSuspend"
|
|
tags: ["lock", "screen", "suspend", "hibernate", "sleep", "automatic"]
|
|
text: I18n.tr("Lock before suspend")
|
|
checked: SettingsData.lockBeforeSuspend
|
|
visible: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
|
|
onToggled: checked => SettingsData.set("lockBeforeSuspend", checked)
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
width: parent.width
|
|
iconName: "movie"
|
|
title: I18n.tr("Video screensaver")
|
|
settingKey: "videoScreensaver"
|
|
|
|
SettingsRow {
|
|
visible: !MultimediaService.available
|
|
body: StyledText {
|
|
text: I18n.tr("QtMultimedia is not available - video screensaver requires qt multimedia services")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.warning
|
|
width: parent.width
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
|
|
SettingsToggleRow {
|
|
settingKey: "lockScreenVideoEnabled"
|
|
tags: ["lock", "screen", "video", "screensaver", "animation", "movie"]
|
|
text: I18n.tr("Play video on lock")
|
|
enabled: MultimediaService.available
|
|
checked: SettingsData.lockScreenVideoEnabled
|
|
onToggled: checked => SettingsData.set("lockScreenVideoEnabled", checked)
|
|
}
|
|
|
|
SettingsTextFieldRow {
|
|
leftIconName: "folder"
|
|
visible: SettingsData.lockScreenVideoEnabled && MultimediaService.available
|
|
text: I18n.tr("Path")
|
|
resetKeys: ["lockScreenVideoPath"]
|
|
value: SettingsData.lockScreenVideoPath
|
|
placeholderText: "/path/to/videos"
|
|
onValueEdited: value => SettingsData.set("lockScreenVideoPath", value)
|
|
|
|
actions: DankButton {
|
|
text: I18n.tr("Browse")
|
|
onClicked: videoBrowserModal.open()
|
|
}
|
|
}
|
|
|
|
SettingsToggleRow {
|
|
settingKey: "lockScreenVideoCycling"
|
|
tags: ["lock", "screen", "video", "screensaver", "cycling", "random", "shuffle"]
|
|
text: I18n.tr("Automatic cycling")
|
|
visible: SettingsData.lockScreenVideoEnabled && MultimediaService.available
|
|
enabled: MultimediaService.available
|
|
checked: SettingsData.lockScreenVideoCycling
|
|
onToggled: checked => SettingsData.set("lockScreenVideoCycling", checked)
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
width: parent.width
|
|
iconName: "key"
|
|
title: I18n.tr("Authentication")
|
|
settingKey: "lockAuthSource"
|
|
SettingsToggleRow {
|
|
settingKey: "lockPamExternallyManaged"
|
|
tags: ["lock", "screen", "pam", "managed", "external", "authentication", "policy"]
|
|
text: I18n.tr("Use system PAM authentication", "system PAM policy toggle")
|
|
checked: SettingsData.lockPamExternallyManaged
|
|
onToggled: checked => SettingsData.set("lockPamExternallyManaged", checked)
|
|
}
|
|
|
|
SettingsToggleRow {
|
|
settingKey: "enableFprint"
|
|
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
|
|
text: I18n.tr("Fingerprint", "lock screen fingerprint authentication toggle")
|
|
description: root.lockFprintControlledByPrimary ? I18n.tr("Managed by the primary PAM source", "factor managed by PAM source status") : root.lockFingerprintDescription()
|
|
descriptionColor: root.lockFprintControlledByPrimary || SettingsData.lockFingerprintReason === "ready" ? Theme.surfaceVariantText : Theme.warning
|
|
checked: SettingsData.enableFprint || root.primaryPamHasFprint
|
|
enabled: root.lockFprintToggleAvailable && !root.lockFprintControlledByPrimary
|
|
onToggled: checked => SettingsData.set("enableFprint", checked)
|
|
}
|
|
|
|
SettingsToggleRow {
|
|
settingKey: "enableU2f"
|
|
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "fido", "authentication", "hardware"]
|
|
text: I18n.tr("Security key")
|
|
description: root.lockU2fControlledByPrimary ? I18n.tr("Managed by the primary PAM source", "factor managed by PAM source status") : root.lockU2fDescription()
|
|
descriptionColor: root.lockU2fControlledByPrimary || SettingsData.lockU2fReason === "ready" ? Theme.surfaceVariantText : Theme.warning
|
|
checked: SettingsData.enableU2f || root.primaryPamHasU2f
|
|
enabled: root.lockU2fToggleAvailable && !root.lockU2fControlledByPrimary
|
|
onToggled: checked => SettingsData.set("enableU2f", checked)
|
|
}
|
|
|
|
SettingsDropdownRow {
|
|
settingKey: "u2fMode"
|
|
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"]
|
|
text: I18n.tr("Security key mode", "lock screen U2F security key mode setting")
|
|
description: I18n.tr("'Alternative' lets the key unlock on its own. 'Second factor' requires password or fingerprint first, then the key.", "lock screen U2F security key mode setting")
|
|
visible: SettingsData.enableU2f && !root.lockU2fControlledByPrimary
|
|
options: [I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method"), I18n.tr("Second factor (AND)", "U2F mode option: key required after password or fingerprint")]
|
|
currentValue: SettingsData.u2fMode === "and" ? I18n.tr("Second factor (AND)", "U2F mode option: key required after password or fingerprint") : I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method")
|
|
onValueChanged: value => {
|
|
if (value === I18n.tr("Second factor (AND)", "U2F mode option: key required after password or fingerprint"))
|
|
SettingsData.set("u2fMode", "and");
|
|
else
|
|
SettingsData.set("u2fMode", "or");
|
|
}
|
|
}
|
|
|
|
SettingsToggleRow {
|
|
settingKey: "lockScreenSecurityKeyShortcutEnabled"
|
|
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "shortcut", "keybind", "authentication"]
|
|
text: I18n.tr("Security key shortcut", "lock screen security key shortcut toggle")
|
|
checked: SettingsData.lockScreenSecurityKeyShortcutEnabled
|
|
visible: SettingsData.enableU2f && SettingsData.u2fMode === "or" && !root.lockU2fControlledByPrimary
|
|
onToggled: checked => SettingsData.set("lockScreenSecurityKeyShortcutEnabled", checked)
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: SettingsData.lockScreenSecurityKeyShortcutEnabled && SettingsData.enableU2f && SettingsData.u2fMode === "or" && !root.lockU2fControlledByPrimary
|
|
body: Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingM
|
|
|
|
Column {
|
|
width: parent.width - securityKeyCapture.width - parent.spacing
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Theme.spacingXS
|
|
|
|
StyledText {
|
|
text: I18n.tr("Key combination", "lock screen security key shortcut key combination setting")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Theme.fontWeightMedium
|
|
color: Theme.surfaceText
|
|
width: parent.width
|
|
horizontalAlignment: Text.AlignLeft
|
|
}
|
|
|
|
StyledText {
|
|
text: securityKeyCapture.captureError !== "" ? securityKeyCapture.captureError : I18n.tr("Press Ctrl+key to set. Esc cancels.", "lock screen security key shortcut key combination capture hint")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: securityKeyCapture.captureError !== "" ? Theme.warning : Theme.surfaceVariantText
|
|
wrapMode: Text.WordWrap
|
|
width: parent.width
|
|
horizontalAlignment: Text.AlignLeft
|
|
}
|
|
}
|
|
|
|
DankButton {
|
|
id: securityKeyCapture
|
|
width: 200
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
focus: capturing
|
|
text: capturing ? I18n.tr("Press key...", "lock screen security key shortcut key combination capture prompt") : SettingsData.lockScreenSecurityKeyShortcut
|
|
backgroundColor: capturing ? Theme.selectedContainer : Theme.chipSurface
|
|
textColor: Theme.surfaceText
|
|
|
|
property bool capturing: false
|
|
property string captureError: ""
|
|
readonly property var reservedKeys: ["A", "E", "B", "F", "U", "K", "W", "H", "D"]
|
|
|
|
function startCapture() {
|
|
captureError = "";
|
|
capturing = true;
|
|
securityKeyCapture.forceActiveFocus();
|
|
}
|
|
|
|
function stopCapture() {
|
|
capturing = false;
|
|
}
|
|
|
|
onClicked: {
|
|
if (capturing)
|
|
stopCapture();
|
|
else
|
|
startCapture();
|
|
}
|
|
|
|
Keys.onPressed: event => {
|
|
if (!securityKeyCapture.capturing)
|
|
return;
|
|
|
|
if (KeyUtils.isModifierKey(event.key))
|
|
return;
|
|
|
|
if (event.key === Qt.Key_Escape) {
|
|
securityKeyCapture.stopCapture();
|
|
event.accepted = true;
|
|
return;
|
|
}
|
|
|
|
const mods = KeyUtils.modsFromEvent(event.modifiers);
|
|
const hasCtrl = mods.includes("Ctrl");
|
|
const hasAlt = mods.includes("Alt") || mods.includes("Super");
|
|
if (!hasCtrl || hasAlt)
|
|
return;
|
|
|
|
const hasShift = mods.includes("Shift");
|
|
const key = KeyUtils.xkbKeyFromQtKey(event.key, !!(event.modifiers & Qt.KeypadModifier), hasShift);
|
|
if (!key)
|
|
return;
|
|
|
|
if (!KeyUtils.qtKeyFromName(key))
|
|
return;
|
|
|
|
if (!hasShift && securityKeyCapture.reservedKeys.indexOf(key.toUpperCase()) !== -1) {
|
|
securityKeyCapture.captureError = I18n.tr("Ctrl+%1 is used for password editing", "lock screen security key shortcut reserved key warning").arg(key.toUpperCase());
|
|
event.accepted = true;
|
|
return;
|
|
}
|
|
|
|
SettingsData.set("lockScreenSecurityKeyShortcut", KeyUtils.formatToken(mods, key));
|
|
securityKeyCapture.captureError = "";
|
|
securityKeyCapture.stopCapture();
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
width: parent.width
|
|
iconName: "monitor"
|
|
title: I18n.tr("Displays")
|
|
settingKey: "lockDisplay"
|
|
SettingsRow {
|
|
body: StyledText {
|
|
text: I18n.tr("Other displays show a solid color for OLED burn-in protection")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
width: parent.width
|
|
wrapMode: Text.Wrap
|
|
}
|
|
}
|
|
|
|
SettingsDisplayPicker {
|
|
width: parent.width
|
|
displayPreferences: SettingsData.screenPreferences?.lockScreen || ["all"]
|
|
onPreferencesChanged: prefs => {
|
|
var p = SettingsData.screenPreferences || {};
|
|
var updated = Object.assign({}, p);
|
|
updated["lockScreen"] = prefs;
|
|
SettingsData.set("screenPreferences", updated);
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
title: I18n.tr("Inactive display color")
|
|
tags: ["lock", "screen", "inactive", "display", "color", "oled"]
|
|
resetKeys: ["lockScreenInactiveColor"]
|
|
visible: {
|
|
const prefs = SettingsData.screenPreferences?.lockScreen;
|
|
return Array.isArray(prefs) && !prefs.includes("all") && prefs.length > 0;
|
|
}
|
|
|
|
Rectangle {
|
|
width: Theme.iconButtonSize
|
|
height: Theme.iconButtonSize
|
|
radius: Theme.cornerRadius
|
|
color: SettingsData.lockScreenInactiveColor
|
|
border.color: Theme.outline
|
|
border.width: Theme.outlineWidth
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
if (!PopoutService.colorPickerModal)
|
|
return;
|
|
PopoutService.colorPickerModal.selectedColor = SettingsData.lockScreenInactiveColor;
|
|
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Inactive display color");
|
|
PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) {
|
|
SettingsData.set("lockScreenInactiveColor", selectedColor);
|
|
};
|
|
PopoutService.colorPickerModal.show();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
width: parent.width
|
|
iconName: "tune"
|
|
title: I18n.tr("Advanced")
|
|
settingKey: "lockScreenAdvanced"
|
|
tags: ["pam", "authentication", "source", "loginctl", "u2f"]
|
|
collapsible: true
|
|
expanded: false
|
|
|
|
SettingsRow {
|
|
body: StyledText {
|
|
text: I18n.tr("Authentication changes apply automatically")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
width: parent.width
|
|
wrapMode: Text.Wrap
|
|
}
|
|
}
|
|
|
|
SettingsDropdownRow {
|
|
settingKey: "lockPamPath"
|
|
resetKeys: []
|
|
tags: ["lock", "screen", "pam", "authentication", "source", "service"]
|
|
text: I18n.tr("Authentication source", "lock screen PAM source setting")
|
|
description: SettingsData.lockPamPath !== "" ? SettingsData.lockPamPath : I18n.tr("Which PAM service the lock screen uses to authenticate", "lock screen PAM source setting")
|
|
options: root.authOptions
|
|
currentValue: root.authCurrentValue
|
|
onValueChanged: value => {
|
|
if (value === root.authAutoLabel) {
|
|
root.authShowCustom = false;
|
|
root.applyAutoAuthSource();
|
|
return;
|
|
}
|
|
if (value === root.authCustomLabel) {
|
|
root.authShowCustom = true;
|
|
return;
|
|
}
|
|
root.authShowCustom = false;
|
|
const svc = root.authServices.find(s => root.authServiceLabel(s) === value);
|
|
if (svc)
|
|
root.validateAndApplyAuthSource(svc.path);
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: root.authShowCustom || root.authCurrentValue === root.authCustomLabel
|
|
body: Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
DankTextField {
|
|
id: customPamField
|
|
outlined: true
|
|
leftIconName: "lock"
|
|
labelText: I18n.tr("Path")
|
|
width: parent.width - validatePamButton.width - Theme.spacingS
|
|
placeholderText: "/etc/pam.d/my-service"
|
|
text: SettingsData.lockPamPath
|
|
}
|
|
|
|
DankButton {
|
|
id: validatePamButton
|
|
text: I18n.tr("Apply changes")
|
|
enabled: !root.authValidateRunning && customPamField.text.trim() !== ""
|
|
onClicked: root.validateAndApplyAuthSource(customPamField.text.trim())
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: root.authValidateMessage !== ""
|
|
body: Rectangle {
|
|
width: parent.width
|
|
height: Math.min(160, authStatusText.implicitHeight + Theme.spacingM * 2)
|
|
radius: Theme.cornerRadius
|
|
color: Theme.floatingWindowFieldColor
|
|
border.color: Theme.outlineMedium
|
|
border.width: Theme.layerOutlineWidth
|
|
|
|
StyledText {
|
|
id: authStatusText
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingM
|
|
text: root.authValidateMessage
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
font.family: "monospace"
|
|
color: !root.authValidateOk ? Theme.error : (root.authValidateWarn ? Theme.warning : Theme.surfaceVariantText)
|
|
wrapMode: Text.Wrap
|
|
verticalAlignment: Text.AlignTop
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsDropdownRow {
|
|
settingKey: "lockU2fPamPath"
|
|
resetKeys: []
|
|
tags: ["lock", "screen", "pam", "u2f", "security", "key", "source", "service"]
|
|
text: I18n.tr("Security key PAM source", "lock screen dedicated U2F PAM source setting")
|
|
description: SettingsData.lockU2fPamPath !== "" ? SettingsData.lockU2fPamPath : I18n.tr("Auto uses an installed or bundled key-only service.", "lock screen dedicated U2F PAM source setting")
|
|
visible: !root.lockU2fControlledByPrimary
|
|
options: [root.authAutoLabel, root.authCustomLabel]
|
|
currentValue: root.u2fAuthCurrentValue
|
|
onValueChanged: value => {
|
|
if (value === root.authAutoLabel) {
|
|
root.u2fShowCustom = false;
|
|
root.applyAutoU2fSource();
|
|
return;
|
|
}
|
|
root.u2fShowCustom = true;
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: !root.lockU2fControlledByPrimary && (root.u2fShowCustom || root.u2fAuthCurrentValue === root.authCustomLabel)
|
|
body: Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
DankTextField {
|
|
id: customU2fPamField
|
|
outlined: true
|
|
leftIconName: "key"
|
|
labelText: I18n.tr("Path")
|
|
width: parent.width - validateU2fPamButton.width - Theme.spacingS
|
|
placeholderText: "/etc/pam.d/dankshell-u2f"
|
|
text: SettingsData.lockU2fPamPath
|
|
}
|
|
|
|
DankButton {
|
|
id: validateU2fPamButton
|
|
text: I18n.tr("Apply changes")
|
|
enabled: !root.u2fValidateRunning && customU2fPamField.text.trim() !== ""
|
|
onClicked: root.validateAndApplyU2fSource(customU2fPamField.text.trim())
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: !root.lockU2fControlledByPrimary && root.u2fValidateMessage !== ""
|
|
body: Rectangle {
|
|
width: parent.width
|
|
height: Math.min(160, u2fStatusText.implicitHeight + Theme.spacingM * 2)
|
|
radius: Theme.cornerRadius
|
|
color: Theme.floatingWindowFieldColor
|
|
border.color: Theme.outlineMedium
|
|
border.width: Theme.layerOutlineWidth
|
|
|
|
StyledText {
|
|
id: u2fStatusText
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingM
|
|
text: root.u2fValidateMessage
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
font.family: "monospace"
|
|
color: !root.u2fValidateOk ? Theme.error : (root.u2fValidateWarn ? Theme.warning : Theme.surfaceVariantText)
|
|
wrapMode: Text.Wrap
|
|
verticalAlignment: Text.AlignTop
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsRow {
|
|
visible: !SessionService.loginctlAvailable
|
|
body: StyledText {
|
|
text: I18n.tr("loginctl not available - lock integration requires DMS socket connection")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.warning
|
|
width: parent.width
|
|
wrapMode: Text.Wrap
|
|
}
|
|
}
|
|
|
|
SettingsToggleRow {
|
|
settingKey: "loginctlLockIntegration"
|
|
tags: ["lock", "screen", "loginctl", "dbus", "integration", "external"]
|
|
text: I18n.tr("loginctl integration")
|
|
description: I18n.tr("Disable when using an external lock screen")
|
|
checked: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
|
|
enabled: SessionService.loginctlAvailable
|
|
onToggled: checked => {
|
|
if (!SessionService.loginctlAvailable)
|
|
return;
|
|
SettingsData.set("loginctlLockIntegration", checked);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|