diff --git a/modules/nexus/PageCompRegistry.qml b/modules/nexus/PageCompRegistry.qml index 53a53edd..b6e9f0bb 100644 --- a/modules/nexus/PageCompRegistry.qml +++ b/modules/nexus/PageCompRegistry.qml @@ -116,6 +116,14 @@ QtObject { BarClock {} } } + }, + Component { + // Services + StackPage { + Component { + ServicesPage {} + } + } } ] diff --git a/modules/nexus/common/SelectRow.qml b/modules/nexus/common/SelectRow.qml new file mode 100644 index 00000000..2c8b00da --- /dev/null +++ b/modules/nexus/common/SelectRow.qml @@ -0,0 +1,65 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services +import qs.modules.nexus.common + +ConnectedRect { + id: root + + property alias label: label.text + property string subtext + property alias menuItems: splitButton.menuItems + property alias active: splitButton.active + property alias fallbackText: splitButton.fallbackText + property alias fallbackIcon: splitButton.fallbackIcon + property alias menuOnTop: splitButton.menuOnTop + + signal selected(item: MenuItem) + + implicitHeight: rowLayout.implicitHeight + rowLayout.anchors.margins * 2 + clip: false + z: splitButton.expanded ? 1 : 0 + + RowLayout { + id: rowLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + anchors.leftMargin: Tokens.padding.largeIncreased + anchors.rightMargin: Tokens.padding.largeIncreased + spacing: Tokens.spacing.medium + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + id: label + + Layout.fillWidth: true + font: Tokens.font.body.small + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + visible: root.subtext + text: root.subtext + color: Colours.palette.m3outline + font: Tokens.font.label.small + elide: Text.ElideRight + } + } + + SplitButton { + id: splitButton + + type: SplitButton.Tonal + stateLayer.onClicked: splitButton.expanded = !splitButton.expanded + menu.onItemSelected: item => root.selected(item) + } + } +} diff --git a/modules/nexus/pages/ServicesPage.qml b/modules/nexus/pages/ServicesPage.qml new file mode 100644 index 00000000..16c92d35 --- /dev/null +++ b/modules/nexus/pages/ServicesPage.qml @@ -0,0 +1,225 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config +import Caelestia.Services +import qs.components +import qs.components.controls +import qs.services +import qs.modules.nexus.common + +PageBase { + id: root + + // Lyrics backends, ordered to match LyricsBackend::Backend (Auto, Local, LRCLIB, NetEase) + readonly property list lyricsItems: [ + MenuItem { + text: qsTr("Auto") + }, + MenuItem { + text: "Local" + }, + MenuItem { + text: "LRCLIB" + }, + MenuItem { + text: "NetEase" + } + ] + + // GPU options + the config string each maps to (see Gpu::parseType) + readonly property list gpuItems: [ + MenuItem { + text: qsTr("Auto") + }, + MenuItem { + text: "NVIDIA" + }, + MenuItem { + text: qsTr("Generic") + }, + MenuItem { + text: qsTr("None") + } + ] + readonly property list gpuValues: ["", "NVIDIA", "GENERIC", "None"] + + function gpuKeyToIndex(key: string): int { + const u = (key ?? "").trim().toUpperCase(); + if (u === "") + return 0; // Auto + if (u === "NVIDIA") + return 1; + if (u === "GENERIC") + return 2; + return 3; // None + } + + title: qsTr("Services") + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + // Detected running players, used as default-player options + Variants { + id: playerVariants + + model: [...new Set(Players.list.map(p => Players.getIdentity(p)).filter(id => id))] + + MenuItem { + required property string modelData + + text: modelData + icon: modelData === GlobalConfig.services.defaultPlayer ? "check" : "" + activeIcon: "music_note" + } + } + + // Polling + SectionHeader { + first: true + text: qsTr("Polling") + } + + StepperRow { + Layout.fillWidth: true + first: true + label: qsTr("Media refresh") + subtext: qsTr("How often the media position updates (ms)") + value: GlobalConfig.dashboard.mediaUpdateInterval + from: 100 + to: 2000 + stepSize: 50 + onMoved: v => GlobalConfig.dashboard.mediaUpdateInterval = v + } + + StepperRow { + Layout.fillWidth: true + label: qsTr("System stats refresh") + subtext: qsTr("CPU, memory and GPU update interval (seconds)") + value: GlobalConfig.dashboard.resourceUpdateInterval / 1000 + from: 0.5 + to: 10 + stepSize: 0.5 + onMoved: v => GlobalConfig.dashboard.resourceUpdateInterval = Math.round(v * 1000) + } + + StepperRow { + Layout.fillWidth: true + last: true + label: qsTr("Wi-Fi rescan") + subtext: qsTr("How often available networks are rescanned (seconds)") + value: GlobalConfig.nexus.networkRescanInterval / 1000 + from: 5 + to: 120 + stepSize: 5 + onMoved: v => GlobalConfig.nexus.networkRescanInterval = Math.round(v * 1000) + } + + // Media & lyrics + SectionHeader { + text: qsTr("Media & lyrics") + } + + SelectRow { + Layout.fillWidth: true + first: true + label: qsTr("Lyrics backend") + subtext: qsTr("Source used to fetch synced lyrics") + menuItems: root.lyricsItems + active: root.lyricsItems[Lyrics.preferredBackend] ?? root.lyricsItems[0] + onSelected: item => Lyrics.preferredBackend = root.lyricsItems.indexOf(item) + } + + SelectRow { + Layout.fillWidth: true + last: true + label: qsTr("Default player") + subtext: qsTr("Preferred media player when several are open") + menuItems: playerVariants.instances + active: menuItems.find(i => i.text === GlobalConfig.services.defaultPlayer) ?? null + fallbackIcon: "music_note" + fallbackText: GlobalConfig.services.defaultPlayer || qsTr("Auto") + onSelected: item => GlobalConfig.services.defaultPlayer = item.text + } + + // Input increments + SectionHeader { + text: qsTr("Input increments") + } + + StepperRow { + Layout.fillWidth: true + first: true + label: qsTr("Volume step") + subtext: qsTr("Amount the volume changes per scroll (%)") + value: Math.round(GlobalConfig.services.audioIncrement * 100) + from: 1 + to: 50 + stepSize: 1 + onMoved: v => GlobalConfig.services.audioIncrement = v / 100 + } + + StepperRow { + Layout.fillWidth: true + label: qsTr("Brightness step") + subtext: qsTr("Amount the brightness changes per scroll (%)") + value: Math.round(GlobalConfig.services.brightnessIncrement * 100) + from: 1 + to: 50 + stepSize: 1 + onMoved: v => GlobalConfig.services.brightnessIncrement = v / 100 + } + + StepperRow { + Layout.fillWidth: true + last: true + label: qsTr("Max volume") + subtext: qsTr("Upper limit for output volume (%)") + value: Math.round(GlobalConfig.services.maxVolume * 100) + from: 50 + to: 200 + stepSize: 5 + onMoved: v => GlobalConfig.services.maxVolume = v / 100 + } + + // Service tuning + SectionHeader { + text: qsTr("Service tuning") + } + + StepperRow { + Layout.fillWidth: true + first: true + label: qsTr("Visualiser bars") + subtext: qsTr("Number of bars in the audio visualisers") + value: GlobalConfig.services.visualiserBars + from: 10 + to: 120 + stepSize: 2 + onMoved: v => GlobalConfig.services.visualiserBars = v + } + + ToggleRow { + Layout.fillWidth: true + text: qsTr("Smart colour scheme") + subtext: qsTr("Derive theme mode and variant from the wallpaper") + checked: GlobalConfig.services.smartScheme + onToggled: GlobalConfig.services.smartScheme = checked + } + + SelectRow { + Layout.fillWidth: true + last: true + label: qsTr("GPU") + subtext: Gpu.name ? qsTr("Monitoring: %1").arg(Gpu.name) : qsTr("Override for GPU type") + menuOnTop: true + menuItems: root.gpuItems + active: root.gpuItems[root.gpuKeyToIndex(GlobalConfig.services.gpuType)] + onSelected: item => GlobalConfig.services.gpuType = root.gpuValues[root.gpuItems.indexOf(item)] + } + } +}