added audio and apps page in settings
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 || ""))
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ StackView {
|
||||
id: logCat
|
||||
|
||||
defaultLogLevel: LoggingCategory.Info
|
||||
name: "caelestia.nexus"
|
||||
name: "ZShell.settings"
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user