mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
1217 lines
43 KiB
QML
1217 lines
43 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
import Quickshell
|
|
import qs.Commons
|
|
import qs.Services.Compositor
|
|
import qs.Services.Hardware
|
|
import qs.Services.Keyboard
|
|
import qs.Services.Location
|
|
import qs.Services.Media
|
|
import qs.Widgets
|
|
import qs.Widgets.AudioSpectrum
|
|
|
|
Item {
|
|
id: root
|
|
anchors.fill: parent
|
|
|
|
required property var lockControl
|
|
required property var batteryIndicator
|
|
required property var keyboardLayout
|
|
required property TextInput passwordInput
|
|
|
|
// Whether to enable lock screen animations (smooth cursor blink).
|
|
// Defaults to false to reduce GPU usage. Set Settings.data.general.lockScreenAnimations = true to restore.
|
|
readonly property bool animationsEnabled: Settings.data.general.lockScreenAnimations || false
|
|
|
|
Component.onCompleted: {
|
|
if (Settings.data.general.autoStartAuth) {
|
|
doUnlock();
|
|
}
|
|
}
|
|
|
|
function doUnlock() {
|
|
if (lockControl) {
|
|
lockControl.tryUnlock();
|
|
}
|
|
}
|
|
|
|
// Timer properties
|
|
readonly property int timerDuration: Settings.data.general.lockScreenCountdownDuration
|
|
property string pendingAction: ""
|
|
property bool timerActive: false
|
|
property int timeRemaining: 0
|
|
readonly property bool weatherReady: Settings.data.location.weatherEnabled && (LocationService.data.weather !== null)
|
|
|
|
// Timer management functions
|
|
function startTimer(action) {
|
|
// Check if global countdown is disabled
|
|
if (!Settings.data.general.enableLockScreenCountdown) {
|
|
executeAction(action);
|
|
return;
|
|
}
|
|
|
|
if (timerActive && pendingAction === action) {
|
|
// Second click - execute immediately
|
|
executeAction(action);
|
|
return;
|
|
}
|
|
|
|
pendingAction = action;
|
|
timeRemaining = timerDuration;
|
|
timerActive = true;
|
|
countdownTimer.start();
|
|
}
|
|
|
|
function cancelTimer() {
|
|
timerActive = false;
|
|
pendingAction = "";
|
|
timeRemaining = 0;
|
|
countdownTimer.stop();
|
|
}
|
|
|
|
function executeAction(action) {
|
|
// Stop timer but don't reset other properties yet
|
|
countdownTimer.stop();
|
|
|
|
// Execute the action
|
|
switch (action) {
|
|
case "logout":
|
|
CompositorService.logout();
|
|
break;
|
|
case "suspend":
|
|
CompositorService.suspend();
|
|
break;
|
|
case "hibernate":
|
|
CompositorService.hibernate();
|
|
break;
|
|
case "reboot":
|
|
CompositorService.reboot();
|
|
break;
|
|
case "userspaceReboot":
|
|
CompositorService.userspaceReboot();
|
|
break;
|
|
case "shutdown":
|
|
CompositorService.shutdown();
|
|
break;
|
|
}
|
|
|
|
// Reset timer state
|
|
cancelTimer();
|
|
}
|
|
|
|
// Countdown timer
|
|
Timer {
|
|
id: countdownTimer
|
|
interval: 100
|
|
repeat: true
|
|
onTriggered: {
|
|
timeRemaining -= interval;
|
|
if (timeRemaining <= 0) {
|
|
executeAction(pendingAction);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compact status indicators container (compact mode only)
|
|
Rectangle {
|
|
width: {
|
|
var hasBattery = batteryIndicator.isReady;
|
|
var hasKeyboard = keyboardLayout.currentLayout !== "Unknown";
|
|
var hasCaps = LockKeysService.capsLockOn;
|
|
var hasCapsSlot = hasBattery || hasKeyboard || hasCaps;
|
|
|
|
var visibleCount = 0;
|
|
if (hasBattery)
|
|
visibleCount++;
|
|
if (hasKeyboard)
|
|
visibleCount++;
|
|
if (hasCapsSlot)
|
|
visibleCount++;
|
|
|
|
if (visibleCount >= 3) {
|
|
return 280;
|
|
} else if (visibleCount === 2) {
|
|
return 200;
|
|
} else if (visibleCount === 1) {
|
|
return 120;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
height: 40
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.bottom: parent.bottom
|
|
anchors.bottomMargin: 96 + (Settings.data.general.compactLockScreen ? 116 : 220)
|
|
topLeftRadius: Style.radiusL
|
|
topRightRadius: Style.radiusL
|
|
color: Color.mSurface
|
|
visible: Settings.data.general.compactLockScreen && (batteryIndicator.isReady || keyboardLayout.currentLayout !== "Unknown" || LockKeysService.capsLockOn)
|
|
|
|
RowLayout {
|
|
id: compactStatusRow
|
|
anchors.centerIn: parent
|
|
spacing: Style.marginL
|
|
|
|
// Battery indicator
|
|
RowLayout {
|
|
spacing: Style.marginS
|
|
visible: batteryIndicator.isReady
|
|
|
|
NIcon {
|
|
icon: batteryIndicator.icon
|
|
pointSize: Style.fontSizeM
|
|
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurfaceVariant
|
|
}
|
|
|
|
NText {
|
|
text: Math.round(batteryIndicator.percent) + "%"
|
|
color: Color.mOnSurfaceVariant
|
|
pointSize: Style.fontSizeM
|
|
}
|
|
}
|
|
|
|
// Keyboard layout indicator
|
|
RowLayout {
|
|
spacing: 6
|
|
visible: keyboardLayout.currentLayout !== "Unknown"
|
|
|
|
NIcon {
|
|
icon: "keyboard"
|
|
pointSize: Style.fontSizeM
|
|
color: Color.mOnSurfaceVariant
|
|
}
|
|
|
|
NText {
|
|
text: keyboardLayout.currentLayout
|
|
color: Color.mOnSurfaceVariant
|
|
pointSize: Style.fontSizeM
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
|
|
// Caps Lock indicator
|
|
RowLayout {
|
|
spacing: 6
|
|
visible: batteryIndicator.isReady || keyboardLayout.currentLayout !== "Unknown" || LockKeysService.capsLockOn
|
|
|
|
NIcon {
|
|
icon: "lock"
|
|
pointSize: Style.fontSizeM
|
|
color: LockKeysService.capsLockOn ? Color.mPrimary : Qt.alpha(Color.mOnSurfaceVariant, 0.5)
|
|
}
|
|
|
|
NText {
|
|
text: I18n.tr("bar.lock-keys.show-caps-lock-label")
|
|
color: LockKeysService.capsLockOn ? Color.mOnSurfaceVariant : Qt.alpha(Color.mOnSurfaceVariant, 0.65)
|
|
pointSize: Style.fontSizeM
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bottom container with weather, password input and controls
|
|
Rectangle {
|
|
id: bottomContainer
|
|
|
|
// Support for removing the session/power buttons at the bottom.
|
|
readonly property int deltaY: Settings.data.general.showSessionButtonsOnLockScreen ? 0 : (Settings.data.general.compactLockScreen ? 36 : 48) + 14
|
|
|
|
height: {
|
|
let calcHeight = Settings.data.general.compactLockScreen ? 120 : 220;
|
|
if (!Settings.data.general.showSessionButtonsOnLockScreen) {
|
|
calcHeight -= bottomContainer.deltaY;
|
|
}
|
|
return calcHeight;
|
|
}
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.bottom: parent.bottom
|
|
anchors.bottomMargin: 100 + bottomContainer.deltaY
|
|
radius: Style.radiusL
|
|
color: Color.mSurface
|
|
|
|
width: Settings.data.general.showHibernateOnLockScreen ? 860 : 810
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 14
|
|
spacing: Style.marginL
|
|
|
|
// Top info row
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 65
|
|
spacing: Style.marginXL
|
|
visible: !Settings.data.general.compactLockScreen
|
|
|
|
// Media widget with visualizer
|
|
Item {
|
|
Layout.preferredWidth: Style.marginM
|
|
visible: MediaService.currentPlayer && MediaService.canPlay
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.preferredWidth: 220
|
|
// Expand to take remaining space when weather is hidden
|
|
Layout.fillWidth: !(Settings.data.location.weatherEnabled && LocationService.data.weather !== null)
|
|
Layout.preferredHeight: 50
|
|
radius: Style.radiusL
|
|
color: "transparent"
|
|
clip: true
|
|
visible: MediaService.currentPlayer && MediaService.canPlay
|
|
|
|
Loader {
|
|
anchors.fill: parent
|
|
anchors.margins: 4
|
|
active: Settings.data.audio.visualizerType === "linear"
|
|
z: 0
|
|
sourceComponent: NLinearSpectrum {
|
|
anchors.fill: parent
|
|
values: SpectrumService.values
|
|
fillColor: Color.mPrimary
|
|
opacity: 0.4
|
|
mirrored: Settings.data.audio.spectrumMirrored
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
anchors.fill: parent
|
|
anchors.margins: 4
|
|
active: Settings.data.audio.visualizerType === "mirrored"
|
|
z: 0
|
|
sourceComponent: NMirroredSpectrum {
|
|
anchors.fill: parent
|
|
values: SpectrumService.values
|
|
fillColor: Color.mPrimary
|
|
opacity: 0.4
|
|
mirrored: Settings.data.audio.spectrumMirrored
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
anchors.fill: parent
|
|
anchors.margins: 4
|
|
active: Settings.data.audio.visualizerType === "wave"
|
|
z: 0
|
|
sourceComponent: NWaveSpectrum {
|
|
anchors.fill: parent
|
|
values: SpectrumService.values
|
|
fillColor: Color.mPrimary
|
|
opacity: 0.4
|
|
mirrored: Settings.data.audio.spectrumMirrored
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 8
|
|
spacing: Style.marginM
|
|
z: 1
|
|
|
|
Rectangle {
|
|
Layout.preferredWidth: 34
|
|
Layout.preferredHeight: 34
|
|
radius: Math.min(Style.radiusL, width / 2)
|
|
color: "transparent"
|
|
clip: true
|
|
|
|
NImageRounded {
|
|
anchors.fill: parent
|
|
anchors.margins: 2
|
|
radius: Math.min(Style.radiusL, width / 2)
|
|
imagePath: MediaService.trackArtUrl
|
|
fallbackIcon: "disc"
|
|
fallbackIconSize: Style.fontSizeM
|
|
borderColor: Color.mOutline
|
|
borderWidth: Style.borderS
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Style.marginXXS
|
|
|
|
NText {
|
|
text: MediaService.trackTitle || "No media"
|
|
pointSize: Style.fontSizeM
|
|
color: Color.mOnSurface
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
NText {
|
|
text: MediaService.trackArtist || ""
|
|
pointSize: Style.fontSizeM
|
|
color: Color.mOnSurfaceVariant
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
|
|
// Media controls (when enabled)
|
|
RowLayout {
|
|
spacing: Style.marginXS
|
|
visible: Settings.data.general.enableLockScreenMediaControls
|
|
Layout.alignment: Qt.AlignHCenter
|
|
|
|
Rectangle {
|
|
width: 28
|
|
height: 28
|
|
radius: Math.min(Style.radiusL, width / 2)
|
|
color: prevButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mOnSurface, 0.1)
|
|
visible: MediaService.canGoPrevious
|
|
|
|
NIcon {
|
|
anchors.centerIn: parent
|
|
icon: "media-prev"
|
|
pointSize: Style.fontSizeM
|
|
color: prevButtonArea.containsMouse ? Color.mOnPrimary : Color.mOnSurface
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: prevButtonArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: MediaService.canGoPrevious ? MediaService.previous() : {}
|
|
}
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: 32
|
|
height: 32
|
|
radius: Math.min(Style.radiusL, width / 2)
|
|
color: playPauseButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mOnSurface, 0.15)
|
|
visible: MediaService.canPlay || MediaService.canPause
|
|
|
|
NIcon {
|
|
anchors.centerIn: parent
|
|
icon: MediaService.isPlaying ? "media-pause" : "media-play"
|
|
pointSize: Style.fontSizeL
|
|
color: playPauseButtonArea.containsMouse ? Color.mOnPrimary : Color.mOnSurface
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: playPauseButtonArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: (MediaService.canPlay || MediaService.canPause) ? MediaService.playPause() : {}
|
|
}
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: 28
|
|
height: 28
|
|
radius: Math.min(Style.radiusL, width / 2)
|
|
color: nextButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mOnSurface, 0.1)
|
|
visible: MediaService.canGoNext
|
|
|
|
NIcon {
|
|
anchors.centerIn: parent
|
|
icon: "media-next"
|
|
pointSize: Style.fontSizeM
|
|
color: nextButtonArea.containsMouse ? Color.mOnPrimary : Color.mOnSurface
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: nextButtonArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: MediaService.canGoNext ? MediaService.next() : {}
|
|
}
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.preferredWidth: 1
|
|
Layout.fillHeight: true
|
|
Layout.rightMargin: 4
|
|
color: Qt.alpha(Color.mOutline, 0.3)
|
|
visible: MediaService.currentPlayer && MediaService.canPlay
|
|
}
|
|
|
|
Item {
|
|
Layout.preferredWidth: Style.marginM
|
|
visible: !(MediaService.currentPlayer && MediaService.canPlay)
|
|
}
|
|
|
|
// Current weather
|
|
RowLayout {
|
|
visible: Settings.data.location.weatherEnabled && LocationService.data.weather !== null
|
|
Layout.preferredWidth: 180
|
|
spacing: Style.marginM
|
|
|
|
Item {
|
|
Layout.preferredWidth: lockMainWeatherIconSide
|
|
Layout.preferredHeight: lockMainWeatherIconSide
|
|
Layout.alignment: Qt.AlignVCenter
|
|
readonly property int lockMainWeatherIconSide: Math.round(Style.fontSizeXXXL * Style.uiScaleRatio * 1.6)
|
|
|
|
NIcon {
|
|
visible: !LocationService.taliaWeatherMascotDayActive || !weatherReady
|
|
anchors.centerIn: parent
|
|
icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode, LocationService.data.weather.current_weather.is_day) : "weather-cloud-off"
|
|
pointSize: Style.fontSizeXXXL
|
|
color: Color.mPrimary
|
|
}
|
|
Loader {
|
|
active: LocationService.taliaWeatherMascotDayActive && weatherReady
|
|
anchors.fill: parent
|
|
asynchronous: true
|
|
sourceComponent: Component {
|
|
Image {
|
|
anchors.fill: parent
|
|
fillMode: Image.PreserveAspectFit
|
|
smooth: true
|
|
mipmap: true
|
|
asynchronous: true
|
|
source: Qt.resolvedUrl(LocationService.taliaWeatherImageFromCode(LocationService.data.weather.current_weather.weathercode, LocationService.data.weather.current_weather.is_day))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Style.marginXXS
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Style.marginL
|
|
|
|
NText {
|
|
text: {
|
|
var temp = LocationService.data.weather.current_weather.temperature;
|
|
var suffix = "C";
|
|
if (Settings.data.location.useFahrenheit) {
|
|
temp = LocationService.celsiusToFahrenheit(temp);
|
|
suffix = "F";
|
|
}
|
|
temp = Math.round(temp);
|
|
return temp + "°" + suffix;
|
|
}
|
|
pointSize: Style.fontSizeXL
|
|
font.weight: Style.fontWeightBold
|
|
color: Color.mOnSurface
|
|
}
|
|
|
|
NText {
|
|
text: {
|
|
var wind = LocationService.data.weather.current_weather.windspeed;
|
|
var unit = "km/h";
|
|
if (Settings.data.location.useFahrenheit) {
|
|
wind = wind * 0.621371; // Convert km/h to mph
|
|
unit = "mph";
|
|
}
|
|
wind = Math.round(wind);
|
|
return wind + " " + unit;
|
|
}
|
|
pointSize: Style.fontSizeM
|
|
color: Color.mOnSurfaceVariant
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Style.marginM
|
|
|
|
NText {
|
|
text: Settings.data.location.name.split(",")[0]
|
|
pointSize: Style.fontSizeM
|
|
color: Color.mOnSurfaceVariant
|
|
visible: !Settings.data.location.hideWeatherCityName
|
|
}
|
|
|
|
NText {
|
|
text: (LocationService.data.weather.current && LocationService.data.weather.current.relativehumidity_2m) ? LocationService.data.weather.current.relativehumidity_2m + "% humidity" : ""
|
|
pointSize: Style.fontSizeM
|
|
color: Color.mOnSurfaceVariant
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Forecast
|
|
RowLayout {
|
|
visible: Settings.data.location.weatherEnabled && LocationService.data.weather !== null
|
|
Layout.preferredWidth: 260
|
|
Layout.rightMargin: 8
|
|
spacing: Style.marginXS
|
|
|
|
Repeater {
|
|
model: MediaService.currentPlayer && MediaService.canPlay ? 2 : 4
|
|
delegate: ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Style.marginXXS + 1
|
|
|
|
NText {
|
|
text: {
|
|
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"));
|
|
return I18n.locale.toString(weatherDate, "ddd");
|
|
}
|
|
pointSize: Style.fontSizeM
|
|
color: Color.mOnSurfaceVariant
|
|
horizontalAlignment: Text.AlignHCenter
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
Item {
|
|
Layout.preferredWidth: lockForecastWeatherIconSide
|
|
Layout.preferredHeight: lockForecastWeatherIconSide
|
|
Layout.alignment: Qt.AlignHCenter
|
|
readonly property int lockForecastWeatherIconSide: Math.round(Style.fontSizeXL * Style.uiScaleRatio * 1.6)
|
|
|
|
NIcon {
|
|
visible: !LocationService.taliaWeatherMascotDayActive
|
|
anchors.centerIn: parent
|
|
icon: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index])
|
|
pointSize: Style.fontSizeXL
|
|
color: Color.mOnSurfaceVariant
|
|
}
|
|
Loader {
|
|
active: LocationService.taliaWeatherMascotDayActive
|
|
anchors.fill: parent
|
|
asynchronous: true
|
|
sourceComponent: Component {
|
|
Image {
|
|
anchors.fill: parent
|
|
fillMode: Image.PreserveAspectFit
|
|
smooth: true
|
|
mipmap: true
|
|
asynchronous: true
|
|
source: Qt.resolvedUrl(LocationService.taliaWeatherImageFromCode(LocationService.data.weather.daily.weathercode[index]))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NText {
|
|
text: {
|
|
var max = LocationService.data.weather.daily.temperature_2m_max[index];
|
|
var min = LocationService.data.weather.daily.temperature_2m_min[index];
|
|
if (Settings.data.location.useFahrenheit) {
|
|
max = LocationService.celsiusToFahrenheit(max);
|
|
min = LocationService.celsiusToFahrenheit(min);
|
|
}
|
|
max = Math.round(max);
|
|
min = Math.round(min);
|
|
return max + "°/" + min + "°";
|
|
}
|
|
pointSize: Style.fontSizeM
|
|
font.weight: Style.fontWeightMedium
|
|
color: Color.mOnSurfaceVariant
|
|
horizontalAlignment: Text.AlignHCenter
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: batteryIndicator.isReady
|
|
}
|
|
|
|
// Battery and Keyboard Layout (full mode only)
|
|
ColumnLayout {
|
|
Layout.alignment: (batteryIndicator.isReady) ? (Qt.AlignRight | Qt.AlignVCenter) : Qt.AlignVCenter
|
|
spacing: Style.marginM
|
|
visible: batteryIndicator.isReady || keyboardLayout.currentLayout !== "Unknown" || LockKeysService.capsLockOn
|
|
|
|
// Battery
|
|
RowLayout {
|
|
spacing: Style.marginXS
|
|
visible: batteryIndicator.isReady
|
|
|
|
NIcon {
|
|
icon: batteryIndicator.icon
|
|
pointSize: Style.fontSizeM
|
|
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurfaceVariant
|
|
}
|
|
|
|
NText {
|
|
text: Math.round(batteryIndicator.percent) + "%"
|
|
color: Color.mOnSurfaceVariant
|
|
pointSize: Style.fontSizeM
|
|
}
|
|
}
|
|
|
|
// Keyboard Layout
|
|
RowLayout {
|
|
spacing: Style.marginXS
|
|
visible: keyboardLayout.currentLayout !== "Unknown"
|
|
|
|
NIcon {
|
|
icon: "keyboard"
|
|
pointSize: Style.fontSizeM
|
|
color: Color.mOnSurfaceVariant
|
|
}
|
|
|
|
NText {
|
|
text: keyboardLayout.currentLayout
|
|
color: Color.mOnSurfaceVariant
|
|
pointSize: Style.fontSizeM
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
|
|
// Caps Lock
|
|
RowLayout {
|
|
spacing: Style.marginXS
|
|
visible: batteryIndicator.isReady || keyboardLayout.currentLayout !== "Unknown" || LockKeysService.capsLockOn
|
|
|
|
NIcon {
|
|
icon: "lock"
|
|
pointSize: Style.fontSizeM
|
|
color: LockKeysService.capsLockOn ? Color.mPrimary : Qt.alpha(Color.mOnSurfaceVariant, 0.5)
|
|
}
|
|
|
|
NText {
|
|
text: I18n.tr("bar.lock-keys.show-caps-lock-label")
|
|
color: LockKeysService.capsLockOn ? Color.mOnSurfaceVariant : Qt.alpha(Color.mOnSurfaceVariant, 0.65)
|
|
pointSize: Style.fontSizeM
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.preferredWidth: Style.marginM
|
|
}
|
|
}
|
|
|
|
// Password input
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 0
|
|
|
|
Item {
|
|
Layout.preferredWidth: Style.marginM
|
|
}
|
|
|
|
Rectangle {
|
|
id: passwordInputContainer
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 48
|
|
radius: Style.iRadiusL
|
|
color: Color.mSurface
|
|
border.color: passwordInput.activeFocus ? Color.mPrimary : Qt.alpha(Color.mOutline, 0.3)
|
|
border.width: passwordInput.activeFocus ? 2 : 1
|
|
|
|
property bool passwordVisible: false
|
|
|
|
// Ctrl + A to highlight the portion
|
|
Shortcut {
|
|
sequence: StandardKey.SelectAll
|
|
enabled: passwordInput.activeFocus
|
|
onActivated: passwordInput.selectAll()
|
|
}
|
|
|
|
// Esc to clear selection
|
|
Shortcut {
|
|
sequences: [StandardKey.Cancel]
|
|
enabled: passwordInput.activeFocus && passwordInput.selectionStart !== passwordInput.selectionEnd
|
|
onActivated: passwordInput.deselect()
|
|
}
|
|
|
|
Row {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 18
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Style.marginL
|
|
|
|
NIcon {
|
|
icon: "login-2"
|
|
pointSize: Style.fontSizeL
|
|
color: passwordInput.activeFocus ? Color.mPrimary : Color.mOnSurfaceVariant
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Row {
|
|
spacing: 0
|
|
|
|
Rectangle {
|
|
width: 2
|
|
height: 20
|
|
color: Color.mPrimary
|
|
visible: passwordInput.activeFocus && passwordInput.text.length === 0
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
// Smooth fade animation (when animations enabled)
|
|
SequentialAnimation on opacity {
|
|
loops: Animation.Infinite
|
|
running: root.animationsEnabled && passwordInput.activeFocus && passwordInput.text.length === 0
|
|
NumberAnimation {
|
|
to: 0
|
|
duration: 530
|
|
}
|
|
NumberAnimation {
|
|
to: 1
|
|
duration: 530
|
|
}
|
|
}
|
|
|
|
// Simple toggle (when animations disabled) — no per-frame repaints
|
|
Timer {
|
|
interval: 530
|
|
running: !root.animationsEnabled && passwordInput.activeFocus && passwordInput.text.length === 0
|
|
repeat: true
|
|
onTriggered: parent.opacity = parent.opacity > 0.5 ? 0 : 1
|
|
}
|
|
}
|
|
|
|
// Host for dots / plain text and the caret (caret x follows passwordInput.cursorPosition)
|
|
Item {
|
|
id: passwordVisualHost
|
|
height: 20
|
|
width: passwordInputContainer.passwordVisible ? Math.min(visiblePasswordPlainText.implicitWidth, 550) : Math.min(passwordDisplayContent.width, 550)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
readonly property real caretVisualX: {
|
|
const len = passwordInput.text.length;
|
|
if (len <= 0)
|
|
return 0;
|
|
if (passwordInputContainer.passwordVisible) {
|
|
const adv = passwordCaretFontMetrics.advanceWidth(passwordInput.text.substring(0, passwordInput.cursorPosition));
|
|
return Math.max(0, Math.min(adv, width));
|
|
}
|
|
const w = passwordDisplayContent.width;
|
|
if (w <= 0)
|
|
return 0;
|
|
return Math.max(0, Math.min((passwordInput.cursorPosition / len) * w, width));
|
|
}
|
|
|
|
// Password dots display with selection support
|
|
Item {
|
|
width: Math.min(passwordDisplayContent.width, 550)
|
|
height: 20
|
|
visible: passwordInput.text.length > 0 && !passwordInputContainer.passwordVisible
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
clip: true
|
|
|
|
// Proportional selection highlight behind the dots
|
|
Rectangle {
|
|
id: selectionHighlight
|
|
visible: passwordInput.selectionStart !== passwordInput.selectionEnd && passwordInput.text.length > 0
|
|
color: Qt.alpha(Color.mPrimary, 0.8)
|
|
height: parent.height + Style.marginS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
x: (passwordInput.selectionStart / passwordInput.text.length) * passwordDisplayContent.width
|
|
width: ((passwordInput.selectionEnd - passwordInput.selectionStart) / passwordInput.text.length) * passwordDisplayContent.width
|
|
}
|
|
|
|
Row {
|
|
id: passwordDisplayContent
|
|
spacing: Style.marginXXXS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
Repeater {
|
|
id: iconRepeater
|
|
model: ScriptModel {
|
|
values: Array(passwordInput.text.length)
|
|
}
|
|
|
|
property list<string> passwordChars: ["circle-filled", "pentagon-filled", "michelin-star-filled", "square-rounded-filled", "guitar-pick-filled", "blob-filled", "triangle-filled"]
|
|
|
|
NIcon {
|
|
id: icon
|
|
|
|
required property int index
|
|
// This will be called with index = -1 when the TextInput is deleted
|
|
// So we make sur index is positive to avoid warning on array accesses
|
|
property bool drawCustomChar: index >= 0 && Settings.data.general.passwordChars
|
|
// Flip color when this dot falls inside the active selection range
|
|
property bool isSelected: index >= 0 && passwordInput.selectionStart !== passwordInput.selectionEnd && index >= passwordInput.selectionStart && index < passwordInput.selectionEnd
|
|
|
|
icon: drawCustomChar ? iconRepeater.passwordChars[index % iconRepeater.passwordChars.length] : "circle-filled"
|
|
pointSize: Style.fontSizeL
|
|
color: isSelected ? Color.mOnPrimary : Color.mPrimary
|
|
opacity: 1.0
|
|
scale: animationsEnabled ? 0.5 : 1
|
|
ParallelAnimation {
|
|
id: iconAnim
|
|
NumberAnimation {
|
|
target: icon
|
|
properties: "scale"
|
|
to: 1
|
|
duration: Style.animationFast
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: Easing.OutInBounce
|
|
}
|
|
}
|
|
Component.onCompleted: {
|
|
if (animationsEnabled) {
|
|
iconAnim.start();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mouse area for click-to-position and drag-to-select
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.IBeamCursor
|
|
|
|
property int dragStartPos: 0
|
|
property bool pendingSelectAll: false
|
|
|
|
// Resets double-click state if user pauses too long between clicks
|
|
Timer {
|
|
id: doubleClickResetTimer
|
|
interval: 600
|
|
onTriggered: parent.pendingSelectAll = false
|
|
}
|
|
|
|
function charIndexFromX(mouseX) {
|
|
if (passwordInput.text.length === 0)
|
|
return 0;
|
|
var charWidth = passwordDisplayContent.width / passwordInput.text.length;
|
|
// floor so clicking anywhere on a dot selects that dot, not the next
|
|
return Math.max(0, Math.min(passwordInput.text.length - 1, Math.floor(mouseX / charWidth)));
|
|
}
|
|
|
|
onPressed: function (mouse) {
|
|
doubleClickResetTimer.stop();
|
|
passwordInput.forceActiveFocus();
|
|
dragStartPos = charIndexFromX(mouse.x);
|
|
passwordInput.cursorPosition = dragStartPos;
|
|
}
|
|
|
|
onPositionChanged: function (mouse) {
|
|
pendingSelectAll = false;
|
|
var curPos = charIndexFromX(mouse.x);
|
|
if (curPos <= dragStartPos) {
|
|
passwordInput.select(curPos, dragStartPos + 1);
|
|
} else {
|
|
passwordInput.select(dragStartPos, curPos + 1);
|
|
}
|
|
}
|
|
|
|
onDoubleClicked: function (mouse) {
|
|
passwordInput.forceActiveFocus();
|
|
if (pendingSelectAll) {
|
|
passwordInput.selectAll();
|
|
pendingSelectAll = false;
|
|
} else {
|
|
var pos = charIndexFromX(mouse.x);
|
|
passwordInput.select(pos, Math.min(pos + 1, passwordInput.text.length));
|
|
pendingSelectAll = true;
|
|
doubleClickResetTimer.restart();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NText {
|
|
id: visiblePasswordPlainText
|
|
text: passwordInput.text
|
|
color: Color.mPrimary
|
|
pointSize: Style.fontSizeM
|
|
visible: passwordInput.text.length > 0 && passwordInputContainer.passwordVisible
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
elide: Text.ElideRight
|
|
width: Math.min(implicitWidth, 550)
|
|
}
|
|
|
|
FontMetrics {
|
|
id: passwordCaretFontMetrics
|
|
font: visiblePasswordPlainText.font
|
|
}
|
|
|
|
Rectangle {
|
|
width: 2
|
|
height: 20
|
|
x: passwordVisualHost.caretVisualX
|
|
color: Color.mPrimary
|
|
// Hide the cursor when text is selected
|
|
visible: passwordInput.activeFocus && passwordInput.text.length > 0 && passwordInput.selectionStart === passwordInput.selectionEnd
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
// Smooth fade animation (when animations enabled)
|
|
SequentialAnimation on opacity {
|
|
loops: Animation.Infinite
|
|
running: root.animationsEnabled && passwordInput.activeFocus && passwordInput.text.length > 0 && passwordInput.selectionStart === passwordInput.selectionEnd
|
|
NumberAnimation {
|
|
to: 0
|
|
duration: 530
|
|
}
|
|
NumberAnimation {
|
|
to: 1
|
|
duration: 530
|
|
}
|
|
}
|
|
|
|
// Simple toggle (when animations disabled) — no per-frame repaints
|
|
Timer {
|
|
interval: 530
|
|
running: !root.animationsEnabled && passwordInput.activeFocus && passwordInput.text.length > 0 && passwordInput.selectionStart === passwordInput.selectionEnd
|
|
repeat: true
|
|
onTriggered: parent.opacity = parent.opacity > 0.5 ? 0 : 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Eye button to toggle password visibility
|
|
Rectangle {
|
|
anchors.right: submitButton.left
|
|
anchors.rightMargin: 4
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 36
|
|
height: 36
|
|
radius: Math.min(Style.iRadiusL, width / 2)
|
|
color: eyeButtonArea.containsMouse ? Color.mPrimary : "transparent"
|
|
visible: passwordInput.text.length > 0
|
|
enabled: !lockContext || !lockContext.unlockInProgress
|
|
|
|
NIcon {
|
|
anchors.centerIn: parent
|
|
icon: parent.parent.passwordVisible ? "eye-off" : "eye"
|
|
pointSize: Style.fontSizeM
|
|
color: eyeButtonArea.containsMouse ? Color.mOnPrimary : Color.mOnSurfaceVariant
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: eyeButtonArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: parent.parent.passwordVisible = !parent.parent.passwordVisible
|
|
}
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
}
|
|
|
|
// Submit button
|
|
Rectangle {
|
|
id: submitButton
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 8
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 36
|
|
height: 36
|
|
radius: Math.min(Style.iRadiusL, width / 2)
|
|
color: submitButtonArea.containsMouse ? Color.mPrimary : "transparent"
|
|
border.color: Color.mPrimary
|
|
border.width: Style.borderS
|
|
enabled: !lockContext || !lockContext.unlockInProgress
|
|
|
|
NIcon {
|
|
anchors.centerIn: parent
|
|
icon: "arrow-forward"
|
|
pointSize: Style.fontSizeM
|
|
color: submitButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: submitButtonArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: root.doUnlock()
|
|
}
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
}
|
|
|
|
Behavior on border.color {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.preferredWidth: Style.marginM
|
|
}
|
|
}
|
|
|
|
// Session control buttons
|
|
RowLayout {
|
|
id: sessionButtonRow
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48
|
|
Layout.alignment: Qt.AlignHCenter
|
|
spacing: Style.marginM
|
|
visible: Settings.data.general.showSessionButtonsOnLockScreen
|
|
|
|
readonly property int buttonCount: Settings.data.general.showHibernateOnLockScreen ? 5 : 4
|
|
readonly property real availableWidth: bottomContainer.width - 48
|
|
readonly property real buttonWidth: (availableWidth - (buttonCount - 1) * spacing) / buttonCount
|
|
readonly property real buttonHeight: sessionButtonRow.height
|
|
|
|
Item {
|
|
Layout.preferredWidth: sessionButtonRow.buttonWidth
|
|
Layout.preferredHeight: sessionButtonRow.buttonHeight
|
|
|
|
NButton {
|
|
anchors.fill: parent
|
|
icon: "logout"
|
|
text: I18n.tr("common.logout")
|
|
outlined: true
|
|
backgroundColor: Color.mOnSurfaceVariant
|
|
textColor: Color.mOnPrimary
|
|
fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM
|
|
iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL
|
|
horizontalAlignment: Qt.AlignHCenter
|
|
buttonRadius: Style.radiusL
|
|
onClicked: startTimer("logout")
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.preferredWidth: sessionButtonRow.buttonWidth
|
|
Layout.preferredHeight: sessionButtonRow.buttonHeight
|
|
|
|
NButton {
|
|
anchors.fill: parent
|
|
icon: "suspend"
|
|
text: I18n.tr("common.suspend")
|
|
outlined: true
|
|
backgroundColor: Color.mOnSurfaceVariant
|
|
textColor: Color.mOnPrimary
|
|
fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM
|
|
iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL
|
|
horizontalAlignment: Qt.AlignHCenter
|
|
buttonRadius: Style.radiusL
|
|
onClicked: startTimer("suspend")
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.preferredWidth: sessionButtonRow.buttonWidth
|
|
Layout.preferredHeight: sessionButtonRow.buttonHeight
|
|
visible: Settings.data.general.showHibernateOnLockScreen
|
|
|
|
NButton {
|
|
anchors.fill: parent
|
|
icon: "hibernate"
|
|
text: I18n.tr("common.hibernate")
|
|
outlined: true
|
|
backgroundColor: Color.mOnSurfaceVariant
|
|
textColor: Color.mOnPrimary
|
|
fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM
|
|
iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL
|
|
horizontalAlignment: Qt.AlignHCenter
|
|
buttonRadius: Style.radiusL
|
|
onClicked: startTimer("hibernate")
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.preferredWidth: sessionButtonRow.buttonWidth
|
|
Layout.preferredHeight: sessionButtonRow.buttonHeight
|
|
|
|
NButton {
|
|
anchors.fill: parent
|
|
icon: "reboot"
|
|
text: I18n.tr("common.reboot")
|
|
outlined: true
|
|
backgroundColor: Color.mOnSurfaceVariant
|
|
textColor: Color.mOnPrimary
|
|
fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM
|
|
iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL
|
|
horizontalAlignment: Qt.AlignHCenter
|
|
buttonRadius: Style.radiusL
|
|
onClicked: startTimer("reboot")
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.preferredWidth: sessionButtonRow.buttonWidth
|
|
Layout.preferredHeight: sessionButtonRow.buttonHeight
|
|
|
|
NButton {
|
|
anchors.fill: parent
|
|
icon: "shutdown"
|
|
text: I18n.tr("common.shutdown")
|
|
outlined: true
|
|
backgroundColor: Color.mError
|
|
textColor: Color.mOnError
|
|
fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM
|
|
iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL
|
|
horizontalAlignment: Qt.AlignHCenter
|
|
buttonRadius: Style.radiusL
|
|
onClicked: startTimer("shutdown")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|