diff --git a/Components/CustomSpinBox.qml b/Components/CustomSpinBox.qml index 7526242..55b4189 100644 --- a/Components/CustomSpinBox.qml +++ b/Components/CustomSpinBox.qml @@ -87,9 +87,15 @@ RowLayout { id: upButton color: root.enabled ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 1) - implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 + implicitHeight: upIcon.implicitHeight + Appearance.padding.extraSmall * 2 implicitWidth: implicitHeight - radius: Appearance.rounding.full + radius: upState.pressed ? Appearance.rounding.small : Appearance.rounding.normal + + Behavior on radius { + Anim { + type: Anim.FastEffects + } + } StateLayer { id: upState @@ -120,9 +126,15 @@ RowLayout { CustomRect { color: root.enabled ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 1) - implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2 + implicitHeight: downIcon.implicitHeight + Appearance.padding.extraSmall * 2 implicitWidth: implicitHeight - radius: Appearance.rounding.full + radius: downState.pressed ? Appearance.rounding.small : Appearance.rounding.normal + + Behavior on radius { + Anim { + type: Anim.FastEffects + } + } StateLayer { id: downState diff --git a/Components/FadeImage.qml b/Components/FadeImage.qml new file mode 100644 index 0000000..b72a203 --- /dev/null +++ b/Components/FadeImage.qml @@ -0,0 +1,79 @@ +import QtQuick +import Quickshell +import qs.Config + +Image { + id: root + + property int fadeInAnim: Anim.DefaultEffects + property int fadeInLargeAnim: Anim.StandardLarge + property int fadeOutAnim: Anim.FastEffects + property bool fadingOut + property bool hadPrevious + property bool preventInit + + function maybeStartInAnim(): void { + if (!preventInit && !opacityInAnim.running && status === Image.Ready) { + opacityInAnim.type = hadPrevious ? fadeInAnim : fadeInLargeAnim; + opacityInAnim.start(); + } + } + + asynchronous: true + fillMode: Image.PreserveAspectCrop + opacity: 0 + retainWhileLoading: true + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } + + Anim on opacity { + id: opacityInAnim + + running: false + to: 1 + } + Behavior on source { + SequentialAnimation { + ScriptAction { + script: opacityInAnim.stop() + } + + PropertyAction { + property: "fadingOut" + target: root + value: true + } + + Anim { + property: "opacity" + target: root + to: 0 + type: root.fadeOutAnim + } + + PropertyAction { + property: "fadingOut" + target: root + value: false + } + + PropertyAction { + property: "hadPrevious" + target: root + value: root.source + } + + PropertyAction { + } + + ScriptAction { + script: root.maybeStartInAnim() + } + } + } + + onPreventInitChanged: maybeStartInAnim() + onStatusChanged: maybeStartInAnim() +} diff --git a/Components/IconTextButton.qml b/Components/IconTextButton.qml index dc20535..0aa93bf 100644 --- a/Components/IconTextButton.qml +++ b/Components/IconTextButton.qml @@ -63,6 +63,7 @@ ButtonBase { id: label Layout.alignment: Qt.AlignVCenter + Layout.topMargin: -1 color: root.onColor font: root.font } diff --git a/Components/VerticalFadeFlickable.qml b/Components/VerticalFadeFlickable.qml new file mode 100644 index 0000000..edd63df --- /dev/null +++ b/Components/VerticalFadeFlickable.qml @@ -0,0 +1,71 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Effects + +CustomFlickable { + 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 + + 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 + } + } +} diff --git a/Drawers/Interactions.qml b/Drawers/Interactions.qml index 6051d5a..a70df18 100644 --- a/Drawers/Interactions.qml +++ b/Drawers/Interactions.qml @@ -298,6 +298,8 @@ Item { root.visibilities.sidebar = false; root.panels.popouts.hasCurrent = false; root.visibilities.launcher = false; + } else { + Config.save(); } } diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index d76e533..d2c4ab4 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -10,7 +10,7 @@ import qs.Modules.Osd as Osd import qs.Components.Toast as Toasts import qs.Modules.Launcher as Launcher import qs.Modules.Resources as Resources -import qs.Modules.Settings as Settings +import qs.Modules.SettingsNew as Settings import qs.Modules.Drawing as Drawing import qs.Modules.Dock as Dock import qs.Modules.Clipboard as Clipboard @@ -184,11 +184,11 @@ Item { Item { id: settingsWrapper - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top clip: true - implicitHeight: settings.implicitHeight * (1 - settings.offsetScale) + implicitHeight: settings.implicitHeight implicitWidth: settings.implicitWidth + x: (root.width - settings.implicitWidth) / 2 + y: (settings.implicitHeight + (root.height - root.bar.implicitHeight - settings.implicitHeight) / 2) * (1 - settings.offsetScale) - settings.implicitHeight - 5 Settings.Wrapper { id: settings diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index bfcb512..cb58a55 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -323,15 +323,13 @@ CustomWindow { PanelBg { id: settingsBg - property real extraHeight: 0.2 + property real extraHeight: 0 deformAmount: 0.03 implicitHeight: panels.settings.height * (1 + extraHeight) implicitWidth: panels.settings.width - panel: panels.settings - radius: Appearance.rounding.large - topLeftRadius: Appearance.rounding.large + Appearance.padding.smaller - topRightRadius: Appearance.rounding.large + Appearance.padding.smaller + panel: panels.settingsWrapper + radius: Appearance.rounding.large + Appearance.padding.normal x: panels.settingsWrapper.x + panels.settings.x + root.borderThickness y: panels.settingsWrapper.y + panels.settings.y + bar.implicitHeight - panels.settings.height * extraHeight } @@ -441,7 +439,7 @@ CustomWindow { resources.transform: Matrix4x4 { matrix: resourcesBg.deformMatrix } - settings.transform: Matrix4x4 { + settingsWrapper.transform: Matrix4x4 { matrix: settingsBg.deformMatrix } sidebar.transform: Matrix4x4 { diff --git a/Effects/Mask.qml b/Effects/Mask.qml new file mode 100644 index 0000000..e8f889d --- /dev/null +++ b/Effects/Mask.qml @@ -0,0 +1,7 @@ +import QtQuick.Effects + +MultiEffect { + maskEnabled: true + maskSpreadAtMin: 1 + maskThresholdMin: 0.5 +} diff --git a/Helpers/Wallpapers.qml b/Helpers/Wallpapers.qml index e31ba1d..1e12dcc 100644 --- a/Helpers/Wallpapers.qml +++ b/Helpers/Wallpapers.qml @@ -21,6 +21,13 @@ Searcher { property bool recentlyChanged property bool showPreview: false + function getCategoryFor(w: FileSystemEntry): string { + let category = w.parentDir.slice(Paths.wallsdir.length + 1); + if (category.includes("/")) + category = category.slice(0, category.indexOf("/")); + return category; + } + function getCrop(screen: string): var { return root.crops[screen]; } diff --git a/Modules/SettingsNew/Common/ConnectedRect.qml b/Modules/SettingsNew/Common/ConnectedRect.qml new file mode 100644 index 0000000..e273ae4 --- /dev/null +++ b/Modules/SettingsNew/Common/ConnectedRect.qml @@ -0,0 +1,14 @@ +import QtQuick +import qs.Components +import qs.Config + +CustomRect { + property bool first + property bool last + + bottomLeftRadius: last ? Appearance.rounding.large : Appearance.rounding.extraSmall + bottomRightRadius: last ? Appearance.rounding.large : Appearance.rounding.extraSmall + color: DynamicColors.tPalette.m3surfaceContainer + topLeftRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall + topRightRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall +} diff --git a/Modules/SettingsNew/Common/HyprTimeInput.qml b/Modules/SettingsNew/Common/HyprTimeInput.qml new file mode 100644 index 0000000..b170d78 --- /dev/null +++ b/Modules/SettingsNew/Common/HyprTimeInput.qml @@ -0,0 +1,659 @@ +import QtQuick +import QtQuick.Layouts +import qs.Config +import qs.Components +import qs.Helpers + +Item { + id: root + + readonly property string endTime: { + var d = new Date(0, 0, 0, 0, 0, 0, 0); + d.setMinutes(object[settings[2]]); + return Qt.formatTime(d, "hh:mm AP"); + } + readonly property bool highlighted: SettingsHighlight.highlightedSetting === name + required property string name + required property var object + required property list settings + property bool shouldBeActive: true + readonly property string startTime: { + var d = new Date(0, 0, 0, 0, 0, 0, 0); + d.setMinutes(object[settings[1]]); + return Qt.formatTime(d, "hh:mm AP"); + } + + function commitChoice(choice: int, setting: string): void { + root.object[setting] = choice; + Config.save(); + Hyprsunset.checkStartup(); + } + + function convertHour(timeValue: int): int { + return Math.floor(timeValue / 60); + } + + function convertMinute(timeValue: int): int { + return timeValue % 60; + } + + function convertToMinutes(hour: int, minute: int): int { + return hour * 60 + minute; + } + + Layout.fillWidth: true + implicitHeight: shouldBeActive ? row.implicitHeight + Appearance.padding.smaller * 2 : 0 + opacity: shouldBeActive ? 1 : 0 + scale: shouldBeActive ? 1 : 0.8 + visible: opacity > 0 + + Behavior on opacity { + Anim { + } + } + Behavior on scale { + Anim { + } + } + Behavior on y { + Anim { + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: -Appearance.padding.smaller + color: DynamicColors.palette.m3primaryContainer + opacity: root.highlighted ? 0.5 : 0 + radius: Appearance.rounding.small + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.normal + } + } + } + + RowLayout { + id: row + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + ColumnLayout { + Layout.fillHeight: true + Layout.fillWidth: true + + CustomText { + id: text + + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + CustomText { + Layout.alignment: Qt.AlignLeft + Layout.preferredWidth: Math.min(contentWidth, optionLayout.x - Appearance.spacing.normal) + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.normal + text: qsTr("Hyprsunset will turn on at %1, and turn off at %2.").arg(root.startTime).arg(root.endTime) + wrapMode: Text.WordWrap + } + } + + ColumnLayout { + id: optionLayout + + Layout.fillHeight: true + Layout.fillWidth: true + + RowLayout { + CustomText { + Layout.preferredWidth: spacer.x + spacer.width + text: qsTr("Start") + } + + CustomText { + Layout.preferredWidth: endMinuteRect.width + endHourRect.width + text: qsTr("End") + } + + CustomText { + Layout.alignment: Qt.AlignLeft | Qt.AlignHCenter + text: qsTr("Enabled: ") + } + + CustomSwitch { + id: enabledSwitch + + Layout.alignment: Qt.AlignRight | Qt.AlignHCenter + checked: root.object[root.settings[0]] + + onToggled: { + root.object[root.settings[0]] = checked; + Config.save(); + } + } + } + + RowLayout { + Layout.fillHeight: true + + CustomRect { + id: startHourRect + + Layout.preferredHeight: 72 + Layout.preferredWidth: 96 + color: startHourField.focus ? DynamicColors.palette.m3onPrimaryContainer : DynamicColors.palette.m3surfaceContainerHighest + implicitHeight: 72 + implicitWidth: 96 + radius: Appearance.rounding.small + + CustomRect { + anchors.fill: parent + border.color: startHourField.focus ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3surfaceContainerHighest + border.width: startHourField.focus ? 2 : 0 + radius: parent.radius - border.width + + Behavior on border.width { + Anim { + } + } + } + + CustomTextField { + id: startHourField + + function setConfigText(setting: string): string { + var val = root.convertHour(root.object[setting]); + if (val === 0) { + return "00"; + } + return String(val); + } + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + clip: true + color: focus ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3onSurface + cursorHeight: height - Appearance.padding.normal * 2 + font.family: "Roboto" + font.letterSpacing: -0.25 + font.pixelSize: 56 + font.weight: 400 + horizontalAlignment: TextInput.AlignHCenter + text: setConfigText(root.settings[1]) + verticalAlignment: TextInput.AlignVCenter + + Keys.onPressed: event => { + if (event.key === Qt.Key_Backspace) { + event.accepted = true; + if (startHourField.text.length >= 2) { + startHourField.text = "0" + startHourField.text[0]; + } else if (startHourField.text.length === 1) { + startHourField.text = "0"; + } + return; + } else if (event.key === Qt.Key_Escape) { + event.accepted = true; + startHourField.text = setConfigText(root.settings[1]); + startHourField.focus = false; + } else if (event.key === Qt.Key_Return) { + startHourField.focus = false; + return; + } else if (event.key === Qt.Key_Tab) { + startMinuteField.focus = true; + } else if (event.key === Qt.Key_Backtab) { + endMinuteField.focus = true; + } + + if (event.text.length === 1 && event.text >= "0" && event.text <= "9") { + event.accepted = true; + var digit = event.text; + var textLen = startHourField.text.length; + + if (textLen >= 2 && startHourField.text[0] !== '0') { + return; + } + + var val = 0; + if (textLen === 0) { + val = parseInt(digit); + } else if (textLen === 1) { + val = parseInt(startHourField.text + digit); + } else { + val = parseInt(startHourField.text[1] + digit); + } + + val = Math.max(0, Math.min(23, val)); + + if (textLen >= 2 && val < 10) { + startHourField.text = "0" + val; + } else { + startHourField.text = val.toString(); + } + } + + event.accepted = true; + } + onCursorPositionChanged: cursorPosition = 2 + onEditingFinished: { + root.commitChoice(root.convertToMinutes(parseInt(startHourField.text), parseInt(startMinuteField.text)), root.settings[1]); + } + onTextEdited: { + if (startHourField.text === "") + return; + var val = parseInt(startHourField.text); + if (isNaN(val)) + return; + val = Math.max(0, Math.min(23, val)); + var newText = val.toString(); + if (newText !== startHourField.text) + startHourField.text = newText; + } + } + } + + CustomText { + id: startSeparator + + font.pointSize: Appearance.font.size.extraLarge + text: ":" + } + + CustomRect { + id: startMinuteRect + + Layout.preferredHeight: 72 + Layout.preferredWidth: 96 + color: startMinuteField.focus ? DynamicColors.palette.m3onPrimaryContainer : DynamicColors.palette.m3surfaceContainerHighest + implicitHeight: 72 + implicitWidth: 96 + radius: Appearance.rounding.small + + CustomRect { + anchors.fill: parent + border.color: startMinuteField.focus ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3surfaceContainerHighest + border.width: startMinuteField.focus ? 2 : 0 + radius: parent.radius - border.width + + Behavior on border.width { + Anim { + } + } + } + + CustomTextField { + id: startMinuteField + + function setConfigText(setting: string): string { + var val = root.convertMinute(root.object[setting]); + if (val === 0) { + return "00"; + } + return String(val); + } + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + clip: true + color: focus ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3onSurface + cursorHeight: height - Appearance.padding.normal * 2 + font.family: "Roboto" + font.letterSpacing: -0.25 + font.pixelSize: 56 + font.weight: 400 + horizontalAlignment: TextInput.AlignHCenter + text: setConfigText(root.settings[1]) + verticalAlignment: TextInput.AlignVCenter + + Keys.onPressed: event => { + if (event.key === Qt.Key_Backspace) { + event.accepted = true; + if (startMinuteField.text.length >= 2) { + startMinuteField.text = "0" + startMinuteField.text[0]; + } else if (startMinuteField.text.length === 1) { + startMinuteField.text = "0"; + } + return; + } else if (event.key === Qt.Key_Escape) { + event.accepted = true; + startMinuteField.text = setConfigText(root.settings[1]); + startMinuteField.focus = false; + } else if (event.key === Qt.Key_Return) { + startMinuteField.focus = false; + return; + } else if (event.key === Qt.Key_Tab) { + endHourField.focus = true; + } else if (event.key === Qt.Key_Backtab) { + startHourField.focus = true; + } + + if (event.text.length === 1 && event.text >= "0" && event.text <= "9") { + event.accepted = true; + var digit = event.text; + var textLen = startMinuteField.text.length; + + if (textLen >= 2 && startMinuteField.text[0] !== '0') { + return; + } + + var val = 0; + if (textLen === 0) { + val = parseInt(digit); + } else if (textLen === 1) { + val = parseInt(startMinuteField.text + digit); + } else { + val = parseInt(startMinuteField.text[1] + digit); + } + + val = Math.max(0, Math.min(59, val)); + + if (textLen >= 2 && val < 10) { + startMinuteField.text = "0" + val; + } else { + startMinuteField.text = val.toString(); + } + } + + event.accepted = true; + } + onCursorPositionChanged: cursorPosition = 2 + onEditingFinished: { + root.commitChoice(root.convertToMinutes(parseInt(startHourField.text), parseInt(startMinuteField.text)), root.settings[1]); + } + onTextEdited: { + if (startMinuteField.text === "") + return; + var val = parseInt(startMinuteField.text); + if (isNaN(val)) + return; + val = Math.max(0, Math.min(23, val)); + var newText = val.toString(); + if (newText !== startMinuteField.text) + startMinuteField.text = newText; + } + } + } + + Item { + id: spacer + + Layout.preferredWidth: Appearance.spacing.large + } + + CustomRect { + id: endHourRect + + Layout.preferredHeight: 72 + Layout.preferredWidth: 96 + color: endHourField.focus ? DynamicColors.palette.m3onPrimaryContainer : DynamicColors.palette.m3surfaceContainerHighest + implicitHeight: 72 + implicitWidth: 96 + radius: Appearance.rounding.small + + CustomRect { + anchors.fill: parent + border.color: endHourField.focus ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3surfaceContainerHighest + border.width: endHourField.focus ? 2 : 0 + radius: parent.radius - border.width + + Behavior on border.width { + Anim { + } + } + } + + CustomTextField { + id: endHourField + + function setConfigText(setting: string): string { + var val = root.convertHour(root.object[setting]); + if (val === 0) { + return "00"; + } + return String(val); + } + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + clip: true + color: focus ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3onSurface + cursorHeight: height - Appearance.padding.normal * 2 + font.family: "Roboto" + font.letterSpacing: -0.25 + font.pixelSize: 56 + font.weight: 400 + horizontalAlignment: TextInput.AlignHCenter + text: setConfigText(root.settings[2]) + verticalAlignment: TextInput.AlignVCenter + + Keys.onPressed: event => { + if (event.key === Qt.Key_Backspace) { + event.accepted = true; + if (endHourField.text.length >= 2) { + endHourField.text = "0" + endHourField.text[0]; + } else if (endHourField.text.length === 1) { + endHourField.text = "0"; + } + return; + } else if (event.key === Qt.Key_Escape) { + event.accepted = true; + endHourField.text = setConfigText(root.settings[2]); + endHourField.focus = false; + } else if (event.key === Qt.Key_Return) { + endHourField.focus = false; + return; + } else if (event.key === Qt.Key_Tab) { + endMinuteField.focus = true; + } else if (event.key === Qt.Key_Backtab) { + startMinuteField.focus = true; + } + + if (event.text.length === 1 && event.text >= "0" && event.text <= "9") { + event.accepted = true; + var digit = event.text; + var textLen = endHourField.text.length; + + if (textLen >= 2 && endHourField.text[0] !== '0') { + return; + } + + var val = 0; + if (textLen === 0) { + val = parseInt(digit); + } else if (textLen === 1) { + val = parseInt(endHourField.text + digit); + } else { + val = parseInt(endHourField.text[1] + digit); + } + + val = Math.max(0, Math.min(23, val)); + + if (textLen >= 2 && val < 10) { + endHourField.text = "0" + val; + } else { + endHourField.text = val.toString(); + } + } + + event.accepted = true; + } + onCursorPositionChanged: cursorPosition = 2 + onEditingFinished: { + root.commitChoice(root.convertToMinutes(parseInt(endHourField.text), parseInt(endMinuteField.text)), root.settings[2]); + } + onTextEdited: { + if (endHourField.text === "") + return; + var val = parseInt(endHourField.text); + if (isNaN(val)) + return; + val = Math.max(0, Math.min(23, val)); + var newText = val.toString(); + if (newText !== endHourField.text) + endHourField.text = newText; + } + } + } + + CustomText { + id: endSeparator + + font.pointSize: Appearance.font.size.extraLarge + text: ":" + } + + CustomRect { + id: endMinuteRect + + Layout.preferredHeight: 72 + Layout.preferredWidth: 96 + color: endMinuteField.focus ? DynamicColors.palette.m3onPrimaryContainer : DynamicColors.palette.m3surfaceContainerHighest + implicitHeight: 72 + implicitWidth: 96 + radius: Appearance.rounding.small + + CustomRect { + anchors.fill: parent + border.color: endMinuteField.focus ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3surfaceContainerHighest + border.width: endMinuteField.focus ? 2 : 0 + radius: parent.radius - border.width + + Behavior on border.width { + Anim { + } + } + } + + CustomTextField { + id: endMinuteField + + function setConfigText(setting: string): string { + var val = root.convertMinute(root.object[setting]); + if (val === 0) { + return "00"; + } + return String(val); + } + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + clip: true + color: focus ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3onSurface + cursorHeight: height - Appearance.padding.normal * 2 + font.family: "Roboto" + font.letterSpacing: -0.25 + font.pixelSize: 56 + font.weight: 400 + horizontalAlignment: TextInput.AlignHCenter + text: setConfigText(root.settings[2]) + verticalAlignment: TextInput.AlignVCenter + + Keys.onPressed: event => { + if (event.key === Qt.Key_Backspace) { + event.accepted = true; + if (endMinuteField.text.length >= 2) { + endMinuteField.text = "0" + endMinuteField.text[0]; + } else if (endMinuteField.text.length === 1) { + endMinuteField.text = "0"; + } + return; + } else if (event.key === Qt.Key_Escape) { + event.accepted = true; + endMinuteField.text = setConfigText(root.settings[2]); + endMinuteField.focus = false; + } else if (event.key === Qt.Key_Return) { + endMinuteField.focus = false; + return; + } else if (event.key === Qt.Key_Tab) { + startHourField.focus = true; + } else if (event.key === Qt.Key_Backtab) { + endHourField.focus = true; + } + + if (event.text.length === 1 && event.text >= "0" && event.text <= "9") { + event.accepted = true; + var digit = event.text; + var textLen = endMinuteField.text.length; + + if (textLen >= 2 && endMinuteField.text[0] !== '0') { + return; + } + + var val = 0; + if (textLen === 0) { + val = parseInt(digit); + } else if (textLen === 1) { + val = parseInt(endMinuteField.text + digit); + } else { + val = parseInt(endMinuteField.text[1] + digit); + } + + val = Math.max(0, Math.min(59, val)); + + if (textLen >= 2 && val < 10) { + endMinuteField.text = "0" + val; + } else { + endMinuteField.text = val.toString(); + } + } + + event.accepted = true; + } + onCursorPositionChanged: cursorPosition = 2 + onEditingFinished: { + root.commitChoice(root.convertToMinutes(parseInt(endHourField.text), parseInt(endMinuteField.text)), root.settings[2]); + } + onTextEdited: { + if (endMinuteField.text === "") + return; + var val = parseInt(endMinuteField.text); + if (isNaN(val)) + return; + val = Math.max(0, Math.min(23, val)); + var newText = val.toString(); + if (newText !== endMinuteField.text) + endMinuteField.text = newText; + } + } + } + } + + RowLayout { + Layout.fillWidth: true + + CustomText { + id: startHour + + Layout.preferredWidth: startSeparator.x + startSeparator.width + text: qsTr("Hour") + } + + CustomText { + id: startMinute + + Layout.preferredWidth: spacer.x + spacer.width - x + text: qsTr("Minute") + } + + CustomText { + Layout.preferredWidth: endSeparator.x + endSeparator.width - x + text: qsTr("Hour") + } + + CustomText { + text: qsTr("Minute") + } + } + } + } +} diff --git a/Modules/SettingsNew/Common/PageBase.qml b/Modules/SettingsNew/Common/PageBase.qml new file mode 100644 index 0000000..64a5b74 --- /dev/null +++ b/Modules/SettingsNew/Common/PageBase.qml @@ -0,0 +1,69 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Modules.SettingsNew +import qs.Components +import qs.Config + +ColumnLayout { + id: root + + readonly property int cappedWidth: Math.min(800, width) + default property Item contentChild + readonly property alias flickable: flickable + property bool isSubPage + required property SettingsState sState + required property string title + + spacing: Appearance.spacing.large + + MouseArea { // Prevent clicks from reaching flickable + Layout.bottomMargin: -flickable.topMargin // Extra height to block clicks on flickable top margin + + implicitHeight: header.implicitHeight - Layout.bottomMargin + implicitWidth: header.implicitWidth + z: 1 + + RowLayout { + id: header + + spacing: Appearance.spacing.large + + Loader { + active: root.isSubPage + asynchronous: true + visible: active + + sourceComponent: IconButton { + icon: "arrow_back" + inactiveColor: DynamicColors.tPalette.m3surfaceContainerHigh + inactiveOnColor: DynamicColors.palette.m3onSurfaceVariant + isRound: true + type: IconButton.Tonal + + onClicked: root.sState.closeSubPage() + } + } + + CustomText { + Layout.fillWidth: true + elide: Text.ElideRight + font.pointSize: Appearance.font.size.large + text: root.title + } + } + } + + VerticalFadeFlickable { + id: flickable + + Layout.fillHeight: true + Layout.fillWidth: true + Layout.topMargin: -topMargin + bottomMargin: Appearance.padding.extraLarge + contentHeight: root.contentChild?.implicitHeight ?? 0 + contentItem.children: [root.contentChild] + topMargin: Appearance.padding.large + } +} diff --git a/Modules/SettingsNew/Common/PopupRow.qml b/Modules/SettingsNew/Common/PopupRow.qml new file mode 100644 index 0000000..34c2f8d --- /dev/null +++ b/Modules/SettingsNew/Common/PopupRow.qml @@ -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) + } +} diff --git a/Modules/SettingsNew/Common/SelectRow.qml b/Modules/SettingsNew/Common/SelectRow.qml new file mode 100644 index 0000000..184a02b --- /dev/null +++ b/Modules/SettingsNew/Common/SelectRow.qml @@ -0,0 +1,64 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +ConnectedRect { + id: root + + property alias active: splitButton.active + property alias fallbackIcon: splitButton.fallbackIcon + property alias fallbackText: splitButton.fallbackText + property alias menuItems: splitButton.menuItems + property alias menuOnTop: splitButton.menuOnTop + property string subtext + property alias text: label.text + + signal selected(item: MenuItem) + + Layout.fillWidth: true + clip: false + implicitHeight: rowLayout.implicitHeight + rowLayout.anchors.margins * 2 + z: splitButton.expanded ? 1 : 0 + + 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 + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + CustomText { + id: label + + Layout.fillWidth: true + elide: Text.ElideRight + font.pointSize: Appearance.font.size.smaller + } + + CustomText { + Layout.fillWidth: true + color: DynamicColors.palette.m3outline + elide: Text.ElideRight + font.pointSize: Appearance.font.size.small + text: root.subtext + visible: root.subtext + } + } + + CustomSplitButton { + id: splitButton + + type: CustomSplitButton.Tonal + + menu.onItemSelected: item => root.selected(item) + stateLayer.onClicked: splitButton.expanded = !splitButton.expanded + } + } +} diff --git a/Modules/SettingsNew/Common/SettingHyprSpinner.qml b/Modules/SettingsNew/Common/SettingHyprSpinner.qml new file mode 100644 index 0000000..e18016f --- /dev/null +++ b/Modules/SettingsNew/Common/SettingHyprSpinner.qml @@ -0,0 +1,214 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config +import qs.Helpers + +Item { + id: root + + readonly property bool highlighted: SettingsHighlight.highlightedSetting === name + required property string name + required property var object + required property list settings + property bool shouldBeActive: true + + function commitChoice(choice: int, setting: string): void { + root.object[setting] = choice; + Config.save(); + } + + function formattedValue(setting: string): string { + const value = root.object[setting]; + + if (value === null || value === undefined) + return ""; + + return String(value); + } + + function hourToAmPm(hour) { + var h = Number(hour) % 24; + var d = new Date(2000, 0, 1, h, 0, 0); + return Qt.formatTime(d, "h AP"); + } + + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: shouldBeActive ? row.implicitHeight + Appearance.padding.smaller * 2 : 0 + opacity: shouldBeActive ? 1 : 0 + scale: shouldBeActive ? 1 : 0.8 + visible: opacity > 0 + + Behavior on opacity { + Anim { + } + } + Behavior on scale { + Anim { + } + } + Behavior on y { + Anim { + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: -Appearance.padding.smaller + color: DynamicColors.palette.m3primaryContainer + opacity: root.highlighted ? 0.5 : 0 + radius: Appearance.rounding.small + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.normal + } + } + } + + RowLayout { + id: row + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + ColumnLayout { + Layout.fillHeight: true + Layout.fillWidth: true + + CustomText { + id: text + + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + CustomText { + Layout.alignment: Qt.AlignLeft + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.normal + text: qsTr("Hyprsunset will turn on at %1, and turn off at %2.").arg(root.hourToAmPm(root.object[root.settings[0]])).arg(root.hourToAmPm(root.object[root.settings[1]])) + } + } + + ColumnLayout { + id: optionLayout + + Layout.fillHeight: true + Layout.preferredWidth: 100 + + RowLayout { + Layout.preferredWidth: optionLayout.width + + CustomText { + Layout.alignment: Qt.AlignLeft | Qt.AlignHCenter + Layout.fillWidth: true + text: qsTr("Enabled: ") + } + + CustomSwitch { + id: enabledSwitch + + Layout.alignment: Qt.AlignRight | Qt.AlignHCenter + checked: root.object[root.settings[2]] + + onToggled: { + root.object[root.settings[2]] = checked; + Config.save(); + } + } + } + + RowLayout { + Layout.preferredWidth: optionLayout.width + z: setting2.expanded ? -1 : 1 + + CustomText { + Layout.alignment: Qt.AlignLeft | Qt.AlignHCenter + Layout.fillWidth: true + text: qsTr("Start: ") + } + + SpinnerButton { + id: setting1 + + Layout.alignment: Qt.AlignRight | Qt.AlignHCenter + Layout.preferredHeight: Appearance.font.size.large + Appearance.padding.smaller * 2 + Layout.preferredWidth: height * 2 + currentIndex: root.object[root.settings[0]] + enabled: enabledSwitch.checked + text: root.formattedValue(root.settings[0]) + + menu.onItemSelected: item => { + root.commitChoice(item, root.settings[0]); + } + } + } + + RowLayout { + Layout.preferredWidth: optionLayout.width + z: setting1.expanded ? -1 : 1 + + CustomText { + Layout.alignment: Qt.AlignLeft | Qt.AlignHCenter + Layout.fillWidth: true + text: qsTr("End: ") + } + + SpinnerButton { + id: setting2 + + Layout.alignment: Qt.AlignRight | Qt.AlignHCenter + Layout.preferredHeight: Appearance.font.size.large + Appearance.padding.smaller * 2 + Layout.preferredWidth: height * 2 + currentIndex: root.object[root.settings[1]] + enabled: enabledSwitch.checked + text: root.formattedValue(root.settings[1]) + + menu.onItemSelected: item => { + root.commitChoice(item, root.settings[1]); + } + } + } + + RowLayout { + Layout.preferredWidth: optionLayout.width + z: -2 + + CustomText { + Layout.alignment: Qt.AlignLeft | Qt.AlignHCenter + Layout.fillWidth: true + text: qsTr("Temp: ") + } + + CustomRect { + id: rect + + Layout.preferredHeight: 33 + Layout.preferredWidth: Math.max(Math.min(textField.contentWidth + Appearance.padding.normal * 2, 200), 50) + color: DynamicColors.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.full + + CustomTextField { + id: textField + + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + implicitWidth: Math.min(contentWidth + Appearance.padding.normal * 2, 200) + text: root.formattedValue(root.settings[3]) + + onEditingFinished: { + root.object[root.settings[3]] = textField.text; + Config.save(); + } + } + } + } + } + } +} diff --git a/Modules/SettingsNew/Common/SpinRow.qml b/Modules/SettingsNew/Common/SpinRow.qml new file mode 100644 index 0000000..91b831d --- /dev/null +++ b/Modules/SettingsNew/Common/SpinRow.qml @@ -0,0 +1,63 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +ConnectedRect { + id: root + + property real from: 0 + property real stepSize: 1 + property string subtext + property alias text: label.text + property real to: 99 + property real value + + signal moved(value: real) + + 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 + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + CustomText { + id: label + + Layout.fillWidth: true + elide: Text.ElideRight + font.pointSize: Appearance.font.size.smaller + } + + CustomText { + Layout.fillWidth: true + color: DynamicColors.palette.m3outline + elide: Text.ElideRight + font.pointSize: Appearance.font.size.small + text: root.subtext + visible: root.subtext + } + } + + CustomSpinBox { + max: root.to + min: root.from + step: root.stepSize + value: root.value + + onValueModified: v => root.moved(v) + } + } +} diff --git a/Modules/SettingsNew/Common/StackPage.qml b/Modules/SettingsNew/Common/StackPage.qml new file mode 100644 index 0000000..867802a --- /dev/null +++ b/Modules/SettingsNew/Common/StackPage.qml @@ -0,0 +1,125 @@ +import QtQuick +import QtQuick.Controls +import qs.Components +import qs.Modules.SettingsNew +import qs.Config + +StackView { + id: root + + readonly property int animMovement: Appearance.padding.extraLarge * 2 + default property list pages + required property SettingsState sState + + function openSubPage(idx: int, immediate: bool): void { + const page = pages[idx]; + if (page) { + push(page, { + sState + }, immediate ? StackView.Immediate : StackView.PushTransition); + } else { + console.warn(logCat, "Attempted to open invalid sub-page index", idx); + sState.closeSubPage(); + } + } + + clip: busy + + popEnter: Transition { + SequentialAnimation { + PropertyAction { + property: "opacity" + value: 0 + } + + PauseAnimation { + duration: Appearance.anim.durations.expressiveEffects + } + + ParallelAnimation { + Anim { + property: "opacity" + to: 1 + type: Anim.SlowEffects + } + + Anim { + from: -root.animMovement + property: "x" + to: 0 + type: Anim.SlowEffects + } + } + } + } + popExit: Transition { + Anim { + property: "opacity" + to: 0 + type: Anim.DefaultEffects + } + } + pushEnter: Transition { + SequentialAnimation { + PropertyAction { + property: "opacity" + value: 0 + } + + PauseAnimation { + duration: Appearance.anim.durations.expressiveEffects + } + + ParallelAnimation { + Anim { + property: "opacity" + to: 1 + type: Anim.SlowEffects + } + + Anim { + from: root.animMovement + property: "x" + to: 0 + type: Anim.SlowEffects + } + } + } + } + pushExit: Transition { + Anim { + property: "opacity" + to: 0 + type: Anim.DefaultEffects + } + } + + Component.onCompleted: { + openSubPage(0, true); + for (const page of sState.subPageIdxStack) + openSubPage(page, true); + } + + LoggingCategory { + id: logCat + + defaultLogLevel: LoggingCategory.Info + name: "caelestia.nexus" + } + + Connections { + function onSubPageClosed(): void { + if (root.depth < root.sState.subPageIdxStack.length) { + console.log(logCat, "Attempted to close page while depth < stack depth. Ignoring."); + return; + } + root.pop(); + } + + function onSubPageOpened(idx: int): void { + root.openSubPage(idx, false); + } + + target: root.sState + } +} diff --git a/Modules/SettingsNew/Common/TimeInput.qml b/Modules/SettingsNew/Common/TimeInput.qml new file mode 100644 index 0000000..f922230 --- /dev/null +++ b/Modules/SettingsNew/Common/TimeInput.qml @@ -0,0 +1,595 @@ +import QtQuick +import QtQuick.Layouts +import ZShell.Components +import qs.Config +import qs.Components +import qs.Helpers + +CustomClippingRect { + id: root + + readonly property string endTime: { + var d = new Date(0, 0, 0, 0, 0, 0, 0); + d.setMinutes(object[settings[2]]); + return Qt.formatTime(d, "hh:mm AP"); + } + required property var object + required property list settings + property bool shouldBeActive: true + readonly property string startTime: { + var d = new Date(0, 0, 0, 0, 0, 0, 0); + d.setMinutes(object[settings[1]]); + return Qt.formatTime(d, "hh:mm AP"); + } + + signal applySettings(startTime: int, endTime: int) + signal close + + function convertHour(timeValue: int): int { + return Math.floor(timeValue / 60); + } + + function convertMinute(timeValue: int): int { + return timeValue % 60; + } + + function convertToMinutes(hour: int, minute: int): int { + return hour * 60 + minute; + } + + color: DynamicColors.palette.m3surfaceContainer + implicitHeight: column.implicitHeight + column.anchors.margins * 2 + buttonRow.implicitHeight + buttonRow.anchors.topMargin + implicitWidth: column.implicitWidth + column.anchors.margins * 2 + radius: Appearance.rounding.large + + ColumnLayout { + id: column + + anchors.left: parent.left + anchors.margins: Appearance.padding.largeIncreased + anchors.right: parent.right + anchors.top: parent.top + spacing: Appearance.spacing.normal + + CustomText { + text: qsTr("Select time") + } + + RowLayout { + CustomRect { + id: startHourRect + + Layout.preferredHeight: 72 + Layout.preferredWidth: 96 + color: startHourField.focus ? DynamicColors.palette.m3onPrimaryContainer : DynamicColors.palette.m3surfaceContainerHighest + implicitHeight: 72 + implicitWidth: 96 + radius: Appearance.rounding.small + + CustomRect { + anchors.fill: parent + border.color: startHourField.focus ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3surfaceContainerHighest + border.width: startHourField.focus ? 2 : 0 + radius: parent.radius - border.width + + Behavior on border.width { + Anim { + } + } + } + + CustomTextField { + id: startHourField + + function setConfigText(setting: string): string { + var val = root.convertHour(root.object[setting]); + if (val === 0) { + return "00"; + } + return String(val); + } + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + clip: true + color: focus ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3onSurface + cursorHeight: height - Appearance.padding.normal * 2 + font.family: "Roboto" + font.letterSpacing: -0.25 + font.pixelSize: 56 + font.weight: 400 + horizontalAlignment: TextInput.AlignHCenter + text: setConfigText(root.settings[1]) + verticalAlignment: TextInput.AlignVCenter + + Keys.onPressed: event => { + if (event.key === Qt.Key_Backspace) { + event.accepted = true; + if (startHourField.text.length >= 2) { + startHourField.text = "0" + startHourField.text[0]; + } else if (startHourField.text.length === 1) { + startHourField.text = "0"; + } + return; + } else if (event.key === Qt.Key_Escape) { + event.accepted = true; + startHourField.text = setConfigText(root.settings[1]); + startHourField.focus = false; + } else if (event.key === Qt.Key_Return) { + startHourField.focus = false; + return; + } else if (event.key === Qt.Key_Tab) { + startMinuteField.focus = true; + } else if (event.key === Qt.Key_Backtab) { + endMinuteField.focus = true; + } + + if (event.text.length === 1 && event.text >= "0" && event.text <= "9") { + event.accepted = true; + var digit = event.text; + var textLen = startHourField.text.length; + + if (textLen >= 2 && startHourField.text[0] !== '0') { + return; + } + + var val = 0; + if (textLen === 0) { + val = parseInt(digit); + } else if (textLen === 1) { + val = parseInt(startHourField.text + digit); + } else { + val = parseInt(startHourField.text[1] + digit); + } + + val = Math.max(0, Math.min(23, val)); + + if (textLen >= 2 && val < 10) { + startHourField.text = "0" + val; + } else { + startHourField.text = val.toString(); + } + } + + event.accepted = true; + } + onCursorPositionChanged: cursorPosition = 2 + onTextEdited: { + if (startHourField.text === "") + return; + var val = parseInt(startHourField.text); + if (isNaN(val)) + return; + val = Math.max(0, Math.min(23, val)); + var newText = val.toString(); + if (newText !== startHourField.text) + startHourField.text = newText; + } + } + } + + CustomText { + id: startSeparator + + font.pointSize: Appearance.font.size.extraLarge + text: ":" + } + + CustomRect { + id: startMinuteRect + + Layout.preferredHeight: 72 + Layout.preferredWidth: 96 + color: startMinuteField.focus ? DynamicColors.palette.m3onPrimaryContainer : DynamicColors.palette.m3surfaceContainerHighest + implicitHeight: 72 + implicitWidth: 96 + radius: Appearance.rounding.small + + CustomRect { + anchors.fill: parent + border.color: startMinuteField.focus ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3surfaceContainerHighest + border.width: startMinuteField.focus ? 2 : 0 + radius: parent.radius - border.width + + Behavior on border.width { + Anim { + } + } + } + + CustomTextField { + id: startMinuteField + + function setConfigText(setting: string): string { + var val = root.convertMinute(root.object[setting]); + if (val === 0) { + return "00"; + } + return String(val); + } + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + clip: true + color: focus ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3onSurface + cursorHeight: height - Appearance.padding.normal * 2 + font.family: "Roboto" + font.letterSpacing: -0.25 + font.pixelSize: 56 + font.weight: 400 + horizontalAlignment: TextInput.AlignHCenter + text: setConfigText(root.settings[1]) + verticalAlignment: TextInput.AlignVCenter + + Keys.onPressed: event => { + if (event.key === Qt.Key_Backspace) { + event.accepted = true; + if (startMinuteField.text.length >= 2) { + startMinuteField.text = "0" + startMinuteField.text[0]; + } else if (startMinuteField.text.length === 1) { + startMinuteField.text = "0"; + } + return; + } else if (event.key === Qt.Key_Escape) { + event.accepted = true; + startMinuteField.text = setConfigText(root.settings[1]); + startMinuteField.focus = false; + } else if (event.key === Qt.Key_Return) { + startMinuteField.focus = false; + return; + } else if (event.key === Qt.Key_Tab) { + endHourField.focus = true; + } else if (event.key === Qt.Key_Backtab) { + startHourField.focus = true; + } + + if (event.text.length === 1 && event.text >= "0" && event.text <= "9") { + event.accepted = true; + var digit = event.text; + var textLen = startMinuteField.text.length; + + if (textLen >= 2 && startMinuteField.text[0] !== '0') { + return; + } + + var val = 0; + if (textLen === 0) { + val = parseInt(digit); + } else if (textLen === 1) { + val = parseInt(startMinuteField.text + digit); + } else { + val = parseInt(startMinuteField.text[1] + digit); + } + + val = Math.max(0, Math.min(59, val)); + + if (textLen >= 2 && val < 10) { + startMinuteField.text = "0" + val; + } else { + startMinuteField.text = val.toString(); + } + } + + event.accepted = true; + } + onCursorPositionChanged: cursorPosition = 2 + onTextEdited: { + if (startMinuteField.text === "") + return; + var val = parseInt(startMinuteField.text); + if (isNaN(val)) + return; + val = Math.max(0, Math.min(23, val)); + var newText = val.toString(); + if (newText !== startMinuteField.text) + startMinuteField.text = newText; + } + } + } + + Item { + id: spacer + + Layout.preferredWidth: Appearance.spacing.large + } + + CustomRect { + id: endHourRect + + Layout.preferredHeight: 72 + Layout.preferredWidth: 96 + color: endHourField.focus ? DynamicColors.palette.m3onPrimaryContainer : DynamicColors.palette.m3surfaceContainerHighest + implicitHeight: 72 + implicitWidth: 96 + radius: Appearance.rounding.small + + CustomRect { + anchors.fill: parent + border.color: endHourField.focus ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3surfaceContainerHighest + border.width: endHourField.focus ? 2 : 0 + radius: parent.radius - border.width + + Behavior on border.width { + Anim { + } + } + } + + CustomTextField { + id: endHourField + + function setConfigText(setting: string): string { + var val = root.convertHour(root.object[setting]); + if (val === 0) { + return "00"; + } + return String(val); + } + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + clip: true + color: focus ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3onSurface + cursorHeight: height - Appearance.padding.normal * 2 + font.family: "Roboto" + font.letterSpacing: -0.25 + font.pixelSize: 56 + font.weight: 400 + horizontalAlignment: TextInput.AlignHCenter + text: setConfigText(root.settings[2]) + verticalAlignment: TextInput.AlignVCenter + + Keys.onPressed: event => { + if (event.key === Qt.Key_Backspace) { + event.accepted = true; + if (endHourField.text.length >= 2) { + endHourField.text = "0" + endHourField.text[0]; + } else if (endHourField.text.length === 1) { + endHourField.text = "0"; + } + return; + } else if (event.key === Qt.Key_Escape) { + event.accepted = true; + endHourField.text = setConfigText(root.settings[2]); + endHourField.focus = false; + } else if (event.key === Qt.Key_Return) { + endHourField.focus = false; + return; + } else if (event.key === Qt.Key_Tab) { + endMinuteField.focus = true; + } else if (event.key === Qt.Key_Backtab) { + startMinuteField.focus = true; + } + + if (event.text.length === 1 && event.text >= "0" && event.text <= "9") { + event.accepted = true; + var digit = event.text; + var textLen = endHourField.text.length; + + if (textLen >= 2 && endHourField.text[0] !== '0') { + return; + } + + var val = 0; + if (textLen === 0) { + val = parseInt(digit); + } else if (textLen === 1) { + val = parseInt(endHourField.text + digit); + } else { + val = parseInt(endHourField.text[1] + digit); + } + + val = Math.max(0, Math.min(23, val)); + + if (textLen >= 2 && val < 10) { + endHourField.text = "0" + val; + } else { + endHourField.text = val.toString(); + } + } + + event.accepted = true; + } + onCursorPositionChanged: cursorPosition = 2 + onTextEdited: { + if (endHourField.text === "") + return; + var val = parseInt(endHourField.text); + if (isNaN(val)) + return; + val = Math.max(0, Math.min(23, val)); + var newText = val.toString(); + if (newText !== endHourField.text) + endHourField.text = newText; + } + } + } + + CustomText { + id: endSeparator + + font.pointSize: Appearance.font.size.extraLarge + text: ":" + } + + CustomRect { + id: endMinuteRect + + Layout.preferredHeight: 72 + Layout.preferredWidth: 96 + color: endMinuteField.focus ? DynamicColors.palette.m3onPrimaryContainer : DynamicColors.palette.m3surfaceContainerHighest + implicitHeight: 72 + implicitWidth: 96 + radius: Appearance.rounding.small + + CustomRect { + anchors.fill: parent + border.color: endMinuteField.focus ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3surfaceContainerHighest + border.width: endMinuteField.focus ? 2 : 0 + radius: parent.radius - border.width + + Behavior on border.width { + Anim { + } + } + } + + CustomTextField { + id: endMinuteField + + function setConfigText(setting: string): string { + var val = root.convertMinute(root.object[setting]); + if (val === 0) { + return "00"; + } + return String(val); + } + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + clip: true + color: focus ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3onSurface + cursorHeight: height - Appearance.padding.normal * 2 + font.family: "Roboto" + font.letterSpacing: -0.25 + font.pixelSize: 56 + font.weight: 400 + horizontalAlignment: TextInput.AlignHCenter + text: setConfigText(root.settings[2]) + verticalAlignment: TextInput.AlignVCenter + + Keys.onPressed: event => { + if (event.key === Qt.Key_Backspace) { + event.accepted = true; + if (endMinuteField.text.length >= 2) { + endMinuteField.text = "0" + endMinuteField.text[0]; + } else if (endMinuteField.text.length === 1) { + endMinuteField.text = "0"; + } + return; + } else if (event.key === Qt.Key_Escape) { + event.accepted = true; + endMinuteField.text = setConfigText(root.settings[2]); + endMinuteField.focus = false; + } else if (event.key === Qt.Key_Return) { + endMinuteField.focus = false; + return; + } else if (event.key === Qt.Key_Tab) { + startHourField.focus = true; + } else if (event.key === Qt.Key_Backtab) { + endHourField.focus = true; + } + + if (event.text.length === 1 && event.text >= "0" && event.text <= "9") { + event.accepted = true; + var digit = event.text; + var textLen = endMinuteField.text.length; + + if (textLen >= 2 && endMinuteField.text[0] !== '0') { + return; + } + + var val = 0; + if (textLen === 0) { + val = parseInt(digit); + } else if (textLen === 1) { + val = parseInt(endMinuteField.text + digit); + } else { + val = parseInt(endMinuteField.text[1] + digit); + } + + val = Math.max(0, Math.min(59, val)); + + if (textLen >= 2 && val < 10) { + endMinuteField.text = "0" + val; + } else { + endMinuteField.text = val.toString(); + } + } + + event.accepted = true; + } + onCursorPositionChanged: cursorPosition = 2 + onTextEdited: { + if (endMinuteField.text === "") + return; + var val = parseInt(endMinuteField.text); + if (isNaN(val)) + return; + val = Math.max(0, Math.min(23, val)); + var newText = val.toString(); + if (newText !== endMinuteField.text) + endMinuteField.text = newText; + } + } + } + } + + RowLayout { + CustomText { + Layout.preferredWidth: spacer.x + spacer.width + text: qsTr("Start") + } + + CustomText { + Layout.preferredWidth: endMinuteRect.width + endHourRect.width + text: qsTr("End") + } + } + } + + RowLayout { + id: buttonRow + + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.margins: Appearance.padding.largeIncreased + anchors.right: parent.right + anchors.top: column.bottom + anchors.topMargin: Appearance.spacing.normal + + Item { + id: buttonSpacer + + Layout.fillWidth: true + } + + ButtonRow { + spacing: Appearance.spacing.normal + + IconTextButton { + font.pointSize: Appearance.font.size.normal + icon: "close" + inactiveColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2) + inactiveOnColor: DynamicColors.palette.m3onSurfaceVariant + isRound: true + isToggle: false + shapeMorph: true + shapeMorphExpansion: pressed ? 12 : 0 + text: "Cancel" + + onClicked: root.close() + } + + IconTextButton { + font.pointSize: Appearance.font.size.normal + icon: "check" + isRound: true + isToggle: false + shapeMorph: true + shapeMorphExpansion: pressed ? 12 : 0 + text: "Apply" + + onClicked: { + const start = root.convertToMinutes(parseInt(startHourField.text)) + parseInt(startMinuteField.text); + const end = root.convertToMinutes(parseInt(endHourField.text)) + parseInt(endMinuteField.text); + root.applySettings(start, end); + } + } + } + } +} diff --git a/Modules/SettingsNew/Common/ToggleRow.qml b/Modules/SettingsNew/Common/ToggleRow.qml new file mode 100644 index 0000000..25e4168 --- /dev/null +++ b/Modules/SettingsNew/Common/ToggleRow.qml @@ -0,0 +1,82 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +CustomSwitch { + id: root + + readonly property alias bg: bg + property alias first: bg.first + property alias last: bg.last + property string subtext + + Layout.fillWidth: true + cLayer: 2 + horizontalPadding: Appearance.padding.largeIncreased + implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2 + implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2 + indicator.anchors.right: right + indicator.anchors.rightMargin: root.horizontalPadding + indicator.anchors.verticalCenter: verticalCenter + verticalPadding: Appearance.padding.normal + + background: ConnectedRect { + id: bg + + StateLayer { + id: stateLayer + + manualPressOverride: root.pressed + } + } + contentItem: Item { + anchors.left: parent.left + anchors.leftMargin: root.horizontalPadding + anchors.right: root.indicator.left + anchors.rightMargin: Appearance.spacing.normal + implicitHeight: column.implicitHeight + implicitWidth: column.implicitWidth + + Column { + id: column + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + + CustomText { + id: label + + anchors.left: parent.left + anchors.right: parent.right + elide: Text.ElideRight + font: { + const f = Qt.font(label.font); + f.pointSize = Appearance.font.size.smaller; + return f; + } + text: root.text + } + + CustomText { + id: subtext + + anchors.left: parent.left + anchors.right: parent.right + color: DynamicColors.palette.m3outline + elide: Text.ElideRight + font: { + const f = Qt.font(subtext.font); + f.pointSize = Appearance.font.size.small; + return f; + } + text: root.subtext + visible: root.subtext + } + } + } + + onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY) +} diff --git a/Modules/SettingsNew/Common/WallItem.qml b/Modules/SettingsNew/Common/WallItem.qml new file mode 100644 index 0000000..e681b47 --- /dev/null +++ b/Modules/SettingsNew/Common/WallItem.qml @@ -0,0 +1,62 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Components +import qs.Config + +Item { + id: root + + property alias imgHeight: imgWrapper.implicitHeight + property alias radius: imgWrapper.radius + property alias source: img.source + + signal clicked + + Layout.fillWidth: true + implicitHeight: layout.implicitHeight + + ColumnLayout { + id: layout + + anchors.fill: parent + spacing: Appearance.spacing.small + + CustomClippingRect { + id: imgWrapper + + Layout.fillWidth: true + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: width + radius: Appearance.rounding.large + + Image { + id: img + + anchors.fill: parent + asynchronous: true + fillMode: Image.PreserveAspectCrop + opacity: status === Image.Ready ? 1 : 0 + retainWhileLoading: true + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } + + Behavior on opacity { + Anim { + type: Anim.SlowEffects + } + } + } + } + } + + StateLayer { + anchors.bottomMargin: layout.implicitHeight - imgWrapper.implicitHeight + + onClicked: root.clicked() + } +} diff --git a/Modules/SettingsNew/Common/WallpaperCropper.qml b/Modules/SettingsNew/Common/WallpaperCropper.qml new file mode 100644 index 0000000..b0888b1 --- /dev/null +++ b/Modules/SettingsNew/Common/WallpaperCropper.qml @@ -0,0 +1,314 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Hyprland +import ZShell.Internal +import qs.Config +import qs.Components +import qs.Helpers + +Item { + id: wrapper + + property bool changesMade: false + property bool shouldBeActive: true + + signal requestCrop + + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: shouldBeActive ? 400 : 0 + opacity: shouldBeActive ? 1 : 0 + scale: shouldBeActive ? 1 : 0.8 + visible: opacity > 0 + + Behavior on opacity { + Anim { + } + } + Behavior on scale { + Anim { + } + } + Behavior on y { + Anim { + } + } + + IconButton { + anchors.margins: Appearance.padding.normal + anchors.right: parent.right + anchors.top: parent.top + icon: "check" + opacity: wrapper.changesMade ? 1 : 0 + scale: wrapper.changesMade ? 1 : 0 + z: 2 + + Behavior on opacity { + Anim { + } + } + Behavior on scale { + Anim { + } + } + + onClicked: { + wrapper.requestCrop(); + wrapper.changesMade = false; + } + } + + RowLayout { + id: root + + anchors.fill: parent + spacing: Appearance.spacing.normal + + Repeater { + model: ScriptModel { + values: [...Quickshell.screens].sort((a, b) => { + return a.x - b.x; + }) + } + + 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 { + id: sliderLayout + + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: 30 + + CustomSlider { + id: zoomSlider + + Layout.fillWidth: true + Layout.leftMargin: Appearance.padding.normal + Layout.preferredHeight: Appearance.padding.larger * 3 + Layout.rightMargin: Appearance.padding.normal + from: 1.0 + implicitHeight: Appearance.padding.larger * 3 + insetIcon: "crop" + to: 5.0 + value: cropRectLoader.item ? cropRectLoader.item.zoom : 1.0 + + onInteraction: value => { + delegate.zoomClipRect(1 + (value * 4)); + wrapper.changesMade = true; + } + } + } + + CachingImage { + id: scaledImg + + property var displayData + property real monitorScale: 1.0 + + anchors.bottom: sliderLayout.top + anchors.bottomMargin: Appearance.spacing.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + asynchronous: true + fillMode: Image.PreserveAspectFit + retainWhileLoading: true + source: Wallpapers.current + sourceSize.height: parent.height + sourceSize.width: parent.width + + onPaintedWidthChanged: { + if (paintedWidth > 0 && cropRectLoader.item) { + cropRectLoader.item.restoreFromData(); + } + } + onSourceChanged: { + if (cropRectLoader.item) { + cropRectLoader.item.restoreFromData(); + } + } + onStatusChanged: { + if (scaledImg.status == Image.Ready && cropRectLoader.item) { + cropRectLoader.item.restoreFromData(); + } + } + + 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 + + sourceComponent: Component { + CustomRect { + id: cropRect + + property real aspectRatio: delegate.modelData.width / delegate.modelData.height + readonly property real baseHeight: baseWidth / aspectRatio + readonly property real baseWidth: { + let fittedHeight = scaledImg.paintedHeight; + let fittedWidth = fittedHeight * aspectRatio; + + if (fittedWidth > scaledImg.paintedWidth) { + fittedWidth = scaledImg.paintedWidth; + fittedHeight = fittedWidth / aspectRatio; + } + + return fittedWidth; + } + readonly property real imageX: (scaledImg.width - scaledImg.paintedWidth) / 2 + readonly property real imageY: (scaledImg.height - scaledImg.paintedHeight) / 2 + property real imgAspectRatio: scaledImg.paintedWidth / scaledImg.paintedHeight + property real zoom: 1.0 + + function centerInImage() { + x = imageX + (scaledImg.paintedWidth - width) / 2; + y = imageY + (scaledImg.paintedHeight - height) / 2; + } + + function clampToBounds() { + x = Math.max(imageX, Math.min(x, imageX + scaledImg.paintedWidth - width)); + + y = Math.max(imageY, Math.min(y, imageY + scaledImg.paintedHeight - height)); + } + + function restoreFromData() { + let data = Wallpapers.getCrop(delegate.modelData.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; + x = imageX + (data.x * scaledImg.paintedWidth); + y = imageY + (data.y * scaledImg.paintedHeight); + + clampToBounds(); + } else { + zoom = 1.0; + centerInImage(); + } + } + + border.color: DynamicColors.palette.m3primary + border.width: 2 + height: baseHeight / zoom + opacity: 1 + width: baseWidth / zoom + + Behavior on opacity { + Anim { + } + } + + Component.onCompleted: { + restoreFromData(); + } + onHeightChanged: clampToBounds() + onWidthChanged: clampToBounds() + } + } + } + + MouseArea { + id: mouse + + 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; + cropRect.y = ny; + } + + anchors.fill: parent + hoverEnabled: true + preventStealing: true + + onPositionChanged: mouse => { + if (pressed) { + updateCrop(mouse.x, mouse.y); + wrapper.changesMade = true; + } + } + onPressed: mouse => { + updateCrop(mouse.x, mouse.y); + wrapper.changesMade = true; + } + onReleased: { + wrapper.changesMade = true; + } + } + } + } + } + } +} diff --git a/Modules/SettingsNew/Content.qml b/Modules/SettingsNew/Content.qml new file mode 100644 index 0000000..026c2f6 --- /dev/null +++ b/Modules/SettingsNew/Content.qml @@ -0,0 +1,72 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import ZShell.Blobs +import qs.Components +import qs.Config + +CustomClippingRect { + id: root + + property color blobColor: DynamicColors.tPalette.m3surfaceContainerLow + readonly property real ratio: 16.0 / 9.0 + readonly property SettingsState sState: SettingsState { + id: sState + + onClose: root.close() + } + + signal close + + implicitHeight: sState.screen.height * 0.7 + implicitWidth: implicitHeight * ratio + radius: Appearance.rounding.large + Appearance.padding.normal + + Behavior on blobColor { + CAnim { + } + } + + BlobGroup { + id: blobGroup + + color: root.blobColor + smoothing: Appearance.rounding.normal + } + + BlobInvertedRect { + anchors.fill: parent + borderBottom: Appearance.padding.normal + borderLeft: navPane.width + navPane.anchors.margins * 2 + borderRight: Appearance.padding.normal + borderTop: Appearance.padding.normal + group: blobGroup + opacity: root.blobColor.a + radius: Appearance.rounding.large + } + + NavPane { + id: navPane + + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.margins: Appearance.padding.large + anchors.top: parent.top + sState: root.sState + width: Math.min(600, Math.round(root.width / 3)) + } + + Pages { + anchors.bottom: parent.bottom + anchors.left: navPane.right + anchors.leftMargin: navPane.anchors.margins + anchors.margins + anchors.margins: Appearance.padding.extraLarge + anchors.right: parent.right + anchors.top: parent.top + sState: root.sState + } + + PopupOverlay { + anchors.fill: parent + } +} diff --git a/Modules/SettingsNew/NavPane.qml b/Modules/SettingsNew/NavPane.qml new file mode 100644 index 0000000..a2ef504 --- /dev/null +++ b/Modules/SettingsNew/NavPane.qml @@ -0,0 +1,25 @@ +import QtQuick +import QtQuick.Layouts +import qs.Modules.SettingsNew.NavPane +import qs.Config + +ColumnLayout { + id: root + + required property SettingsState sState + + spacing: Appearance.spacing.large + + SearchBar { + Layout.fillWidth: true + sState: root.sState + } + + NavLocations { + Layout.bottomMargin: -bottomMargin + Layout.fillHeight: true + Layout.fillWidth: true + Layout.topMargin: -topMargin + sState: root.sState + } +} diff --git a/Modules/SettingsNew/NavPane/NavLocations.qml b/Modules/SettingsNew/NavPane/NavLocations.qml new file mode 100644 index 0000000..0b8b41a --- /dev/null +++ b/Modules/SettingsNew/NavPane/NavLocations.qml @@ -0,0 +1,125 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config +import qs.Modules.SettingsNew + +Flickable { + id: root + + required property SettingsState sState + + bottomMargin: Appearance.padding.large + contentHeight: content.implicitHeight + topMargin: Appearance.padding.large + + ColumnLayout { + id: content + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.extraSmall + + Repeater { + id: list + + model: PageRegistry.pages + + CustomRect { + id: item + + required property int index + readonly property bool isCategoryEnd: index === list.model.length - 1 || PageRegistry.pages[index + 1].category !== modelData.category + readonly property bool isCategoryStart: index === 0 || PageRegistry.pages[index - 1].category !== modelData.category + readonly property bool isCurrentPage: index === root.sState.currentPageIdx + required property var modelData + + Layout.fillWidth: true + Layout.topMargin: index !== 0 && isCategoryStart ? Appearance.spacing.small : 0 + bottomLeftRadius: stateLayer.pressed ? Appearance.rounding.medium : isCurrentPage ? Appearance.rounding.large : isCategoryEnd ? Appearance.rounding.normal : Appearance.rounding.extraSmall + bottomRightRadius: stateLayer.pressed ? Appearance.rounding.medium : isCurrentPage ? Appearance.rounding.large : isCategoryEnd ? Appearance.rounding.normal : Appearance.rounding.extraSmall + color: isCurrentPage ? DynamicColors.palette.m3secondaryContainer : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) + implicitHeight: { + const h = layout.implicitHeight + layout.anchors.margins * 2; + return h % 2 === 0 ? h : h + 1; + } + topLeftRadius: stateLayer.pressed ? Appearance.rounding.medium : isCurrentPage ? Appearance.rounding.large : isCategoryStart ? Appearance.rounding.normal : Appearance.rounding.extraSmall + topRightRadius: stateLayer.pressed ? Appearance.rounding.medium : isCurrentPage ? Appearance.rounding.large : isCategoryStart ? Appearance.rounding.normal : Appearance.rounding.extraSmall + + RadiusBehavior on bottomLeftRadius { + } + RadiusBehavior on bottomRightRadius { + } + RadiusBehavior on topLeftRadius { + } + RadiusBehavior on topRightRadius { + } + + StateLayer { + id: stateLayer + + anchors.fill: parent + bottomLeftRadius: parent.bottomLeftRadius + bottomRightRadius: parent.bottomRightRadius + topLeftRadius: parent.topLeftRadius + topRightRadius: parent.topRightRadius + + onClicked: root.sState.currentPageIdx = item.index + } + + RowLayout { + id: layout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.small + + CustomRect { + Layout.bottomMargin: -1 + Layout.fillHeight: true + Layout.topMargin: -1 + color: item.isCurrentPage ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondaryContainer + implicitWidth: height + radius: Appearance.rounding.full + + MaterialIcon { + anchors.centerIn: parent + anchors.verticalCenterOffset: 1 + color: item.isCurrentPage ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSecondaryContainer + fill: item.modelData.noFill ? 0 : 1 + font.pointSize: Appearance.font.size.large + grade: 25 + text: item.modelData.icon + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + CustomText { + Layout.fillWidth: true + elide: Text.ElideRight + text: item.modelData.name + } + + CustomText { + Layout.fillWidth: true + color: DynamicColors.palette.m3onSurfaceVariant + elide: Text.ElideRight + text: item.modelData.description + } + } + } + } + } + } + + component RadiusBehavior: Behavior { + Anim { + type: Anim.DefaultEffects + } + } +} diff --git a/Modules/SettingsNew/NavPane/SearchBar.qml b/Modules/SettingsNew/NavPane/SearchBar.qml new file mode 100644 index 0000000..0dd0844 --- /dev/null +++ b/Modules/SettingsNew/NavPane/SearchBar.qml @@ -0,0 +1,75 @@ +import QtQuick +import QtQuick.Layouts +import qs.Modules.SettingsNew +import qs.Components +import qs.Config + +CustomRect { + id: root + + required property SettingsState sState + + border.color: DynamicColors.palette.m3outlineVariant + color: DynamicColors.tPalette.m3surfaceContainerLowest + implicitHeight: searchLayout.implicitHeight + Appearance.padding.normal * 2 + radius: Appearance.rounding.full + + Behavior on border.color { + CAnim { + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.IBeamCursor + + onClicked: searchField.focus = true + } + + RowLayout { + id: searchLayout + + anchors.left: parent.left + anchors.margins: Appearance.padding.large + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Appearance.spacing.small + + MaterialIcon { + color: DynamicColors.palette.m3onSurfaceVariant + text: "search" + } + + CustomTextField { + id: searchField + + Layout.fillHeight: true + Layout.fillWidth: true + color: DynamicColors.palette.m3onSurfaceVariant + placeholderText: qsTr("Search settings") + placeholderTextColor: DynamicColors.palette.m3onSurfaceVariant + + Binding { + property: "searchOpen" + target: root.sState + value: searchField.text.length > 0 + } + } + + IconButton { + icon: "close" + isRound: true + opacity: searchField.text.length > 0 ? 1 : 0 + padding: Appearance.padding.extraSmall + type: IconButton.Text + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + + onClicked: searchField.clear() + } + } +} diff --git a/Modules/SettingsNew/PageCompRegistry.qml b/Modules/SettingsNew/PageCompRegistry.qml new file mode 100644 index 0000000..0ab4db1 --- /dev/null +++ b/Modules/SettingsNew/PageCompRegistry.qml @@ -0,0 +1,73 @@ +pragma Singleton + +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config +import qs.Modules.SettingsNew.Common +import qs.Modules.SettingsNew.Pages +import qs.Modules.SettingsNew.Pages.Wallpaper + +QtObject { + id: root + + readonly property list pageComps: [ + // Appearance + Component { + // Wallpaper & style + StackPage { + Component { + Wallpaper { + } + } + + Component { + WallpaperSelect { + } + } + } + }, + + // Screenshot + Component { + StackPage { + Component { + Screenshot { + } + } + } + } + ] + readonly property Component placeholderComp: Component { + PlaceholderComp { + } + } + + component PlaceholderComp: Item { + property SettingsState sState // To avoid the warning from non-existent property + + ColumnLayout { + anchors.centerIn: parent + spacing: Appearance.padding.extraSmall + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + color: DynamicColors.palette.m3outlineVariant + font.pointSize: Appearance.font.size.extraLarge + text: "handyman" + } + + CustomText { + Layout.alignment: Qt.AlignHCenter + color: DynamicColors.palette.m3outlineVariant + text: qsTr("Page under construction") + } + + CustomText { + Layout.alignment: Qt.AlignHCenter + color: DynamicColors.palette.m3outlineVariant + text: qsTr("This page will be available in a future update.") + } + } + } +} diff --git a/Modules/SettingsNew/PageRegistry.qml b/Modules/SettingsNew/PageRegistry.qml new file mode 100644 index 0000000..e116e6a --- /dev/null +++ b/Modules/SettingsNew/PageRegistry.qml @@ -0,0 +1,64 @@ +pragma Singleton + +import QtQuick +import Quickshell + +Singleton { + id: root + + readonly property list pages: [ + // Appearance + { + name: qsTr("Appearance"), + icon: "colors", + description: qsTr("Colors, animations, font, wallpaper"), + category: "appearance" + }, + { + name: qsTr("Screenshot"), + icon: "screenshot_region", + description: qsTr("Set screenshot effects"), + category: "appearance" + }, + + // Connectivity + { + name: qsTr("Network"), + icon: "wifi", + description: qsTr("Wi-Fi, ethernet"), + category: "connectivity" + }, + { + name: qsTr("Connected devices"), + icon: "devices_other", + description: qsTr("Bluetooth, paiting"), + category: "connectivity" + }, + { + name: qsTr("Audio"), + icon: "volume_up", + description: qsTr("App volumes, sound devices"), + category: "connectivity" + }, + + // Shell + { + name: qsTr("Panels"), + icon: "dock_to_bottom", + description: qsTr("Bar, dashboard, launcher, sidebar"), + category: "shell" + }, + { + name: qsTr("Apps"), + icon: "apps", + description: qsTr("Default apps, favorites, hidden apps"), + category: "shell" + }, + { + name: qsTr("Services"), + icon: "build", + description: qsTr("Poll intervals, audio and brightness increments"), + category: "shell" + }, + ] +} diff --git a/Modules/SettingsNew/Pages.qml b/Modules/SettingsNew/Pages.qml new file mode 100644 index 0000000..e85acd0 --- /dev/null +++ b/Modules/SettingsNew/Pages.qml @@ -0,0 +1,100 @@ +import QtQuick +import qs.Components +import qs.Config + +Item { + id: root + + property int animOff + property Item currentItem + property int lastPageIdx + required property SettingsState sState + + function loadPage(idx: int): void { + if (currentItem) + currentItem.destroy(); + + const comp = PageCompRegistry.pageComps[idx] ?? PageCompRegistry.placeholderComp; + const incubator = comp.incubateObject(container, { + sState + }); + + const attach = () => { + incubator.object.anchors.fill = container; + currentItem = incubator.object; + }; + + if (incubator.status === Component.Ready) + attach(); + else + incubator.onStatusChanged = status => { + if (status === Component.Ready) + attach(); + }; + } + + Item { + id: container + + anchors.fill: parent + layer.enabled: opacity < 1 + objectName: "PageContainer" + + Component.onCompleted: root.loadPage(root.sState.currentPageIdx) + } + + Connections { + function onCurrentPageIdxChanged(): void { + switchAnim.complete(); + root.animOff = Appearance.padding.normal * (root.sState.currentPageIdx > root.lastPageIdx ? 1 : -1); + switchAnim.start(); + root.lastPageIdx = root.sState.currentPageIdx; + } + + target: root.sState + } + + SequentialAnimation { + id: switchAnim + + Anim { + property: "opacity" + target: container + to: 0 + type: Anim.DefaultEffects + } + + ScriptAction { + script: root.loadPage(root.sState.currentPageIdx) + } + + PropertyAction { + property: "topMargin" + target: container.anchors + value: root.animOff + } + + PropertyAction { + property: "bottomMargin" + target: container.anchors + value: -root.animOff + } + + ParallelAnimation { + Anim { + from: 0 + property: "opacity" + target: container + to: 1 + type: Anim.SlowEffects + } + + Anim { + properties: "topMargin,bottomMargin" + target: container.anchors + to: 0 + type: Anim.SlowEffects + } + } + } +} diff --git a/Modules/SettingsNew/Pages/Screenshot.qml b/Modules/SettingsNew/Pages/Screenshot.qml new file mode 100644 index 0000000..a4c3cb3 --- /dev/null +++ b/Modules/SettingsNew/Pages/Screenshot.qml @@ -0,0 +1,137 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import ZShell.Components +import qs.Helpers +import qs.Components +import qs.Config +import qs.Modules.SettingsNew +import qs.Modules.SettingsNew.Common + +PageBase { + id: root + + title: qsTr("Screenshot effects") + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + spacing: Appearance.spacing.large + width: root.cappedWidth + + ToggleRow { + checked: Config.screenshot.enable_pp + first: true + text: qsTr("Enable effects") + + onToggled: Config.screenshot.enable_pp = checked + } + + SelectRow { + Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing + active: Config.screenshot.mode === "manual" ? menuItems[0] : menuItems[1] + enabled: Config.screenshot.enable_pp + last: true + subtext: qsTr("Automatic or manual effect values") + text: qsTr("Effects mode") + + menuItems: [ + MenuItem { + icon: "build" + text: qsTr("Manual") + value: "manual" + }, + MenuItem { + icon: "rotate_auto" + text: qsTr("Auto") + value: "auto" + } + ] + + onSelected: item => { + Config.screenshot.mode = item.value; + Config.save(); + } + } + + ToggleRow { + checked: Config.screenshot.rounding + enabled: Config.screenshot.enable_pp && Config.screenshot.mode === "manual" + first: true + text: qsTr("Enable rounded corners") + + onToggled: Config.screenshot.rounding = checked + } + + SpinRow { + Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing + enabled: Config.screenshot.enable_pp && Config.screenshot.mode === "manual" && Config.screenshot.rounding + from: 0 + stepSize: 1 + text: qsTr("Corner radius") + to: 50 + value: Config.screenshot.radius + + onMoved: value => { + const newVal = Math.floor(value); + Config.screenshot.radius = newVal; + } + } + + ToggleRow { + Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing + checked: Config.screenshot.shadow + enabled: Config.screenshot.enable_pp && Config.screenshot.mode === "manual" + text: qsTr("Enable shadow") + + onToggled: Config.screenshot.shadow = checked + } + + SpinRow { + Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing + enabled: Config.screenshot.enable_pp && Config.screenshot.mode === "manual" && Config.screenshot.shadow + from: 0 + stepSize: 1 + text: qsTr("Shadow blur amount") + to: 100 + value: Config.screenshot.shadow_blur + + onMoved: value => { + const newVal = Math.floor(value); + Config.screenshot.shadow_blur = newVal; + } + } + + SpinRow { + Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing + enabled: Config.screenshot.enable_pp && Config.screenshot.mode === "manual" && Config.screenshot.shadow + from: -100 + stepSize: 10 + text: qsTr("Shadow horizontal offset") + to: 100 + value: Config.screenshot.shadow_offset_x + + onMoved: value => { + const newVal = Math.floor(value); + Config.screenshot.shadow_offset_x = newVal; + } + } + + SpinRow { + Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing + enabled: Config.screenshot.enable_pp && Config.screenshot.mode === "manual" && Config.screenshot.shadow + from: -100 + last: true + stepSize: 10 + text: qsTr("Shadow vertical offset") + to: 100 + value: Config.screenshot.shadow_offset_y + + onMoved: value => { + const newVal = Math.floor(value); + Config.screenshot.shadow_offset_y = newVal; + } + } + } +} diff --git a/Modules/SettingsNew/Pages/Wallpaper.qml b/Modules/SettingsNew/Pages/Wallpaper.qml new file mode 100644 index 0000000..4edf637 --- /dev/null +++ b/Modules/SettingsNew/Pages/Wallpaper.qml @@ -0,0 +1,220 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import ZShell.Components +import qs.Helpers +import qs.Components +import qs.Config +import qs.Modules.SettingsNew +import qs.Modules.SettingsNew.Common + +PageBase { + id: root + + title: qsTr("Wallpaper & style") + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + spacing: Appearance.spacing.large + width: root.cappedWidth + + CustomClippingRect { + 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); + } + 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; + } + } + } + } + } + + IconTextButton { + Layout.alignment: Qt.AlignHCenter + enabled: Config.background.enabled + horizontalPadding: Appearance.padding.extraLarge + icon: "wallpaper" + isRound: true + shapeMorph: true + text: qsTr("Wallpapers") + type: IconTextButton.Tonal + verticalPadding: Appearance.padding.normal + + onClicked: root.sState.openSubPage(1) // Wallpaper page + } + + ToggleRow { + checked: Config.background.enabled + first: true + text: qsTr("Display wallpaper") + + onToggled: Config.background.enabled = checked + } + + ToggleRow { + Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing + checked: DynamicColors.transparency.enabled + subtext: qsTr("Base %1, layers %2").arg(DynamicColors.transparency.base).arg(DynamicColors.transparency.layers) + text: qsTr("Transparency") + + onToggled: Config.appearance.transparency.enabled = checked + } + + ToggleRow { + Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing + checked: !DynamicColors.light + last: true + text: qsTr("Dark theme") + + onToggled: DynamicColors.setMode(checked ? "dark" : "light") + } + + PopupRow { + 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) + text: qsTr("Schedule dark mode") + + popup: Component { + TimeInput { + object: Config.general.color + settings: ["scheduleDark", "scheduleDarkStart", "scheduleDarkEnd"] + + onApplySettings: (start, end) => { + Config.general.color.scheduleDarkStart = start; + Config.general.color.scheduleDarkEnd = end; + Config.save(); + ModeScheduler.checkStartup(); + PopupManager.requestClose(); + } + onClose: PopupManager.requestClose() + } + } + + onClicked: value => { + Config.general.color.scheduleDark = value; + } + } + + PopupRow { + Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing + checked: Config.general.color.scheduleHyprsunset + last: true + subtext: qsTr("Hyprsunset will turn on at %1, and turn off at %2.").arg(Config.general.color.scheduleHyprsunsetStart).arg(Config.general.color.scheduleHyprsunsetEnd) + text: qsTr("Schedule hyprsunset") + + popup: Component { + TimeInput { + object: Config.general.color + settings: ["scheduleHyprsunset", "scheduleHyprsunsetStart", "scheduleHyprsunsetEnd"] + + onApplySettings: (start, end) => { + Config.general.color.scheduleHyprsunsetStart = start; + Config.general.color.scheduleHyprsunsetEnd = end; + Config.save(); + Hyprsunset.checkStartup(); + PopupManager.requestClose(); + } + onClose: PopupManager.requestClose() + } + } + + onClicked: value => { + Config.general.color.scheduleHyprsunset = value; + } + } + } +} diff --git a/Modules/SettingsNew/Pages/Wallpaper/WallpaperSelect.qml b/Modules/SettingsNew/Pages/Wallpaper/WallpaperSelect.qml new file mode 100644 index 0000000..2578303 --- /dev/null +++ b/Modules/SettingsNew/Pages/Wallpaper/WallpaperSelect.qml @@ -0,0 +1,135 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import ZShell.Models +import qs.Paths +import qs.Helpers +import qs.Components +import qs.Config +import qs.Modules.SettingsNew.Common + +PageBase { + id: root + + isSubPage: true + title: qsTr("Wallpapers") + + Item { + 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 + + CustomText { + Layout.topMargin: Appearance.spacing.large + font.pointSize: Appearance.font.size.large + text: qsTr("Wallpapers") + } + + GridLayout { + Layout.fillWidth: true + columnSpacing: Appearance.spacing.extraSmall + columns: 4 + rowSpacing: Appearance.spacing.extraSmall + visible: localWalls.count > 0 + + Repeater { + id: localWalls + + model: { + const walls = Wallpapers.list; + var baseDir = Paths.wallsdir; + const categories = {}; + const list = []; + for (const w of walls) { + var parentDir = w.parentDir; + if (!parentDir.endsWith("/")) + parentDir = parentDir + "/"; + + if (!baseDir.endsWith("/")) + baseDir = baseDir + "/"; + + if (parentDir !== baseDir) { + const category = Wallpapers.getCategoryFor(w); + if (category && (!(category in categories) || categories[category].name.localeCompare(w.name) > 0)) + categories[category] = w; + } else { + list.push(w); + } + } + list.push(...Object.values(categories)); + list.sort((a, b) => ((a.parentDir === baseDir) - (b.parentDir === baseDir)) || a.name.localeCompare(b.name)); + while (list.length < 4) + list.push(null); + + return list; + } + + WallItem { + required property FileSystemEntry modelData + + enabled: modelData + + // Empty placeholders for sizing + opacity: modelData ? 1 : 0 + 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 + active: localWalls.count === 0 + asynchronous: true + visible: active + + sourceComponent: CustomRect { + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: noWallsLayout.implicitHeight + Appearance.padding.extraLarge * 3 + radius: Appearance.rounding.large + + ColumnLayout { + id: noWallsLayout + + anchors.centerIn: parent + spacing: Appearance.spacing.extraSmall + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + color: DynamicColors.palette.m3outline + text: "hide_image" + } + + CustomText { + Layout.alignment: Qt.AlignHCenter + color: DynamicColors.palette.m3outline + text: qsTr("No local wallpapers found") + } + } + } + } + } + } +} diff --git a/Modules/SettingsNew/PopupManager.qml b/Modules/SettingsNew/PopupManager.qml new file mode 100644 index 0000000..4b37c02 --- /dev/null +++ b/Modules/SettingsNew/PopupManager.qml @@ -0,0 +1,20 @@ +pragma Singleton + +import Quickshell +import QtQuick + +Singleton { + id: root + + property bool closed: true + property Component currentPopup: null + + function requestClose(): void { + closed = true; + } + + function requestOpen(component: Component): void { + currentPopup = component; + closed = false; + } +} diff --git a/Modules/SettingsNew/PopupOverlay.qml b/Modules/SettingsNew/PopupOverlay.qml new file mode 100644 index 0000000..e3f0aca --- /dev/null +++ b/Modules/SettingsNew/PopupOverlay.qml @@ -0,0 +1,46 @@ +import QtQuick +import qs.Config +import qs.Components + +CustomRect { + id: root + + readonly property bool closing: PopupManager.closed + readonly property var currentPopup: PopupManager.currentPopup + property bool shouldBeVisible: loader.status === Loader.Ready + + color: Qt.alpha(DynamicColors.palette.m3shadow, 0.3) + opacity: shouldBeVisible && !closing ? 1 : 0 + visible: opacity > 0 + + Behavior on opacity { + Anim { + } + } + + onVisibleChanged: if (!visible) + PopupManager.currentPopup = null + + CustomMouseArea { + anchors.fill: parent + hoverEnabled: true + preventStealing: true + propagateComposedEvents: false + + onClicked: { + const insideItemWidth = mouseX < loader.x + loader.item.width && mouseX > loader.x; + const insideItemHeight = mouseY < loader.y + loader.item.height && mouseY > loader.y; + if (insideItemHeight && insideItemWidth) + return; + + PopupManager.requestClose(); + } + } + + Loader { + id: loader + + anchors.centerIn: parent + sourceComponent: root.currentPopup + } +} diff --git a/Modules/SettingsNew/SettingsState.qml b/Modules/SettingsNew/SettingsState.qml new file mode 100644 index 0000000..06a753d --- /dev/null +++ b/Modules/SettingsNew/SettingsState.qml @@ -0,0 +1,33 @@ +import QtQuick +import Quickshell +import Quickshell.Bluetooth + +QtObject { + id: root + + property bool animatingContainer + property int currentPageIdx + property bool isWindow + property ShellScreen screen + property bool searchOpen + property DesktopEntry selectedApp + property BluetoothDevice selectedBtDevice + property string selectedWallpaperCategory + property list subPageIdxStack + + signal close + signal subPageClosed + signal subPageOpened(idx: int) + + function closeSubPage(): void { + subPageClosed(); + subPageIdxStack.pop(); + } + + function openSubPage(idx: int): void { + subPageIdxStack.push(idx); + subPageOpened(idx); + } + + onCurrentPageIdxChanged: subPageIdxStack.length = 0 +} diff --git a/Modules/SettingsNew/Wrapper.qml b/Modules/SettingsNew/Wrapper.qml new file mode 100644 index 0000000..1895ae7 --- /dev/null +++ b/Modules/SettingsNew/Wrapper.qml @@ -0,0 +1,47 @@ +import Quickshell +import QtQuick +import qs.Components +import qs.Config +import qs.Helpers + +Item { + id: root + + property real offsetScale: shouldBeActive ? 0 : 1 + required property var panels + required property ShellScreen screen + readonly property bool shouldBeActive: visibilities.settings + required property PersistentProperties visibilities + + implicitHeight: content.implicitHeight + implicitWidth: content.implicitWidth + opacity: 1 - offsetScale + visible: offsetScale < 1 + + Behavior on offsetScale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + CustomClippingRect { + anchors.fill: parent + + Loader { + id: content + + active: root.shouldBeActive || root.visible + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + + sourceComponent: Content { + sState.animatingContainer: content.opacity < 1 + sState.currentPageIdx: ["wallpaper"][0] + sState.screen: root.screen + + onClose: console.log("shouldclose") + } + } + } +} diff --git a/Paths/Paths.qml b/Paths/Paths.qml index 27131f1..4847062 100644 --- a/Paths/Paths.qml +++ b/Paths/Paths.qml @@ -19,7 +19,7 @@ Singleton { readonly property string recsdir: Quickshell.env("ZSHELL_RECORDINGS_DIR") || `${videos}/Recordings` readonly property string state: `${Quickshell.env("XDG_STATE_HOME") || `${home}/.local/state`}/zshell` readonly property string videos: Quickshell.env("XDG_VIDEOS_DIR") || `${home}/Videos` - readonly property string wallsdir: Quickshell.env("ZSHELL_WALLPAPERS_DIR") || absolutePath(Config.wallpaperPath) + readonly property string wallsdir: Quickshell.env("ZSHELL_WALLPAPERS_DIR") || absolutePath(Config.general.wallpaperPath) function absolutePath(path: string): string { return toLocalFile(path.replace("~", home));