added audio and apps page in settings
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 18s
Python / test (pull_request) Successful in 28s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m6s

This commit is contained in:
2026-06-23 22:32:56 +02:00
parent 2366901b24
commit 6a80961b2b
29 changed files with 2080 additions and 640 deletions
+2 -1
View File
@@ -20,6 +20,7 @@ MouseArea {
return (Math.sqrt(Math.max(d1, d2, d3, d4)) + (shapeMorph ? 24 : 0)) * 1.3;
}
property real endRadiusAtPress
property bool manualHoverOverride
property bool manualPressOverride
property real pressX: width / 2
property real pressY: height / 2
@@ -27,7 +28,7 @@ MouseArea {
readonly property alias rect: base
property bool shapeMorph
property bool showHoverBackground: true
property real stateOpacity: containsMouse ? 0.08 : 0
property real stateOpacity: containsMouse || manualHoverOverride ? 0.08 : 0
property alias topLeftRadius: base.topLeftRadius
property alias topRightRadius: base.topRightRadius
+72
View File
@@ -0,0 +1,72 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Effects
CustomListView {
id: root
property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
property real fadeAmount: 0.2
property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
function fadeShouldBeActive(isStart: bool): bool {
// When content is smaller than flickable size, hide fade when rebound starts
if (contentHeight + topMargin + bottomMargin < height && rebound.running && ((isStart ? verticalOvershoot > 0 : verticalOvershoot < 0)))
return false;
if (isStart)
return visibleArea.yPosition > 0;
return visibleArea.yPosition + visibleArea.heightRatio < 1;
}
flickableDirection: Flickable.VerticalFlick
layer.enabled: true
orientation: ListView.Vertical
Behavior on bottomFadeOpacity {
Anim {
type: Anim.SlowEffects
}
}
layer.effect: Mask {
maskSource: mask
Rectangle {
id: mask
anchors.fill: parent
layer.enabled: true
visible: false
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
color: Qt.rgba(0, 0, 0, root.topFadeOpacity)
position: 0
}
GradientStop {
color: Qt.rgba(0, 0, 0, 1)
position: root.fadeAmount
}
GradientStop {
color: Qt.rgba(0, 0, 0, 1)
position: 1 - root.fadeAmount
}
GradientStop {
color: Qt.rgba(0, 0, 0, root.bottomFadeOpacity)
position: 1
}
}
}
}
Behavior on topFadeOpacity {
Anim {
type: Anim.SlowEffects
}
}
}
+2
View File
@@ -245,6 +245,8 @@ Singleton {
return {
maxAppsShown: launcher.maxAppsShown,
maxWallpapers: launcher.maxWallpapers,
hiddenApps: launcher.hiddenApps,
favoriteApps: launcher.favoriteApps,
uwsm: launcher.uwsm,
actionPrefix: launcher.actionPrefix,
specialPrefix: launcher.specialPrefix,
+2
View File
@@ -84,6 +84,8 @@ JsonObject {
dangerous: false
},
]
property list<string> favoriteApps: []
property list<string> hiddenApps: []
property int maxAppsShown: 10
property int maxWallpapers: 7
property Sizes sizes: Sizes {
+26
View File
@@ -0,0 +1,26 @@
pragma Singleton
import Quickshell
Singleton {
property var _regexCache: ({})
function testRegexList(filterList: list<string>, target: string): bool {
const regexChecker = /^\^.*\$$/;
for (const filter of filterList) {
if (regexChecker.test(filter)) {
let re = _regexCache[filter];
if (!re) {
re = new RegExp(filter);
_regexCache[filter] = re;
}
if (re.test(target))
return true;
} else {
if (filter === target)
return true;
}
}
return false;
}
}
+15
View File
@@ -65,5 +65,20 @@ Item {
width: root.width - icon.width - Appearance.rounding.normal * 2
}
}
Loader {
id: favoriteIcon
active: root.modelData && Strings.testRegexList(Config.launcher.favoriteApps, root.modelData.id)
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
asynchronous: true
sourceComponent: MaterialIcon {
color: DynamicColors.palette.m3primary
fill: 1
text: "favorite"
}
}
}
}
+2 -1
View File
@@ -74,7 +74,8 @@ Searcher {
AppDb {
id: appDb
entries: DesktopEntries.applications.values
entries: DesktopEntries.applications.values.filter(a => !Strings.testRegexList(Config.launcher.hiddenApps, a.id))
favoriteApps: Config.launcher.favoriteApps
path: `${Paths.state}/apps.sqlite`
}
}
@@ -0,0 +1,96 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import qs.Components
import qs.Config
ItemList {
id: root
property int currentId: -1
property string iconName: "speaker"
property var nodes: []
signal selected(node: PwNode)
last: true
showList: true
delegate: Item {
id: device
readonly property bool active: device.modelData?.id === root.currentId
required property int index
required property PwNode modelData
anchors.left: root.list.contentItem.left
anchors.right: root.list.contentItem.right
implicitHeight: deviceLayout.implicitHeight + deviceLayout.anchors.margins * 2
StateLayer {
bottomLeftRadius: device.index === root?.list.count - 1 ? Appearance.rounding.large : radius
bottomRightRadius: device.index === root?.list.count - 1 ? Appearance.rounding.large : radius
radius: Appearance.rounding.extraSmall
onClicked: root.selected(device.modelData)
}
RowLayout {
id: deviceLayout
anchors.fill: parent
anchors.leftMargin: Appearance.padding.largeIncreased
anchors.margins: Appearance.padding.large
anchors.rightMargin: Appearance.padding.largeIncreased
spacing: Appearance.spacing.normal
CustomRect {
color: device.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondaryContainer
implicitHeight: devIcon.implicitHeight + Appearance.padding.normal * 2
implicitWidth: implicitHeight
radius: Appearance.rounding.full
MaterialIcon {
id: devIcon
anchors.centerIn: parent
color: device.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSecondaryContainer
fill: device.active ? 1 : 0
font.pointSize: Appearance.font.size.large
text: root.iconName
Behavior on fill {
Anim {
}
}
}
}
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pointSize: Appearance.font.size.smaller
text: device.modelData?.description || device.modelData?.name || qsTr("Unknown")
}
MaterialIcon {
color: DynamicColors.palette.m3primary
font.pointSize: Appearance.font.size.large
opacity: device.active ? 1 : 0
text: "check"
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
}
}
model: ScriptModel {
values: [...root.nodes].sort((a, b) => (a.description || a.name || "").localeCompare(b.description || b.name || ""))
}
}
+127
View File
@@ -0,0 +1,127 @@
import QtQuick
import ZShell.Blobs
import qs.Components
import qs.Config
Item {
id: root
property real animDriver
property alias color: blobGroup.color
default required property Item content
property bool hoverOverride
readonly property alias hovered: btn.containsMouse
property alias icon: icon.text
property bool open
property int padding
property bool pressOverride
property int topMovement: Appearance.padding.large
implicitHeight: btn.implicitHeight * 0.9
implicitWidth: btn.implicitWidth * 0.9
Binding {
property: "opacity"
target: root.content
value: root.animDriver
}
BlobGroup {
id: blobGroup
color: DynamicColors.palette.m3surfaceContainerHighest
smoothing: Appearance.rounding.medium
Behavior on color {
CAnim {
}
}
}
BlobRect {
id: btnRect
anchors.fill: parent
anchors.margins: (!(btn.pressed || root.pressOverride) && (btn.containsMouse || root.hoverOverride) ? -Appearance.padding.extraSmall : 0) + (root.open ? -Appearance.padding.extraSmall : 0)
group: blobGroup
radius: root.open ? Appearance.rounding.large : Appearance.rounding.medium
Behavior on anchors.margins {
Anim {
}
}
Behavior on radius {
Anim {
type: Anim.DefaultEffects
}
}
}
BlobRect {
id: rect
anchors.right: parent.right
anchors.top: parent.top
deformScale: 0.00001
group: blobGroup
implicitHeight: parent.height
implicitWidth: parent.width
radius: Appearance.rounding.large
states: State {
name: "open"
when: root.open
PropertyChanges {
rect.anchors.rightMargin: root.width - Appearance.spacing.small
rect.anchors.topMargin: -root.topMovement
rect.implicitHeight: root.content.implicitHeight + root.padding * 2
rect.implicitWidth: root.content.implicitWidth + root.padding * 2
root.animDriver: 1
}
}
transitions: Transition {
Anim {
properties: "rightMargin,implicitWidth"
}
Anim {
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
properties: "topMargin,implicitHeight"
}
Anim {
property: "animDriver"
type: Anim.DefaultEffects
}
}
MouseArea { // MouseArea to catch inputs
anchors.fill: parent
children: [root.content]
clip: true
}
}
MouseArea {
id: btn
anchors.centerIn: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
implicitHeight: icon.implicitHeight + Appearance.padding.extraSmall * 2
implicitWidth: implicitHeight
onClicked: root.open = !root.open
MaterialIcon {
id: icon
anchors.centerIn: parent
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.medium
text: "view_apps"
}
}
}
+116
View File
@@ -0,0 +1,116 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Config
ConnectedRect {
id: root
property alias delegate: list.delegate
property int extraHeight
readonly property alias list: list
property alias model: list.model
property string placeholderIcon
property string placeholderText
property bool showList
Layout.fillWidth: true
clip: true
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: (showList && list.count > 0 ? list.contentHeight : placeholder.implicitHeight + Appearance.padding.extraLarge * 2) + extraHeight
Behavior on implicitHeight {
Anim {
}
}
Loader {
id: placeholder
active: opacity > 0
anchors.centerIn: parent
opacity: root.showList && list.count > 0 ? 0 : 1
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
sourceComponent: ColumnLayout {
spacing: Appearance.spacing.extraSmall
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
animate: true
color: DynamicColors.palette.m3outline
font.pointSize: Appearance.font.size.large
text: root.placeholderIcon
}
CustomText {
Layout.alignment: Qt.AlignHCenter
animate: true
color: DynamicColors.palette.m3outline
font.pointSize: Appearance.font.size.large
text: root.placeholderText
}
}
}
ListView {
id: list
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
interactive: false
opacity: root.showList ? 1 : 0
spacing: 0
add: Transition {
Anim {
from: 0
property: "opacity"
to: 1
type: Anim.DefaultEffects
}
}
displaced: Transition {
Anim {
property: "opacity"
to: 1
type: Anim.DefaultEffects
}
Anim {
property: "y"
}
}
move: Transition {
Anim {
property: "opacity"
to: 1
type: Anim.DefaultEffects
}
Anim {
property: "y"
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
remove: Transition {
Anim {
property: "opacity"
to: 0
type: Anim.DefaultEffects
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Config
ConnectedRect {
id: root
property alias icon: icon.text
property alias label: label.text
property alias status: status.text
signal clicked
Layout.fillWidth: true
implicitHeight: navLayout.implicitHeight + navLayout.anchors.margins * 2
StateLayer {
onClicked: root.clicked()
}
RowLayout {
id: navLayout
anchors.fill: parent
anchors.leftMargin: Appearance.padding.largeIncreased
anchors.margins: Appearance.padding.normal
anchors.rightMargin: Appearance.padding.largeIncreased
spacing: Appearance.spacing.small
MaterialIcon {
id: icon
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.medium
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
CustomText {
id: label
Layout.fillWidth: true
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
}
CustomText {
id: status
Layout.fillWidth: true
animate: true
color: DynamicColors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
visible: text
}
}
MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.medium
text: "chevron_right"
}
}
}
+68
View File
@@ -0,0 +1,68 @@
import QtQuick
import QtQuick.Layouts
import qs.Config
import qs.Components
import qs.Modules.SettingsNew
ConnectedRect {
id: root
property alias checked: switchButton.checked
property int horizontalPadding: Appearance.padding.largeIncreased
required property Component popup
property alias subtext: subtext.text
property alias text: text.text
property int verticalPadding: Appearance.padding.normal
signal clicked(checked: bool)
Layout.fillWidth: true
implicitHeight: layout.implicitHeight + verticalPadding * 2
Column {
id: layout
anchors.left: parent.left
anchors.leftMargin: root.horizontalPadding
anchors.right: icon.left
anchors.rightMargin: Appearance.padding.normal
anchors.verticalCenter: parent.verticalCenter
CustomText {
id: text
font.pointSize: Appearance.font.size.smaller
}
CustomText {
id: subtext
color: DynamicColors.palette.m3outline
font.pointSize: Appearance.font.size.small
wrapMode: Text.WordWrap
}
}
MaterialIcon {
id: icon
anchors.right: switchButton.left
anchors.rightMargin: Appearance.spacing.normal
anchors.verticalCenter: parent.verticalCenter
text: "open_in_new"
}
StateLayer {
onClicked: PopupManager.requestOpen(root.popup)
}
CustomSwitch {
id: switchButton
anchors.right: parent.right
anchors.rightMargin: root.horizontalPadding
anchors.verticalCenter: parent.verticalCenter
onClicked: root.clicked(checked)
}
}
+1
View File
@@ -64,6 +64,7 @@ ColumnLayout {
bottomMargin: Appearance.padding.extraLarge
contentHeight: root.contentChild?.implicitHeight ?? 0
contentItem.children: [root.contentChild]
fadeAmount: 0.1
topMargin: Appearance.padding.large
}
}
+94 -41
View File
@@ -1,68 +1,121 @@
import QtQuick
import QtQuick.Layouts
import qs.Config
import Quickshell
import qs.Components
import qs.Modules.SettingsNew
import qs.Config
import qs.Drawers
ConnectedRect {
id: root
property alias checked: switchButton.checked
property int horizontalPadding: Appearance.padding.largeIncreased
required property Component popup
property alias subtext: subtext.text
property alias text: text.text
property int verticalPadding: Appearance.padding.normal
signal clicked(checked: bool)
default required property Item content
property alias icon: icon.text
property bool keepPopupAsChild
property alias label: label.text
readonly property alias popup: popup
property alias status: status.text
Layout.fillWidth: true
implicitHeight: layout.implicitHeight + verticalPadding * 2
implicitHeight: navLayout.implicitHeight + navLayout.anchors.margins * 2
Column {
id: layout
StateLayer {
id: stateLayer
anchors.left: parent.left
anchors.leftMargin: root.horizontalPadding
anchors.right: icon.left
anchors.rightMargin: Appearance.padding.normal
anchors.verticalCenter: parent.verticalCenter
manualHoverOverride: popup.hovered && !popup.open
CustomText {
id: text
font.pointSize: Appearance.font.size.smaller
onClicked: popup.open = true
}
CustomText {
id: subtext
RowLayout {
id: navLayout
color: DynamicColors.palette.m3outline
font.pointSize: Appearance.font.size.small
wrapMode: Text.WordWrap
}
}
anchors.fill: parent
anchors.leftMargin: Appearance.padding.largeIncreased
anchors.margins: Appearance.padding.normal
anchors.rightMargin: Appearance.padding.largeIncreased
spacing: Appearance.spacing.small
MaterialIcon {
id: icon
anchors.right: switchButton.left
anchors.rightMargin: Appearance.spacing.normal
anchors.verticalCenter: parent.verticalCenter
text: "open_in_new"
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.medium
}
StateLayer {
onClicked: PopupManager.requestOpen(root.popup)
ColumnLayout {
Layout.fillWidth: true
spacing: 0
CustomText {
id: label
Layout.fillWidth: true
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
}
CustomSwitch {
id: switchButton
CustomText {
id: status
anchors.right: parent.right
anchors.rightMargin: root.horizontalPadding
anchors.verticalCenter: parent.verticalCenter
Layout.fillWidth: true
animate: true
color: DynamicColors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
visible: text
}
}
onClicked: root.clicked(checked)
Item {
id: triggerArea
implicitHeight: popup.implicitHeight
implicitWidth: popup.implicitWidth
TransformWatcher {
id: tWatcher
a: area.parent
b: triggerArea
}
MouseArea {
id: area
anchors.fill: parent
cursorShape: undefined
enabled: popup.open
hoverEnabled: true
parent: {
if (root.keepPopupAsChild)
return triggerArea;
const win = QsWindow.window;
const contentWin = win as Windows; // If inside the drawer content window, put it inside the interaction wrapper so hover works
return contentWin ? contentWin.interactionWrapper : (win as QsWindow).contentItem;
}
z: popup.animDriver > 0 ? 1 : 0
onClicked: popup.open = false
BlobPopup {
id: popup
color: open || hovered || stateLayer.containsMouse ? DynamicColors.palette.m3secondaryContainer : DynamicColors.palette.m3surfaceContainerHighest
content: root.content
hoverOverride: stateLayer.containsMouse
padding: Appearance.padding.small
pressOverride: stateLayer.pressed
x: {
tWatcher.transform;
return triggerArea.mapToItem(area.parent, 0, 0).x;
}
y: {
tWatcher.transform;
return triggerArea.mapToItem(area.parent, 0, 0).y;
}
}
}
}
}
}
@@ -0,0 +1,16 @@
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Config
CustomText {
property bool first
Layout.bottomMargin: Appearance.spacing.extraSmall
Layout.fillWidth: true
Layout.leftMargin: Appearance.padding.small
Layout.topMargin: first ? 0 : Appearance.spacing.large - ((parent as ColumnLayout).spacing ?? 0)
color: DynamicColors.palette.m3onSurfaceVariant
elide: Text.ElideRight
font.pointSize: Appearance.font.size.medium
}
+81
View File
@@ -0,0 +1,81 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Config
ConnectedRect {
id: root
property alias icon: slider.insetIcon
property alias label: label.text
property real value
property alias valueLabel: valueLabel.text
signal moved(value: real)
Layout.fillWidth: true
implicitHeight: rowLayout.implicitHeight + rowLayout.anchors.margins + rowLayout.anchors.topMargin
RowLayout {
id: rowLayout
anchors.fill: parent
anchors.margins: Appearance.padding.largeIncreased
anchors.topMargin: Appearance.padding.large
spacing: Appearance.spacing.small
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
CustomText {
id: label
Layout.fillWidth: true
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
}
CustomText {
id: valueLabel
color: DynamicColors.palette.m3outline
font.pointSize: Appearance.font.size.small
}
}
CustomMouseArea {
function onWheel(event: WheelEvent): void {
const step = Config.services.audioIncrement;
if (event.angleDelta.y > 0)
root.moved(Math.min(1, root.value + step));
else if (event.angleDelta.y < 0)
root.moved(Math.max(0, root.value - step));
}
Layout.fillWidth: true
implicitHeight: Appearance.padding.larger * 3
CustomSlider {
id: slider
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
enabled: root.enabled
implicitHeight: parent.implicitHeight
radius: Appearance.rounding.small
value: root.value
onInteraction: v => root.moved(v)
}
}
}
}
}
+1 -1
View File
@@ -104,7 +104,7 @@ StackView {
id: logCat
defaultLogLevel: LoggingCategory.Info
name: "caelestia.nexus"
name: "ZShell.settings"
}
Connections {
+105 -82
View File
@@ -4,7 +4,7 @@ import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import ZShell.Internal
import ZShell.Components
import qs.Config
import qs.Components
import qs.Helpers
@@ -13,13 +13,71 @@ Item {
id: wrapper
property bool changesMade: false
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
property var screens: []
property int selectedScreenIndex: 0
property bool shouldBeActive: true
signal requestCrop
function applyCrop(): void {
if (!cropRectLoader.item || !currentScreen)
return;
const cropRect = cropRectLoader.item;
const cropXPercent = (cropRect.x - cropRect.imageX) / scaledImg.paintedWidth;
const cropYPercent = (cropRect.y - cropRect.imageY) / scaledImg.paintedHeight;
const cropWidthPercent = cropRect.width / scaledImg.paintedWidth;
const cropHeightPercent = cropRect.height / scaledImg.paintedHeight;
Wallpapers.setCrop(currentScreen.name, Qt.rect(cropXPercent, cropYPercent, cropWidthPercent, cropHeightPercent), cropRect.zoom);
}
function refreshScreens(): void {
screens = [...Quickshell.screens].sort((a, b) => a.x - b.x);
if (screens.length === 0) {
selectedScreenIndex = 0;
return;
}
selectedScreenIndex = Math.max(0, Math.min(selectedScreenIndex, screens.length - 1));
}
function selectScreen(index: int): void {
if (index < 0 || index >= screens.length || index === selectedScreenIndex)
return;
selectedScreenIndex = index;
if (cropRectLoader.item)
Qt.callLater(syncCropToScreen);
}
function syncCropToScreen(): void {
if (cropRectLoader.item)
cropRectLoader.item.restoreFromData();
}
function zoomClipRect(zoom: real): void {
if (!cropRectLoader.item)
return;
const cropRect = cropRectLoader.item;
const centerX = cropRect.x + cropRect.width * 0.5;
const centerY = cropRect.y + cropRect.height * 0.5;
cropRect.zoom = zoom;
cropRect.x = centerX - cropRect.width * 0.5;
cropRect.y = centerY - cropRect.height * 0.5;
cropRect.clampToBounds();
}
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: shouldBeActive ? 400 : 0
implicitHeight: shouldBeActive ? 430 : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
@@ -37,6 +95,14 @@ Item {
}
}
Component.onCompleted: {
refreshScreens();
Qt.callLater(syncCropToScreen);
}
onSelectedScreenIndexChanged: {
Qt.callLater(syncCropToScreen);
}
IconButton {
anchors.margins: Appearance.padding.normal
anchors.right: parent.right
@@ -56,72 +122,40 @@ Item {
}
onClicked: {
wrapper.requestCrop();
wrapper.applyCrop();
wrapper.changesMade = false;
}
}
RowLayout {
id: root
ButtonRow {
id: screenSelector
anchors.fill: parent
spacing: Appearance.spacing.normal
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.extraLarge * 2
anchors.right: parent.right
anchors.rightMargin: Appearance.padding.extraLarge * 2
anchors.top: parent.top
implicitHeight: 34
spacing: Appearance.spacing.small
Repeater {
model: ScriptModel {
values: [...Quickshell.screens].sort((a, b) => {
return a.x - b.x;
})
model: wrapper.screens
delegate: TextButton {
required property int index
readonly property bool isCurrent: wrapper.selectedScreenIndex === index
required property var modelData
fillWidth: true
inactiveColor: isCurrent ? DynamicColors.palette.m3primary : Qt.alpha(DynamicColors.palette.m3surfaceContainer, 0.7)
inactiveOnColor: isCurrent ? DynamicColors.palette.m3onPrimary : Qt.alpha(DynamicColors.palette.m3onSurface, 0.7)
isRound: true
shapeMorph: true
text: modelData.name
onClicked: wrapper.selectScreen(index)
}
Item {
id: delegate
required property ShellScreen modelData
function applyCrop(): void {
if (!cropRectLoader.item)
return;
const cropRect = cropRectLoader.item;
// We need to calculate the exact percentage coordinates that map perfectly
// to our C++ backend, regardless of current display scaling
const cropXPercent = (cropRect.x - cropRect.imageX) / scaledImg.paintedWidth;
const cropYPercent = (cropRect.y - cropRect.imageY) / scaledImg.paintedHeight;
const cropWidthPercent = cropRect.width / scaledImg.paintedWidth;
const cropHeightPercent = cropRect.height / scaledImg.paintedHeight;
const finalRect = Qt.rect(cropXPercent, cropYPercent, cropWidthPercent, cropHeightPercent);
// We just pass the percentages directly to the backend
Wallpapers.setCrop(delegate.modelData.name, finalRect, cropRect.zoom);
}
function zoomClipRect(zoom: real): void {
if (!cropRectLoader.item)
return;
const cropRect = cropRectLoader.item;
let oldCenterX = cropRect.x + cropRect.width * 0.5;
let oldCenterY = cropRect.y + cropRect.height * 0.5;
cropRect.zoom = zoom;
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 {
@@ -146,13 +180,13 @@ Item {
value: cropRectLoader.item ? cropRectLoader.item.zoom : 1.0
onInteraction: value => {
delegate.zoomClipRect(1 + (value * 4));
wrapper.zoomClipRect(1 + (value * 4));
wrapper.changesMade = true;
}
}
}
CachingImage {
Image {
id: scaledImg
property var displayData
@@ -162,7 +196,8 @@ Item {
anchors.bottomMargin: Appearance.spacing.normal
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.top: screenSelector.bottom
anchors.topMargin: Appearance.spacing.normal
asynchronous: true
fillMode: Image.PreserveAspectFit
retainWhileLoading: true
@@ -186,27 +221,16 @@ Item {
}
}
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
}
Loader {
id: cropRectLoader
active: scaledImg.paintedWidth > 0
active: scaledImg.paintedWidth > 0 && wrapper.currentScreen
sourceComponent: Component {
CustomRect {
id: cropRect
property real aspectRatio: delegate.modelData.width / delegate.modelData.height
property real aspectRatio: wrapper.currentScreen ? wrapper.currentScreen.width / wrapper.currentScreen.height : 1
readonly property real baseHeight: baseWidth / aspectRatio
readonly property real baseWidth: {
let fittedHeight = scaledImg.paintedHeight;
@@ -231,12 +255,14 @@ Item {
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 = Wallpapers.getCrop(delegate.modelData.name);
if (!wrapper.currentScreen)
return;
let data = Wallpapers.getCrop(wrapper.currentScreen.name);
if (data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001)) {
zoom = data.zoom > 0 ? data.zoom : 1.0;
@@ -276,13 +302,13 @@ Item {
function updateCrop(mouseX, mouseY) {
if (!cropRectLoader.item)
return;
const cropRect = cropRectLoader.item;
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;
@@ -308,7 +334,4 @@ Item {
}
}
}
}
}
}
}
+51 -2
View File
@@ -7,6 +7,8 @@ import qs.Config
import qs.Modules.SettingsNew.Common
import qs.Modules.SettingsNew.Pages
import qs.Modules.SettingsNew.Pages.Wallpaper
import qs.Modules.SettingsNew.Pages.Audio
import qs.Modules.SettingsNew.Pages.Apps
QtObject {
id: root
@@ -27,15 +29,62 @@ QtObject {
}
}
},
// Screenshot
Component {
// Screenshot
StackPage {
Component {
Screenshot {
}
}
}
},
// Connectivity
Component {
PlaceholderComp {
}
},
Component {
PlaceholderComp {
}
},
Component {
// Audio
StackPage {
Component {
AudioPage {
}
}
Component {
AppVolumes {
}
}
}
},
// Shell
Component {
PlaceholderComp {
}
},
Component {
StackPage {
Component {
AppsPage {
}
}
Component {
AllApps {
}
}
Component {
AppInfo {
}
}
}
}
]
readonly property Component placeholderComp: Component {
+100
View File
@@ -0,0 +1,100 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import qs.Components
import qs.Config
import qs.Helpers
import qs.Modules.SettingsNew.Common
PageBase {
id: root
isSubPage: true
title: qsTr("All apps")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Appearance.spacing.extraSmall / 2
width: root.cappedWidth
Repeater {
id: list
model: [...DesktopEntries.applications.values].sort((a, b) => a.name.localeCompare(b.name))
ConnectedRect {
id: appItem
required property int index
required property DesktopEntry modelData
Layout.fillWidth: true
first: index === 0
implicitHeight: appRow.implicitHeight + appRow.anchors.margins * 2
last: index === list.count - 1
StateLayer {
onClicked: {
root.sState.selectedApp = appItem.modelData;
root.sState.openSubPage(2);
}
}
RowLayout {
id: appRow
anchors.fill: parent
anchors.leftMargin: Appearance.padding.largeIncreased
anchors.margins: Appearance.padding.normal
anchors.rightMargin: Appearance.padding.largeIncreased
spacing: Appearance.spacing.small
IconImage {
asynchronous: true
implicitSize: Math.round(Appearance.font.size.large * 1.8)
source: Quickshell.iconPath(appItem.modelData.icon, "image-missing")
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
text: appItem.modelData.name
}
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
text: (appItem.modelData.comment || appItem.modelData.genericName) ?? ""
visible: text
}
}
MaterialIcon {
color: DynamicColors.palette.m3primary
fill: 1
font.pointSize: Appearance.font.size.small
text: "favorite"
visible: Strings.testRegexList(Config.launcher.favoriteApps, appItem.modelData.id)
}
MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.medium
text: "chevron_right"
}
}
}
}
}
}
+172
View File
@@ -0,0 +1,172 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import qs.Components
import qs.Config
import qs.Helpers
import qs.Modules.SettingsNew.Common
PageBase {
id: root
readonly property DesktopEntry app: sState.selectedApp
readonly property bool favoriteByRegex: app && matchedByRegex(Config.launcher.favoriteApps, app.id)
readonly property bool hiddenByRegex: app && matchedByRegex(Config.launcher.hiddenApps, app.id)
function isRegexEntry(s: string): bool {
return /^\^.*\$$/.test(s);
}
function matchedByRegex(filterList: list<string>, id: string): bool {
return filterList.some(f => isRegexEntry(f) && new RegExp(f).test(id));
}
isSubPage: true
title: qsTr("App info")
onAppChanged: {
// Auto close when app lost
if (!app)
sState.closeSubPage();
}
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Appearance.spacing.extraSmall / 2
width: root.cappedWidth
// Header
RowLayout {
Layout.bottomMargin: Appearance.spacing.large
Layout.fillWidth: true
Layout.leftMargin: Appearance.padding.small
spacing: Appearance.spacing.large
IconImage {
asynchronous: true
implicitSize: Math.round(Appearance.font.size.large * 3)
source: Quickshell.iconPath(root.app?.icon, "image-missing")
}
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.extraSmall / 2
CustomText {
Layout.fillWidth: true
font.pointSize: Appearance.font.size.medium
text: root.app?.name ?? ""
wrapMode: Text.WordWrap
}
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3outline
font.pointSize: Appearance.font.size.small
text: (root.app?.comment || root.app?.genericName) ?? ""
visible: text
wrapMode: Text.WordWrap
}
}
}
// Launcher
SectionHeader {
first: true
text: qsTr("Launcher")
}
ToggleRow {
checked: root.app && Strings.testRegexList(Config.launcher.favoriteApps, root.app.id)
enabled: !root.favoriteByRegex
first: true
subtext: root.favoriteByRegex ? qsTr("Matched by a regex in favoriteApps — edit the config file to change") : qsTr("Pin to the top of the launcher")
text: qsTr("Favorite")
onToggled: {
const apps = Config.launcher.favoriteApps;
Config.launcher.favoriteApps = checked ? [...apps, root.app.id] : apps.filter(a => a !== root.app.id);
}
}
ToggleRow {
checked: root.app && Strings.testRegexList(Config.launcher.hiddenApps, root.app.id)
enabled: !root.hiddenByRegex
last: true
subtext: root.hiddenByRegex ? qsTr("Matched by a regex in hiddenApps — edit the config file to change") : qsTr("Hide from the launcher")
text: qsTr("Hidden")
onToggled: {
const apps = Config.launcher.hiddenApps;
Config.launcher.hiddenApps = checked ? [...apps, root.app.id] : apps.filter(a => a !== root.app.id);
}
}
// Details
SectionHeader {
text: qsTr("Details")
}
WrapInfoRow {
id: appId
first: true
label: qsTr("App ID")
labelComp.Layout.preferredWidth: Math.max(labelComp.implicitWidth, command.labelComp.implicitWidth)
value: root.app?.id ?? ""
}
WrapInfoRow {
id: command
label: qsTr("Command")
labelComp.Layout.preferredWidth: Math.max(labelComp.implicitWidth, appId.labelComp.implicitWidth)
last: true
value: (root.app?.command ?? []).join(" ")
}
}
component WrapInfoRow: ConnectedRect {
id: row
property alias label: label.text
readonly property alias labelComp: label
property alias value: value.text
Layout.fillWidth: true
implicitHeight: rowLayout.implicitHeight + rowLayout.anchors.margins * 2
RowLayout {
id: rowLayout
anchors.fill: parent
anchors.leftMargin: Appearance.padding.largeIncreased
anchors.margins: Appearance.padding.normal
anchors.rightMargin: Appearance.padding.largeIncreased
spacing: Appearance.spacing.small
CustomText {
id: label
Layout.alignment: Qt.AlignTop
font.pointSize: Appearance.font.size.small
}
Item {
Layout.fillWidth: true
}
CustomText {
id: value
Layout.fillWidth: true
Layout.maximumWidth: implicitWidth + 1 // Whyyyyyyyyy
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
}
}
}
+177
View File
@@ -0,0 +1,177 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import ZShell
import qs.Helpers
import qs.Components
import qs.Config
import qs.Modules.SettingsNew.Common
PageBase {
id: root
title: qsTr("Apps")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Appearance.spacing.extraSmall / 2
width: root.cappedWidth
// Default applications
SectionHeader {
first: true
text: qsTr("Default applications")
}
DefaultRow {
first: true
icon: "terminal"
label: qsTr("Terminal")
status: Config.general.apps.terminal.join(" ")
onSelected: app => Config.general.apps.terminal = app.command
}
DefaultRow {
icon: "volume_up"
label: qsTr("Audio")
status: Config.general.apps.audio.join(" ")
onSelected: app => Config.general.apps.audio = app.command
}
DefaultRow {
icon: "play_circle"
label: qsTr("Media playback")
status: Config.general.apps.playback.join(" ")
onSelected: app => Config.general.apps.playback = app.command
}
DefaultRow {
icon: "folder"
label: qsTr("File manager")
last: true
status: Config.general.apps.explorer.join(" ")
onSelected: app => Config.general.apps.explorer = app.command
}
// Library
SectionHeader {
text: qsTr("Library")
}
NavRow {
first: true
icon: "apps"
label: qsTr("All apps")
last: true
status: qsTr("Browse installed apps, set favorites and hidden")
onClicked: root.sState.openSubPage(1)
}
}
component DefaultRow: PopupRow {
id: row
readonly property int popupHeight: root.flickable.height - y + root.flickable.contentY - Appearance.padding.large - Appearance.padding.extraLarge
signal selected(app: DesktopEntry)
keepPopupAsChild: {
if (root.sState.animatingContainer || root.opacity < 1)
return true;
let p = root.parent;
while (p && p.objectName !== "PageContainer")
p = p.parent;
return p?.opacity < 1;
}
popup.topMovement: Math.max(0 - popupHeight, Appearance.padding.large)
Loader {
active: row.popup.animDriver > 0
anchors.centerIn: parent
sourceComponent: VerticalFadeListView {
id: list
fadeAmount: 0.05
implicitHeight: ZUtils.clamp(row.popupHeight, 200, 800)
implicitWidth: 300
model: {
const apps = [...DesktopEntries.applications.values];
const favorited = new Set(apps.filter(a => Strings.testRegexList(Config.launcher.favoriteApps, a.id)));
return apps.sort((a, b) => (favorited.has(b) - favorited.has(a)) || a.name.localeCompare(b.name));
}
delegate: StateLayer {
id: appItem
required property int index
required property DesktopEntry modelData
anchors.fill: undefined
anchors.left: list.contentItem.left
anchors.right: list.contentItem.right
implicitHeight: itemLayout.implicitHeight + itemLayout.anchors.margins * 2
radius: Appearance.rounding.small
onClicked: {
row.popup.open = false;
row.selected(modelData);
}
RowLayout {
id: itemLayout
anchors.fill: parent
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.small
IconImage {
asynchronous: true
implicitSize: Math.round(Appearance.font.size.large * 1.8)
source: Quickshell.iconPath(appItem.modelData.icon, "image-missing")
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
text: appItem.modelData.name
}
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
text: (appItem.modelData.comment || appItem.modelData.genericName) ?? ""
visible: text
}
}
MaterialIcon {
color: DynamicColors.palette.m3primary
fill: 1
font.pointSize: Appearance.font.size.small
text: "favorite"
visible: Strings.testRegexList(Config.launcher.favoriteApps, appItem.modelData.id)
}
}
}
}
}
}
}
@@ -0,0 +1,69 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import qs.Modules.SettingsNew.Common
import qs.Helpers
import qs.Components
import qs.Config
import qs.Daemons
PageBase {
id: root
isSubPage: true
title: qsTr("App volumes")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Appearance.spacing.extraSmall / 2
width: root.cappedWidth
CustomText {
Layout.bottomMargin: Appearance.spacing.small
Layout.fillWidth: true
Layout.leftMargin: Appearance.padding.small
color: DynamicColors.palette.m3outline
font.pointSize: Appearance.font.size.small
text: qsTr("Adjust the volume of individual apps currently playing audio.")
wrapMode: Text.WordWrap
}
ItemList {
id: streamList
color: list.count === 0 ? DynamicColors.tPalette.m3surfaceContainer : "transparent"
first: true
last: true
list.spacing: Appearance.spacing.extraSmall / 2
placeholderIcon: "music_off"
placeholderText: qsTr("No apps playing audio")
showList: true
delegate: SliderRow {
id: stream
required property int index
required property PwNode modelData
anchors.left: streamList.list.contentItem.left
anchors.right: streamList.list.contentItem.right
enabled: !stream.modelData?.audio?.muted
first: index === 0
icon: Icons.getVolumeIcon(stream.modelData?.audio?.volume ?? 0, stream.modelData?.audio?.muted ?? false)
label: Audio.getStreamName(stream.modelData)
last: index === streamList.list.count - 1
value: stream.modelData?.audio?.volume ?? 0
valueLabel: Math.round(value * 100) + "%"
onMoved: v => Audio.setStreamVolume(stream.modelData, v)
}
model: ScriptModel {
values: [...Audio.streams]
}
}
}
}
+136
View File
@@ -0,0 +1,136 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.Daemons
import qs.Config
import qs.Components
import qs.Modules.SettingsNew.Common
import qs.Helpers
PageBase {
id: root
title: qsTr("Audio")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Appearance.spacing.extraSmall / 2
width: root.cappedWidth
// Output
SliderRow {
enabled: !Audio.muted
first: true
icon: Icons.getVolumeIcon(Audio.volume, Audio.muted)
label: qsTr("Output")
value: Audio.volume
valueLabel: Math.round(value * 100) + "%"
onMoved: v => Audio.setVolume(v)
}
ToggleRow {
checked: Audio.muted
text: qsTr("Muted")
onToggled: Audio.setStreamMuted(Audio.sink, checked)
}
AudioDeviceList {
currentId: Audio.sink?.id ?? -1
iconName: "speaker"
nodes: Audio.sinks
placeholderIcon: "speaker"
placeholderText: qsTr("No output devices")
onSelected: node => Audio.setAudioSink(node)
}
// Input
SliderRow {
Layout.topMargin: Appearance.spacing.large - parent.spacing
enabled: !Audio.sourceMuted
first: true
icon: Icons.getMicVolumeIcon(Audio.sourceVolume, Audio.sourceMuted)
label: qsTr("Input")
value: Audio.sourceVolume
valueLabel: Math.round(value * 100) + "%"
onMoved: v => Audio.setSourceVolume(v)
}
ToggleRow {
checked: Audio.sourceMuted
text: qsTr("Muted")
onToggled: Audio.setStreamMuted(Audio.source, checked)
}
AudioDeviceList {
currentId: Audio.source?.id ?? -1
iconName: "mic"
nodes: Audio.sources
placeholderIcon: "mic_off"
placeholderText: qsTr("No input devices")
onSelected: node => Audio.setAudioSource(node)
}
// Per-app volumes
ConnectedRect {
Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.large - parent.spacing
first: true
implicitHeight: appLayout.implicitHeight + appLayout.anchors.margins * 2
last: true
StateLayer {
onClicked: root.sState.openSubPage(1)
}
RowLayout {
id: appLayout
anchors.fill: parent
anchors.leftMargin: Appearance.padding.largeIncreased
anchors.margins: Appearance.padding.normal
anchors.rightMargin: Appearance.padding.largeIncreased
spacing: Appearance.spacing.small
MaterialIcon {
font.pointSize: Appearance.font.size.medium
text: "tune"
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
text: qsTr("App volumes")
}
CustomText {
Layout.fillWidth: true
animate: true
color: DynamicColors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
text: Audio.streams.length === 0 ? qsTr("No apps playing audio") : Audio.streams.length === 1 ? qsTr("1 app playing audio") : qsTr("%1 apps playing audio").arg(Audio.streams.length)
}
}
MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.medium
text: "chevron_right"
}
}
}
}
}
+2 -1
View File
@@ -68,6 +68,7 @@ PageBase {
Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing
enabled: Config.screenshot.enable_pp && Config.screenshot.mode === "manual" && Config.screenshot.rounding
from: 0
last: true
stepSize: 1
text: qsTr("Corner radius")
to: 50
@@ -80,9 +81,9 @@ PageBase {
}
ToggleRow {
Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing
checked: Config.screenshot.shadow
enabled: Config.screenshot.enable_pp && Config.screenshot.mode === "manual"
first: true
text: qsTr("Enable shadow")
onToggled: Config.screenshot.shadow = checked
+6 -93
View File
@@ -20,105 +20,18 @@ PageBase {
spacing: Appearance.spacing.large
width: root.cappedWidth
CustomClippingRect {
Item {
id: wallWrapper
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: {
const screen = root.sState.screen;
const cWidth = root.cappedWidth;
return Math.min(Math.round(cWidth * 0.4), cWidth / screen.width * screen.height);
}
implicitHeight: cropper.height
implicitWidth: {
const screen = root.sState.screen;
return implicitHeight / screen.height * screen.width;
}
radius: Appearance.rounding.large
Loader {
active: opacity > 0
anchors.centerIn: parent
opacity: Config.background.enabled ? 0 : 1
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
sourceComponent: ColumnLayout {
spacing: Appearance.spacing.extraSmall
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
text: "hide_image"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
text: qsTr("Wallpaper disabled")
}
}
}
Item {
anchors.fill: parent
opacity: Config.background.enabled ? 1 : 0
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
Loader {
id: wallIndicatorLoader
active: opacity > 0
anchors.fill: parent
opacity: 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
sourceComponent: CustomRect {
color: DynamicColors.palette.m3primaryContainer
radius: Appearance.rounding.normal
}
}
Timer {
id: wallLoadDebounceTimer
interval: 100
onTriggered: {
if (wallImg.status !== Image.Ready)
wallIndicatorLoader.opacity = 1;
}
}
FadeImage {
id: wallImg
anchors.fill: parent
fadeInAnim: Anim.SlowEffects
fadeOutAnim: Anim.DefaultEffects
preventInit: wallIndicatorLoader.opacity > 0
source: Wallpapers.current
onSourceChanged: wallLoadDebounceTimer.restart()
onStatusChanged: {
if (status === Image.Ready) {
wallLoadDebounceTimer.stop();
wallIndicatorLoader.opacity = 0;
}
}
}
WallpaperCropper {
id: cropper
}
}
@@ -162,7 +75,7 @@ PageBase {
onToggled: DynamicColors.setMode(checked ? "dark" : "light")
}
PopupRow {
OverlayRow {
checked: Config.general.color.scheduleDark
first: true
subtext: qsTr("Dark mode will turn on at %1, and turn off at %2.").arg(Config.general.color.scheduleDarkStart).arg(Config.general.color.scheduleDarkEnd)
@@ -189,7 +102,7 @@ PageBase {
}
}
PopupRow {
OverlayRow {
Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing
checked: Config.general.color.scheduleHyprsunset
last: true
@@ -15,20 +15,10 @@ PageBase {
isSubPage: true
title: qsTr("Wallpapers")
Item {
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
implicitHeight: childrenRect.height
WallpaperCropper {
id: cropper
}
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.margins: Appearance.spacing.normal
anchors.top: cropper.bottom
spacing: Appearance.spacing.small
width: root.cappedWidth
@@ -87,17 +77,12 @@ PageBase {
source: String(modelData?.path ?? "")
onClicked: {
if (modelData.parentDir !== Paths.wallsdir) {
root.sState.selectedWallpaperCategory = Wallpapers.getCategoryFor(modelData);
root.sState.openSubPage(2); // Category page
} else {
Wallpapers.setWallpaper(modelData.path);
root.sState.closeSubPage();
}
}
}
}
}
Loader {
Layout.fillWidth: true
@@ -131,5 +116,4 @@ PageBase {
}
}
}
}
}
+72 -13
View File
@@ -1,9 +1,12 @@
#include "appdb.hpp"
#include <qloggingcategory.h>
#include <qsqldatabase.h>
#include <qsqlquery.h>
#include <quuid.h>
Q_LOGGING_CATEGORY(lcAppDb, "ZShell.appdb", QtInfoMsg)
namespace ZShell {
AppEntry::AppEntry(QObject* entry, unsigned int frequency, QObject* parent)
@@ -11,7 +14,7 @@ AppEntry::AppEntry(QObject* entry, unsigned int frequency, QObject* parent)
, m_entry(entry)
, m_frequency(frequency) {
const auto mo = m_entry->metaObject();
const auto tmo = metaObject();
const auto tmo = &AppEntry::staticMetaObject;
for (const auto& prop :
{ "name", "comment", "execString", "startupClass", "genericName", "categories", "keywords" }) {
@@ -162,6 +165,39 @@ void AppDb::setEntries(const QObjectList& entries) {
m_timer->start();
}
QStringList AppDb::favoriteApps() const {
return m_favoriteApps;
}
void AppDb::setFavoriteApps(const QStringList& favApps) {
if (m_favoriteApps == favApps) {
return;
}
m_favoriteApps = favApps;
emit favoriteAppsChanged();
m_favoriteAppsRegex.clear();
m_favoriteAppsRegex.reserve(m_favoriteApps.size());
for (const QString& item : std::as_const(m_favoriteApps)) {
const QRegularExpression re(regexifyString(item));
if (re.isValid()) {
m_favoriteAppsRegex << re;
} else {
qCWarning(lcAppDb) << "setFavoriteApps: regular expression is not valid:" << re.pattern();
}
}
emit appsChanged();
}
QString AppDb::regexifyString(const QString& original) const {
if (original.startsWith('^') && original.endsWith('$'))
return original;
const QString escaped = QRegularExpression::escape(original);
return QStringLiteral("^%1$").arg(escaped);
}
QQmlListProperty<AppEntry> AppDb::apps() {
return QQmlListProperty<AppEntry>(this, &getSortedApps());
}
@@ -179,28 +215,48 @@ void AppDb::incrementFrequency(const QString& id) {
auto* app = m_apps.value(id);
if (app) {
const auto before = getSortedApps();
app->incrementFrequency();
if (before != getSortedApps()) {
getSortedApps();
if (before != m_sortedApps) {
emit appsChanged();
}
} else {
qWarning() << "AppDb::incrementFrequency: could not find app with id" << id;
qCWarning(lcAppDb) << "incrementFrequency: could not find app with id" << id;
}
}
QList<AppEntry*>& AppDb::getSortedApps() const {
m_sortedApps = m_apps.values();
std::sort(m_sortedApps.begin(), m_sortedApps.end(), [](AppEntry* a, AppEntry* b) {
if (a->frequency() != b->frequency()) {
return a->frequency() > b->frequency();
// Pre-compute favorite status to avoid repeated regex matching during sort
QSet<QString> favSet;
favSet.reserve(m_sortedApps.size());
for (const auto* app : std::as_const(m_sortedApps)) {
if (isFavorite(app))
favSet.insert(app->id());
}
std::sort(m_sortedApps.begin(), m_sortedApps.end(), [&favSet](AppEntry* a, AppEntry* b) {
const bool aIsFav = favSet.contains(a->id());
const bool bIsFav = favSet.contains(b->id());
if (aIsFav != bIsFav)
return aIsFav;
if (a->frequency() != b->frequency())
return a->frequency() > b->frequency();
return a->name().localeAwareCompare(b->name()) < 0;
});
return m_sortedApps;
}
bool AppDb::isFavorite(const AppEntry* app) const {
for (const QRegularExpression& re : m_favoriteAppsRegex) {
if (re.match(app->id()).hasMatch()) {
return true;
}
}
return false;
}
quint32 AppDb::getFrequency(const QString& id) const {
auto db = QSqlDatabase::database(m_uuid);
QSqlQuery query(db);
@@ -222,7 +278,8 @@ void AppDb::updateAppFrequencies() {
app->setFrequency(getFrequency(app->id()));
}
if (before != getSortedApps()) {
getSortedApps();
if (before != m_sortedApps) {
emit appsChanged();
}
}
@@ -249,11 +306,13 @@ void AppDb::updateApps() {
newIds.insert(entry->property("id").toString());
}
for (auto it = m_apps.keyBegin(); it != m_apps.keyEnd(); ++it) {
const auto& id = *it;
if (!newIds.contains(id)) {
for (auto it = m_apps.begin(); it != m_apps.end();) {
if (!newIds.contains(it.key())) {
dirty = true;
m_apps.take(id)->deleteLater();
it.value()->deleteLater();
it = m_apps.erase(it);
} else {
++it;
}
}
+10
View File
@@ -4,6 +4,7 @@
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qregularexpression.h>
#include <qtimer.h>
namespace ZShell {
@@ -66,6 +67,7 @@ QML_ELEMENT
Q_PROPERTY(QString uuid READ uuid CONSTANT)
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED)
Q_PROPERTY(QObjectList entries READ entries WRITE setEntries NOTIFY entriesChanged REQUIRED)
Q_PROPERTY(QStringList favoriteApps READ favoriteApps WRITE setFavoriteApps NOTIFY favoriteAppsChanged REQUIRED)
Q_PROPERTY(QQmlListProperty<ZShell::AppEntry> apps READ apps NOTIFY appsChanged)
public:
@@ -79,6 +81,9 @@ void setPath(const QString& path);
[[nodiscard]] QObjectList entries() const;
void setEntries(const QObjectList& entries);
[[nodiscard]] QStringList favoriteApps() const;
void setFavoriteApps(const QStringList& favApps);
[[nodiscard]] QQmlListProperty<AppEntry> apps();
Q_INVOKABLE void incrementFrequency(const QString& id);
@@ -86,6 +91,7 @@ Q_INVOKABLE void incrementFrequency(const QString& id);
signals:
void pathChanged();
void entriesChanged();
void favoriteAppsChanged();
void appsChanged();
private:
@@ -94,10 +100,14 @@ QTimer* m_timer;
const QString m_uuid;
QString m_path;
QObjectList m_entries;
QStringList m_favoriteApps;
QList<QRegularExpression> m_favoriteAppsRegex;
QHash<QString, AppEntry*> m_apps;
mutable QList<AppEntry*> m_sortedApps;
QString regexifyString(const QString& original) const;
QList<AppEntry*>& getSortedApps() const;
bool isFavorite(const AppEntry* app) const;
quint32 getFrequency(const QString& id) const;
void updateAppFrequencies();
void updateApps();