add noctalia and fuzzel
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pam
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
signal unlocked
|
||||
signal failed
|
||||
|
||||
property string currentText: ""
|
||||
property bool waitingForPassword: false
|
||||
property bool unlockInProgress: false
|
||||
property bool showFailure: false
|
||||
property bool showInfo: false
|
||||
property string errorMessage: ""
|
||||
property string infoMessage: ""
|
||||
|
||||
readonly property string pamConfigDirectory: "/etc/pam.d"
|
||||
property string pamConfig: Quickshell.env("NOCTALIA_PAM_SERVICE") || "login"
|
||||
property bool pamReady: false
|
||||
|
||||
Component.onCompleted: {
|
||||
if (Quickshell.env("NOCTALIA_PAM_SERVICE")) {
|
||||
Logger.i("LockContext", "NOCTALIA_PAM_SERVICE is set, using system PAM config: /etc/pam.d/" + pamConfig);
|
||||
pamReady = true;
|
||||
} else {
|
||||
Logger.i("LockContext", "Probing for best PAM service...");
|
||||
detectPamServiceProc.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: detectPamServiceProc
|
||||
command: ["sh", "-c", "
|
||||
if [ -f /etc/pam.d/login ]; then echo 'login'; exit 0; fi;
|
||||
if [ -f /etc/pam.d/system-auth ]; then echo 'system-auth'; exit 0; fi;
|
||||
if [ -f /etc/pam.d/common-auth ]; then echo 'common-auth'; exit 0; fi;
|
||||
echo 'login';
|
||||
"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const service = String(text || "").trim();
|
||||
if (service.length > 0) {
|
||||
root.pamConfig = service;
|
||||
Logger.i("LockContext", "Detected PAM service: " + service);
|
||||
} else {
|
||||
Logger.w("LockContext", "Failed to detect PAM service, defaulting to login");
|
||||
}
|
||||
root.pamReady = true;
|
||||
}
|
||||
}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
onPamReadyChanged: {
|
||||
if (pamReady) {
|
||||
if (Settings.data.general.autoStartAuth && currentText === "") {
|
||||
pam.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onShowInfoChanged: {
|
||||
if (showInfo) {
|
||||
showFailure = false;
|
||||
}
|
||||
}
|
||||
|
||||
onShowFailureChanged: {
|
||||
if (showFailure) {
|
||||
showInfo = false;
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentTextChanged: {
|
||||
if (currentText !== "") {
|
||||
showInfo = false;
|
||||
showFailure = false;
|
||||
if (!waitingForPassword) {
|
||||
pam.abort();
|
||||
}
|
||||
if (Settings.data.general.allowPasswordWithFprintd) {
|
||||
occupyFingerprintSensorProc.running = true;
|
||||
}
|
||||
} else {
|
||||
occupyFingerprintSensorProc.running = false;
|
||||
if (pamReady && Settings.data.general.autoStartAuth) {
|
||||
pam.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tryUnlock() {
|
||||
if (!pamReady) {
|
||||
Logger.w("LockContext", "PAM not ready yet, ignoring unlock attempt");
|
||||
return;
|
||||
}
|
||||
|
||||
if (waitingForPassword) {
|
||||
pam.respond(currentText);
|
||||
unlockInProgress = true;
|
||||
waitingForPassword = false;
|
||||
showInfo = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.i("LockContext", "Starting PAM authentication for user:", pam.user);
|
||||
pam.start();
|
||||
}
|
||||
|
||||
Process {
|
||||
id: occupyFingerprintSensorProc
|
||||
command: ["fprintd-verify"]
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: pam
|
||||
configDirectory: root.pamConfigDirectory
|
||||
config: root.pamConfig
|
||||
user: HostService.username
|
||||
|
||||
onPamMessage: {
|
||||
Logger.i("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:", responseRequired);
|
||||
|
||||
if (this.responseRequired) {
|
||||
Logger.i("LockContext", "Responding to PAM with password");
|
||||
if (root.currentText !== "") {
|
||||
this.respond(root.currentText);
|
||||
unlockInProgress = true;
|
||||
} else {
|
||||
root.waitingForPassword = true;
|
||||
infoMessage = I18n.tr("lock-screen.password");
|
||||
showInfo = true;
|
||||
}
|
||||
} else if (messageIsError) {
|
||||
errorMessage = message;
|
||||
showFailure = true;
|
||||
} else {
|
||||
infoMessage = message;
|
||||
showInfo = true;
|
||||
}
|
||||
}
|
||||
|
||||
onCompleted: result => {
|
||||
Logger.i("LockContext", "PAM completed with result:", result);
|
||||
if (result === PamResult.Success) {
|
||||
Logger.i("LockContext", "Authentication successful");
|
||||
root.unlocked();
|
||||
} else {
|
||||
Logger.i("LockContext", "Authentication failed");
|
||||
root.currentText = "";
|
||||
errorMessage = I18n.tr("authentication.failed");
|
||||
showFailure = true;
|
||||
root.failed();
|
||||
}
|
||||
root.unlockInProgress = false;
|
||||
}
|
||||
|
||||
onError: {
|
||||
Logger.i("LockContext", "PAM error:", error, "message:", message);
|
||||
errorMessage = message || "Authentication error";
|
||||
showFailure = true;
|
||||
root.unlockInProgress = false;
|
||||
root.failed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pam
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.Hardware
|
||||
import qs.Services.Keyboard
|
||||
import qs.Services.Media
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Loader {
|
||||
id: root
|
||||
active: false
|
||||
|
||||
// Track if the visualizer should be shown (lockscreen active + media playing + non-compact mode)
|
||||
readonly property bool needsSpectrum: root.active && !Settings.data.general.compactLockScreen && Settings.data.audio.visualizerType !== "" && Settings.data.audio.visualizerType !== "none"
|
||||
|
||||
onActiveChanged: {
|
||||
if (root.active && root.needsSpectrum) {
|
||||
SpectrumService.registerComponent("lockscreen");
|
||||
} else {
|
||||
SpectrumService.unregisterComponent("lockscreen");
|
||||
}
|
||||
|
||||
if (root.active) {
|
||||
LockKeysService.registerComponent("lockscreen");
|
||||
} else {
|
||||
LockKeysService.unregisterComponent("lockscreen");
|
||||
}
|
||||
}
|
||||
|
||||
onNeedsSpectrumChanged: {
|
||||
if (root.needsSpectrum) {
|
||||
SpectrumService.registerComponent("lockscreen");
|
||||
} else {
|
||||
SpectrumService.unregisterComponent("lockscreen");
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Register with panel service
|
||||
PanelService.lockScreen = this;
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
SpectrumService.unregisterComponent("lockscreen");
|
||||
LockKeysService.unregisterComponent("lockscreen");
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: unloadAfterUnlockTimer
|
||||
interval: 250
|
||||
repeat: false
|
||||
onTriggered: root.active = false
|
||||
}
|
||||
|
||||
function scheduleUnloadAfterUnlock() {
|
||||
unloadAfterUnlockTimer.start();
|
||||
}
|
||||
|
||||
sourceComponent: Component {
|
||||
Item {
|
||||
id: lockContainer
|
||||
|
||||
LockContext {
|
||||
id: lockContext
|
||||
onUnlocked: {
|
||||
lockSession.locked = false;
|
||||
root.scheduleUnloadAfterUnlock();
|
||||
lockContext.currentText = "";
|
||||
}
|
||||
onFailed: {
|
||||
lockContext.currentText = "";
|
||||
}
|
||||
}
|
||||
|
||||
// Whether any monitor from the user's lockScreenMonitors list is currently connected.
|
||||
readonly property bool anyConfiguredMonitorConnected: {
|
||||
const configured = Settings.data.general.lockScreenMonitors;
|
||||
if (!configured || configured.length === 0)
|
||||
return false;
|
||||
return (Quickshell.screens || []).some(s => configured.includes(s.name));
|
||||
}
|
||||
|
||||
WlSessionLock {
|
||||
id: lockSession
|
||||
locked: root.active
|
||||
|
||||
WlSessionLockSurface {
|
||||
id: lockSurface
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: true
|
||||
sourceComponent: (!lockContainer.anyConfiguredMonitorConnected || Settings.data.general.lockScreenMonitors.includes(lockSurface.screen?.name)) ? fullLockScreenComponent : blackScreenComponent
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fullLockScreenComponent
|
||||
|
||||
Item {
|
||||
Item {
|
||||
id: batteryIndicator
|
||||
|
||||
property bool isReady: BatteryService.batteryReady
|
||||
property real percent: BatteryService.batteryPercentage
|
||||
property bool charging: BatteryService.batteryCharging
|
||||
property bool pluggedIn: BatteryService.batteryPluggedIn
|
||||
property bool batteryVisible: isReady
|
||||
property string icon: BatteryService.batteryIcon
|
||||
}
|
||||
|
||||
Item {
|
||||
id: keyboardLayout
|
||||
property string currentLayout: KeyboardLayoutService.currentLayout
|
||||
}
|
||||
|
||||
// Background with wallpaper, gradient, and screen corners
|
||||
LockScreenBackground {
|
||||
id: backgroundComponent
|
||||
screen: lockSurface.screen
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
// Mouse area to trigger focus on cursor movement (workaround for Hyprland focus issues)
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
onEntered: {
|
||||
// Avoid repeatedly forcing focus on every mouse move.
|
||||
// This can churn text-input surface state during monitor/suspend transitions.
|
||||
if (passwordInput && !passwordInput.activeFocus) {
|
||||
passwordInput.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Header with avatar, welcome, time, date
|
||||
LockScreenHeader {
|
||||
id: headerComponent
|
||||
}
|
||||
|
||||
// Info notification
|
||||
Rectangle {
|
||||
width: infoRowLayout.implicitWidth + Style.marginXL * 1.5
|
||||
height: 50
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: (Settings.data.general.compactLockScreen ? 280 : 360) * Style.uiScaleRatio
|
||||
radius: Style.radiusL
|
||||
color: Color.mTertiary
|
||||
visible: lockContext.showInfo && lockContext.infoMessage && !panelComponent.timerActive
|
||||
opacity: visible ? 1.0 : 0.0
|
||||
|
||||
RowLayout {
|
||||
id: infoRowLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: "circle-key"
|
||||
pointSize: Style.fontSizeXL
|
||||
color: Color.mOnTertiary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: lockContext.infoMessage
|
||||
color: Color.mOnTertiary
|
||||
pointSize: Style.fontSizeL
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error notification
|
||||
Rectangle {
|
||||
width: errorRowLayout.implicitWidth + Style.marginXL * 1.5
|
||||
height: 50
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: (Settings.data.general.compactLockScreen ? 280 : 360) * Style.uiScaleRatio
|
||||
radius: Style.radiusL
|
||||
color: Color.mError
|
||||
visible: lockContext.showFailure && lockContext.errorMessage && !panelComponent.timerActive
|
||||
opacity: visible ? 1.0 : 0.0
|
||||
|
||||
RowLayout {
|
||||
id: errorRowLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: "alert-circle"
|
||||
pointSize: Style.fontSizeXL
|
||||
color: Color.mOnError
|
||||
}
|
||||
|
||||
NText {
|
||||
text: lockContext.errorMessage || "Authentication failed"
|
||||
color: Color.mOnError
|
||||
pointSize: Style.fontSizeL
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Countdown notification
|
||||
Rectangle {
|
||||
width: countdownRowLayout.implicitWidth + Style.marginXL * 1.5
|
||||
height: 50
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: (Settings.data.general.compactLockScreen ? 280 : 360) * Style.uiScaleRatio
|
||||
radius: Style.radiusL
|
||||
color: Color.mSurface
|
||||
visible: panelComponent.timerActive
|
||||
opacity: visible ? 1.0 : 0.0
|
||||
|
||||
RowLayout {
|
||||
id: countdownRowLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: "clock"
|
||||
pointSize: Style.fontSizeXL
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("session-menu.action-in-seconds", {
|
||||
"action": I18n.tr("common." + panelComponent.pendingAction),
|
||||
"seconds": Math.ceil(panelComponent.timeRemaining / 1000)
|
||||
})
|
||||
color: Color.mOnSurface
|
||||
pointSize: Style.fontSizeL
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "x"
|
||||
tooltipText: I18n.tr("session-menu.cancel-timer")
|
||||
baseSize: 32
|
||||
colorBg: Qt.alpha(Color.mPrimary, 0.1)
|
||||
colorFg: Color.mPrimary
|
||||
colorBgHover: Color.mPrimary
|
||||
onClicked: panelComponent.cancelTimer()
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hidden input that receives actual text
|
||||
TextInput {
|
||||
id: passwordInput
|
||||
width: 0
|
||||
height: 0
|
||||
visible: false
|
||||
enabled: !lockContext.unlockInProgress
|
||||
echoMode: TextInput.Password
|
||||
passwordMaskDelay: 0
|
||||
|
||||
// Bidirectional sync — avoids a declarative binding which breaks on input
|
||||
onTextChanged: {
|
||||
if (lockContext.currentText !== text)
|
||||
lockContext.currentText = text;
|
||||
}
|
||||
Connections {
|
||||
target: lockContext
|
||||
function onCurrentTextChanged() {
|
||||
if (passwordInput.text !== lockContext.currentText)
|
||||
passwordInput.text = lockContext.currentText;
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: function (event) {
|
||||
if (Keybinds.checkKey(event, 'enter', Settings)) {
|
||||
lockContext.tryUnlock();
|
||||
event.accepted = true;
|
||||
}
|
||||
if (Keybinds.checkKey(event, 'escape', Settings) && panelComponent.timerActive) {
|
||||
panelComponent.cancelTimer();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: forceActiveFocus()
|
||||
}
|
||||
|
||||
// Main panel with password, weather, media, session controls
|
||||
LockScreenPanel {
|
||||
id: panelComponent
|
||||
lockControl: lockContext
|
||||
batteryIndicator: batteryIndicator
|
||||
keyboardLayout: keyboardLayout
|
||||
passwordInput: passwordInput
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: blackScreenComponent
|
||||
|
||||
// Black surface for disabled monitors — still captures keyboard for password entry
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
|
||||
TextInput {
|
||||
id: blackScreenPasswordInput
|
||||
width: 0
|
||||
height: 0
|
||||
visible: false
|
||||
enabled: !lockContext.unlockInProgress
|
||||
echoMode: TextInput.Password
|
||||
passwordMaskDelay: 0
|
||||
|
||||
// Bidirectional sync — avoids a declarative binding which breaks on input
|
||||
onTextChanged: {
|
||||
if (lockContext.currentText !== text)
|
||||
lockContext.currentText = text;
|
||||
}
|
||||
Connections {
|
||||
target: lockContext
|
||||
function onCurrentTextChanged() {
|
||||
if (blackScreenPasswordInput.text !== lockContext.currentText)
|
||||
blackScreenPasswordInput.text = lockContext.currentText;
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: function (event) {
|
||||
if (Keybinds.checkKey(event, 'enter', Settings)) {
|
||||
lockContext.tryUnlock();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: forceActiveFocus()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
onPositionChanged: blackScreenPasswordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.Power
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
id: root
|
||||
anchors.fill: parent
|
||||
|
||||
// Cached wallpaper path - exposed for parent components
|
||||
property string resolvedWallpaperPath: ""
|
||||
property color tintColor: Settings.data.colorSchemes.darkMode ? Color.mSurface : Color.mOnSurface
|
||||
|
||||
required property var screen
|
||||
|
||||
// Request preprocessed wallpaper when lock screen becomes active or dimensions change
|
||||
Component.onCompleted: {
|
||||
if (screen) {
|
||||
Qt.callLater(requestCachedWallpaper);
|
||||
}
|
||||
}
|
||||
|
||||
onWidthChanged: {
|
||||
if (screen && width > 0 && height > 0) {
|
||||
Qt.callLater(requestCachedWallpaper);
|
||||
}
|
||||
}
|
||||
|
||||
onHeightChanged: {
|
||||
if (screen && width > 0 && height > 0) {
|
||||
Qt.callLater(requestCachedWallpaper);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for wallpaper changes
|
||||
Connections {
|
||||
target: WallpaperService
|
||||
function onWallpaperChanged(screenName, path) {
|
||||
if (screen && screenName === screen.name) {
|
||||
Qt.callLater(requestCachedWallpaper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for display scale changes
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onDisplayScalesChanged() {
|
||||
if (screen && width > 0 && height > 0) {
|
||||
Qt.callLater(requestCachedWallpaper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function requestCachedWallpaper() {
|
||||
if (!screen || width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for solid color mode first
|
||||
if (Settings.data.wallpaper.useSolidColor) {
|
||||
resolvedWallpaperPath = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const originalPath = WallpaperService.getWallpaper(screen.name) || "";
|
||||
if (originalPath === "") {
|
||||
resolvedWallpaperPath = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle solid color paths
|
||||
if (WallpaperService.isSolidColorPath(originalPath)) {
|
||||
resolvedWallpaperPath = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ImageCacheService || !ImageCacheService.initialized) {
|
||||
// Fallback to original if services not ready
|
||||
resolvedWallpaperPath = originalPath;
|
||||
return;
|
||||
}
|
||||
|
||||
const compositorScale = CompositorService.getDisplayScale(screen.name);
|
||||
const targetWidth = Math.round(width * compositorScale);
|
||||
const targetHeight = Math.round(height * compositorScale);
|
||||
if (targetWidth <= 0 || targetHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't set resolvedWallpaperPath until cache is ready
|
||||
// This prevents loading the original huge image
|
||||
ImageCacheService.getLarge(originalPath, targetWidth, targetHeight, function (cachedPath, success) {
|
||||
if (success) {
|
||||
resolvedWallpaperPath = cachedPath;
|
||||
} else {
|
||||
// Only fall back to original if caching failed
|
||||
resolvedWallpaperPath = originalPath;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Background - solid color or black fallback
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Settings.data.wallpaper.useSolidColor ? Settings.data.wallpaper.solidColor : "#000000"
|
||||
}
|
||||
|
||||
Image {
|
||||
id: lockBgImage
|
||||
visible: source !== "" && Settings.data.wallpaper.enabled && !Settings.data.wallpaper.useSolidColor && (!PowerProfileService.noctaliaPerformanceMode || !Settings.data.noctaliaPerformance.disableWallpaper)
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: resolvedWallpaperPath
|
||||
cache: false
|
||||
smooth: true
|
||||
mipmap: false
|
||||
antialiasing: true
|
||||
|
||||
layer.enabled: Settings.data.general.lockScreenBlur > 0 && !PowerProfileService.noctaliaPerformanceMode
|
||||
layer.smooth: false
|
||||
layer.effect: MultiEffect {
|
||||
blurEnabled: true
|
||||
blur: Settings.data.general.lockScreenBlur
|
||||
blurMax: 48
|
||||
}
|
||||
|
||||
// Tint overlay
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.tintColor
|
||||
opacity: Settings.data.general.lockScreenTint
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: !Settings.data.wallpaper.useSolidColor
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: Qt.alpha(Color.mShadow, 0.4)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.3
|
||||
color: Qt.alpha(Color.mShadow, 0.2)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.7
|
||||
color: Qt.alpha(Color.mShadow, 0.25)
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: Qt.alpha(Color.mShadow, 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Screen corners for lock screen
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: Settings.data.general.showScreenCorners
|
||||
|
||||
property color cornerColor: Settings.data.general.forceBlackScreenCorners ? "black" : Color.mSurface
|
||||
property real cornerRadius: Style.screenRadius
|
||||
property real cornerSize: Style.screenRadius
|
||||
|
||||
// Top-left concave corner
|
||||
Canvas {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
width: parent.cornerSize
|
||||
height: parent.cornerSize
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
smooth: false
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
if (!ctx)
|
||||
return;
|
||||
ctx.reset();
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
ctx.fillStyle = parent.cornerColor;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
ctx.globalCompositeOperation = "destination-out";
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.beginPath();
|
||||
ctx.arc(width, height, parent.cornerRadius, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
onWidthChanged: if (available)
|
||||
requestPaint()
|
||||
onHeightChanged: if (available)
|
||||
requestPaint()
|
||||
}
|
||||
|
||||
// Top-right concave corner
|
||||
Canvas {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
width: parent.cornerSize
|
||||
height: parent.cornerSize
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
smooth: true
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
if (!ctx)
|
||||
return;
|
||||
ctx.reset();
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
ctx.fillStyle = parent.cornerColor;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
ctx.globalCompositeOperation = "destination-out";
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, height, parent.cornerRadius, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
onWidthChanged: if (available)
|
||||
requestPaint()
|
||||
onHeightChanged: if (available)
|
||||
requestPaint()
|
||||
}
|
||||
|
||||
// Bottom-left concave corner
|
||||
Canvas {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
width: parent.cornerSize
|
||||
height: parent.cornerSize
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
smooth: true
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
if (!ctx)
|
||||
return;
|
||||
ctx.reset();
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
ctx.fillStyle = parent.cornerColor;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
ctx.globalCompositeOperation = "destination-out";
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.beginPath();
|
||||
ctx.arc(width, 0, parent.cornerRadius, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
onWidthChanged: if (available)
|
||||
requestPaint()
|
||||
onHeightChanged: if (available)
|
||||
requestPaint()
|
||||
}
|
||||
|
||||
// Bottom-right concave corner
|
||||
Canvas {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
width: parent.cornerSize
|
||||
height: parent.cornerSize
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
smooth: true
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
if (!ctx)
|
||||
return;
|
||||
ctx.reset();
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
ctx.fillStyle = parent.cornerColor;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
ctx.globalCompositeOperation = "destination-out";
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, parent.cornerRadius, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
onWidthChanged: if (available)
|
||||
requestPaint()
|
||||
onHeightChanged: if (available)
|
||||
requestPaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
// Time, Date, and User Profile Container
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
readonly property bool animationsEnabled: Settings.data.general.lockScreenAnimations || false
|
||||
|
||||
// Use timer-driven properties instead of Time.now to avoid per-frame repaints.
|
||||
// Time.now updates every frame (~60+ Hz); these update only when needed.
|
||||
property date currentTime: new Date()
|
||||
property date currentDate: new Date()
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: root.currentTime = new Date()
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 60000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: root.currentDate = new Date()
|
||||
}
|
||||
|
||||
width: Math.max(500, contentRow.implicitWidth + 32)
|
||||
height: Math.max(120, contentRow.implicitHeight + 32)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 100
|
||||
radius: Style.radiusL
|
||||
color: Color.mSurface
|
||||
border.color: Qt.alpha(Color.mOutline, 0.2)
|
||||
border.width: Style.borderS
|
||||
|
||||
RowLayout {
|
||||
id: contentRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.margin2XL
|
||||
|
||||
// Left side: Avatar
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 70
|
||||
Layout.preferredHeight: 70
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: "transparent"
|
||||
border.color: Qt.alpha(Color.mPrimary, 0.8)
|
||||
border.width: Style.borderM
|
||||
|
||||
SequentialAnimation on border.color {
|
||||
loops: Animation.Infinite
|
||||
running: root.animationsEnabled
|
||||
ColorAnimation {
|
||||
to: Qt.alpha(Color.mPrimary, 1.0)
|
||||
duration: 2000
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
ColorAnimation {
|
||||
to: Qt.alpha(Color.mPrimary, 0.8)
|
||||
duration: 2000
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NImageRounded {
|
||||
anchors.centerIn: parent
|
||||
width: 66
|
||||
height: 66
|
||||
radius: width / 2
|
||||
imagePath: Settings.preprocessPath(Settings.data.general.avatarImage)
|
||||
fallbackIcon: "person"
|
||||
|
||||
SequentialAnimation on scale {
|
||||
loops: Animation.Infinite
|
||||
running: root.animationsEnabled
|
||||
NumberAnimation {
|
||||
to: 1.02
|
||||
duration: 4000
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 4000
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Center: User Info Column (left-aligned text)
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: Style.marginXXS
|
||||
|
||||
// Welcome back + Username on one line
|
||||
NText {
|
||||
text: I18n.tr("system.welcome-back") + " " + HostService.displayName + "!"
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: Color.mOnSurface
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
|
||||
// Date below
|
||||
NText {
|
||||
text: {
|
||||
var dateString = I18n.locale.toString(root.currentDate, I18n.dateFormat());
|
||||
return dateString.charAt(0).toUpperCase() + dateString.slice(1);
|
||||
}
|
||||
pointSize: Style.fontSizeXL
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer to push time to the right
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Clock
|
||||
Item {
|
||||
Layout.preferredWidth: Settings.data.general.clockStyle === "analog" ? 70 : (Settings.data.general.clockStyle === "custom" ? 90 : 70)
|
||||
Layout.preferredHeight: Settings.data.general.clockStyle === "analog" ? 70 : (Settings.data.general.clockStyle === "custom" ? 90 : 70)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
// Analog Clock
|
||||
NClock {
|
||||
anchors.centerIn: parent
|
||||
width: 70
|
||||
height: 70
|
||||
visible: Settings.data.general.clockStyle === "analog"
|
||||
now: root.currentTime
|
||||
clockStyle: "analog"
|
||||
backgroundColor: "transparent"
|
||||
clockColor: Color.mOnSurface
|
||||
secondHandColor: Color.mPrimary
|
||||
}
|
||||
|
||||
// Digital Clock (Standard)
|
||||
NClock {
|
||||
anchors.centerIn: parent
|
||||
width: 70
|
||||
height: 70
|
||||
visible: Settings.data.general.clockStyle === "digital"
|
||||
now: root.currentTime
|
||||
clockStyle: "digital"
|
||||
showProgress: true
|
||||
progressColor: Color.mPrimary
|
||||
backgroundColor: "transparent"
|
||||
clockColor: Color.mOnSurface
|
||||
hoursFontSize: Style.fontSizeL
|
||||
minutesFontSize: Style.fontSizeL
|
||||
hoursFontWeight: Style.fontWeightBold
|
||||
minutesFontWeight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
// Custom Clock (Stacked)
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
visible: Settings.data.general.clockStyle === "custom"
|
||||
spacing: -3
|
||||
|
||||
Repeater {
|
||||
model: I18n.locale.toString(root.currentTime, (Settings.data.general.clockFormat || "hh\\nmm").replace(/\\n/g, "\n")).split("\n")
|
||||
NText {
|
||||
text: modelData
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.maximumWidth: parent.width
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user