13 Commits

15 changed files with 434 additions and 161 deletions
+1
View File
@@ -242,6 +242,7 @@ Singleton {
recolorLogo: lock.recolorLogo,
enableFprint: lock.enableFprint,
showNotifContent: lock.showNotifContent,
showNotifIcon: lock.showNotifIcon,
maxFprintTries: lock.maxFprintTries,
blurAmount: lock.blurAmount,
sizes: {
+1
View File
@@ -6,6 +6,7 @@ JsonObject {
property int maxFprintTries: 3
property bool recolorLogo: false
property bool showNotifContent: false
property bool showNotifIcon: true
property Sizes sizes: Sizes {
}
+20 -20
View File
@@ -16,27 +16,14 @@ Scope {
property bool launching: false
property string promptMessage: ""
readonly property var selectedSession: sessionIndex >= 0 ? sessions[sessionIndex] : null
readonly property var selectedUser: Users.selectedUser
property int sessionIndex: sessions.length > 0 ? 0 : -1
property var sessions: []
readonly property string userFace: selectedUser ? selectedUser.face : ""
readonly property string username: Users.selectedUsername
// User handling - now uses the Users singleton
readonly property var users: Users.users
readonly property var selectedUser: Users.selectedUser
readonly property string username: Users.selectedUsername
readonly property string userFace: selectedUser ? selectedUser.face : ""
// User selection functions (delegate to Users singleton)
function selectUser(username: string): bool {
return Users.selectUser(username);
}
function selectNextUser(): void {
Users.selectNext();
}
function selectPreviousUser(): void {
Users.selectPrevious();
}
signal flashMsg
@@ -58,11 +45,11 @@ Scope {
event.accepted = true;
return;
}
if (event.text && !/[\r\n]/.test(event.text)) {
} else if (event.key === Qt.Key_Escape) {
buffer = "";
} else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) {
// No illegal characters (you are insane if you use unicode in your password)
buffer += event.text;
event.accepted = true;
}
}
@@ -81,6 +68,19 @@ Scope {
Greetd.launch(selectedSession.command, [], true);
}
function selectNextUser(): void {
Users.selectNext();
}
function selectPreviousUser(): void {
Users.selectPrevious();
}
// User selection functions (delegate to Users singleton)
function selectUser(username: string): bool {
return Users.selectUser(username);
}
function submit(): void {
errorMessage = "";
+14
View File
@@ -6,7 +6,21 @@ import Quickshell.Services.UPower
Singleton {
id: root
readonly property real batteryPercent: UPower.displayDevice.percentage
readonly property list<UPowerDevice> devices: UPower.devices.values
readonly property UPowerDevice displayDevice: UPower.displayDevice
readonly property bool onBattery: UPower.onBattery
// property bool toastShown
//
// Connections {
// target: UPower
//
// function onPercentageChanged(): {
// if (root.batteryPercent >= 0.2 && toastShown)
// return;
//
// root.toastShown = true;
// Toaster.toast(qsTr("Battery "))
// }
// }
}
+50
View File
@@ -1,7 +1,9 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Io
import QtQuick
import ZShell.Models
import qs.Config
import qs.Modules
@@ -12,11 +14,17 @@ Searcher {
id: root
property string actualCurrent: WallpaperPath.currentWallpaperPath
property alias crops: adapter.monitorCrops
readonly property string current: showPreview ? previewPath : actualCurrent
property alias monitorCrops: monitorCrops
property string previewPath
property bool recentlyChanged
property bool showPreview: false
function getCrop(screen: string): var {
return root.crops[screen];
}
function preview(path: string): void {
previewPath = path;
if (Config.general.color.schemeGeneration)
@@ -24,9 +32,35 @@ Searcher {
showPreview = true;
}
function setCrop(screen: string, rect: rect, scaledRect: rect, zoom: real): void {
let updated = Object.assign({}, root.crops);
if (zoom <= 0)
zoom = 1.0;
else if (zoom > 5.0)
zoom = 5.0;
updated[screen] = {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
scaledX: scaledRect.x,
scaledY: scaledRect.y,
scaledWidth: scaledRect.width,
scaledHeight: scaledRect.height,
zoom: zoom
};
root.crops = updated;
monitorCrops.writeAdapter();
monitorCrops.reload();
}
function setWallpaper(path: string): void {
actualCurrent = path;
WallpaperPath.currentWallpaperPath = path;
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 0, 0), Qt.rect(0, 0, 0, 0), 1.0));
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
if (Config.general.color.schemeGeneration)
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]);
@@ -53,6 +87,22 @@ Searcher {
target: "wallpaper"
}
FileView {
id: monitorCrops
path: `${Paths.state}/wallpaper-crops.json`
watchChanges: true
onAdapterUpdated: writeAdapter()
onFileChanged: reload()
JsonAdapter {
id: adapter
property var monitorCrops: ({})
}
}
FileSystemModel {
id: wallpapers
+1
View File
@@ -58,6 +58,7 @@ CustomRect {
fillMode: Image.PreserveAspectCrop
height: Config.notifs.sizes.image
source: Qt.resolvedUrl(root.image)
visible: Config.lock.showNotifIcon
width: Config.notifs.sizes.image
}
}
+2
View File
@@ -30,6 +30,8 @@ Scope {
} else {
buffer = buffer.slice(0, -1);
}
} else if (event.key === Qt.Key_Escape) {
buffer = "";
} else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) {
// No illegal characters (you are insane if you use unicode in your password)
buffer += event.text;
+3 -2
View File
@@ -1,3 +1,4 @@
import Quickshell
import QtQuick.Layouts
import qs.Modules.Settings.Controls
import qs.Config
@@ -5,6 +6,8 @@ import qs.Config
SettingsPage {
id: root
required property ShellScreen screen
SettingsSection {
sectionId: "Wallpaper"
@@ -33,8 +36,6 @@ SettingsPage {
}
WallpaperCropper {
Layout.fillWidth: true
Layout.preferredHeight: 600
}
}
@@ -50,6 +50,15 @@ SettingsPage {
Separator {
}
SettingSwitch {
name: "Show notification icon"
object: Config.lock
setting: "showNotifIcon"
}
Separator {
}
SettingSpinBox {
min: 0
name: "Blur amount"
+3 -1
View File
@@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Widgets
import QtQuick
@@ -20,7 +22,6 @@ Item {
required property PersistentProperties visibilities
function scrollToSetting(section: string, settingName: string) {
// Wait for the StackView transition to complete, then scroll
root.pendingSection = section;
root.pendingSetting = settingName;
scrollTimer.restart();
@@ -157,6 +158,7 @@ Item {
id: background
Cat.Background {
screen: root.screen
}
}
+231 -84
View File
@@ -1,113 +1,260 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Hyprland
import ZShell.Internal
import qs.Config
import qs.Components
import qs.Helpers
Item {
id: root
id: wrapper
Image {
id: imageView
property bool changesMade: false
property real displayH: paintedHeight
property real displayW: paintedWidth
property real displayX: (width - paintedWidth) * 0.5
property real displayY: (height - paintedHeight) * 0.5
property real scaleX: sourceW / displayW
property real scaleY: sourceH / displayH
property real sourceH: Quickshell.screens[0].height
property real sourceW: Quickshell.screens[0].width
signal requestCrop
anchors.fill: parent
fillMode: Image.PreserveAspectFit
smooth: true
source: Wallpapers.current
}
Layout.fillWidth: true
Layout.preferredHeight: 400
Item {
id: overlay
IconButton {
anchors.margins: Appearance.padding.normal
anchors.right: parent.right
anchors.top: parent.top
icon: "check"
opacity: wrapper.changesMade ? 1 : 0
scale: wrapper.changesMade ? 1 : 0
z: 2
clip: true
height: imageView.displayH
width: imageView.displayW
x: imageView.displayX
y: imageView.displayY
CustomRect {
id: cropRect
property real aspectRatio: Quickshell.screens[0].width / Quickshell.screens[0].height
readonly property rect sourceRect: Qt.rect(x * imageView.scaleX, y * imageView.scaleY, width * imageView.scaleX, height * imageView.scaleY)
property real zoom: Config.background.zoom
function clampToBounds() {
x = Math.max(0, Math.min(x, overlay.width - width));
y = Math.max(0, Math.min(y, overlay.height - height));
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
border.color: DynamicColors.palette.m3primary
border.width: 2
color: DynamicColors.tPalette.m3primary
height: width / aspectRatio
radius: Appearance.rounding.small
visible: imageView.status === Image.Ready
width: Math.min(overlay.width / zoom, overlay.height * aspectRatio / zoom)
x: Config.background.sourceClipX / imageView.scaleX
y: Config.background.sourceClipY / imageView.scaleY
}
MouseArea {
function updateCrop(mouseX, mouseY) {
let nx = mouseX - cropRect.width * 0.5;
let ny = mouseY - cropRect.height * 0.5;
onClicked: {
wrapper.requestCrop();
wrapper.changesMade = false;
}
}
nx = Math.max(0, Math.min(nx, overlay.width - cropRect.width));
RowLayout {
id: root
ny = Math.max(0, Math.min(ny, overlay.height - cropRect.height));
anchors.fill: parent
spacing: Appearance.spacing.normal
cropRect.x = nx;
cropRect.y = ny;
Repeater {
model: ScriptModel {
values: [...Quickshell.screens].sort((a, b) => {
return a.x - b.x;
})
}
anchors.fill: parent
hoverEnabled: true
preventStealing: true
Item {
id: delegate
onPositionChanged: mouse => {
if (pressed)
updateCrop(mouse.x, mouse.y);
}
onPressed: mouse => {
updateCrop(mouse.x, mouse.y);
}
onReleased: {
Wallpapers.recentlyChanged = false;
Config.background.sourceClipX = cropRect.sourceRect.x;
Config.background.sourceClipY = cropRect.sourceRect.y;
Config.background.sourceClipW = cropRect.sourceRect.width;
Config.background.sourceClipH = cropRect.sourceRect.height;
Config.save();
}
onWheel: wheel => {
let oldCenterX = cropRect.x + cropRect.width * 0.5;
let oldCenterY = cropRect.y + cropRect.height * 0.5;
required property ShellScreen modelData
if (wheel.angleDelta.y > 0)
cropRect.zoom *= 1.1;
else
cropRect.zoom /= 1.1;
function applyCrop(): void {
const croprect = cropRect.mapToItem(scaledImg, 0, 0, cropRect.width, cropRect.height);
const upscaledRect = Qt.rect((croprect.x - cropRect.imageX) / scaledImg.paintedWidth, (croprect.y - cropRect.imageY) / scaledImg.paintedHeight, croprect.width / scaledImg.paintedWidth, croprect.height / scaledImg.paintedHeight);
Wallpapers.setCrop(delegate.modelData.name, upscaledRect, croprect, cropRect.zoom);
}
cropRect.zoom = Math.max(1.0, Math.min(cropRect.zoom, 10.0));
Config.background.zoom = cropRect.zoom;
function zoomClipRect(zoom: real): void {
let oldCenterX = cropRect.x + cropRect.width * 0.5;
let oldCenterY = cropRect.y + cropRect.height * 0.5;
cropRect.x = oldCenterX - cropRect.width * 0.5;
cropRect.y = oldCenterY - cropRect.height * 0.5;
cropRect.zoom = zoom;
cropRect.clampToBounds();
cropRect.x = oldCenterX - cropRect.width * 0.5;
cropRect.y = oldCenterY - cropRect.height * 0.5;
cropRect.clampToBounds();
}
Layout.fillHeight: true
Layout.fillWidth: true
Connections {
function onRequestCrop(): void {
delegate.applyCrop();
}
target: wrapper
}
RowLayout {
id: sliderLayout
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: 30
spacing: Appearance.spacing.large
CustomText {
text: qsTr("Crop scale")
}
CustomSlider {
id: zoomSlider
Layout.fillWidth: true
Layout.preferredHeight: 10
from: 1.0
to: 5.0
value: cropRect.zoom
onMoved: {
delegate.zoomClipRect(value);
wrapper.changesMade = true;
}
}
}
CachingImage {
id: scaledImg
property var displayData
property real monitorScale: 1.0
anchors.bottom: sliderLayout.top
anchors.bottomMargin: Appearance.spacing.normal
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
asynchronous: true
fillMode: Image.PreserveAspectFit
// retainWhileLoading: true
source: Wallpapers.current
sourceSize.height: parent.height
sourceSize.width: parent.width
onPaintedWidthChanged: {
if (paintedWidth > 0) {
scaledImg.displayData = Wallpapers.getCrop(delegate.modelData.name);
cropRect.zoom = Wallpapers.getCrop(delegate.modelData.name).zoom;
cropRect.restoreFromData();
}
}
onSourceChanged: cropRect.clampToBounds()
onStatusChanged: if (scaledImg.status == Image.Ready)
cropRect.clampToBounds()
CustomText {
id: monitorId
anchors.centerIn: parent
color: Qt.alpha(DynamicColors.palette.m3surface, 0.85)
font.pointSize: Appearance.font.size.large * 4
style: Text.Outline
styleColor: DynamicColors.palette.m3onSurface
text: delegate.modelData.name
}
CustomRect {
id: cropRect
property real aspectRatio: delegate.modelData.width / delegate.modelData.height
readonly property real baseHeight: baseWidth / aspectRatio
readonly property real baseWidth: {
let fittedHeight = scaledImg.paintedHeight;
let fittedWidth = fittedHeight * aspectRatio;
if (fittedWidth > scaledImg.paintedWidth) {
fittedWidth = scaledImg.paintedWidth;
fittedHeight = fittedWidth / aspectRatio;
}
return fittedWidth;
}
readonly property real imageX: (scaledImg.width - scaledImg.paintedWidth) / 2
readonly property real imageY: (scaledImg.height - scaledImg.paintedHeight) / 2
property real imgAspectRatio: scaledImg.paintedWidth / scaledImg.paintedHeight
property real zoom: scaledImg.displayData.zoom
function centerInImage() {
x = imageX + (scaledImg.paintedWidth - width) / 2;
y = imageY + (scaledImg.paintedHeight - height) / 2;
}
function clampToBounds() {
x = Math.max(imageX, Math.min(x, imageX + scaledImg.paintedWidth - width));
y = Math.max(imageY, Math.min(y, imageY + scaledImg.paintedHeight - height));
}
function restoreFromData() {
let data = scaledImg.displayData;
if (data && data.scaledX !== 0 || data.scaledY !== 0 || data.scaledWidth !== 0 || data.scaledHeight !== 0) {
x = data.scaledX;
y = data.scaledY;
clampToBounds();
} else {
zoom = 1.0;
centerInImage();
}
}
border.color: DynamicColors.palette.m3primary
border.width: 2
height: baseHeight / zoom
opacity: 1
width: baseWidth / zoom
Behavior on opacity {
Anim {
}
}
Component.onCompleted: clampToBounds()
onHeightChanged: clampToBounds()
onWidthChanged: clampToBounds()
}
MouseArea {
id: mouse
function updateCrop(mouseX, mouseY) {
let nx = mouseX - cropRect.width * 0.5;
let ny = mouseY - cropRect.height * 0.5;
nx = Math.max(cropRect.imageX, Math.min(nx, cropRect.imageX + scaledImg.paintedWidth - cropRect.width));
ny = Math.max(cropRect.imageY, Math.min(ny, cropRect.imageY + scaledImg.paintedHeight - cropRect.height));
cropRect.x = nx;
cropRect.y = ny;
}
anchors.fill: parent
hoverEnabled: true
preventStealing: true
onPositionChanged: mouse => {
if (pressed) {
updateCrop(mouse.x, mouse.y);
wrapper.changesMade = true;
}
}
onPressed: mouse => {
updateCrop(mouse.x, mouse.y);
wrapper.changesMade = true;
}
onReleased: {
wrapper.changesMade = true;
}
}
}
}
}
}
+35 -41
View File
@@ -23,26 +23,13 @@ GridView {
delegate: Item {
required property int index
readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent
readonly property real itemMargin: Appearance.spacing.normal / 2
readonly property real itemRadius: Appearance.rounding.normal
readonly property real itemMargin: Appearance.spacing.normal
readonly property real itemRadius: Appearance.rounding.small
required property var modelData
height: root.cellHeight
width: root.cellWidth
StateLayer {
function onClicked(): void {
Wallpapers.setWallpaper(modelData.path);
}
anchors.bottomMargin: itemMargin
anchors.fill: parent
anchors.leftMargin: itemMargin
anchors.rightMargin: itemMargin
anchors.topMargin: itemMargin
radius: itemRadius
}
CustomClippingRect {
id: image
@@ -53,8 +40,6 @@ GridView {
anchors.topMargin: itemMargin
antialiasing: true
color: DynamicColors.tPalette.m3surfaceContainer
layer.enabled: true
layer.smooth: true
radius: itemRadius
CachingImage {
@@ -100,6 +85,33 @@ GridView {
}
}
Rectangle {
anchors.fill: parent
antialiasing: true
border.color: DynamicColors.palette.m3primary
border.width: isCurrent ? 2 : 0
color: "transparent"
radius: itemRadius + 2
smooth: true
Behavior on border.width {
NumberAnimation {
duration: 150
easing.type: Easing.OutQuad
}
}
MaterialIcon {
anchors.margins: Appearance.padding.small
anchors.right: parent.right
anchors.top: parent.top
color: DynamicColors.palette.m3primary
font.pointSize: Appearance.font.size.large
text: "check_circle"
visible: isCurrent
}
}
Timer {
id: fallbackTimer
@@ -112,35 +124,17 @@ GridView {
}
}
Rectangle {
StateLayer {
function onClicked(): void {
Wallpapers.setWallpaper(modelData.path);
}
anchors.bottomMargin: itemMargin
anchors.fill: parent
anchors.leftMargin: itemMargin
anchors.rightMargin: itemMargin
anchors.topMargin: itemMargin
antialiasing: true
border.color: DynamicColors.palette.m3primary
border.width: isCurrent ? 2 : 0
color: "transparent"
radius: itemRadius - border.width
smooth: true
Behavior on border.width {
NumberAnimation {
duration: 150
easing.type: Easing.OutQuad
}
}
MaterialIcon {
anchors.margins: Appearance.padding.small
anchors.right: parent.right
anchors.top: parent.top
color: DynamicColors.palette.m3primary
font.pointSize: Appearance.font.size.large
text: "check_circle"
visible: isCurrent
}
radius: itemRadius
}
}
}
+1
View File
@@ -1,5 +1,6 @@
import QtQuick.Layouts
import QtQuick
import QtQuick.VectorImage
import Quickshell
import Quickshell.Services.SystemTray
import qs.Modules
+56 -13
View File
@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import QtQuick
import qs.Components
import qs.Helpers
@@ -12,31 +13,73 @@ Item {
required property ShellScreen screen
property string source: Wallpapers.current
function refreshData(): void {
Hyprland.refreshMonitors();
const scale = Hyprland.monitorFor(root.screen).scale;
if (scale > 0 && img.resScale !== scale) {
img.resScale = scale;
img.sourceSize.width = root.screen.width * scale;
}
const displayData = Wallpapers.getCrop(root.screen.name);
const displayRect = Qt.rect(img.sourceSize.width * displayData.x, img.implicitHeight * displayData.y, img.sourceSize.width * displayData.width, img.implicitHeight * displayData.height);
img.anchors.fill = null;
img.zoom = displayData.zoom;
img.x = -(displayRect.x * displayData.zoom / img.resScale);
img.y = -(displayRect.y * displayData.zoom / img.resScale);
}
anchors.fill: parent
Image {
id: img
anchors.fill: parent
property int displayH
property int displayW
property real resScale
property real zoom: 1.0
asynchronous: true
fillMode: Image.PreserveAspectCrop
height: implicitHeight * zoom / resScale
opacity: 1
retainWhileLoading: true
source: root.source
sourceClipRect: Wallpapers.recentlyChanged ? null : Qt.rect(Config.background.sourceClipX, Config.background.sourceClipY, Config.background.sourceClipW, Config.background.sourceClipH)
sourceSize.height: root.screen.height
sourceSize.width: root.screen.width
sourceSize.width: root.screen.width * resScale
width: implicitWidth * zoom / resScale
onSourceChanged: {
if (Wallpapers.recentlyChanged) {
Config.background.sourceClipH = 0;
Config.background.sourceClipW = 0;
Config.background.sourceClipY = 0;
Config.background.sourceClipX = 0;
Config.background.zoom = 1.0;
Config.save();
Behavior on height {
Anim {
}
Wallpapers.recentlyChanged = true;
}
Behavior on width {
Anim {
}
}
Behavior on x {
Anim {
}
}
Behavior on y {
Anim {
}
}
onStatusChanged: {
if (img.status == Image.Ready) {
root.refreshData();
}
}
Connections {
function onAdapterUpdated(): void {
root.refreshData();
}
function onLoaded(): void {
root.refreshData();
}
target: Wallpapers.monitorCrops
}
}
}
+7
View File
@@ -311,6 +311,13 @@ export const settingsIndex = [
section: "Lockscreen",
keywords: ["notification", "hide", "privacy"],
},
{
name: "Show notification icon",
category: "lockscreen",
categoryName: "Lockscreen",
section: "Lockscreen",
keywords: ["notification", "hide", "icon"],
},
{
name: "Blur amount",
category: "lockscreen",