This commit is contained in:
2026-05-28 21:45:21 +02:00
parent 653439ac24
commit a356a3d369
610 changed files with 205679 additions and 0 deletions
@@ -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