diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings')
62 files changed, 36852 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/AboutTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/AboutTab.qml new file mode 100644 index 0000000..67c2c21 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/AboutTab.qml @@ -0,0 +1,900 @@ +import QtQuick +import QtQuick.Effects +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: aboutTab + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property bool isHyprland: CompositorService.isHyprland + property bool isNiri: CompositorService.isNiri + property bool isSway: CompositorService.isSway + property bool isScroll: CompositorService.isScroll + property bool isMiracle: CompositorService.isMiracle + property bool isDwl: CompositorService.isDwl + property bool isLabwc: CompositorService.isLabwc + + property string compositorName: { + if (isHyprland) + return "hyprland"; + if (isSway) + return "sway"; + if (isScroll) + return "scroll"; + if (isMiracle) + return "miracle"; + if (isDwl) + return "mangowc"; + if (isLabwc) + return "labwc"; + return "niri"; + } + + property string compositorLogo: { + if (isHyprland) + return "/assets/hyprland.svg"; + if (isSway) + return "/assets/sway.svg"; + if (isScroll) + return "/assets/sway.svg"; + if (isMiracle) + return "/assets/miraclewm.svg"; + if (isDwl) + return "/assets/mango.png"; + if (isLabwc) + return "/assets/labwc.png"; + return "/assets/niri.svg"; + } + + property string compositorUrl: { + if (isHyprland) + return "https://hypr.land"; + if (isSway) + return "https://swaywm.org"; + if (isScroll) + return "https://github.com/dawsers/scroll"; + if (isMiracle) + return "https://github.com/miracle-wm-org/miracle-wm"; + if (isDwl) + return "https://github.com/DreamMaoMao/mangowc"; + if (isLabwc) + return "https://labwc.github.io/"; + return "https://github.com/niri-wm/niri"; + } + + property string compositorTooltip: { + if (isHyprland) + return I18n.tr("Hyprland Website"); + if (isSway) + return I18n.tr("Sway Website"); + if (isScroll) + return I18n.tr("Scroll GitHub"); + if (isMiracle) + return I18n.tr("Scroll GitHub"); + if (isDwl) + return I18n.tr("mangowc GitHub"); + if (isLabwc) + return I18n.tr("LabWC Website"); + return I18n.tr("niri GitHub"); + } + + property string dmsDiscordUrl: "https://discord.gg/ppWTpKmPgT" + property string dmsDiscordTooltip: I18n.tr("niri/dms Discord") + + property string compositorDiscordUrl: { + if (isHyprland) + return "https://discord.com/invite/hQ9XvMUjjr"; + if (isDwl) + return "https://discord.gg/CPjbDxesh5"; + return ""; + } + + property string compositorDiscordTooltip: { + if (isHyprland) + return I18n.tr("Hyprland Discord Server"); + if (isDwl) + return I18n.tr("mangowc Discord Server"); + return ""; + } + + property string redditUrl: "https://reddit.com/r/niri" + property string redditTooltip: I18n.tr("r/niri Subreddit") + + property string ircUrl: "https://web.libera.chat/gamja/?channels=#labwc" + property string ircTooltip: I18n.tr("LabWC IRC Channel") + + property bool showMatrix: isNiri && !isHyprland && !isSway && !isScroll && !isMiracle && !isDwl && !isLabwc + property bool showCompositorDiscord: isHyprland || isDwl + property bool showReddit: isNiri && !isHyprland && !isSway && !isScroll && !isMiracle && !isDwl && !isLabwc + property bool showIrc: isLabwc + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + // ASCII Art Header + StyledRect { + width: parent.width + height: asciiSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 0 + + Column { + id: asciiSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: parent.width < 350 ? Theme.spacingM : Theme.spacingL + + property bool compactLogo: parent.width < 400 + property bool hideLogo: parent.width < 280 + + Image { + id: logoImage + + visible: !parent.hideLogo + anchors.verticalCenter: parent.verticalCenter + width: parent.compactLogo ? 80 : 120 + height: width * (569.94629 / 506.50931) + fillMode: Image.PreserveAspectFit + smooth: true + mipmap: true + asynchronous: true + source: "file://" + Theme.shellDir + "/assets/danklogonormal.svg" + layer.enabled: true + layer.smooth: true + layer.mipmap: true + layer.effect: MultiEffect { + saturation: 0 + colorization: 1 + colorizationColor: Theme.primary + } + } + + Text { + anchors.verticalCenter: parent.verticalCenter + text: "DANK LINUX" + font.pixelSize: parent.compactLogo ? 32 : 48 + font.weight: Font.Bold + font.family: interFont.name + color: Theme.surfaceText + antialiasing: true + + FontLoader { + id: interFont + source: Qt.resolvedUrl("../../assets/fonts/inter/InterVariable.ttf") + } + } + } + + StyledText { + text: { + if (!SystemUpdateService.shellVersion && !DMSService.cliVersion) + return "dms"; + + let version = SystemUpdateService.shellVersion || ""; + let cliVersion = DMSService.cliVersion || ""; + + // Debian/Ubuntu/OpenSUSE git format: 1.0.3+git2264.c5c5ce84 + let match = version.match(/^([\d.]+)\+git(\d+)\./); + if (match) { + return `dms (git) v${match[1]}-${match[2]}`; + } + + // Fedora COPR git format: 0.0.git.2267.d430cae9 + match = version.match(/^[\d.]+\.git\.(\d+)\./); + if (match) { + function extractBaseVersion(value) { + if (!value) + return ""; + let baseMatch = value.match(/(\d+\.\d+\.\d+)/); + if (baseMatch) + return baseMatch[1]; + baseMatch = value.match(/(\d+\.\d+)/); + if (baseMatch) + return baseMatch[1]; + return ""; + } + + let baseVersion = extractBaseVersion(cliVersion); + if (!baseVersion) + baseVersion = extractBaseVersion(SystemUpdateService.semverVersion); + if (baseVersion) { + return `dms (git) v${baseVersion}-${match[1]}`; + } + return `dms (git) v${match[1]}`; + } + + // Stable release format: 1.0.3 + match = version.match(/^([\d.]+)$/); + if (match) { + return `dms v${match[1]}`; + } + + if (!version && cliVersion) { + match = cliVersion.match(/^([\d.]+)\+git(\d+)\./); + if (match) { + return `dms (git) v${match[1]}-${match[2]}`; + } + match = cliVersion.match(/^([\d.]+)$/); + if (match) { + return `dms v${match[1]}`; + } + return `dms ${cliVersion}`; + } + + return `dms ${version}`; + } + font.pixelSize: Theme.fontSizeXLarge + font.weight: Font.Bold + color: Theme.surfaceText + horizontalAlignment: Text.AlignHCenter + width: parent.width + } + + StyledText { + visible: SystemUpdateService.shellCodename.length > 0 + text: `"${SystemUpdateService.shellCodename}"` + font.pixelSize: Theme.fontSizeMedium + font.italic: true + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + width: parent.width + } + + Row { + id: resourceButtonsRow + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingS + + property bool compactMode: parent.width < 450 + + DankButton { + id: docsButton + text: resourceButtonsRow.compactMode ? "" : I18n.tr("Docs") + iconName: "menu_book" + iconSize: 18 + backgroundColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) + textColor: Theme.surfaceText + onClicked: Qt.openUrlExternally("https://danklinux.com/docs") + onHoveredChanged: { + if (hovered) + resourceTooltip.show(resourceButtonsRow.compactMode ? I18n.tr("Docs") + " - danklinux.com/docs" : "danklinux.com/docs", docsButton, 0, 0, "bottom"); + else + resourceTooltip.hide(); + } + } + + DankButton { + id: pluginsButton + text: resourceButtonsRow.compactMode ? "" : I18n.tr("Plugins") + iconName: "extension" + iconSize: 18 + backgroundColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) + textColor: Theme.surfaceText + onClicked: Qt.openUrlExternally("https://plugins.danklinux.com") + onHoveredChanged: { + if (hovered) + resourceTooltip.show(resourceButtonsRow.compactMode ? I18n.tr("Plugins") + " - plugins.danklinux.com" : "plugins.danklinux.com", pluginsButton, 0, 0, "bottom"); + else + resourceTooltip.hide(); + } + } + + DankButton { + id: githubButton + text: resourceButtonsRow.compactMode ? "" : I18n.tr("GitHub") + iconName: "code" + iconSize: 18 + backgroundColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) + textColor: Theme.surfaceText + onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/DankMaterialShell") + onHoveredChanged: { + if (hovered) + resourceTooltip.show(resourceButtonsRow.compactMode ? "GitHub - AvengeMedia/DankMaterialShell" : "github.com/AvengeMedia/DankMaterialShell", githubButton, 0, 0, "bottom"); + else + resourceTooltip.hide(); + } + } + + DankButton { + id: kofiButton + text: resourceButtonsRow.compactMode ? "" : I18n.tr("Ko-fi") + iconName: "favorite" + iconSize: 18 + backgroundColor: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + textColor: Theme.primary + onClicked: Qt.openUrlExternally("https://ko-fi.com/danklinux") + onHoveredChanged: { + if (hovered) + resourceTooltip.show(resourceButtonsRow.compactMode ? I18n.tr("Ko-fi") + " - ko-fi.com/danklinux" : "ko-fi.com/danklinux", kofiButton, 0, 0, "bottom"); + else + resourceTooltip.hide(); + } + } + } + + DankTooltipV2 { + id: resourceTooltip + } + + Item { + id: communityIcons + anchors.horizontalCenter: parent.horizontalCenter + height: 24 + width: { + let baseWidth = compositorButton.width + dmsDiscordButton.width + Theme.spacingM; + if (showMatrix) { + baseWidth += matrixButton.width + 4; + } + if (showIrc) { + baseWidth += ircButton.width + Theme.spacingM; + } + if (showCompositorDiscord) { + baseWidth += compositorDiscordButton.width + Theme.spacingM; + } + if (showReddit) { + baseWidth += redditButton.width + Theme.spacingM; + } + return baseWidth; + } + + Item { + id: compositorButton + width: 24 + height: 24 + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: -2 + x: 0 + + property bool hovered: false + property string tooltipText: compositorTooltip + + Image { + anchors.fill: parent + source: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Modules/Settings/", "") + compositorLogo + sourceSize: Qt.size(24, 24) + smooth: true + fillMode: Image.PreserveAspectFit + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.hovered = true + onExited: parent.hovered = false + onClicked: Qt.openUrlExternally(compositorUrl) + } + } + + Item { + id: matrixButton + width: 30 + height: 24 + x: compositorButton.x + compositorButton.width + 4 + visible: showMatrix + + property bool hovered: false + property string tooltipText: I18n.tr("niri Matrix Chat") + + Image { + anchors.fill: parent + source: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Modules/Settings/", "") + "/assets/matrix-logo-white.svg" + sourceSize: Qt.size(28, 18) + smooth: true + fillMode: Image.PreserveAspectFit + layer.enabled: true + + layer.effect: MultiEffect { + colorization: 1 + colorizationColor: Theme.surfaceText + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.hovered = true + onExited: parent.hovered = false + onClicked: Qt.openUrlExternally("https://matrix.to/#/#niri:matrix.org") + } + } + + Item { + id: ircButton + width: 24 + height: 24 + x: compositorButton.x + compositorButton.width + Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + visible: showIrc + + property bool hovered: false + property string tooltipText: ircTooltip + + DankIcon { + anchors.centerIn: parent + name: "forum" + size: 20 + color: Theme.surfaceText + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.hovered = true + onExited: parent.hovered = false + onClicked: Qt.openUrlExternally(ircUrl) + } + } + + Item { + id: dmsDiscordButton + width: 20 + height: 20 + x: { + if (showMatrix) + return matrixButton.x + matrixButton.width + Theme.spacingM; + if (showIrc) + return ircButton.x + ircButton.width + Theme.spacingM; + return compositorButton.x + compositorButton.width + Theme.spacingM; + } + anchors.verticalCenter: parent.verticalCenter + + property bool hovered: false + property string tooltipText: dmsDiscordTooltip + + Image { + anchors.fill: parent + source: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Modules/Settings/", "") + "/assets/discord.svg" + sourceSize: Qt.size(20, 20) + smooth: true + fillMode: Image.PreserveAspectFit + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.hovered = true + onExited: parent.hovered = false + onClicked: Qt.openUrlExternally(dmsDiscordUrl) + } + } + + Item { + id: compositorDiscordButton + width: 20 + height: 20 + x: dmsDiscordButton.x + dmsDiscordButton.width + Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + visible: showCompositorDiscord + + property bool hovered: false + property string tooltipText: compositorDiscordTooltip + + Image { + anchors.fill: parent + source: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Modules/Settings/", "") + "/assets/discord.svg" + sourceSize: Qt.size(20, 20) + smooth: true + fillMode: Image.PreserveAspectFit + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.hovered = true + onExited: parent.hovered = false + onClicked: Qt.openUrlExternally(compositorDiscordUrl) + } + } + + Item { + id: redditButton + width: 20 + height: 20 + x: showCompositorDiscord ? compositorDiscordButton.x + compositorDiscordButton.width + Theme.spacingM : dmsDiscordButton.x + dmsDiscordButton.width + Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + visible: showReddit + + property bool hovered: false + property string tooltipText: redditTooltip + + Image { + anchors.fill: parent + source: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Modules/Settings/", "") + "/assets/reddit.svg" + sourceSize: Qt.size(20, 20) + smooth: true + fillMode: Image.PreserveAspectFit + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.hovered = true + onExited: parent.hovered = false + onClicked: Qt.openUrlExternally(redditUrl) + } + } + } + } + } + + // Project Information + StyledRect { + width: parent.width + height: projectSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 0 + + Column { + id: projectSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "info" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("About") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + StyledText { + text: I18n.tr('dms is a highly customizable, modern desktop shell with a <a href="https://m3.material.io/" style="text-decoration:none; color:%1;">material 3 inspired</a> design.<br /><br/>It is built with <a href="https://quickshell.org" style="text-decoration:none; color:%1;">Quickshell</a>, a QT6 framework for building desktop shells, and <a href="https://go.dev" style="text-decoration:none; color:%1;">Go</a>, a statically typed, compiled programming language.').arg(Theme.primary) + textFormat: Text.RichText + font.pixelSize: Theme.fontSizeMedium + linkColor: Theme.primary + onLinkActivated: url => Qt.openUrlExternally(url) + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.WordWrap + + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + } + } + } + } + + StyledRect { + visible: DMSService.isConnected + width: parent.width + height: backendSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 0 + + Column { + id: backendSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "dns" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Backend") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + anchors.left: parent.left + spacing: Theme.spacingL + + Column { + spacing: 2 + + StyledText { + text: I18n.tr("Version") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: DMSService.cliVersion || "—" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + horizontalAlignment: Text.AlignLeft + } + } + + Rectangle { + width: 1 + height: 32 + color: Theme.outlineVariant + } + + Column { + spacing: 2 + + StyledText { + text: I18n.tr("API") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: `v${DMSService.apiVersion}` + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + horizontalAlignment: Text.AlignLeft + } + } + + Rectangle { + width: 1 + height: 32 + color: Theme.outlineVariant + } + + Column { + spacing: 2 + + StyledText { + text: I18n.tr("Status") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignLeft + } + + Row { + spacing: 4 + + Rectangle { + width: 8 + height: 8 + radius: 4 + color: Theme.success + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Connected") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + horizontalAlignment: Text.AlignLeft + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: DMSService.capabilities.length > 0 + + StyledText { + text: I18n.tr("Capabilities") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Flow { + width: parent.width + spacing: 6 + + Repeater { + model: DMSService.capabilities + + Rectangle { + width: capText.implicitWidth + 16 + height: 26 + radius: 13 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + + StyledText { + id: capText + anchors.centerIn: parent + text: modelData + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + } + } + } + } + } + } + } + + StyledRect { + width: parent.width + height: toolsSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 0 + + Column { + id: toolsSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "build" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Tools") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + anchors.left: parent.left + spacing: Theme.spacingS + + DankButton { + text: I18n.tr("Show Welcome") + iconName: "waving_hand" + backgroundColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) + textColor: Theme.surfaceText + onClicked: FirstLaunchService.showWelcome() + } + + DankButton { + text: I18n.tr("System Check") + iconName: "vital_signs" + backgroundColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) + textColor: Theme.surfaceText + onClicked: FirstLaunchService.showDoctor() + } + } + } + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: I18n.tr('<a href="https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE" style="text-decoration:none; color:%1;">MIT License</a>').arg(Theme.surfaceVariantText) + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + textFormat: Text.RichText + wrapMode: Text.NoWrap + onLinkActivated: url => Qt.openUrlExternally(url) + + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + } + } + } + } + + // Community tooltip - positioned absolutely above everything + Rectangle { + id: communityTooltip + parent: aboutTab + z: 1000 + + property var hoveredButton: { + if (compositorButton.hovered) + return compositorButton; + if (matrixButton.visible && matrixButton.hovered) + return matrixButton; + if (ircButton.visible && ircButton.hovered) + return ircButton; + if (dmsDiscordButton.hovered) + return dmsDiscordButton; + if (compositorDiscordButton.visible && compositorDiscordButton.hovered) + return compositorDiscordButton; + if (redditButton.visible && redditButton.hovered) + return redditButton; + return null; + } + + property string tooltipText: hoveredButton ? hoveredButton.tooltipText : "" + + visible: hoveredButton !== null && tooltipText !== "" + width: tooltipLabel.implicitWidth + 24 + height: tooltipLabel.implicitHeight + 12 + + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.width: 0 + border.color: Theme.outlineMedium + + x: hoveredButton ? hoveredButton.mapToItem(aboutTab, hoveredButton.width / 2, 0).x - width / 2 : 0 + y: hoveredButton ? communityIcons.mapToItem(aboutTab, 0, 0).y - height - 8 : 0 + + ElevationShadow { + anchors.fill: parent + z: -1 + level: Theme.elevationLevel1 + fallbackOffset: 1 + targetRadius: communityTooltip.radius + targetColor: communityTooltip.color + borderColor: communityTooltip.border.color + borderWidth: communityTooltip.border.width + shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2 + shadowEnabled: Theme.elevationEnabled + } + + StyledText { + id: tooltipLabel + anchors.centerIn: parent + text: communityTooltip.tooltipText + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/AudioTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/AudioTab.qml new file mode 100644 index 0000000..c054791 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/AudioTab.qml @@ -0,0 +1,747 @@ +import QtQuick +import Quickshell.Services.Pipewire +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property var outputDevices: [] + property var inputDevices: [] + property bool showEditDialog: false + property var editingDevice: null + property string editingDeviceType: "" + property string newDeviceName: "" + property bool isReloadingAudio: false + property var hiddenOutputDeviceNames: SessionData.hiddenOutputDeviceNames ?? [] + property var hiddenInputDeviceNames: SessionData.hiddenInputDeviceNames ?? [] + property bool showHiddenOutputDevices: false + property bool showHiddenInputDevices: false + + function persistHiddenOutputDeviceNames(deviceNames) { + const uniqueNames = [...new Set(deviceNames)]; + hiddenOutputDeviceNames = uniqueNames; + SessionData.setHiddenOutputDeviceNames(uniqueNames); + } + + function persistHiddenInputDeviceNames(deviceNames) { + const uniqueNames = [...new Set(deviceNames)]; + hiddenInputDeviceNames = uniqueNames; + SessionData.setHiddenInputDeviceNames(uniqueNames); + } + + function updateDeviceList() { + const allNodes = Pipewire.nodes.values; + + // Sort devices: active first, then alphabetically by name + const sortDevices = (a, b) => { + if (a === AudioService.sink && b !== AudioService.sink) + return -1; + if (b === AudioService.sink && a !== AudioService.sink) + return 1; + const nameA = AudioService.displayName(a).toLowerCase(); + const nameB = AudioService.displayName(b).toLowerCase(); + return nameA.localeCompare(nameB); + }; + + const outputs = allNodes.filter(node => { + return node.audio && node.isSink && !node.isStream; + }); + outputDevices = outputs.sort(sortDevices); + + const inputs = allNodes.filter(node => { + return node.audio && !node.isSink && !node.isStream; + }); + + const sortInputs = (a, b) => { + if (a === AudioService.source && b !== AudioService.source) + return -1; + if (b === AudioService.source && a !== AudioService.source) + return 1; + const nameA = AudioService.displayName(a).toLowerCase(); + const nameB = AudioService.displayName(b).toLowerCase(); + return nameA.localeCompare(nameB); + }; + + inputDevices = inputs.sort(sortInputs); + } + + Component.onCompleted: { + hiddenOutputDeviceNames = SessionData.hiddenOutputDeviceNames ?? []; + hiddenInputDeviceNames = SessionData.hiddenInputDeviceNames ?? []; + updateDeviceList(); + } + + Connections { + target: Pipewire.nodes + function onValuesChanged() { + root.updateDeviceList(); + } + } + + Connections { + target: AudioService + function onWireplumberReloadStarted() { + root.isReloadingAudio = true; + } + function onWireplumberReloadCompleted(success) { + Qt.callLater(() => { + delayTimer.start(); + }); + } + function onDeviceAliasChanged(nodeName, newAlias) { + root.updateDeviceList(); + } + } + + Timer { + id: delayTimer + interval: 2000 + repeat: false + onTriggered: { + root.isReloadingAudio = false; + root.updateDeviceList(); + } + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + tab: "audio" + tags: ["audio", "device", "output", "speaker"] + title: I18n.tr("Output Devices", "Audio settings: speaker/headphone devices") + settingKey: "audioOutputDevices" + iconName: "volume_up" + + Column { + width: parent.width + spacing: Theme.spacingM + + StyledText { + width: parent.width + text: I18n.tr("Set custom names for your audio output devices", "Audio settings description") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignLeft + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + } + + Repeater { + model: root.outputDevices.filter(d => !root.hiddenOutputDeviceNames.includes(d.name)) + + delegate: Column { + required property var modelData + width: parent?.width ?? 0 + spacing: 0 + + DeviceAliasRow { + deviceNode: modelData + deviceType: "output" + showHideButton: true + + onEditRequested: device => { + root.editingDevice = device; + root.editingDeviceType = "output"; + root.newDeviceName = AudioService.displayName(device); + root.showEditDialog = true; + } + + onResetRequested: device => { + AudioService.removeDeviceAlias(device.name); + } + + onHideRequested: device => { + root.persistHiddenOutputDeviceNames([...root.hiddenOutputDeviceNames, device.name]); + } + } + + Item { + width: parent.width + height: 36 + + StyledText { + id: maxVolLabel + text: I18n.tr("Max Volume", "Audio settings: maximum volume limit per device") + " · " + maxVolSlider.value + "%" + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + Theme.iconSize + Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignLeft + } + + DankSlider { + id: maxVolSlider + anchors.left: maxVolLabel.right + anchors.leftMargin: Theme.spacingS + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + height: 36 + minimum: 100 + maximum: 200 + step: 5 + showValue: true + wheelEnabled: false + centerMinimum: true + unit: "%" + onSliderValueChanged: newValue => { + SessionData.setDeviceMaxVolume(modelData.name, newValue); + } + } + + Binding { + target: maxVolSlider + property: "value" + value: SessionData.deviceMaxVolumes[modelData.name] ?? 100 + when: !maxVolSlider.isDragging + } + } + } + } + + StyledText { + width: parent.width + text: I18n.tr("No output devices found", "Audio settings empty state") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + visible: root.outputDevices.filter(d => !root.hiddenOutputDeviceNames.includes(d.name)).length === 0 && root.hiddenOutputDeviceNames.length === 0 + topPadding: Theme.spacingM + } + + Column { + width: parent.width + spacing: 0 + visible: root.hiddenOutputDeviceNames.length > 0 + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + Item { + width: parent.width + height: 36 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "visibility_off" + size: Theme.iconSize - 4 + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Hidden (%1)", "count of hidden audio devices").arg(root.hiddenOutputDeviceNames.length) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankIcon { + name: root.showHiddenOutputDevices ? "expand_less" : "expand_more" + size: Theme.iconSize - 4 + color: Theme.surfaceVariantText + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.showHiddenOutputDevices = !root.showHiddenOutputDevices + } + } + + Column { + width: parent.width + spacing: 0 + visible: root.showHiddenOutputDevices + + Repeater { + model: root.outputDevices.filter(d => root.hiddenOutputDeviceNames.includes(d.name)) + + delegate: DeviceAliasRow { + required property var modelData + deviceNode: modelData + deviceType: "output" + isHidden: true + showHideButton: true + + onHideRequested: device => { + root.persistHiddenOutputDeviceNames(root.hiddenOutputDeviceNames.filter(n => n !== device.name)); + } + + onResetRequested: device => { + AudioService.removeDeviceAlias(device.name); + } + } + } + } + } + } + } + + SettingsCard { + tab: "audio" + tags: ["audio", "device", "input", "microphone"] + title: I18n.tr("Input Devices") + settingKey: "audioInputDevices" + iconName: "mic" + + Column { + width: parent.width + spacing: Theme.spacingM + + StyledText { + width: parent.width + text: I18n.tr("Set custom names for your audio input devices", "Audio settings description") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignLeft + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + } + + Repeater { + model: root.inputDevices.filter(d => !root.hiddenInputDeviceNames.includes(d.name)) + + delegate: DeviceAliasRow { + required property var modelData + + deviceNode: modelData + deviceType: "input" + showHideButton: true + + onEditRequested: device => { + root.editingDevice = device; + root.editingDeviceType = "input"; + root.newDeviceName = AudioService.displayName(device); + root.showEditDialog = true; + } + + onResetRequested: device => { + AudioService.removeDeviceAlias(device.name); + } + + onHideRequested: device => { + root.persistHiddenInputDeviceNames([...root.hiddenInputDeviceNames, device.name]); + } + } + } + + StyledText { + width: parent.width + text: I18n.tr("No input devices found", "Audio settings empty state") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + visible: root.inputDevices.filter(d => !root.hiddenInputDeviceNames.includes(d.name)).length === 0 && root.hiddenInputDeviceNames.length === 0 + topPadding: Theme.spacingM + } + + Column { + width: parent.width + spacing: 0 + visible: root.hiddenInputDeviceNames.length > 0 + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + Item { + width: parent.width + height: 36 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "visibility_off" + size: Theme.iconSize - 4 + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Hidden (%1)", "count of hidden audio devices").arg(root.hiddenInputDeviceNames.length) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankIcon { + name: root.showHiddenInputDevices ? "expand_less" : "expand_more" + size: Theme.iconSize - 4 + color: Theme.surfaceVariantText + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.showHiddenInputDevices = !root.showHiddenInputDevices + } + } + + Column { + width: parent.width + spacing: 0 + visible: root.showHiddenInputDevices + + Repeater { + model: root.inputDevices.filter(d => root.hiddenInputDeviceNames.includes(d.name)) + + delegate: DeviceAliasRow { + required property var modelData + deviceNode: modelData + deviceType: "input" + isHidden: true + showHideButton: true + + onHideRequested: device => { + root.persistHiddenInputDeviceNames(root.hiddenInputDeviceNames.filter(n => n !== device.name)); + } + + onResetRequested: device => { + AudioService.removeDeviceAlias(device.name); + } + } + } + } + } + } + } + } + } + + Rectangle { + id: loadingOverlay + anchors.fill: parent + color: Theme.withAlpha(Theme.surface, 0.9) + visible: root.isReloadingAudio + z: 100 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingL + + Rectangle { + width: 80 + height: 80 + radius: 40 + color: Theme.primaryContainer + anchors.horizontalCenter: parent.horizontalCenter + + DankIcon { + id: spinningIcon + name: "refresh" + size: 40 + color: Theme.primary + anchors.centerIn: parent + + RotationAnimator { + target: spinningIcon + from: 0 + to: 360 + duration: 1500 + loops: Animation.Infinite + running: loadingOverlay.visible + } + } + } + + Column { + spacing: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + + StyledText { + text: I18n.tr("Restarting audio system...", "Loading overlay while WirePlumber restarts") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("This may take a few seconds", "Loading overlay subtitle") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + Rectangle { + id: dialogOverlay + anchors.fill: parent + visible: root.showEditDialog + color: Theme.withAlpha(Theme.surface, 0.8) + z: 1000 + + MouseArea { + anchors.fill: parent + onClicked: { + root.showEditDialog = false; + } + } + + Rectangle { + id: editDialog + anchors.centerIn: parent + width: Math.min(500, parent.width - Theme.spacingL * 4) + height: dialogContent.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.width: 1 + border.color: Theme.outlineMedium + + MouseArea { + anchors.fill: parent + onClicked: {} + } + + Column { + id: dialogContent + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingL + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: root.editingDeviceType === "input" ? "mic" : "speaker" + size: Theme.iconSize + 8 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - 8 + spacing: 4 + + StyledText { + text: I18n.tr("Set Custom Device Name", "Audio device rename dialog title") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold + color: Theme.surfaceText + width: parent.width + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: root.editingDevice?.name ?? "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + elide: Text.ElideRight + horizontalAlignment: Text.AlignLeft + } + + StyledText { + visible: AudioService.hasDeviceAlias(root.editingDevice?.name ?? "") + text: I18n.tr("Original: %1", "Shows the original device name before renaming").arg(AudioService.originalName(root.editingDevice)) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + elide: Text.ElideRight + opacity: 0.7 + horizontalAlignment: Text.AlignLeft + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Custom Name", "Audio device rename dialog field label") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + DankTextField { + id: nameInput + width: parent.width + placeholderText: I18n.tr("Enter device name...", "Audio device rename dialog placeholder") + text: root.newDeviceName + normalBorderColor: Theme.outlineMedium + focusedBorderColor: Theme.primary + showClearButton: true + + onTextChanged: { + root.newDeviceName = text; + } + + Keys.onReturnPressed: { + if (text.trim() !== "") { + saveButtonMouseArea.clicked(null); + } + } + + Keys.onEscapePressed: { + root.showEditDialog = false; + } + + Component.onCompleted: { + Qt.callLater(() => { + forceActiveFocus(); + selectAll(); + }); + } + } + + StyledText { + width: parent.width + text: I18n.tr("Press Enter and the audio system will restart to apply the change", "Audio device rename dialog hint") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignLeft + } + } + + Row { + LayoutMirroring.enabled: false + width: parent.width + spacing: Theme.spacingM + layoutDirection: Qt.RightToLeft + + Rectangle { + id: saveButton + width: saveButtonContent.width + Theme.spacingL * 2 + height: Theme.buttonHeight + radius: Theme.cornerRadius + color: saveButtonMouseArea.containsMouse ? Theme.primaryContainer : Theme.primary + enabled: root.newDeviceName.trim() !== "" + opacity: enabled ? 1.0 : 0.5 + + Row { + id: saveButtonContent + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "check" + size: Theme.iconSize - 4 + color: Theme.onPrimary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Save") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.onPrimary + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: saveButtonMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: parent.enabled + onClicked: { + if (root.editingDevice && root.newDeviceName.trim() !== "") { + AudioService.setDeviceAlias(root.editingDevice.name, root.newDeviceName); + root.showEditDialog = false; + } + } + } + } + + Rectangle { + width: cancelButtonText.width + Theme.spacingL * 2 + height: Theme.buttonHeight + radius: Theme.cornerRadius + color: cancelButtonMouseArea.containsMouse ? Theme.surfaceHover : "transparent" + border.width: 1 + border.color: Theme.outline + + StyledText { + id: cancelButtonText + text: I18n.tr("Cancel") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.centerIn: parent + } + + MouseArea { + id: cancelButtonMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.showEditDialog = false; + } + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/ClipboardTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/ClipboardTab.qml new file mode 100644 index 0000000..d477bb9 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/ClipboardTab.qml @@ -0,0 +1,463 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + property var config: ({}) + property bool configLoaded: false + property bool configError: false + property bool saving: false + + readonly property var maxHistoryOptions: [ + { + text: "25", + value: 25 + }, + { + text: "50", + value: 50 + }, + { + text: "100", + value: 100 + }, + { + text: "200", + value: 200 + }, + { + text: "500", + value: 500 + }, + { + text: "1,000", + value: 1000 + }, + { + text: "10,000", + value: 10000 + }, + { + text: "15,000", + value: 15000 + }, + { + text: "20,000", + value: 20000 + }, + { + text: "30,000", + value: 30000 + }, + { + text: "50,000", + value: 50000 + }, + { + text: "100,000", + value: 100000 + }, + { + text: "∞", + value: -1 + } + ] + + readonly property var maxEntrySizeOptions: [ + { + text: "1 MB", + value: 1048576 + }, + { + text: "2 MB", + value: 2097152 + }, + { + text: "5 MB", + value: 5242880 + }, + { + text: "10 MB", + value: 10485760 + }, + { + text: "20 MB", + value: 20971520 + }, + { + text: "50 MB", + value: 52428800 + } + ] + + readonly property var autoClearOptions: [ + { + text: I18n.tr("Never"), + value: 0 + }, + { + text: I18n.tr("1 day"), + value: 1 + }, + { + text: I18n.tr("3 days"), + value: 3 + }, + { + text: I18n.tr("7 days"), + value: 7 + }, + { + text: I18n.tr("14 days"), + value: 14 + }, + { + text: I18n.tr("30 days"), + value: 30 + }, + { + text: I18n.tr("90 days"), + value: 90 + } + ] + + readonly property var maxPinnedOptions: [ + { + text: "5", + value: 5 + }, + { + text: "10", + value: 10 + }, + { + text: "15", + value: 15 + }, + { + text: "25", + value: 25 + }, + { + text: "50", + value: 50 + }, + { + text: "100", + value: 100 + } + ] + + function getMaxHistoryText(value) { + if (value <= 0) + return "∞"; + for (let opt of maxHistoryOptions) { + if (opt.value === value) + return opt.text; + } + return value.toLocaleString(); + } + + function getMaxEntrySizeText(value) { + for (let opt of maxEntrySizeOptions) { + if (opt.value === value) + return opt.text; + } + const mb = Math.round(value / 1048576); + return mb + " MB"; + } + + function getAutoClearText(value) { + for (let opt of autoClearOptions) { + if (opt.value === value) + return opt.text; + } + return value + " " + I18n.tr("days"); + } + + function getMaxPinnedText(value) { + for (let opt of maxPinnedOptions) { + if (opt.value === value) + return opt.text; + } + return value.toString(); + } + + function loadConfig() { + configLoaded = false; + configError = false; + DMSService.sendRequest("clipboard.getConfig", null, response => { + if (response.error) { + configError = true; + return; + } + config = response.result || {}; + configLoaded = true; + }); + } + + function saveConfig(key, value) { + const params = {}; + params[key] = value; + saving = true; + DMSService.sendRequest("clipboard.setConfig", params, response => { + saving = false; + if (response.error) { + ToastService.showError(I18n.tr("Failed to save clipboard setting"), response.error); + return; + } + const updated = JSON.parse(JSON.stringify(config)); + updated[key] = value; + config = updated; + }); + } + + Component.onCompleted: { + if (DMSService.isConnected) + loadConfig(); + } + + Connections { + target: DMSService + function onIsConnectedChanged() { + if (DMSService.isConnected) + loadConfig(); + } + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + Rectangle { + width: parent.width + height: warningContent.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) + visible: !DMSService.isConnected || configError + + Row { + id: warningContent + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + DankIcon { + name: "info" + size: Theme.iconSizeSmall + color: Theme.warning + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + font.pixelSize: Theme.fontSizeSmall + text: !DMSService.isConnected ? I18n.tr("DMS service is not connected. Clipboard settings are unavailable.") : I18n.tr("Failed to load clipboard configuration.") + wrapMode: Text.WordWrap + width: parent.width - Theme.iconSizeSmall - Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + } + } + } + + SettingsCard { + tab: "clipboard" + tags: ["clipboard", "history", "limit"] + title: I18n.tr("History Settings") + iconName: "history" + visible: configLoaded + + SettingsDropdownRow { + id: maxHistoryDropdown + tab: "clipboard" + tags: ["clipboard", "history", "max", "limit"] + settingKey: "maxHistory" + text: I18n.tr("Maximum History") + description: I18n.tr("Maximum number of clipboard entries to keep") + options: root.maxHistoryOptions.map(opt => opt.text) + + Component.onCompleted: { + currentValue = root.getMaxHistoryText(root.config.maxHistory ?? 100); + } + + onValueChanged: value => { + for (let opt of root.maxHistoryOptions) { + if (opt.text === value) { + root.saveConfig("maxHistory", opt.value); + return; + } + } + } + + Connections { + target: root + function onConfigChanged() { + maxHistoryDropdown.currentValue = root.getMaxHistoryText(root.config.maxHistory ?? 100); + } + } + } + + SettingsDropdownRow { + id: maxEntrySizeDropdown + tab: "clipboard" + tags: ["clipboard", "entry", "size", "limit"] + settingKey: "maxEntrySize" + text: I18n.tr("Maximum Entry Size") + description: I18n.tr("Maximum size per clipboard entry") + options: root.maxEntrySizeOptions.map(opt => opt.text) + + Component.onCompleted: { + currentValue = root.getMaxEntrySizeText(root.config.maxEntrySize ?? 5242880); + } + + onValueChanged: value => { + for (let opt of root.maxEntrySizeOptions) { + if (opt.text === value) { + root.saveConfig("maxEntrySize", opt.value); + return; + } + } + } + + Connections { + target: root + function onConfigChanged() { + maxEntrySizeDropdown.currentValue = root.getMaxEntrySizeText(root.config.maxEntrySize ?? 5242880); + } + } + } + + SettingsDropdownRow { + id: autoClearDaysDropdown + tab: "clipboard" + tags: ["clipboard", "auto", "clear", "days"] + settingKey: "autoClearDays" + text: I18n.tr("Auto-Clear After") + description: I18n.tr("Automatically delete entries older than this") + options: root.autoClearOptions.map(opt => opt.text) + + Component.onCompleted: { + currentValue = root.getAutoClearText(root.config.autoClearDays ?? 0); + } + + onValueChanged: value => { + for (let opt of root.autoClearOptions) { + if (opt.text === value) { + root.saveConfig("autoClearDays", opt.value); + return; + } + } + } + + Connections { + target: root + function onConfigChanged() { + autoClearDaysDropdown.currentValue = root.getAutoClearText(root.config.autoClearDays ?? 0); + } + } + } + + SettingsDropdownRow { + id: maxPinnedDropdown + tab: "clipboard" + tags: ["clipboard", "pinned", "max", "limit"] + settingKey: "maxPinned" + text: I18n.tr("Maximum Pinned Entries") + description: I18n.tr("Maximum number of entries that can be saved") + options: root.maxPinnedOptions.map(opt => opt.text) + + function updateValue() { + if (root.configLoaded) { + currentValue = root.getMaxPinnedText(root.config.maxPinned ?? 25); + } + } + + Component.onCompleted: { + updateValue(); + } + + onValueChanged: value => { + for (let opt of root.maxPinnedOptions) { + if (opt.text === value) { + root.saveConfig("maxPinned", opt.value); + return; + } + } + } + + Connections { + target: root + function onConfigLoadedChanged() { + if (root.configLoaded) { + maxPinnedDropdown.updateValue(); + } + } + function onConfigChanged() { + maxPinnedDropdown.updateValue(); + } + } + } + } + + SettingsCard { + tab: "clipboard" + tags: ["clipboard", "behavior"] + title: I18n.tr("Behavior") + iconName: "settings" + visible: configLoaded + + SettingsToggleRow { + tab: "clipboard" + tags: ["clipboard", "clear", "startup"] + settingKey: "clearAtStartup" + text: I18n.tr("Clear at Startup") + description: I18n.tr("Clear all history when server starts") + checked: root.config.clearAtStartup ?? false + onToggled: checked => root.saveConfig("clearAtStartup", checked) + } + + SettingsToggleRow { + tab: "clipboard" + tags: ["clipboard", "enter", "paste", "behavior"] + settingKey: "clipboardEnterToPaste" + text: I18n.tr("Enter to Paste") + description: I18n.tr("Press Enter to paste, Shift+Enter to copy", "Clipboard behavior setting description") + checked: SettingsData.clipboardEnterToPaste + onToggled: checked => SettingsData.set("clipboardEnterToPaste", checked) + } + } + + SettingsCard { + tab: "clipboard" + tags: ["clipboard", "advanced", "disable"] + title: I18n.tr("Advanced") + iconName: "tune" + collapsible: true + expanded: false + visible: configLoaded + + SettingsToggleRow { + tab: "clipboard" + tags: ["clipboard", "disable", "history"] + settingKey: "disabled" + text: I18n.tr("Disable History Persistence") + description: I18n.tr("Clipboard works but nothing saved to disk") + checked: root.config.disabled ?? false + onToggled: checked => root.saveConfig("disabled", checked) + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DankBarTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DankBarTab.qml new file mode 100644 index 0000000..a4bd87d --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DankBarTab.qml @@ -0,0 +1,1563 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: dankBarTab + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property var parentModal: null + property string selectedBarId: "default" + + property var selectedBarConfig: { + selectedBarId; + SettingsData.barConfigs; + const index = SettingsData.barConfigs.findIndex(cfg => cfg.id === selectedBarId); + return index !== -1 ? SettingsData.barConfigs[index] : SettingsData.barConfigs[0]; + } + + property bool selectedBarIsVertical: { + selectedBarId; + const pos = selectedBarConfig?.position ?? SettingsData.Position.Top; + return pos === SettingsData.Position.Left || pos === SettingsData.Position.Right; + } + + Timer { + id: horizontalBarChangeDebounce + interval: 500 + repeat: false + onTriggered: { + const verticalBars = SettingsData.barConfigs.filter(cfg => { + const pos = cfg.position ?? SettingsData.Position.Top; + return pos === SettingsData.Position.Left || pos === SettingsData.Position.Right; + }); + + verticalBars.forEach(bar => { + if (!bar.enabled) + return; + SettingsData.updateBarConfig(bar.id, { + enabled: false + }); + Qt.callLater(() => SettingsData.updateBarConfig(bar.id, { + enabled: true + })); + }); + } + } + + function _isBarActive(c) { + if (!c.enabled) + return false; + const prefs = c.screenPreferences || ["all"]; + if (prefs.length > 0) + return true; + return (c.showOnLastDisplay ?? true) && Quickshell.screens.length === 1; + } + + function notifyHorizontalBarChange() { + const configs = SettingsData.barConfigs; + if (configs.length < 2) + return; + + const hasHorizontal = configs.some(c => { + if (!_isBarActive(c)) + return false; + const p = c.position ?? SettingsData.Position.Top; + return p === SettingsData.Position.Top || p === SettingsData.Position.Bottom; + }); + if (!hasHorizontal) + return; + + const hasVertical = configs.some(c => { + if (!_isBarActive(c)) + return false; + const p = c.position ?? SettingsData.Position.Top; + return p === SettingsData.Position.Left || p === SettingsData.Position.Right; + }); + if (!hasVertical) + return; + + horizontalBarChangeDebounce.restart(); + } + + function createNewBar() { + if (SettingsData.barConfigs.length >= 4) + return; + const defaultBar = SettingsData.getBarConfig("default"); + if (!defaultBar) + return; + const newId = "bar" + Date.now(); + const newBar = { + id: newId, + name: "Bar " + (SettingsData.barConfigs.length + 1), + enabled: true, + position: defaultBar.position ?? 0, + screenPreferences: [], + showOnLastDisplay: false, + leftWidgets: defaultBar.leftWidgets || [], + centerWidgets: defaultBar.centerWidgets || [], + rightWidgets: defaultBar.rightWidgets || [], + spacing: defaultBar.spacing ?? 4, + innerPadding: defaultBar.innerPadding ?? 4, + bottomGap: defaultBar.bottomGap ?? 0, + transparency: defaultBar.transparency ?? 1.0, + widgetTransparency: defaultBar.widgetTransparency ?? 1.0, + squareCorners: defaultBar.squareCorners ?? false, + noBackground: defaultBar.noBackground ?? false, + gothCornersEnabled: defaultBar.gothCornersEnabled ?? false, + gothCornerRadiusOverride: defaultBar.gothCornerRadiusOverride ?? false, + gothCornerRadiusValue: defaultBar.gothCornerRadiusValue ?? 12, + borderEnabled: defaultBar.borderEnabled ?? false, + borderColor: defaultBar.borderColor || "surfaceText", + borderOpacity: defaultBar.borderOpacity ?? 1.0, + borderThickness: defaultBar.borderThickness ?? 1, + widgetOutlineEnabled: defaultBar.widgetOutlineEnabled ?? false, + widgetOutlineColor: defaultBar.widgetOutlineColor || "primary", + widgetOutlineOpacity: defaultBar.widgetOutlineOpacity ?? 1.0, + widgetOutlineThickness: defaultBar.widgetOutlineThickness ?? 1, + widgetPadding: defaultBar.widgetPadding ?? 8, + maximizeWidgetIcons: defaultBar.maximizeWidgetIcons ?? false, + maximizeWidgetText: defaultBar.maximizeWidgetText ?? false, + removeWidgetPadding: defaultBar.removeWidgetPadding ?? false, + fontScale: defaultBar.fontScale ?? 1.0, + iconScale: defaultBar.iconScale ?? 1.0, + autoHide: defaultBar.autoHide ?? false, + autoHideDelay: defaultBar.autoHideDelay ?? 250, + showOnWindowsOpen: defaultBar.showOnWindowsOpen ?? false, + openOnOverview: defaultBar.openOnOverview ?? false, + visible: defaultBar.visible ?? true, + popupGapsAuto: defaultBar.popupGapsAuto ?? true, + popupGapsManual: defaultBar.popupGapsManual ?? 4, + maximizeDetection: defaultBar.maximizeDetection ?? true, + scrollEnabled: defaultBar.scrollEnabled ?? true, + scrollXBehavior: defaultBar.scrollXBehavior ?? "column", + scrollYBehavior: defaultBar.scrollYBehavior ?? "workspace", + shadowIntensity: defaultBar.shadowIntensity ?? 0, + shadowOpacity: defaultBar.shadowOpacity ?? 60, + shadowDirectionMode: defaultBar.shadowDirectionMode ?? "inherit", + shadowDirection: defaultBar.shadowDirection ?? "top", + shadowColorMode: defaultBar.shadowColorMode ?? "default", + shadowCustomColor: defaultBar.shadowCustomColor ?? "#000000" + }; + SettingsData.addBarConfig(newBar); + selectedBarId = newId; + } + + function deleteBar(barId) { + if (barId === "default") + return; + if (SettingsData.barConfigs.length <= 1) + return; + SettingsData.deleteBarConfig(barId); + selectedBarId = "default"; + } + + function toggleBarEnabled(barId) { + if (barId === "default") + return; + const config = SettingsData.getBarConfig(barId); + if (!config) + return; + SettingsData.updateBarConfig(barId, { + enabled: !config.enabled + }); + } + + function getBarScreenPreferences(barId) { + const config = SettingsData.getBarConfig(barId); + return config?.screenPreferences || ["all"]; + } + + function setBarScreenPreferences(barId, prefs) { + SettingsData.updateBarConfig(barId, { + screenPreferences: prefs + }); + notifyHorizontalBarChange(); + } + + function getBarShowOnLastDisplay(barId) { + const config = SettingsData.getBarConfig(barId); + return config?.showOnLastDisplay ?? true; + } + + function setBarShowOnLastDisplay(barId, value) { + SettingsData.updateBarConfig(barId, { + showOnLastDisplay: value + }); + if (Quickshell.screens.length === 1) + notifyHorizontalBarChange(); + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + iconName: "dashboard" + title: I18n.tr("Bar Configurations") + settingKey: "barConfigurations" + + RowLayout { + width: parent.width + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Manage up to 4 independent bar configurations. Each bar has its own position, widgets, styling, and display assignment.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + DankButton { + text: I18n.tr("Add Bar") + iconName: "add" + buttonHeight: 32 + visible: SettingsData.barConfigs.length < 4 + onClicked: dankBarTab.createNewBar() + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: SettingsData.barConfigs + + Rectangle { + id: barCard + required property var modelData + required property int index + + width: parent.width + height: barCardContent.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: dankBarTab.selectedBarId === modelData.id ? Theme.withAlpha(Theme.primary, 0.15) : Theme.surfaceVariant + border.width: dankBarTab.selectedBarId === modelData.id ? 2 : 0 + border.color: Theme.primary + + Row { + id: barCardContent + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + Column { + width: parent.width - deleteBtn.width - Theme.spacingM + spacing: Theme.spacingXS / 2 + + StyledText { + text: barCard.modelData.name || "Bar " + (barCard.index + 1) + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: { + SettingsData.barConfigs; + const cfg = SettingsData.getBarConfig(barCard.modelData.id); + switch (cfg?.position ?? SettingsData.Position.Top) { + case SettingsData.Position.Top: + return I18n.tr("Top"); + case SettingsData.Position.Bottom: + return I18n.tr("Bottom"); + case SettingsData.Position.Left: + return I18n.tr("Left"); + case SettingsData.Position.Right: + return I18n.tr("Right"); + default: + return I18n.tr("Top"); + } + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: { + SettingsData.barConfigs; + const cfg = SettingsData.getBarConfig(barCard.modelData.id); + const prefs = cfg?.screenPreferences || ["all"]; + if (prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")) + return I18n.tr("All displays"); + return prefs.length === 1 + ? I18n.tr("%1 display").arg(prefs.length) + : I18n.tr("%1 displays").arg(prefs.length); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: { + SettingsData.barConfigs; + const cfg = SettingsData.getBarConfig(barCard.modelData.id); + const left = cfg?.leftWidgets?.length || 0; + const center = cfg?.centerWidgets?.length || 0; + const right = cfg?.rightWidgets?.length || 0; + return I18n.tr("%1 widgets").replace("%1", left + center + right); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignLeft + visible: { + SettingsData.barConfigs; + const cfg = SettingsData.getBarConfig(barCard.modelData.id); + return !cfg?.enabled && barCard.modelData.id !== "default"; + } + } + + StyledText { + text: I18n.tr("Disabled") + font.pixelSize: Theme.fontSizeSmall + color: Theme.error + horizontalAlignment: Text.AlignLeft + visible: { + SettingsData.barConfigs; + const cfg = SettingsData.getBarConfig(barCard.modelData.id); + return !cfg?.enabled && barCard.modelData.id !== "default"; + } + } + } + } + + DankActionButton { + id: deleteBtn + buttonSize: 32 + iconName: "delete" + iconSize: 16 + backgroundColor: Theme.withAlpha(Theme.error, 0.15) + iconColor: Theme.error + visible: barCard.modelData.id !== "default" + enabled: SettingsData.barConfigs.length > 1 + anchors.verticalCenter: parent.verticalCenter + onClicked: dankBarTab.deleteBar(barCard.modelData.id) + } + } + + MouseArea { + anchors.fill: parent + z: -1 + cursorShape: Qt.PointingHandCursor + onClicked: dankBarTab.selectedBarId = barCard.modelData.id + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on border.width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } + + SettingsCard { + iconName: selectedBarConfig?.enabled ? "visibility" : "visibility_off" + title: I18n.tr("Enable Bar") + visible: selectedBarId !== "default" + + SettingsToggleRow { + text: I18n.tr("Toggle visibility of this bar configuration") + checked: { + selectedBarId; + return selectedBarConfig?.enabled ?? false; + } + onToggled: toggled => dankBarTab.toggleBarEnabled(selectedBarId) + } + } + + SettingsCard { + iconName: "display_settings" + title: I18n.tr("Display Assignment") + settingKey: "barDisplay" + visible: selectedBarConfig?.enabled + + StyledText { + width: parent.width + text: I18n.tr("Configure which displays show \"%1\"").replace("%1", selectedBarConfig?.name || "this bar") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignLeft + } + + Column { + id: displayAssignmentColumn + width: parent.width + spacing: Theme.spacingS + + property bool showingAll: { + const prefs = selectedBarConfig?.screenPreferences || ["all"]; + return prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all"); + } + + SettingsToggleRow { + text: I18n.tr("All displays") + checked: displayAssignmentColumn.showingAll + onToggled: checked => { + if (checked) { + dankBarTab.setBarScreenPreferences(selectedBarId, ["all"]); + } else { + dankBarTab.setBarScreenPreferences(selectedBarId, []); + } + } + } + + SettingsToggleRow { + text: I18n.tr("Show on Last Display") + checked: selectedBarConfig?.showOnLastDisplay ?? true + visible: !displayAssignmentColumn.showingAll + onToggled: checked => dankBarTab.setBarShowOnLastDisplay(selectedBarId, checked) + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + visible: !displayAssignmentColumn.showingAll + } + + Column { + width: parent.width + spacing: Theme.spacingXS + visible: !displayAssignmentColumn.showingAll + + Repeater { + model: Quickshell.screens + + delegate: SettingsToggleRow { + id: screenToggle + required property var modelData + + text: SettingsData.getScreenDisplayName(modelData) + description: modelData.width + "×" + modelData.height + " • " + (SettingsData.displayNameMode === "system" ? (modelData.model || "Unknown Model") : modelData.name) + checked: { + const prefs = selectedBarConfig?.screenPreferences || []; + if (typeof prefs[0] === "string" && prefs[0] === "all") + return false; + return SettingsData.isScreenInPreferences(modelData, prefs); + } + onToggled: checked => { + let currentPrefs = selectedBarConfig?.screenPreferences || []; + if (typeof currentPrefs[0] === "string" && currentPrefs[0] === "all") + currentPrefs = []; + + const screenModelIndex = SettingsData.getScreenModelIndex(modelData); + + let newPrefs = currentPrefs.filter(pref => { + if (typeof pref === "string") + return false; + if (pref.modelIndex !== undefined && screenModelIndex >= 0) + return !(pref.model === modelData.model && pref.modelIndex === screenModelIndex); + return pref.name !== modelData.name || pref.model !== modelData.model; + }); + + if (checked) { + const prefObj = { + name: modelData.name, + model: modelData.model || "" + }; + if (screenModelIndex >= 0) + prefObj.modelIndex = screenModelIndex; + newPrefs.push(prefObj); + } + + dankBarTab.setBarScreenPreferences(selectedBarId, newPrefs); + } + } + } + } + } + } + + SettingsCard { + iconName: "vertical_align_center" + title: I18n.tr("Position") + settingKey: "barPosition" + visible: selectedBarConfig?.enabled + + Item { + width: parent.width + height: positionButtonGroup.height + + DankButtonGroup { + id: positionButtonGroup + anchors.horizontalCenter: parent.horizontalCenter + model: [I18n.tr("Top"), I18n.tr("Bottom"), I18n.tr("Left"), I18n.tr("Right")] + currentIndex: { + selectedBarId; + const config = SettingsData.getBarConfig(selectedBarId); + const pos = config?.position ?? 0; + switch (pos) { + case SettingsData.Position.Top: + return 0; + case SettingsData.Position.Bottom: + return 1; + case SettingsData.Position.Left: + return 2; + case SettingsData.Position.Right: + return 3; + default: + return 0; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + let newPos = 0; + switch (index) { + case 0: + newPos = SettingsData.Position.Top; + break; + case 1: + newPos = SettingsData.Position.Bottom; + break; + case 2: + newPos = SettingsData.Position.Left; + break; + case 3: + newPos = SettingsData.Position.Right; + break; + } + SettingsData.updateBarConfig(selectedBarId, { + position: newPos + }); + notifyHorizontalBarChange(); + } + } + } + } + + SettingsCard { + iconName: "visibility_off" + title: I18n.tr("Visibility") + settingKey: "barVisibility" + visible: selectedBarConfig?.enabled + + SettingsToggleRow { + text: I18n.tr("Auto-hide") + checked: selectedBarConfig?.autoHide ?? false + onToggled: toggled => { + SettingsData.updateBarConfig(selectedBarId, { + autoHide: toggled + }); + notifyHorizontalBarChange(); + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: selectedBarConfig?.autoHide ?? false + leftPadding: Theme.spacingM + + Rectangle { + width: parent.width - parent.leftPadding + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsSliderRow { + id: hideDelaySlider + width: parent.width - parent.parent.leftPadding + text: I18n.tr("Hide Delay") + value: selectedBarConfig?.autoHideDelay ?? 250 + minimum: 0 + maximum: 2000 + unit: "ms" + defaultValue: 250 + onSliderValueChanged: newValue => { + SettingsData.updateBarConfig(selectedBarId, { + autoHideDelay: newValue + }); + } + + Binding { + target: hideDelaySlider + property: "value" + value: selectedBarConfig?.autoHideDelay ?? 250 + restoreMode: Binding.RestoreBinding + } + } + + SettingsToggleRow { + width: parent.width - parent.leftPadding + visible: CompositorService.isNiri || CompositorService.isHyprland + text: I18n.tr("Hide When Windows Open") + checked: selectedBarConfig?.showOnWindowsOpen ?? false + onToggled: toggled => { + SettingsData.updateBarConfig(selectedBarId, { + showOnWindowsOpen: toggled + }); + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsToggleRow { + text: I18n.tr("Manual Show/Hide") + checked: selectedBarConfig?.visible ?? true + onToggled: toggled => { + SettingsData.updateBarConfig(selectedBarId, { + visible: toggled + }); + notifyHorizontalBarChange(); + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsToggleRow { + text: I18n.tr("Click Through") + checked: selectedBarConfig?.clickThrough ?? false + onToggled: toggled => SettingsData.updateBarConfig(selectedBarId, { + clickThrough: toggled + }) + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + visible: CompositorService.isNiri + } + + SettingsToggleRow { + visible: CompositorService.isNiri + text: I18n.tr("Show on Overview") + checked: selectedBarConfig?.openOnOverview ?? false + onToggled: toggled => { + SettingsData.updateBarConfig(selectedBarId, { + openOnOverview: toggled + }); + } + } + } + + SettingsToggleCard { + iconName: "fit_screen" + title: I18n.tr("Maximize Detection") + description: I18n.tr("Remove gaps and border when windows are maximized") + visible: selectedBarConfig?.enabled && (CompositorService.isNiri || CompositorService.isHyprland) + checked: selectedBarConfig?.maximizeDetection ?? true + onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { + maximizeDetection: checked + }) + } + + SettingsToggleCard { + iconName: "mouse" + title: I18n.tr("Scroll Wheel") + description: I18n.tr("Control workspaces and columns by scrolling on the bar") + visible: selectedBarConfig?.enabled + checked: selectedBarConfig?.scrollEnabled ?? true + onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { + scrollEnabled: checked + }) + + SettingsButtonGroupRow { + text: I18n.tr("Y Axis") + model: CompositorService.isNiri ? [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] : [I18n.tr("None"), I18n.tr("Workspace")] + currentIndex: { + switch (selectedBarConfig?.scrollYBehavior || "workspace") { + case "none": + return 0; + case "workspace": + return 1; + case "column": + return 2; + default: + return 1; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + let behavior = "workspace"; + switch (index) { + case 0: + behavior = "none"; + break; + case 1: + behavior = "workspace"; + break; + case 2: + behavior = "column"; + break; + } + SettingsData.updateBarConfig(selectedBarId, { + scrollYBehavior: behavior + }); + } + } + + SettingsButtonGroupRow { + text: I18n.tr("X Axis") + visible: CompositorService.isNiri + model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] + currentIndex: { + switch (selectedBarConfig?.scrollXBehavior || "column") { + case "none": + return 0; + case "workspace": + return 1; + case "column": + return 2; + default: + return 2; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + let behavior = "column"; + switch (index) { + case 0: + behavior = "none"; + break; + case 1: + behavior = "workspace"; + break; + case 2: + behavior = "column"; + break; + } + SettingsData.updateBarConfig(selectedBarId, { + scrollXBehavior: behavior + }); + } + } + } + + SettingsCard { + iconName: "space_bar" + title: I18n.tr("Spacing") + settingKey: "barSpacing" + visible: selectedBarConfig?.enabled + + SettingsSliderRow { + id: edgeSpacingSlider + text: I18n.tr("Edge Spacing") + value: selectedBarConfig?.spacing ?? 4 + minimum: 0 + maximum: 32 + defaultValue: 4 + onSliderDragFinished: finalValue => { + SettingsData.updateBarConfig(selectedBarId, { + spacing: finalValue + }); + } + + Binding { + target: edgeSpacingSlider + property: "value" + value: selectedBarConfig?.spacing ?? 4 + restoreMode: Binding.RestoreBinding + } + } + + SettingsSliderRow { + id: exclusiveZoneSlider + text: I18n.tr("Exclusive Zone Offset") + value: selectedBarConfig?.bottomGap ?? 0 + minimum: -50 + maximum: 50 + defaultValue: 0 + onSliderDragFinished: finalValue => { + SettingsData.updateBarConfig(selectedBarId, { + bottomGap: finalValue + }); + } + + Binding { + target: exclusiveZoneSlider + property: "value" + value: selectedBarConfig?.bottomGap ?? 0 + restoreMode: Binding.RestoreBinding + } + } + + SettingsSliderRow { + id: sizeSlider + text: I18n.tr("Size") + value: selectedBarConfig?.innerPadding ?? 4 + minimum: -8 + maximum: 24 + defaultValue: 4 + onSliderDragFinished: finalValue => { + SettingsData.updateBarConfig(selectedBarId, { + innerPadding: finalValue + }); + } + + Binding { + target: sizeSlider + property: "value" + value: selectedBarConfig?.innerPadding ?? 4 + restoreMode: Binding.RestoreBinding + } + } + + SettingsSliderRow { + id: widgetPaddingSlider + text: I18n.tr("Padding") + value: selectedBarConfig?.widgetPadding ?? 8 + minimum: 0 + maximum: 32 + unit: "px" + defaultValue: 8 + opacity: (selectedBarConfig?.removeWidgetPadding ?? false) ? 0.5 : 1.0 + enabled: !(selectedBarConfig?.removeWidgetPadding ?? false) + onSliderValueChanged: newValue => { + SettingsData.updateBarConfig(selectedBarId, { + widgetPadding: newValue + }); + } + + Binding { + target: widgetPaddingSlider + property: "value" + value: selectedBarConfig?.widgetPadding ?? 12 + restoreMode: Binding.RestoreBinding + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsToggleRow { + text: I18n.tr("Auto Popup Gaps") + checked: selectedBarConfig?.popupGapsAuto ?? true + onToggled: checked => { + SettingsData.updateBarConfig(selectedBarId, { + popupGapsAuto: checked + }); + } + } + + Column { + width: parent.width + leftPadding: Theme.spacingM + spacing: Theme.spacingM + visible: !(selectedBarConfig?.popupGapsAuto ?? true) + + Rectangle { + width: parent.width - parent.leftPadding + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsSliderRow { + id: popupGapsManualSlider + width: parent.width - parent.parent.leftPadding + text: I18n.tr("Manual Gap Size") + value: selectedBarConfig?.popupGapsManual ?? 4 + minimum: 0 + maximum: 50 + defaultValue: 4 + onSliderDragFinished: finalValue => { + SettingsData.updateBarConfig(selectedBarId, { + popupGapsManual: finalValue + }); + } + + Binding { + target: popupGapsManualSlider + property: "value" + value: selectedBarConfig?.popupGapsManual ?? 4 + restoreMode: Binding.RestoreBinding + } + } + } + } + + SettingsSliderCard { + id: fontScaleSliderCard + iconName: "text_fields" + title: I18n.tr("Font Scale") + description: I18n.tr("Scale DankBar font sizes independently") + visible: selectedBarConfig?.enabled + minimum: 50 + maximum: 200 + value: Math.round((selectedBarConfig?.fontScale ?? 1.0) * 100) + unit: "%" + defaultValue: 100 + onSliderValueChanged: newValue => { + SettingsData.updateBarConfig(selectedBarId, { + fontScale: newValue / 100 + }); + } + + Binding { + target: fontScaleSliderCard + property: "value" + value: Math.round((selectedBarConfig?.fontScale ?? 1.0) * 100) + restoreMode: Binding.RestoreBinding + } + } + + SettingsSliderCard { + id: iconScaleSliderCard + iconName: "interests" + title: I18n.tr("Icon Scale") + description: I18n.tr("Scale DankBar icon sizes independently") + visible: selectedBarConfig?.enabled + minimum: 50 + maximum: 200 + value: Math.round((selectedBarConfig?.iconScale ?? 1.0) * 100) + unit: "%" + defaultValue: 100 + onSliderValueChanged: newValue => { + SettingsData.updateBarConfig(selectedBarId, { + iconScale: newValue / 100 + }); + } + + Binding { + target: iconScaleSliderCard + property: "value" + value: Math.round((selectedBarConfig?.iconScale ?? 1.0) * 100) + restoreMode: Binding.RestoreBinding + } + } + + SettingsCard { + iconName: "opacity" + title: I18n.tr("Transparency") + settingKey: "barTransparency" + visible: selectedBarConfig?.enabled + + SettingsSliderRow { + id: barTransparencySlider + text: I18n.tr("Bar Transparency") + value: (selectedBarConfig?.transparency ?? 1.0) * 100 + minimum: 0 + maximum: 100 + unit: "%" + defaultValue: 100 + onSliderDragFinished: finalValue => { + SettingsData.updateBarConfig(selectedBarId, { + transparency: finalValue / 100 + }); + } + + Binding { + target: barTransparencySlider + property: "value" + value: (selectedBarConfig?.transparency ?? 1.0) * 100 + restoreMode: Binding.RestoreBinding + } + } + + SettingsSliderRow { + id: widgetTransparencySlider + text: I18n.tr("Widget Transparency") + value: (selectedBarConfig?.widgetTransparency ?? 1.0) * 100 + minimum: 0 + maximum: 100 + unit: "%" + defaultValue: 100 + onSliderDragFinished: finalValue => { + SettingsData.updateBarConfig(selectedBarId, { + widgetTransparency: finalValue / 100 + }); + } + + Binding { + target: widgetTransparencySlider + property: "value" + value: (selectedBarConfig?.widgetTransparency ?? 1.0) * 100 + restoreMode: Binding.RestoreBinding + } + } + } + + SettingsCard { + id: shadowCard + iconName: "layers" + title: I18n.tr("Shadow Override", "bar shadow settings card") + settingKey: "barShadow" + collapsible: true + expanded: true + visible: selectedBarConfig?.enabled + + readonly property bool shadowActive: (selectedBarConfig?.shadowIntensity ?? 0) > 0 + readonly property bool isCustomColor: (selectedBarConfig?.shadowColorMode ?? "default") === "custom" + readonly property string directionSource: selectedBarConfig?.shadowDirectionMode ?? "inherit" + + StyledText { + width: parent.width + text: I18n.tr("Enable a custom override below to set per-bar shadow intensity, opacity, and color.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignLeft + } + + SettingsToggleRow { + text: I18n.tr("Custom Shadow Override") + description: I18n.tr("Override the global shadow with per-bar settings") + checked: shadowCard.shadowActive + onToggled: checked => { + if (checked) { + SettingsData.updateBarConfig(selectedBarId, { + shadowIntensity: 12, + shadowOpacity: 60 + }); + } else { + SettingsData.updateBarConfig(selectedBarId, { + shadowIntensity: 0 + }); + } + } + } + + SettingsSliderRow { + visible: shadowCard.shadowActive + text: I18n.tr("Intensity", "shadow intensity slider") + minimum: 0 + maximum: 100 + unit: "px" + defaultValue: 12 + value: selectedBarConfig?.shadowIntensity ?? 0 + onSliderValueChanged: newValue => SettingsData.updateBarConfig(selectedBarId, { + shadowIntensity: newValue + }) + } + + SettingsSliderRow { + visible: shadowCard.shadowActive + text: I18n.tr("Opacity") + minimum: 10 + maximum: 100 + unit: "%" + defaultValue: 60 + value: selectedBarConfig?.shadowOpacity ?? 60 + onSliderValueChanged: newValue => SettingsData.updateBarConfig(selectedBarId, { + shadowOpacity: newValue + }) + } + + SettingsDropdownRow { + visible: shadowCard.shadowActive + text: I18n.tr("Direction Source", "bar shadow direction source") + description: I18n.tr("Choose how this bar resolves shadow direction") + settingKey: "barShadowDirectionSource" + options: [I18n.tr("Inherit Global (Default)", "bar shadow direction source option"), I18n.tr("Auto (Bar-aware)", "bar shadow direction source option"), I18n.tr("Manual", "bar shadow direction source option")] + currentValue: { + switch (shadowCard.directionSource) { + case "autoBar": + return I18n.tr("Auto (Bar-aware)", "bar shadow direction source option"); + case "manual": + return I18n.tr("Manual", "bar shadow direction source option"); + default: + return I18n.tr("Inherit Global (Default)", "bar shadow direction source option"); + } + } + onValueChanged: value => { + if (value === I18n.tr("Auto (Bar-aware)", "bar shadow direction source option")) { + SettingsData.updateBarConfig(selectedBarId, { + shadowDirectionMode: "autoBar" + }); + } else if (value === I18n.tr("Manual", "bar shadow direction source option")) { + SettingsData.updateBarConfig(selectedBarId, { + shadowDirectionMode: "manual" + }); + } else { + SettingsData.updateBarConfig(selectedBarId, { + shadowDirectionMode: "inherit" + }); + } + } + } + + SettingsDropdownRow { + visible: shadowCard.shadowActive && shadowCard.directionSource === "manual" + text: I18n.tr("Manual Direction", "bar manual shadow direction") + description: I18n.tr("Use a fixed shadow direction for this bar") + settingKey: "barShadowDirectionManual" + options: [I18n.tr("Top", "shadow direction option"), I18n.tr("Top Left", "shadow direction option"), I18n.tr("Top Right", "shadow direction option"), I18n.tr("Bottom", "shadow direction option")] + currentValue: { + switch (selectedBarConfig?.shadowDirection) { + case "topLeft": + return I18n.tr("Top Left", "shadow direction option"); + case "topRight": + return I18n.tr("Top Right", "shadow direction option"); + case "bottom": + return I18n.tr("Bottom", "shadow direction option"); + default: + return I18n.tr("Top", "shadow direction option"); + } + } + onValueChanged: value => { + if (value === I18n.tr("Top Left", "shadow direction option")) { + SettingsData.updateBarConfig(selectedBarId, { + shadowDirection: "topLeft" + }); + } else if (value === I18n.tr("Top Right", "shadow direction option")) { + SettingsData.updateBarConfig(selectedBarId, { + shadowDirection: "topRight" + }); + } else if (value === I18n.tr("Bottom", "shadow direction option")) { + SettingsData.updateBarConfig(selectedBarId, { + shadowDirection: "bottom" + }); + } else { + SettingsData.updateBarConfig(selectedBarId, { + shadowDirection: "top" + }); + } + } + } + + Column { + visible: shadowCard.shadowActive + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Color") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + horizontalAlignment: Text.AlignLeft + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + } + + Item { + width: parent.width + height: shadowColorGroup.implicitHeight + + DankButtonGroup { + id: shadowColorGroup + anchors.horizontalCenter: parent.horizontalCenter + buttonPadding: parent.width < 420 ? Theme.spacingXS : Theme.spacingS + minButtonWidth: parent.width < 420 ? 36 : 56 + textSize: parent.width < 420 ? Theme.fontSizeSmall : Theme.fontSizeMedium + model: [I18n.tr("Default (Black)"), I18n.tr("Surface", "shadow color option"), I18n.tr("Primary"), I18n.tr("Secondary"), I18n.tr("Custom")] + selectionMode: "single" + currentIndex: { + switch (selectedBarConfig?.shadowColorMode || "default") { + case "surface": + return 1; + case "primary": + return 2; + case "secondary": + return 3; + case "custom": + return 4; + default: + return 0; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + let mode = "default"; + switch (index) { + case 1: + mode = "surface"; + break; + case 2: + mode = "primary"; + break; + case 3: + mode = "secondary"; + break; + case 4: + mode = "custom"; + break; + } + SettingsData.updateBarConfig(selectedBarId, { + shadowColorMode: mode + }); + } + } + } + + Rectangle { + visible: selectedBarConfig?.shadowColorMode === "custom" + width: 32 + height: 32 + radius: 16 + color: selectedBarConfig?.shadowCustomColor ?? "#000000" + border.color: Theme.outline + border.width: 1 + anchors.horizontalCenter: parent.horizontalCenter + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + PopoutService.colorPickerModal.selectedColor = selectedBarConfig?.shadowCustomColor ?? "#000000"; + PopoutService.colorPickerModal.pickerTitle = I18n.tr("Color"); + PopoutService.colorPickerModal.onColorSelectedCallback = function (color) { + SettingsData.updateBarConfig(selectedBarId, { + shadowCustomColor: color.toString() + }); + }; + PopoutService.colorPickerModal.show(); + } + } + } + } + } + + SettingsCard { + iconName: "rounded_corner" + title: I18n.tr("Corners & Background") + settingKey: "barCorners" + collapsible: true + expanded: false + visible: selectedBarConfig?.enabled + + SettingsToggleRow { + text: I18n.tr("Square Corners") + checked: selectedBarConfig?.squareCorners ?? false + onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { + squareCorners: checked + }) + } + + SettingsToggleRow { + text: I18n.tr("No Background") + checked: selectedBarConfig?.noBackground ?? false + onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { + noBackground: checked + }) + } + + SettingsToggleRow { + text: I18n.tr("Maximize Widget Icons") + checked: selectedBarConfig?.maximizeWidgetIcons ?? false + onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { + maximizeWidgetIcons: checked + }) + } + + SettingsToggleRow { + text: I18n.tr("Maximize Widget Text") + checked: selectedBarConfig?.maximizeWidgetText ?? false + onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { + maximizeWidgetText: checked + }) + } + + SettingsToggleRow { + text: I18n.tr("Remove Widget Padding") + checked: selectedBarConfig?.removeWidgetPadding ?? false + onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { + removeWidgetPadding: checked + }) + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsToggleRow { + text: I18n.tr("Goth Corners") + checked: selectedBarConfig?.gothCornersEnabled ?? false + onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { + gothCornersEnabled: checked + }) + } + + SettingsToggleRow { + text: I18n.tr("Corner Radius Override") + checked: selectedBarConfig?.gothCornerRadiusOverride ?? false + visible: selectedBarConfig?.gothCornersEnabled ?? false + onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { + gothCornerRadiusOverride: checked + }) + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: (selectedBarConfig?.gothCornersEnabled ?? false) && (selectedBarConfig?.gothCornerRadiusOverride ?? false) + leftPadding: Theme.spacingM + + SettingsSliderRow { + id: gothCornerRadiusSlider + width: parent.width - parent.leftPadding + text: I18n.tr("Goth Corner Radius") + value: selectedBarConfig?.gothCornerRadiusValue ?? 12 + minimum: 0 + maximum: 64 + defaultValue: 12 + onSliderDragFinished: finalValue => { + SettingsData.updateBarConfig(selectedBarId, { + gothCornerRadiusValue: finalValue + }); + } + + Binding { + target: gothCornerRadiusSlider + property: "value" + value: selectedBarConfig?.gothCornerRadiusValue ?? 12 + restoreMode: Binding.RestoreBinding + } + } + } + } + + SettingsToggleCard { + iconName: "border_style" + title: I18n.tr("Border") + visible: selectedBarConfig?.enabled + checked: selectedBarConfig?.borderEnabled ?? false + onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { + borderEnabled: checked + }) + + SettingsButtonGroupRow { + text: I18n.tr("Color") + model: ["Surface", "Secondary", "Primary"] + currentIndex: { + switch (selectedBarConfig?.borderColor || "surfaceText") { + case "surfaceText": + return 0; + case "secondary": + return 1; + case "primary": + return 2; + default: + return 0; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + let newColor = "surfaceText"; + switch (index) { + case 0: + newColor = "surfaceText"; + break; + case 1: + newColor = "secondary"; + break; + case 2: + newColor = "primary"; + break; + } + SettingsData.updateBarConfig(selectedBarId, { + borderColor: newColor + }); + } + } + + SettingsSliderRow { + id: borderOpacitySlider + text: I18n.tr("Opacity") + value: (selectedBarConfig?.borderOpacity ?? 1.0) * 100 + minimum: 0 + maximum: 100 + unit: "%" + defaultValue: 100 + onSliderDragFinished: finalValue => { + SettingsData.updateBarConfig(selectedBarId, { + borderOpacity: finalValue / 100 + }); + } + + Binding { + target: borderOpacitySlider + property: "value" + value: (selectedBarConfig?.borderOpacity ?? 1.0) * 100 + restoreMode: Binding.RestoreBinding + } + } + + SettingsSliderRow { + id: borderThicknessSlider + text: I18n.tr("Thickness") + value: selectedBarConfig?.borderThickness ?? 1 + minimum: 1 + maximum: 10 + unit: "px" + defaultValue: 1 + onSliderDragFinished: finalValue => { + SettingsData.updateBarConfig(selectedBarId, { + borderThickness: finalValue + }); + } + + Binding { + target: borderThicknessSlider + property: "value" + value: selectedBarConfig?.borderThickness ?? 1 + restoreMode: Binding.RestoreBinding + } + } + } + + SettingsToggleCard { + iconName: "highlight" + title: I18n.tr("Widget Outline") + visible: selectedBarConfig?.enabled + checked: selectedBarConfig?.widgetOutlineEnabled ?? false + onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { + widgetOutlineEnabled: checked + }) + + SettingsButtonGroupRow { + text: I18n.tr("Color") + model: ["Surface", "Secondary", "Primary"] + currentIndex: { + switch (selectedBarConfig?.widgetOutlineColor || "primary") { + case "surfaceText": + return 0; + case "secondary": + return 1; + case "primary": + return 2; + default: + return 2; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + let newColor = "primary"; + switch (index) { + case 0: + newColor = "surfaceText"; + break; + case 1: + newColor = "secondary"; + break; + case 2: + newColor = "primary"; + break; + } + SettingsData.updateBarConfig(selectedBarId, { + widgetOutlineColor: newColor + }); + } + } + + SettingsSliderRow { + id: widgetOutlineOpacitySlider + text: I18n.tr("Opacity") + value: (selectedBarConfig?.widgetOutlineOpacity ?? 1.0) * 100 + minimum: 0 + maximum: 100 + unit: "%" + defaultValue: 100 + onSliderDragFinished: finalValue => { + SettingsData.updateBarConfig(selectedBarId, { + widgetOutlineOpacity: finalValue / 100 + }); + } + + Binding { + target: widgetOutlineOpacitySlider + property: "value" + value: (selectedBarConfig?.widgetOutlineOpacity ?? 1.0) * 100 + restoreMode: Binding.RestoreBinding + } + } + + SettingsSliderRow { + id: widgetOutlineThicknessSlider + text: I18n.tr("Thickness") + value: selectedBarConfig?.widgetOutlineThickness ?? 1 + minimum: 1 + maximum: 10 + unit: "px" + defaultValue: 1 + onSliderDragFinished: finalValue => { + SettingsData.updateBarConfig(selectedBarId, { + widgetOutlineThickness: finalValue + }); + } + + Binding { + target: widgetOutlineThicknessSlider + property: "value" + value: selectedBarConfig?.widgetOutlineThickness ?? 1 + restoreMode: Binding.RestoreBinding + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetBrowser.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetBrowser.qml new file mode 100644 index 0000000..4c23596 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetBrowser.qml @@ -0,0 +1,477 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +FloatingWindow { + id: root + + property bool disablePopupTransparency: true + property string searchQuery: "" + property var filteredWidgets: [] + property int selectedIndex: -1 + property bool keyboardNavigationActive: false + property var parentModal: null + + signal widgetAdded(string widgetType) + + function updateFilteredWidgets() { + const allWidgets = DesktopWidgetRegistry.registeredWidgetsList || []; + var filtered = []; + + if (!searchQuery || searchQuery.length === 0) { + filtered = allWidgets.slice(); + } else { + var query = searchQuery.toLowerCase(); + for (var i = 0; i < allWidgets.length; i++) { + var widget = allWidgets[i]; + var name = widget.name ? widget.name.toLowerCase() : ""; + var description = widget.description ? widget.description.toLowerCase() : ""; + var id = widget.id ? widget.id.toLowerCase() : ""; + + if (name.indexOf(query) !== -1 || description.indexOf(query) !== -1 || id.indexOf(query) !== -1) + filtered.push(widget); + } + } + + filtered.sort((a, b) => { + if (a.featured !== b.featured) + return a.featured ? -1 : 1; + return 0; + }); + + filteredWidgets = filtered; + selectedIndex = -1; + keyboardNavigationActive = false; + } + + function selectNext() { + if (filteredWidgets.length === 0) + return; + keyboardNavigationActive = true; + selectedIndex = Math.min(selectedIndex + 1, filteredWidgets.length - 1); + } + + function selectPrevious() { + if (filteredWidgets.length === 0) + return; + keyboardNavigationActive = true; + selectedIndex = Math.max(selectedIndex - 1, -1); + if (selectedIndex === -1) + keyboardNavigationActive = false; + } + + function selectWidget() { + if (selectedIndex < 0 || selectedIndex >= filteredWidgets.length) + return; + var widget = filteredWidgets[selectedIndex]; + addWidget(widget); + } + + function addWidget(widget) { + const widgetType = widget.id; + const defaultConfig = DesktopWidgetRegistry.getDefaultConfig(widgetType); + const name = widget.name || widgetType; + SettingsData.createDesktopWidgetInstance(widgetType, name, defaultConfig); + root.widgetAdded(widgetType); + root.hide(); + } + + function show() { + updateFilteredWidgets(); + if (parentModal) + parentModal.shouldHaveFocus = false; + visible = true; + Qt.callLater(() => { + searchField.forceActiveFocus(); + }); + } + + function hide() { + visible = false; + if (!parentModal) + return; + parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible); + Qt.callLater(() => { + if (parentModal && parentModal.modalFocusScope) + parentModal.modalFocusScope.forceActiveFocus(); + }); + } + + objectName: "desktopWidgetBrowser" + title: I18n.tr("Add Desktop Widget") + minimumSize: Qt.size(400, 350) + implicitWidth: 500 + implicitHeight: 550 + color: Theme.surfaceContainer + visible: false + + onVisibleChanged: { + if (visible) { + updateFilteredWidgets(); + Qt.callLater(() => { + searchField.forceActiveFocus(); + }); + return; + } + searchQuery = ""; + filteredWidgets = []; + selectedIndex = -1; + keyboardNavigationActive = false; + if (!parentModal) + return; + parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible); + Qt.callLater(() => { + if (parentModal && parentModal.modalFocusScope) + parentModal.modalFocusScope.forceActiveFocus(); + }); + } + + Connections { + target: DesktopWidgetRegistry + function onRegistryChanged() { + if (root.visible) + root.updateFilteredWidgets(); + } + } + + FocusScope { + id: widgetKeyHandler + + anchors.fill: parent + focus: true + + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Escape: + root.hide(); + event.accepted = true; + return; + case Qt.Key_Down: + root.selectNext(); + event.accepted = true; + return; + case Qt.Key_Up: + root.selectPrevious(); + event.accepted = true; + return; + case Qt.Key_Return: + case Qt.Key_Enter: + if (root.keyboardNavigationActive) { + root.selectWidget(); + } else if (root.filteredWidgets.length > 0) { + root.addWidget(root.filteredWidgets[0]); + } + event.accepted = true; + return; + } + if (event.modifiers & Qt.ControlModifier) { + switch (event.key) { + case Qt.Key_N: + case Qt.Key_J: + root.selectNext(); + event.accepted = true; + return; + case Qt.Key_P: + case Qt.Key_K: + root.selectPrevious(); + event.accepted = true; + return; + } + } + } + + Column { + anchors.fill: parent + spacing: 0 + + Item { + id: titleBar + width: parent.width + height: 48 + + MouseArea { + anchors.fill: parent + onPressed: windowControls.tryStartMove() + onDoubleClicked: windowControls.tryToggleMaximize() + } + + Rectangle { + anchors.fill: parent + color: Theme.withAlpha(Theme.surfaceContainer, 0.5) + } + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingL + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "add_circle" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Add Desktop Widget") + font.pixelSize: Theme.fontSizeXLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + DankActionButton { + visible: windowControls.supported + circular: false + iconName: root.maximized ? "fullscreen_exit" : "fullscreen" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: windowControls.tryToggleMaximize() + } + + DankActionButton { + circular: false + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: root.hide() + } + } + } + + Item { + width: parent.width + height: parent.height - titleBar.height + + Column { + id: contentColumn + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Select a widget to add to your desktop. Each widget is a separate instance with its own settings.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + width: parent.width + wrapMode: Text.WordWrap + } + + DankTextField { + id: searchField + width: parent.width + height: 48 + cornerRadius: Theme.cornerRadius + backgroundColor: Theme.surfaceContainerHigh + normalBorderColor: Theme.outlineMedium + focusedBorderColor: Theme.primary + leftIconName: "search" + leftIconSize: Theme.iconSize + leftIconColor: Theme.surfaceVariantText + leftIconFocusedColor: Theme.primary + showClearButton: true + textColor: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + placeholderText: I18n.tr("Search widgets...") + text: root.searchQuery + focus: true + ignoreLeftRightKeys: true + keyForwardTargets: [widgetKeyHandler] + onTextEdited: { + root.searchQuery = text; + root.updateFilteredWidgets(); + } + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + root.hide(); + event.accepted = true; + return; + } + if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) + event.accepted = false; + } + } + + DankListView { + id: widgetList + + width: parent.width + height: parent.height - y + spacing: Theme.spacingS + model: root.filteredWidgets + clip: true + + delegate: Rectangle { + id: delegateRoot + + required property var modelData + required property int index + + width: widgetList.width + height: 72 + radius: Theme.cornerRadius + property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex + color: isSelected ? Theme.primarySelected : widgetArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.surfaceVariant, 0.3) + border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.outline, 0.2) + border.width: isSelected ? 2 : 1 + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + Rectangle { + width: 44 + height: 44 + radius: Theme.cornerRadius + color: Theme.primarySelected + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: delegateRoot.modelData.icon || "widgets" + size: Theme.iconSize + color: Theme.primary + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + width: parent.width - 44 - Theme.iconSize - Theme.spacingM * 3 + + Row { + spacing: Theme.spacingS + + StyledText { + text: delegateRoot.modelData.name || delegateRoot.modelData.id + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + Rectangle { + visible: delegateRoot.modelData.featured || false + width: featuredWidgetRow.implicitWidth + Theme.spacingXS * 2 + height: 18 + radius: 9 + color: Theme.withAlpha(Theme.secondary, 0.15) + border.color: Theme.withAlpha(Theme.secondary, 0.4) + border.width: 1 + anchors.verticalCenter: parent.verticalCenter + + Row { + id: featuredWidgetRow + anchors.centerIn: parent + spacing: 2 + + DankIcon { + name: "star" + size: 10 + color: Theme.secondary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("featured") + font.pixelSize: Theme.fontSizeSmall - 2 + color: Theme.secondary + font.weight: Font.Medium + } + } + } + + Rectangle { + visible: delegateRoot.modelData.type === "plugin" + width: pluginLabel.implicitWidth + Theme.spacingXS * 2 + height: 18 + radius: 9 + color: Theme.withAlpha(Theme.secondary, 0.15) + anchors.verticalCenter: parent.verticalCenter + + StyledText { + id: pluginLabel + anchors.centerIn: parent + text: I18n.tr("Plugin") + font.pixelSize: Theme.fontSizeSmall - 2 + color: Theme.secondary + } + } + } + + StyledText { + text: delegateRoot.modelData.description || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + elide: Text.ElideRight + width: parent.width + wrapMode: Text.WordWrap + maximumLineCount: 2 + visible: text !== "" + } + } + + DankIcon { + name: "add" + size: Theme.iconSize - 4 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: widgetArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.addWidget(delegateRoot.modelData) + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + footer: Item { + width: widgetList.width + height: emptyText.visible ? 60 : 0 + + StyledText { + id: emptyText + visible: root.filteredWidgets.length === 0 + text: root.searchQuery.length > 0 ? I18n.tr("No widgets match your search") : I18n.tr("No widgets available") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignHCenter + anchors.centerIn: parent + } + } + } + } + } + } + } + + FloatingWindowControls { + id: windowControls + targetWindow: root + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetInstanceCard.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetInstanceCard.qml new file mode 100644 index 0000000..4852d6b --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetInstanceCard.qml @@ -0,0 +1,460 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets +import qs.Modules.Settings.DesktopWidgetSettings as DWS + +SettingsCard { + id: root + + required property var instanceData + property bool isExpanded: false + property bool confirmingDelete: false + + readonly property string instanceId: instanceData?.id ?? "" + readonly property string widgetType: instanceData?.widgetType ?? "" + readonly property var widgetDef: DesktopWidgetRegistry.getWidget(widgetType) + readonly property string widgetName: instanceData?.name ?? widgetDef?.name ?? widgetType + + signal deleteRequested + signal duplicateRequested + + property Component clockSettingsComponent: Component { + DWS.ClockSettings {} + } + + property Component systemMonitorSettingsComponent: Component { + DWS.SystemMonitorSettings {} + } + + property Component pluginSettingsComponent: Component { + DWS.PluginDesktopWidgetSettings { + instanceId: root.instanceId + instanceData: root.instanceData + widgetType: root.widgetType + widgetDef: root.widgetDef + } + } + + width: parent?.width ?? 400 + iconName: widgetDef?.icon ?? "widgets" + title: widgetName + collapsible: true + expanded: isExpanded + + onExpandedChanged: isExpanded = expanded + + headerActions: [ + DankToggle { + checked: instanceData?.enabled ?? true + onToggled: isChecked => { + if (!root.instanceId) + return; + SettingsData.updateDesktopWidgetInstance(root.instanceId, { + enabled: isChecked + }); + } + }, + DankActionButton { + id: menuButton + iconName: "more_vert" + onClicked: { + if (actionsMenu.opened) { + actionsMenu.close(); + return; + } + actionsMenu.open(); + } + + Popup { + id: actionsMenu + x: -width + parent.width + y: parent.height + Theme.spacingXS + width: 160 + padding: Theme.spacingXS + modal: false + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + onClosed: root.confirmingDelete = false + + background: Rectangle { + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.color: Theme.outlineLight + border.width: 1 + } + + contentItem: Column { + spacing: 2 + + Rectangle { + width: parent.width + height: Theme.iconSizeLarge + radius: Theme.cornerRadius + color: duplicateArea.containsMouse ? Theme.primaryHover : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "content_copy" + size: Theme.iconSizeSmall + color: Theme.surfaceText + } + + StyledText { + text: I18n.tr("Duplicate") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + } + } + + MouseArea { + id: duplicateArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + actionsMenu.close(); + root.duplicateRequested(); + } + } + } + + Rectangle { + width: parent.width + height: Theme.iconSizeLarge + radius: Theme.cornerRadius + color: deleteArea.containsMouse ? Theme.errorHover : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: root.confirmingDelete ? "warning" : "delete" + size: Theme.iconSizeSmall + color: Theme.error + } + + StyledText { + text: root.confirmingDelete ? I18n.tr("Confirm Delete") : I18n.tr("Delete") + font.pixelSize: Theme.fontSizeSmall + color: Theme.error + } + } + + MouseArea { + id: deleteArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (root.confirmingDelete) { + actionsMenu.close(); + root.deleteRequested(); + return; + } + root.confirmingDelete = true; + } + } + } + } + } + } + ] + + Column { + width: parent.width + spacing: 0 + visible: root.isExpanded + opacity: visible ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Item { + width: parent.width + height: nameRow.height + Theme.spacingM * 2 + + Row { + id: nameRow + x: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + width: parent.width - Theme.spacingM * 2 + + StyledText { + text: I18n.tr("Name") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + width: 80 + horizontalAlignment: Text.AlignLeft + } + + DankTextField { + width: parent.width - 80 - Theme.spacingM + text: root.widgetName + onEditingFinished: { + if (!root.instanceId) + return; + SettingsData.updateDesktopWidgetInstance(root.instanceId, { + name: text + }); + } + } + } + } + + SettingsDivider {} + + Item { + width: parent.width + height: groupRow.height + Theme.spacingM * 2 + visible: (SettingsData.desktopWidgetGroups || []).length > 0 + + Row { + id: groupRow + x: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + width: parent.width - Theme.spacingM * 2 + + StyledText { + text: I18n.tr("Group") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + width: 80 + horizontalAlignment: Text.AlignLeft + } + + DankDropdown { + id: groupDropdown + width: parent.width - 80 - Theme.spacingM + compactMode: true + + property var groupsData: { + const groups = SettingsData.desktopWidgetGroups || []; + const items = [ + { + value: "", + label: I18n.tr("None") + } + ]; + for (const g of groups) { + items.push({ + value: g.id, + label: g.name + }); + } + return items; + } + + options: groupsData.map(g => g.label) + currentValue: { + const currentGroup = root.instanceData?.group ?? ""; + const item = groupsData.find(g => g.value === currentGroup); + return item?.label ?? I18n.tr("None"); + } + + onValueChanged: value => { + if (!root.instanceId) + return; + const item = groupsData.find(g => g.label === value); + const groupId = item?.value ?? ""; + SettingsData.updateDesktopWidgetInstance(root.instanceId, { + group: groupId || null + }); + } + } + } + } + + SettingsDivider { + visible: (SettingsData.desktopWidgetGroups || []).length > 0 + } + + SettingsToggleRow { + text: I18n.tr("Show on Overlay") + checked: instanceData?.config?.showOnOverlay ?? false + onToggled: isChecked => { + if (!root.instanceId) + return; + SettingsData.updateDesktopWidgetInstanceConfig(root.instanceId, { + showOnOverlay: isChecked + }); + } + } + + SettingsDivider { + visible: CompositorService.isNiri + } + + SettingsToggleRow { + visible: CompositorService.isNiri + text: I18n.tr("Show on Overview") + checked: instanceData?.config?.showOnOverview ?? false + onToggled: isChecked => { + if (!root.instanceId) + return; + SettingsData.updateDesktopWidgetInstanceConfig(root.instanceId, { + showOnOverview: isChecked + }); + } + } + + SettingsDivider { + visible: CompositorService.isNiri + } + + SettingsToggleRow { + visible: CompositorService.isNiri + text: I18n.tr("Show on Overview Only") + checked: instanceData?.config?.showOnOverviewOnly ?? false + onToggled: isChecked => { + if (!root.instanceId) + return; + SettingsData.updateDesktopWidgetInstanceConfig(root.instanceId, { + showOnOverviewOnly: isChecked + }); + } + } + + SettingsDivider {} + + SettingsToggleRow { + text: I18n.tr("Click Through") + description: I18n.tr("Allow clicks to pass through the widget") + checked: instanceData?.config?.clickThrough ?? false + onToggled: isChecked => { + if (!root.instanceId) + return; + SettingsData.updateDesktopWidgetInstanceConfig(root.instanceId, { + clickThrough: isChecked + }); + } + } + + SettingsDivider {} + + SettingsToggleRow { + text: I18n.tr("Sync Position Across Screens") + description: I18n.tr("Use the same position and size on all displays") + checked: instanceData?.config?.syncPositionAcrossScreens ?? false + onToggled: isChecked => { + if (!root.instanceId) + return; + if (isChecked) + SettingsData.syncDesktopWidgetPositionToAllScreens(root.instanceId); + SettingsData.updateDesktopWidgetInstanceConfig(root.instanceId, { + syncPositionAcrossScreens: isChecked + }); + } + } + + SettingsDivider {} + + Item { + width: parent.width + height: ipcColumn.height + Theme.spacingM * 2 + + Column { + id: ipcColumn + x: Theme.spacingM + width: parent.width - Theme.spacingM * 2 + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Command") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Rectangle { + width: parent.width + height: ipcText.height + Theme.spacingS * 2 + radius: Theme.cornerRadius / 2 + color: Theme.surfaceHover + + Row { + x: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + width: parent.width - Theme.spacingS * 2 + + StyledText { + id: ipcText + text: "dms ipc call desktopWidget toggleOverlay " + root.instanceId + font.pixelSize: Theme.fontSizeSmall + font.family: Theme.monoFontFamily + color: Theme.surfaceVariantText + width: parent.width - copyBtn.width - Theme.spacingS + elide: Text.ElideMiddle + anchors.verticalCenter: parent.verticalCenter + } + + DankButton { + id: copyBtn + iconName: "content_copy" + backgroundColor: "transparent" + textColor: Theme.surfaceText + buttonHeight: 28 + horizontalPadding: 4 + anchors.verticalCenter: parent.verticalCenter + onClicked: { + Quickshell.execDetached(["dms", "cl", "copy", "dms ipc call desktopWidget toggleOverlay " + root.instanceId]); + ToastService.showInfo(I18n.tr("Copied to clipboard")); + } + } + } + } + } + } + + SettingsDivider {} + + Loader { + id: settingsLoader + width: parent.width + active: root.isExpanded && root.widgetType !== "" + + sourceComponent: { + switch (root.widgetType) { + case "desktopClock": + return clockSettingsComponent; + case "systemMonitor": + return systemMonitorSettingsComponent; + default: + return pluginSettingsComponent; + } + } + + onLoaded: { + if (!item) + return; + item.instanceId = root.instanceId; + item.instanceData = Qt.binding(() => root.instanceData); + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetSettings/ClockSettings.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetSettings/ClockSettings.qml new file mode 100644 index 0000000..7bd02b4 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetSettings/ClockSettings.qml @@ -0,0 +1,163 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Widgets +import qs.Modules.Settings.Widgets + +Column { + id: root + + property string instanceId: "" + property var instanceData: null + + readonly property var cfg: instanceData?.config ?? {} + + function updateConfig(key, value) { + if (!instanceId) + return; + var updates = {}; + updates[key] = value; + SettingsData.updateDesktopWidgetInstanceConfig(instanceId, updates); + } + + width: parent?.width ?? 400 + spacing: 0 + + SettingsDropdownRow { + text: I18n.tr("Clock Style") + options: [I18n.tr("Digital"), I18n.tr("Analog"), I18n.tr("Stacked")] + currentValue: { + switch (cfg.style) { + case "analog": + return I18n.tr("Analog"); + case "stacked": + return I18n.tr("Stacked"); + default: + return I18n.tr("Digital"); + } + } + onValueChanged: value => { + switch (value) { + case I18n.tr("Analog"): + root.updateConfig("style", "analog"); + return; + case I18n.tr("Stacked"): + root.updateConfig("style", "stacked"); + return; + default: + root.updateConfig("style", "digital"); + } + } + } + + SettingsDivider { + visible: cfg.style === "analog" + } + + SettingsToggleRow { + visible: cfg.style === "analog" + text: I18n.tr("Show Hour Numbers") + checked: cfg.showAnalogNumbers ?? false + onToggled: checked => root.updateConfig("showAnalogNumbers", checked) + } + + SettingsDivider { + visible: cfg.style === "analog" + } + + SettingsToggleRow { + visible: cfg.style === "analog" + text: I18n.tr("Show Seconds") + checked: cfg.showAnalogSeconds ?? true + onToggled: checked => root.updateConfig("showAnalogSeconds", checked) + } + + SettingsDivider { + visible: cfg.style === "digital" || cfg.style === "stacked" + } + + SettingsToggleRow { + visible: cfg.style === "digital" || cfg.style === "stacked" + text: I18n.tr("Show Seconds") + checked: cfg.showDigitalSeconds ?? false + onToggled: checked => root.updateConfig("showDigitalSeconds", checked) + } + + SettingsDivider {} + + SettingsToggleRow { + text: I18n.tr("Show Date") + checked: cfg.showDate ?? true + onToggled: checked => root.updateConfig("showDate", checked) + } + + SettingsDivider {} + + SettingsSliderRow { + text: I18n.tr("Transparency") + minimum: 0 + maximum: 100 + value: Math.round((cfg.transparency ?? 0.8) * 100) + unit: "%" + onSliderValueChanged: newValue => root.updateConfig("transparency", newValue / 100) + } + + SettingsDivider {} + + SettingsColorPicker { + colorMode: cfg.colorMode ?? "primary" + customColor: cfg.customColor ?? "#ffffff" + onColorModeSelected: mode => root.updateConfig("colorMode", mode) + onCustomColorSelected: selectedColor => root.updateConfig("customColor", selectedColor.toString()) + } + + SettingsDivider {} + + SettingsDisplayPicker { + displayPreferences: cfg.displayPreferences ?? ["all"] + onPreferencesChanged: prefs => root.updateConfig("displayPreferences", prefs) + } + + SettingsDivider {} + + Item { + width: parent.width + height: resetRow.height + Theme.spacingM * 2 + + Row { + id: resetRow + x: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankButton { + text: I18n.tr("Reset Position") + backgroundColor: Theme.surfaceHover + textColor: Theme.surfaceText + buttonHeight: 36 + onClicked: { + if (!root.instanceId) + return; + SettingsData.updateDesktopWidgetInstance(root.instanceId, { + positions: {} + }); + } + } + + DankButton { + text: I18n.tr("Reset Size") + backgroundColor: Theme.surfaceHover + textColor: Theme.surfaceText + buttonHeight: 36 + onClicked: { + if (!root.instanceId) + return; + SettingsData.updateDesktopWidgetInstance(root.instanceId, { + positions: {} + }); + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetSettings/PluginDesktopWidgetSettings.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetSettings/PluginDesktopWidgetSettings.qml new file mode 100644 index 0000000..949e33f --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetSettings/PluginDesktopWidgetSettings.qml @@ -0,0 +1,137 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Column { + id: root + + property string instanceId: "" + property var instanceData: null + property string widgetType: "" + property var widgetDef: null + + readonly property var cfg: instanceData?.config ?? {} + readonly property string settingsPath: widgetDef?.settingsComponent ?? "" + + function updateConfig(key, value) { + if (!instanceId) + return; + var updates = {}; + updates[key] = value; + SettingsData.updateDesktopWidgetInstanceConfig(instanceId, updates); + } + + QtObject { + id: instanceScopedPluginService + + readonly property var availablePlugins: PluginService.availablePlugins + readonly property var loadedPlugins: PluginService.loadedPlugins + readonly property var pluginDesktopComponents: PluginService.pluginDesktopComponents + + signal pluginDataChanged(string pluginId) + signal pluginLoaded(string pluginId) + signal pluginUnloaded(string pluginId) + + function loadPluginData(pluginId, key, defaultValue) { + const cfg = root.instanceData?.config; + if (cfg && key in cfg) + return cfg[key]; + return SettingsData.getPluginSetting(root.widgetType, key, defaultValue); + } + + function savePluginData(pluginId, key, value) { + root.updateConfig(key, value); + Qt.callLater(() => pluginDataChanged(root.widgetType)); + return true; + } + + function getPluginVariants(pluginId) { + return PluginService.getPluginVariants(pluginId); + } + + function isPluginLoaded(pluginId) { + return PluginService.isPluginLoaded(pluginId); + } + } + + width: parent?.width ?? 400 + spacing: 0 + + Loader { + id: pluginSettingsLoader + width: parent.width + active: root.settingsPath !== "" + + source: root.settingsPath + + onLoaded: { + if (!item) + return; + if (item.instanceId !== undefined) + item.instanceId = root.instanceId; + if (item.instanceData !== undefined) + item.instanceData = Qt.binding(() => root.instanceData); + if (item.pluginService !== undefined) + item.pluginService = instanceScopedPluginService; + if (item.reloadChildValues) + Qt.callLater(item.reloadChildValues); + } + } + + Column { + width: parent.width + spacing: 0 + visible: root.settingsPath === "" + + SettingsDisplayPicker { + displayPreferences: cfg.displayPreferences ?? ["all"] + onPreferencesChanged: prefs => root.updateConfig("displayPreferences", prefs) + } + + SettingsDivider {} + + Item { + width: parent.width + height: resetRow.height + Theme.spacingM * 2 + + Row { + id: resetRow + x: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankButton { + text: I18n.tr("Reset Position") + backgroundColor: Theme.surfaceHover + textColor: Theme.surfaceText + buttonHeight: 36 + onClicked: { + if (!root.instanceId) + return; + SettingsData.updateDesktopWidgetInstance(root.instanceId, { + positions: {} + }); + } + } + + DankButton { + text: I18n.tr("Reset Size") + backgroundColor: Theme.surfaceHover + textColor: Theme.surfaceText + buttonHeight: 36 + onClicked: { + if (!root.instanceId) + return; + SettingsData.updateDesktopWidgetInstance(root.instanceId, { + positions: {} + }); + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetSettings/SystemMonitorSettings.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetSettings/SystemMonitorSettings.qml new file mode 100644 index 0000000..5ff156f --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetSettings/SystemMonitorSettings.qml @@ -0,0 +1,444 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Column { + id: root + + property string instanceId: "" + property var instanceData: null + + readonly property var cfg: instanceData?.config ?? {} + + function updateConfig(key, value) { + if (!instanceId) + return; + var updates = {}; + updates[key] = value; + SettingsData.updateDesktopWidgetInstanceConfig(instanceId, updates); + } + + width: parent?.width ?? 400 + spacing: 0 + + SettingsToggleRow { + text: I18n.tr("Show Header") + checked: cfg.showHeader ?? true + onToggled: checked => root.updateConfig("showHeader", checked) + } + + SettingsDivider {} + + Item { + width: parent.width + height: graphIntervalColumn.height + Theme.spacingM * 2 + + Column { + id: graphIntervalColumn + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Graph Time Range") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + } + + DankButtonGroup { + model: ["1m", "5m", "10m", "30m"] + currentIndex: { + switch (cfg.graphInterval ?? 60) { + case 60: + return 0; + case 300: + return 1; + case 600: + return 2; + case 1800: + return 3; + default: + return 0; + } + } + buttonHeight: 32 + minButtonWidth: 48 + textSize: Theme.fontSizeSmall + checkEnabled: false + onSelectionChanged: (index, selected) => { + if (!selected) + return; + const values = [60, 300, 600, 1800]; + root.updateConfig("graphInterval", values[index]); + } + } + } + } + + SettingsDivider {} + + SettingsToggleRow { + text: I18n.tr("CPU") + checked: cfg.showCpu ?? true + onToggled: checked => root.updateConfig("showCpu", checked) + } + + SettingsDivider { + visible: cfg.showCpu ?? true + } + + SettingsToggleRow { + visible: cfg.showCpu ?? true + text: I18n.tr("CPU Graph") + checked: cfg.showCpuGraph ?? true + onToggled: checked => root.updateConfig("showCpuGraph", checked) + } + + SettingsDivider { + visible: cfg.showCpu ?? true + } + + SettingsToggleRow { + visible: cfg.showCpu ?? true + text: I18n.tr("CPU Temperature") + checked: cfg.showCpuTemp ?? true + onToggled: checked => root.updateConfig("showCpuTemp", checked) + } + + SettingsDivider {} + + SettingsToggleRow { + text: I18n.tr("GPU Temperature") + checked: cfg.showGpuTemp ?? false + onToggled: checked => root.updateConfig("showGpuTemp", checked) + } + + SettingsDivider { + visible: (cfg.showGpuTemp ?? false) && DgopService.availableGpus.length > 0 + } + + Item { + width: parent.width + height: gpuSelectColumn.height + Theme.spacingM * 2 + visible: (cfg.showGpuTemp ?? false) && DgopService.availableGpus.length > 0 + + Column { + id: gpuSelectColumn + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("GPU") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + } + + Column { + width: parent.width + spacing: Theme.spacingXS + + Repeater { + model: DgopService.availableGpus + + Rectangle { + required property var modelData + + readonly property bool isSelected: (cfg.gpuPciId ?? "") === modelData.pciId + + width: parent.width + height: 44 + radius: Theme.cornerRadius + color: isSelected ? Theme.primarySelected : Theme.surfaceHover + border.color: isSelected ? Theme.primary : "transparent" + border.width: 2 + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: "videocam" + size: Theme.iconSizeSmall + color: isSelected ? Theme.primary : Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSizeSmall - Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + + StyledText { + text: modelData.displayName || "Unknown GPU" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + width: parent.width + elide: Text.ElideRight + } + + StyledText { + text: modelData.driver || "" + font.pixelSize: Theme.fontSizeSmall - 2 + color: Theme.surfaceVariantText + visible: text !== "" + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.updateConfig("gpuPciId", modelData.pciId) + } + } + } + } + } + } + + SettingsDivider {} + + SettingsToggleRow { + text: I18n.tr("Memory") + checked: cfg.showMemory ?? true + onToggled: checked => root.updateConfig("showMemory", checked) + } + + SettingsDivider { + visible: cfg.showMemory ?? true + } + + SettingsToggleRow { + visible: cfg.showMemory ?? true + text: I18n.tr("Memory Graph") + checked: cfg.showMemoryGraph ?? true + onToggled: checked => root.updateConfig("showMemoryGraph", checked) + } + + SettingsDivider {} + + SettingsToggleRow { + text: I18n.tr("Network") + checked: cfg.showNetwork ?? true + onToggled: checked => root.updateConfig("showNetwork", checked) + } + + SettingsDivider { + visible: cfg.showNetwork ?? true + } + + SettingsToggleRow { + visible: cfg.showNetwork ?? true + text: I18n.tr("Network Graph") + checked: cfg.showNetworkGraph ?? true + onToggled: checked => root.updateConfig("showNetworkGraph", checked) + } + + SettingsDivider {} + + SettingsToggleRow { + text: I18n.tr("Disk") + checked: cfg.showDisk ?? true + onToggled: checked => root.updateConfig("showDisk", checked) + } + + SettingsDivider {} + + SettingsToggleRow { + text: I18n.tr("Top Processes") + checked: cfg.showTopProcesses ?? false + onToggled: checked => root.updateConfig("showTopProcesses", checked) + } + + SettingsDivider { + visible: cfg.showTopProcesses ?? false + } + + Item { + width: parent.width + height: topProcessesColumn.height + Theme.spacingM * 2 + visible: cfg.showTopProcesses ?? false + + Column { + id: topProcessesColumn + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + StyledText { + width: parent.width - processCountButtons.width - Theme.spacingM + text: I18n.tr("Process Count") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + DankButtonGroup { + id: processCountButtons + model: ["3", "5", "10"] + currentIndex: { + switch (cfg.topProcessCount ?? 3) { + case 3: + return 0; + case 5: + return 1; + case 10: + return 2; + default: + return 0; + } + } + buttonHeight: 32 + minButtonWidth: 36 + textSize: Theme.fontSizeSmall + checkEnabled: false + onSelectionChanged: (index, selected) => { + if (!selected) + return; + const values = [3, 5, 10]; + root.updateConfig("topProcessCount", values[index]); + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + + StyledText { + width: parent.width - sortByButtons.width - Theme.spacingM + text: I18n.tr("Sort By") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + DankButtonGroup { + id: sortByButtons + model: ["CPU", "MEM"] + currentIndex: (cfg.topProcessSortBy ?? "cpu") === "cpu" ? 0 : 1 + buttonHeight: 32 + minButtonWidth: 48 + textSize: Theme.fontSizeSmall + checkEnabled: false + onSelectionChanged: (index, selected) => { + if (!selected) + return; + root.updateConfig("topProcessSortBy", index === 0 ? "cpu" : "memory"); + } + } + } + } + } + + SettingsDivider {} + + SettingsDropdownRow { + text: I18n.tr("Layout") + options: [I18n.tr("Auto"), I18n.tr("Grid"), I18n.tr("List")] + currentValue: { + switch (cfg.layoutMode ?? "auto") { + case "grid": + return I18n.tr("Grid"); + case "list": + return I18n.tr("List"); + default: + return I18n.tr("Auto"); + } + } + onValueChanged: value => { + switch (value) { + case I18n.tr("Grid"): + root.updateConfig("layoutMode", "grid"); + return; + case I18n.tr("List"): + root.updateConfig("layoutMode", "list"); + return; + default: + root.updateConfig("layoutMode", "auto"); + } + } + } + + SettingsDivider {} + + SettingsSliderRow { + text: I18n.tr("Transparency") + minimum: 0 + maximum: 100 + value: Math.round((cfg.transparency ?? 0.8) * 100) + unit: "%" + onSliderValueChanged: newValue => root.updateConfig("transparency", newValue / 100) + } + + SettingsDivider {} + + SettingsColorPicker { + colorMode: cfg.colorMode ?? "primary" + customColor: cfg.customColor ?? "#ffffff" + onColorModeSelected: mode => root.updateConfig("colorMode", mode) + onCustomColorSelected: selectedColor => root.updateConfig("customColor", selectedColor.toString()) + } + + SettingsDivider {} + + SettingsDisplayPicker { + displayPreferences: cfg.displayPreferences ?? ["all"] + onPreferencesChanged: prefs => root.updateConfig("displayPreferences", prefs) + } + + SettingsDivider {} + + Item { + width: parent.width + height: resetRow.height + Theme.spacingM * 2 + + Row { + id: resetRow + x: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankButton { + text: I18n.tr("Reset Position") + backgroundColor: Theme.surfaceHover + textColor: Theme.surfaceText + buttonHeight: 36 + onClicked: { + if (!root.instanceId) + return; + SettingsData.updateDesktopWidgetInstance(root.instanceId, { + positions: {} + }); + } + } + + DankButton { + text: I18n.tr("Reset Size") + backgroundColor: Theme.surfaceHover + textColor: Theme.surfaceText + buttonHeight: 36 + onClicked: { + if (!root.instanceId) + return; + SettingsData.updateDesktopWidgetInstance(root.instanceId, { + positions: {} + }); + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetsTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetsTab.qml new file mode 100644 index 0000000..5fb2794 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DesktopWidgetsTab.qml @@ -0,0 +1,708 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property var expandedStates: ({}) + property var groupCollapsedStates: ({}) + property var parentModal: null + property string editingGroupId: "" + property string newGroupName: "" + + readonly property var allInstances: SettingsData.desktopWidgetInstances || [] + readonly property var allGroups: SettingsData.desktopWidgetGroups || [] + + function showWidgetBrowser() { + widgetBrowserLoader.active = true; + if (widgetBrowserLoader.item) + widgetBrowserLoader.item.show(); + } + + function showDesktopPluginBrowser() { + desktopPluginBrowserLoader.active = true; + if (desktopPluginBrowserLoader.item) + desktopPluginBrowserLoader.item.show(); + } + + LazyLoader { + id: widgetBrowserLoader + active: false + + DesktopWidgetBrowser { + parentModal: root.parentModal + onWidgetAdded: widgetType => { + ToastService.showInfo(I18n.tr("Widget added")); + } + } + } + + LazyLoader { + id: desktopPluginBrowserLoader + active: false + + PluginBrowser { + parentModal: root.parentModal + typeFilter: "desktop-widget" + } + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + width: parent.width + iconName: "widgets" + title: I18n.tr("Desktop Widgets") + + Column { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + spacing: Theme.spacingM + + StyledText { + width: parent.width + text: I18n.tr("Add and configure widgets that appear on your desktop") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignLeft + } + + Row { + spacing: Theme.spacingM + + DankButton { + text: I18n.tr("Add Widget") + iconName: "add" + onClicked: root.showWidgetBrowser() + } + + DankButton { + text: I18n.tr("Browse Plugins") + iconName: "store" + onClicked: root.showDesktopPluginBrowser() + } + } + } + } + + SettingsCard { + width: parent.width + iconName: "folder" + title: I18n.tr("Groups") + collapsible: true + expanded: root.allGroups.length > 0 + + Column { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + spacing: Theme.spacingM + + StyledText { + width: parent.width + text: I18n.tr("Organize widgets into collapsible groups") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignLeft + } + + Row { + spacing: Theme.spacingS + width: parent.width + + DankTextField { + id: newGroupField + width: parent.width - addGroupBtn.width - Theme.spacingS + placeholderText: I18n.tr("New group name...") + text: root.newGroupName + onTextChanged: root.newGroupName = text + onAccepted: { + if (!text.trim()) + return; + SettingsData.createDesktopWidgetGroup(text.trim()); + root.newGroupName = ""; + text = ""; + } + } + + DankButton { + id: addGroupBtn + iconName: "add" + text: I18n.tr("Add") + enabled: root.newGroupName.trim().length > 0 + onClicked: { + SettingsData.createDesktopWidgetGroup(root.newGroupName.trim()); + root.newGroupName = ""; + newGroupField.text = ""; + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingXS + visible: root.allGroups.length > 0 + + Repeater { + model: root.allGroups + + Rectangle { + id: groupItem + required property var modelData + required property int index + + width: parent.width + height: 40 + radius: Theme.cornerRadius + color: groupMouseArea.containsMouse ? Theme.surfaceHover : Theme.surfaceContainer + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: "folder" + size: Theme.iconSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Loader { + active: root.editingGroupId === groupItem.modelData.id + width: active ? parent.width - Theme.iconSizeSmall - deleteGroupBtn.width - Theme.spacingS * 3 : 0 + height: active ? 32 : 0 + anchors.verticalCenter: parent.verticalCenter + + sourceComponent: DankTextField { + text: groupItem.modelData.name + onAccepted: { + if (!text.trim()) + return; + SettingsData.updateDesktopWidgetGroup(groupItem.modelData.id, { + name: text.trim() + }); + root.editingGroupId = ""; + } + onEditingFinished: { + if (!text.trim()) + return; + SettingsData.updateDesktopWidgetGroup(groupItem.modelData.id, { + name: text.trim() + }); + root.editingGroupId = ""; + } + Component.onCompleted: forceActiveFocus() + } + } + + StyledText { + visible: root.editingGroupId !== groupItem.modelData.id + text: groupItem.modelData.name + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + width: parent.width - Theme.iconSizeSmall - deleteGroupBtn.width - Theme.spacingS * 3 + } + + DankActionButton { + id: deleteGroupBtn + iconName: "delete" + anchors.verticalCenter: parent.verticalCenter + onClicked: { + SettingsData.removeDesktopWidgetGroup(groupItem.modelData.id); + ToastService.showInfo(I18n.tr("Group removed")); + } + } + } + + MouseArea { + id: groupMouseArea + anchors.fill: parent + hoverEnabled: true + onDoubleClicked: root.editingGroupId = groupItem.modelData.id + } + } + } + } + } + } + + Repeater { + model: root.allGroups + + Column { + id: groupSection + required property var modelData + required property int index + + readonly property string groupId: modelData.id + readonly property var groupInstances: root.allInstances.filter(inst => inst.group === groupId) + + width: mainColumn.width + spacing: Theme.spacingM + visible: groupInstances.length > 0 + + Rectangle { + width: parent.width + height: 44 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingS + + DankIcon { + name: (root.groupCollapsedStates[groupSection.groupId] ?? false) ? "expand_more" : "expand_less" + size: Theme.iconSize + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + DankIcon { + name: "folder" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: groupSection.modelData.name + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "(" + groupSection.groupInstances.length + ")" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + var states = Object.assign({}, root.groupCollapsedStates); + states[groupSection.groupId] = !(states[groupSection.groupId] ?? false); + root.groupCollapsedStates = states; + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: !(root.groupCollapsedStates[groupSection.groupId] ?? false) + leftPadding: Theme.spacingM + + Repeater { + model: ScriptModel { + objectProp: "id" + values: groupSection.groupInstances + } + + Item { + id: groupDelegateItem + required property var modelData + required property int index + + property bool held: groupDragArea.pressed + property real originalY: y + + readonly property string instanceIdRef: modelData.id + readonly property var liveInstanceData: { + const instances = root.allInstances; + return instances.find(inst => inst.id === instanceIdRef) ?? modelData; + } + + width: groupSection.width - Theme.spacingM + height: groupCard.height + z: held ? 2 : 1 + + DesktopWidgetInstanceCard { + id: groupCard + width: parent.width + headerLeftPadding: 20 + instanceData: groupDelegateItem.liveInstanceData + isExpanded: root.expandedStates[groupDelegateItem.instanceIdRef] ?? false + + onExpandedChanged: { + if (expanded === (root.expandedStates[groupDelegateItem.instanceIdRef] ?? false)) + return; + var states = Object.assign({}, root.expandedStates); + states[groupDelegateItem.instanceIdRef] = expanded; + root.expandedStates = states; + } + + onDuplicateRequested: SettingsData.duplicateDesktopWidgetInstance(groupDelegateItem.instanceIdRef) + + onDeleteRequested: { + SettingsData.removeDesktopWidgetInstance(groupDelegateItem.instanceIdRef); + ToastService.showInfo(I18n.tr("Widget removed")); + } + } + + MouseArea { + id: groupDragArea + anchors.left: parent.left + anchors.top: parent.top + width: 40 + height: 50 + hoverEnabled: true + cursorShape: Qt.SizeVerCursor + drag.target: groupDelegateItem.held ? groupDelegateItem : undefined + drag.axis: Drag.YAxis + preventStealing: true + + onPressed: { + groupDelegateItem.z = 2; + groupDelegateItem.originalY = groupDelegateItem.y; + } + onReleased: { + groupDelegateItem.z = 1; + if (!drag.active) { + groupDelegateItem.y = groupDelegateItem.originalY; + return; + } + const spacing = Theme.spacingM; + const itemH = groupDelegateItem.height + spacing; + var newIndex = Math.round(groupDelegateItem.y / itemH); + newIndex = Math.max(0, Math.min(newIndex, groupSection.groupInstances.length - 1)); + if (newIndex !== groupDelegateItem.index) + SettingsData.reorderDesktopWidgetInstanceInGroup(groupDelegateItem.instanceIdRef, groupSection.groupId, newIndex); + groupDelegateItem.y = groupDelegateItem.originalY; + } + } + + DankIcon { + x: Theme.spacingL - 2 + y: Theme.spacingL + (Theme.iconSize / 2) - (size / 2) + name: "drag_indicator" + size: 18 + color: Theme.outline + opacity: groupDragArea.containsMouse || groupDragArea.pressed ? 1 : 0.5 + } + + Behavior on y { + enabled: !groupDragArea.pressed && !groupDragArea.drag.active + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } + } + + Column { + id: ungroupedSection + width: parent.width + spacing: Theme.spacingM + visible: ungroupedInstances.length > 0 + + readonly property var ungroupedInstances: root.allInstances.filter(inst => { + if (!inst.group) + return true; + return !root.allGroups.some(g => g.id === inst.group); + }) + + Rectangle { + width: parent.width + height: 44 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + visible: root.allGroups.length > 0 + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingS + + DankIcon { + name: (root.groupCollapsedStates["_ungrouped"] ?? false) ? "expand_more" : "expand_less" + size: Theme.iconSize + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + DankIcon { + name: "widgets" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Ungrouped") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "(" + ungroupedSection.ungroupedInstances.length + ")" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + var states = Object.assign({}, root.groupCollapsedStates); + states["_ungrouped"] = !(states["_ungrouped"] ?? false); + root.groupCollapsedStates = states; + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: !(root.groupCollapsedStates["_ungrouped"] ?? false) + leftPadding: root.allGroups.length > 0 ? Theme.spacingM : 0 + + Repeater { + model: ScriptModel { + objectProp: "id" + values: ungroupedSection.ungroupedInstances + } + + Item { + id: ungroupedDelegateItem + required property var modelData + required property int index + + property bool held: ungroupedDragArea.pressed + property real originalY: y + + readonly property string instanceIdRef: modelData.id + readonly property var liveInstanceData: { + const instances = root.allInstances; + return instances.find(inst => inst.id === instanceIdRef) ?? modelData; + } + + width: ungroupedSection.width - (root.allGroups.length > 0 ? Theme.spacingM : 0) + height: ungroupedCard.height + z: held ? 2 : 1 + + DesktopWidgetInstanceCard { + id: ungroupedCard + width: parent.width + headerLeftPadding: 20 + instanceData: ungroupedDelegateItem.liveInstanceData + isExpanded: root.expandedStates[ungroupedDelegateItem.instanceIdRef] ?? false + + onExpandedChanged: { + if (expanded === (root.expandedStates[ungroupedDelegateItem.instanceIdRef] ?? false)) + return; + var states = Object.assign({}, root.expandedStates); + states[ungroupedDelegateItem.instanceIdRef] = expanded; + root.expandedStates = states; + } + + onDuplicateRequested: SettingsData.duplicateDesktopWidgetInstance(ungroupedDelegateItem.instanceIdRef) + + onDeleteRequested: { + SettingsData.removeDesktopWidgetInstance(ungroupedDelegateItem.instanceIdRef); + ToastService.showInfo(I18n.tr("Widget removed")); + } + } + + MouseArea { + id: ungroupedDragArea + anchors.left: parent.left + anchors.top: parent.top + width: 40 + height: 50 + hoverEnabled: true + cursorShape: Qt.SizeVerCursor + drag.target: ungroupedDelegateItem.held ? ungroupedDelegateItem : undefined + drag.axis: Drag.YAxis + preventStealing: true + + onPressed: { + ungroupedDelegateItem.z = 2; + ungroupedDelegateItem.originalY = ungroupedDelegateItem.y; + } + onReleased: { + ungroupedDelegateItem.z = 1; + if (!drag.active) { + ungroupedDelegateItem.y = ungroupedDelegateItem.originalY; + return; + } + const spacing = Theme.spacingM; + const itemH = ungroupedDelegateItem.height + spacing; + var newIndex = Math.round(ungroupedDelegateItem.y / itemH); + newIndex = Math.max(0, Math.min(newIndex, ungroupedSection.ungroupedInstances.length - 1)); + if (newIndex !== ungroupedDelegateItem.index) + SettingsData.reorderDesktopWidgetInstanceInGroup(ungroupedDelegateItem.instanceIdRef, null, newIndex); + ungroupedDelegateItem.y = ungroupedDelegateItem.originalY; + } + } + + DankIcon { + x: Theme.spacingL - 2 + y: Theme.spacingL + (Theme.iconSize / 2) - (size / 2) + name: "drag_indicator" + size: 18 + color: Theme.outline + opacity: ungroupedDragArea.containsMouse || ungroupedDragArea.pressed ? 1 : 0.5 + } + + Behavior on y { + enabled: !ungroupedDragArea.pressed && !ungroupedDragArea.drag.active + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } + + StyledText { + visible: root.allInstances.length === 0 + text: I18n.tr("No widgets added. Click \"Add Widget\" to get started.") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignLeft + } + + SettingsCard { + width: parent.width + iconName: "info" + title: I18n.tr("Help") + + Column { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + Rectangle { + width: 40 + height: 40 + radius: 20 + color: Theme.primarySelected + + DankIcon { + anchors.centerIn: parent + name: "drag_pan" + size: Theme.iconSize + color: Theme.primary + } + } + + Column { + spacing: 2 + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 40 - Theme.spacingM + + StyledText { + text: I18n.tr("Move Widget") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: I18n.tr("Right-click and drag anywhere on the widget") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + + Rectangle { + width: 40 + height: 40 + radius: 20 + color: Theme.primarySelected + + DankIcon { + anchors.centerIn: parent + name: "open_in_full" + size: Theme.iconSize + color: Theme.primary + } + } + + Column { + spacing: 2 + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 40 - Theme.spacingM + + StyledText { + text: I18n.tr("Resize Widget") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: I18n.tr("Right-click and drag the bottom-right corner") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml new file mode 100644 index 0000000..b6e253b --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml @@ -0,0 +1,1837 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtCore +import QtQuick +import Quickshell +import qs.Common +import qs.Services + +Singleton { + id: root + + readonly property bool hasOutputBackend: WlrOutputService.wlrOutputAvailable + readonly property var wlrOutputs: WlrOutputService.outputs + property var outputs: ({}) + property var savedOutputs: ({}) + property var allOutputs: buildAllOutputsMap() + + property var includeStatus: ({ + "exists": false, + "included": false + }) + property bool checkingInclude: false + property bool fixingInclude: false + + property var pendingChanges: ({}) + property var pendingNiriChanges: ({}) + property var pendingHyprlandChanges: ({}) + property var originalNiriSettings: null + property var originalHyprlandSettings: null + property var originalOutputs: null + property string originalDisplayNameMode: "" + property bool formatChanged: originalDisplayNameMode !== "" && originalDisplayNameMode !== SettingsData.displayNameMode + property bool hasPendingChanges: Object.keys(pendingChanges).length > 0 || Object.keys(pendingNiriChanges).length > 0 || Object.keys(pendingHyprlandChanges).length > 0 || formatChanged + + property bool validatingConfig: false + property string validationError: "" + + property var currentOutputSet: [] + property string matchedProfile: "" + property bool profilesLoading: false + property var validatedProfiles: ({}) + property bool manualActivation: false + + signal changesApplied(var changeDescriptions) + signal changesConfirmed + signal changesReverted + signal profileActivated(string profileId, string profileName) + signal profileSaved(string profileId, string profileName) + signal profileDeleted(string profileId) + signal profileError(string message) + + function buildCurrentOutputSet() { + const connected = []; + for (const name in outputs) { + const output = outputs[name]; + connected.push(getOutputIdentifier(output, name)); + } + return connected.sort(); + } + + function getOutputIdentifier(output, outputName) { + if (SettingsData.displayNameMode === "model" && output?.make && output?.model) { + if (CompositorService.isNiri) { + const serial = output.serial || "Unknown"; + return output.make + " " + output.model + " " + serial; + } + return output.make + " " + output.model; + } + return outputName; + } + + function validateProfiles() { + const compositor = CompositorService.compositor; + const profiles = SettingsData.getDisplayProfiles(compositor); + const profilesDir = getProfilesDir(); + const ext = getProfileExtension(); + + if (!profilesDir) { + validatedProfiles = {}; + return; + } + + const profileIds = Object.keys(profiles); + if (profileIds.length === 0) { + validatedProfiles = {}; + return; + } + + const fileChecks = profileIds.map(id => profilesDir + "/" + id + ext).join(" "); + Proc.runCommand("validate-profiles", ["sh", "-c", `for f in ${fileChecks}; do [ -f "$f" ] && echo "$f"; done`], (output, exitCode) => { + const existingFiles = new Set(output.trim().split("\n").filter(f => f)); + const validated = {}; + for (const profileId of profileIds) { + const profileFile = profilesDir + "/" + profileId + ext; + if (existingFiles.has(profileFile)) + validated[profileId] = profiles[profileId]; + else + SettingsData.removeDisplayProfile(compositor, profileId); + } + validatedProfiles = validated; + matchedProfile = findMatchingProfile(); + }); + } + + function findMatchingProfile() { + const profiles = validatedProfiles; + + console.log("[Profile Match] Current outputs:", JSON.stringify(currentOutputSet)); + + let bestMatch = ""; + let bestScore = -1; + let bestUpdatedAt = 0; + + for (const profileId in profiles) { + const profile = profiles[profileId]; + const profileSet = new Set(profile.outputSet); + + console.log("[Profile Match] Checking", profile.name, "outputSet:", JSON.stringify(profile.outputSet)); + + let allCurrentPresent = true; + for (const output of currentOutputSet) { + if (!profileSet.has(output)) { + console.log("[Profile Match] - Missing output:", output); + allCurrentPresent = false; + break; + } + } + if (!allCurrentPresent) { + console.log("[Profile Match] - SKIP: not all current outputs present"); + continue; + } + + const disconnectedCount = profile.outputSet.length - currentOutputSet.length; + const score = currentOutputSet.length * 100 - disconnectedCount; + const updatedAt = profile.updatedAt || profile.createdAt || 0; + console.log("[Profile Match] - MATCH score:", score, "(disconnected:", disconnectedCount, "updatedAt:", updatedAt + ")"); + + if (score > bestScore || (score === bestScore && updatedAt > bestUpdatedAt)) { + bestScore = score; + bestMatch = profileId; + bestUpdatedAt = updatedAt; + } + } + console.log("[Profile Match] Best match:", bestMatch, "score:", bestScore); + return bestMatch; + } + + function getProfilesDir() { + const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)); + switch (CompositorService.compositor) { + case "niri": + return configDir + "/niri/dms/profiles"; + case "hyprland": + return configDir + "/hypr/dms/profiles"; + case "dwl": + return configDir + "/mango/dms/profiles"; + default: + return ""; + } + } + + function getProfileExtension() { + return CompositorService.compositor === "niri" ? ".kdl" : ".conf"; + } + + function createProfile(name) { + const compositor = CompositorService.compositor; + const profileId = "profile_" + Date.now() + "_" + Math.random().toString(36).substr(2, 6); + const outputSet = buildCurrentOutputSet(); + const now = Date.now(); + + const profileData = { + "id": profileId, + "name": name, + "outputSet": outputSet, + "createdAt": now, + "updatedAt": now + }; + + const profilesDir = getProfilesDir(); + const profileFile = profilesDir + "/" + profileId + getProfileExtension(); + const paths = getConfigPaths(); + if (!paths) { + profileError(I18n.tr("Compositor not supported")); + return; + } + + profilesLoading = true; + Proc.runCommand("create-profile-dir", ["mkdir", "-p", profilesDir], (output, exitCode) => { + if (exitCode !== 0) { + profilesLoading = false; + profileError(I18n.tr("Failed to create profiles directory")); + return; + } + Proc.runCommand("copy-profile", ["cp", "-L", paths.outputsFile, profileFile], (output2, exitCode2) => { + if (exitCode2 !== 0) { + profilesLoading = false; + profileError(I18n.tr("Failed to save profile file")); + return; + } + SettingsData.setDisplayProfile(compositor, profileId, profileData); + SettingsData.setActiveDisplayProfile(compositor, profileId); + const updated = JSON.parse(JSON.stringify(validatedProfiles)); + updated[profileId] = profileData; + validatedProfiles = updated; + Proc.runCommand("link-new-profile", ["ln", "-sf", profileFile, paths.outputsFile], () => { + profilesLoading = false; + currentOutputSet = outputSet; + matchedProfile = profileId; + profileSaved(profileId, name); + }); + }); + }); + } + + function renameProfile(profileId, newName) { + const compositor = CompositorService.compositor; + const profiles = SettingsData.getDisplayProfiles(compositor); + const profile = profiles[profileId]; + if (!profile) { + profileError(I18n.tr("Profile not found")); + return; + } + profile.name = newName; + profile.updatedAt = Date.now(); + SettingsData.setDisplayProfile(compositor, profileId, profile); + } + + function deleteProfile(profileId) { + const compositor = CompositorService.compositor; + const profilesDir = getProfilesDir(); + const profileFile = profilesDir + "/" + profileId + getProfileExtension(); + const isActive = SettingsData.getActiveDisplayProfile(compositor) === profileId; + + profilesLoading = true; + Proc.runCommand("delete-profile", ["rm", "-f", profileFile], (output, exitCode) => { + profilesLoading = false; + SettingsData.removeDisplayProfile(compositor, profileId); + if (isActive) { + SettingsData.setActiveDisplayProfile(compositor, ""); + backendWriteOutputsConfig(allOutputs); + } + const updated = JSON.parse(JSON.stringify(validatedProfiles)); + delete updated[profileId]; + validatedProfiles = updated; + matchedProfile = findMatchingProfile(); + profileDeleted(profileId); + }); + } + + function activateProfile(profileId) { + const compositor = CompositorService.compositor; + const profiles = SettingsData.getDisplayProfiles(compositor); + const profile = profiles[profileId]; + if (!profile) { + profileError(I18n.tr("Profile not found")); + return; + } + + const profilesDir = getProfilesDir(); + const profileFile = profilesDir + "/" + profileId + getProfileExtension(); + const paths = getConfigPaths(); + if (!paths) + return; + + manualActivation = true; + profilesLoading = true; + Proc.runCommand("activate-profile", ["ln", "-sf", profileFile, paths.outputsFile], (output, exitCode) => { + if (exitCode !== 0) { + profilesLoading = false; + manualActivation = false; + profileError(I18n.tr("Failed to activate profile - file not found")); + return; + } + SettingsData.setActiveDisplayProfile(compositor, profileId); + + const reloadCmd = CompositorService.isNiri ? ["niri", "msg", "action", "reload-config-or-panic"] : CompositorService.isHyprland ? ["hyprctl", "reload"] : []; + if (reloadCmd.length > 0) { + Proc.runCommand("reload-compositor", reloadCmd, (output2, exitCode2) => { + profilesLoading = false; + WlrOutputService.requestState(); + profileActivated(profileId, profile.name); + manualActivationTimer.restart(); + }); + } else { + profilesLoading = false; + profileActivated(profileId, profile.name); + manualActivationTimer.restart(); + } + }); + } + + Timer { + id: manualActivationTimer + interval: 2000 + onTriggered: root.manualActivation = false + } + + Timer { + id: autoSelectDebounceTimer + interval: 800 + onTriggered: root.doAutoSelectProfile() + } + + // ! TODO - auto profile switching is buggy on niri and other compositors, might need a longer debounce before updating output configuration idk + function autoSelectProfile() { + return; // disabled + autoSelectDebounceTimer.restart(); + } + + function doAutoSelectProfile() { + return; // disabled + if (!SettingsData.displayProfileAutoSelect || manualActivation) + return; + currentOutputSet = buildCurrentOutputSet(); + const matched = findMatchingProfile(); + matchedProfile = matched; + if (matched && matched !== SettingsData.getActiveDisplayProfile(CompositorService.compositor)) + activateProfile(matched); + } + + function deleteDisconnectedOutput(outputName) { + if (outputs[outputName]?.connected) + return; + + const updated = JSON.parse(JSON.stringify(savedOutputs)); + delete updated[outputName]; + savedOutputs = updated; + + const mergedOutputs = {}; + for (const name in outputs) + mergedOutputs[name] = outputs[name]; + for (const name in updated) + mergedOutputs[name] = updated[name]; + + backendWriteOutputsConfig(mergedOutputs); + } + + function buildAllOutputsMap() { + const result = {}; + for (const name in savedOutputs) { + result[name] = Object.assign({}, savedOutputs[name], { + "connected": false + }); + } + for (const name in outputs) { + result[name] = Object.assign({}, outputs[name], { + "connected": true + }); + } + return result; + } + + onOutputsChanged: { + allOutputs = buildAllOutputsMap(); + currentOutputSet = buildCurrentOutputSet(); + matchedProfile = findMatchingProfile(); + // ! TODO - auto profile switching disabled for now + // if (SettingsData.displayProfileAutoSelect) + // Qt.callLater(autoSelectProfile); + } + onSavedOutputsChanged: allOutputs = buildAllOutputsMap() + + Connections { + target: WlrOutputService + function onStateChanged() { + root.outputs = root.buildOutputsMap(); + root.reloadSavedOutputs(); + } + } + + Component.onCompleted: { + outputs = buildOutputsMap(); + reloadSavedOutputs(); + checkIncludeStatus(); + currentOutputSet = buildCurrentOutputSet(); + validateProfiles(); + } + + function reloadSavedOutputs() { + const paths = getConfigPaths(); + if (!paths) { + savedOutputs = {}; + return; + } + + Proc.runCommand("load-saved-outputs", ["cat", paths.outputsFile], (content, exitCode) => { + if (exitCode !== 0 || !content.trim()) { + savedOutputs = {}; + return; + } + const parsed = parseOutputsConfig(content); + const filtered = filterDisconnectedOnly(parsed); + savedOutputs = filtered; + + if (CompositorService.isHyprland) { + initHyprlandSettingsFromConfig(parsed); + syncHyprlandVrrFromConfig(parsed); + } + if (CompositorService.isNiri) + syncNiriVrrFromConfig(parsed); + }); + } + + function initHyprlandSettingsFromConfig(parsedOutputs) { + const current = JSON.parse(JSON.stringify(SettingsData.hyprlandOutputSettings)); + let changed = false; + + for (const outputName in parsedOutputs) { + const output = parsedOutputs[outputName]; + const settings = output.hyprlandSettings; + if (!settings) + continue; + + if (current[outputName]) + continue; + + const hasSettings = settings.colorManagement || settings.bitdepth || settings.sdrBrightness !== undefined || settings.sdrSaturation !== undefined; + if (!hasSettings) + continue; + + current[outputName] = {}; + if (settings.colorManagement) + current[outputName].colorManagement = settings.colorManagement; + if (settings.bitdepth) + current[outputName].bitdepth = settings.bitdepth; + if (settings.sdrBrightness !== undefined) + current[outputName].sdrBrightness = settings.sdrBrightness; + if (settings.sdrSaturation !== undefined) + current[outputName].sdrSaturation = settings.sdrSaturation; + changed = true; + } + + if (changed) { + SettingsData.hyprlandOutputSettings = current; + SettingsData.saveSettings(); + } + } + + function syncHyprlandVrrFromConfig(parsedOutputs) { + const current = JSON.parse(JSON.stringify(SettingsData.hyprlandOutputSettings)); + let changed = false; + for (const outputName in parsedOutputs) { + const settings = parsedOutputs[outputName]?.hyprlandSettings; + const fromConfig = settings?.vrrFullscreenOnly ?? false; + const stored = current[outputName]?.vrrFullscreenOnly ?? false; + if (fromConfig === stored) + continue; + if (!current[outputName]) + current[outputName] = {}; + if (fromConfig) + current[outputName].vrrFullscreenOnly = true; + else + delete current[outputName].vrrFullscreenOnly; + changed = true; + } + if (changed) { + SettingsData.hyprlandOutputSettings = current; + SettingsData.saveSettings(); + } + } + + function syncNiriVrrFromConfig(parsedOutputs) { + let changed = false; + for (const outputName in parsedOutputs) { + const output = parsedOutputs[outputName]; + const current = SettingsData.getNiriOutputSetting(outputName, "vrrOnDemand", false); + const fromConfig = output.vrr_on_demand ?? false; + if (current === fromConfig) + continue; + SettingsData.setNiriOutputSetting(outputName, "vrrOnDemand", fromConfig || undefined); + changed = true; + } + if (changed) + SettingsData.saveSettings(); + } + + function filterDisconnectedOnly(parsedOutputs) { + const result = {}; + const liveNames = Object.keys(outputs); + const liveByIdentifier = {}; + for (const name of liveNames) { + const o = outputs[name]; + if (o?.make && o?.model) { + const serial = o.serial || "Unknown"; + const id = (o.make + " " + o.model + " " + serial).trim(); + liveByIdentifier[id] = true; + liveByIdentifier[o.make + " " + o.model] = true; + } + liveByIdentifier[name] = true; + } + + for (const savedName in parsedOutputs) { + const trimmed = savedName.trim(); + if (!liveByIdentifier[trimmed]) + result[savedName] = parsedOutputs[savedName]; + } + return result; + } + + function parseOutputsConfig(content) { + switch (CompositorService.compositor) { + case "niri": + return parseNiriOutputs(content); + case "hyprland": + return parseHyprlandOutputs(content); + case "dwl": + return parseMangoOutputs(content); + default: + return {}; + } + } + + function parseNiriOutputs(content) { + const result = {}; + const outputRegex = /output\s+"([^"]+)"\s*\{([^}]*)\}/g; + let match; + while ((match = outputRegex.exec(content)) !== null) { + const name = match[1]; + const body = match[2]; + + const modeMatch = body.match(/mode\s+"(\d+)x(\d+)@([\d.]+)"/); + const posMatch = body.match(/position\s+x=(-?\d+)\s+y=(-?\d+)/); + const scaleMatch = body.match(/scale\s+([\d.]+)/); + const transformMatch = body.match(/transform\s+"([^"]+)"/); + const vrrMatch = body.match(/variable-refresh-rate/); + const vrrOnDemandMatch = body.match(/variable-refresh-rate\s+on-demand=true/); + + result[name] = { + "name": name, + "logical": { + "x": posMatch ? parseInt(posMatch[1]) : 0, + "y": posMatch ? parseInt(posMatch[2]) : 0, + "scale": scaleMatch ? parseFloat(scaleMatch[1]) : 1.0, + "transform": transformMatch ? transformMatch[1] : "Normal" + }, + "modes": modeMatch ? [ + { + "width": parseInt(modeMatch[1]), + "height": parseInt(modeMatch[2]), + "refresh_rate": Math.round(parseFloat(modeMatch[3]) * 1000) + } + ] : [], + "current_mode": 0, + "vrr_enabled": !!vrrMatch, + "vrr_on_demand": !!vrrOnDemandMatch, + "vrr_supported": true + }; + } + return result; + } + + function parseHyprlandOutputs(content) { + const result = {}; + const lines = content.split("\n"); + for (const line of lines) { + const disableMatch = line.match(/^\s*monitor\s*=\s*([^,]+),\s*disable\s*$/); + if (disableMatch) { + const name = disableMatch[1].trim(); + result[name] = { + "name": name, + "logical": { + "x": 0, + "y": 0, + "scale": 1.0, + "transform": "Normal" + }, + "modes": [], + "current_mode": -1, + "vrr_enabled": false, + "vrr_supported": false, + "hyprlandSettings": { + "disabled": true + } + }; + continue; + } + const match = line.match(/^\s*monitor\s*=\s*([^,]+),\s*(\d+)x(\d+)@([\d.]+),\s*(-?\d+)x(-?\d+),\s*([\d.]+)/); + if (!match) + continue; + const name = match[1].trim(); + const rest = line.substring(line.indexOf(match[7]) + match[7].length); + + let transform = 0, vrrMode = 0, bitdepth = undefined, cm = undefined; + let sdrBrightness = undefined, sdrSaturation = undefined; + + const transformMatch = rest.match(/,\s*transform,\s*(\d+)/); + if (transformMatch) + transform = parseInt(transformMatch[1]); + + const vrrMatch = rest.match(/,\s*vrr,\s*(\d+)/); + if (vrrMatch) + vrrMode = parseInt(vrrMatch[1]); + + const bitdepthMatch = rest.match(/,\s*bitdepth,\s*(\d+)/); + if (bitdepthMatch) + bitdepth = parseInt(bitdepthMatch[1]); + + const cmMatch = rest.match(/,\s*cm,\s*(\w+)/); + if (cmMatch) + cm = cmMatch[1]; + + const sdrBrightnessMatch = rest.match(/,\s*sdrbrightness,\s*([\d.]+)/); + if (sdrBrightnessMatch) + sdrBrightness = parseFloat(sdrBrightnessMatch[1]); + + const sdrSaturationMatch = rest.match(/,\s*sdrsaturation,\s*([\d.]+)/); + if (sdrSaturationMatch) + sdrSaturation = parseFloat(sdrSaturationMatch[1]); + + let mirror = ""; + const mirrorMatch = rest.match(/,\s*mirror,\s*([^,\s]+)/); + if (mirrorMatch) + mirror = mirrorMatch[1]; + + result[name] = { + "name": name, + "logical": { + "x": parseInt(match[5]), + "y": parseInt(match[6]), + "scale": parseFloat(match[7]), + "transform": hyprlandToTransform(transform) + }, + "modes": [ + { + "width": parseInt(match[2]), + "height": parseInt(match[3]), + "refresh_rate": Math.round(parseFloat(match[4]) * 1000) + } + ], + "current_mode": 0, + "vrr_enabled": vrrMode >= 1, + "vrr_supported": true, + "hyprlandSettings": { + "bitdepth": bitdepth, + "colorManagement": cm, + "sdrBrightness": sdrBrightness, + "sdrSaturation": sdrSaturation, + "vrrFullscreenOnly": vrrMode === 2 ? true : undefined + }, + "mirror": mirror + }; + } + return result; + } + + function hyprlandToTransform(value) { + switch (value) { + case 0: + return "Normal"; + case 1: + return "90"; + case 2: + return "180"; + case 3: + return "270"; + case 4: + return "Flipped"; + case 5: + return "Flipped90"; + case 6: + return "Flipped180"; + case 7: + return "Flipped270"; + default: + return "Normal"; + } + } + + function parseMangoOutputs(content) { + const result = {}; + const lines = content.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed.startsWith("monitorrule=")) + continue; + + const params = {}; + for (const pair of trimmed.substring("monitorrule=".length).split(",")) { + const colonIdx = pair.indexOf(":"); + if (colonIdx < 0) + continue; + params[pair.substring(0, colonIdx).trim()] = pair.substring(colonIdx + 1).trim(); + } + + const name = params.name; + if (!name) + continue; + + result[name] = { + "name": name, + "logical": { + "x": parseInt(params.x || "0"), + "y": parseInt(params.y || "0"), + "scale": parseFloat(params.scale || "1"), + "transform": mangoToTransform(parseInt(params.rr || "0")) + }, + "modes": [ + { + "width": parseInt(params.width || "1920"), + "height": parseInt(params.height || "1080"), + "refresh_rate": parseFloat(params.refresh || "60") * 1000 + } + ], + "current_mode": 0, + "vrr_enabled": parseInt(params.vrr || "0") === 1, + "vrr_supported": true + }; + } + return result; + } + + function mangoToTransform(value) { + switch (value) { + case 0: + return "Normal"; + case 1: + return "90"; + case 2: + return "180"; + case 3: + return "270"; + case 4: + return "Flipped"; + case 5: + return "Flipped90"; + case 6: + return "Flipped180"; + case 7: + return "Flipped270"; + default: + return "Normal"; + } + } + + function getConfigPaths() { + const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)); + switch (CompositorService.compositor) { + case "niri": + return { + "configFile": configDir + "/niri/config.kdl", + "outputsFile": configDir + "/niri/dms/outputs.kdl", + "grepPattern": 'include.*"dms/outputs.kdl"', + "includeLine": 'include "dms/outputs.kdl"' + }; + case "hyprland": + return { + "configFile": configDir + "/hypr/hyprland.conf", + "outputsFile": configDir + "/hypr/dms/outputs.conf", + "grepPattern": 'source.*dms/outputs.conf', + "includeLine": "source = ./dms/outputs.conf" + }; + case "dwl": + return { + "configFile": configDir + "/mango/config.conf", + "outputsFile": configDir + "/mango/dms/outputs.conf", + "grepPattern": 'source.*dms/outputs.conf', + "includeLine": "source=./dms/outputs.conf" + }; + default: + return null; + } + } + + function checkIncludeStatus() { + const compositor = CompositorService.compositor; + if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") { + includeStatus = { + "exists": false, + "included": false + }; + return; + } + + const filename = (compositor === "niri") ? "outputs.kdl" : "outputs.conf"; + const compositorArg = (compositor === "dwl") ? "mangowc" : compositor; + + checkingInclude = true; + Proc.runCommand("check-outputs-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => { + checkingInclude = false; + if (exitCode !== 0) { + includeStatus = { + "exists": false, + "included": false + }; + return; + } + try { + includeStatus = JSON.parse(output.trim()); + } catch (e) { + includeStatus = { + "exists": false, + "included": false + }; + } + }); + } + + function fixOutputsInclude() { + const paths = getConfigPaths(); + if (!paths) + return; + + fixingInclude = true; + const outputsDir = paths.outputsFile.substring(0, paths.outputsFile.lastIndexOf("/")); + const unixTime = Math.floor(Date.now() / 1000); + const backupFile = paths.configFile + ".backup" + unixTime; + + Proc.runCommand("fix-outputs-include", ["sh", "-c", `cp "${paths.configFile}" "${backupFile}" 2>/dev/null; ` + `mkdir -p "${outputsDir}" && ` + `touch "${paths.outputsFile}" && ` + `if ! grep -v '^[[:space:]]*\\(//\\|#\\)' "${paths.configFile}" 2>/dev/null | grep -q '${paths.grepPattern}'; then ` + `echo '' >> "${paths.configFile}" && ` + `echo '${paths.includeLine}' >> "${paths.configFile}"; fi`], (output, exitCode) => { + fixingInclude = false; + if (exitCode !== 0) + return; + checkIncludeStatus(); + WlrOutputService.requestState(); + }); + } + + function buildOutputsMap() { + const map = {}; + for (const output of wlrOutputs) { + const normalizedModes = (output.modes || []).map(m => ({ + "id": m.id, + "width": m.width, + "height": m.height, + "refresh_rate": m.refresh, + "preferred": m.preferred ?? false + })); + map[output.name] = { + "name": output.name, + "make": output.make || "", + "model": output.model || "", + "serial": output.serialNumber || "", + "modes": normalizedModes, + "current_mode": normalizedModes.findIndex(m => m.id === output.currentMode?.id), + "vrr_supported": output.adaptiveSyncSupported ?? false, + "vrr_enabled": output.adaptiveSync === 1, + "logical": { + "x": output.x ?? 0, + "y": output.y ?? 0, + "width": output.currentMode?.width ?? 1920, + "height": output.currentMode?.height ?? 1080, + "scale": output.scale ?? 1.0, + "transform": mapWlrTransform(output.transform) + } + }; + } + return map; + } + + function mapWlrTransform(wlrTransform) { + switch (wlrTransform) { + case 0: + return "Normal"; + case 1: + return "90"; + case 2: + return "180"; + case 3: + return "270"; + case 4: + return "Flipped"; + case 5: + return "Flipped90"; + case 6: + return "Flipped180"; + case 7: + return "Flipped270"; + default: + return "Normal"; + } + } + + function mapTransformToWlr(transform) { + switch (transform) { + case "Normal": + return 0; + case "90": + return 1; + case "180": + return 2; + case "270": + return 3; + case "Flipped": + return 4; + case "Flipped90": + return 5; + case "Flipped180": + return 6; + case "Flipped270": + return 7; + default: + return 0; + } + } + + function backendFetchOutputs() { + WlrOutputService.requestState(); + } + + function backendWriteOutputsConfig(outputsData) { + switch (CompositorService.compositor) { + case "niri": + NiriService.generateOutputsConfig(outputsData); + break; + case "hyprland": + HyprlandService.generateOutputsConfig(outputsData, buildMergedHyprlandSettings()); + break; + case "dwl": + DwlService.generateOutputsConfig(outputsData); + break; + default: + WlrOutputService.applyOutputsConfig(outputsData, outputs); + break; + } + } + + function normalizeOutputPositions(outputsData) { + const names = Object.keys(outputsData); + if (names.length === 0) + return outputsData; + + let minX = Infinity; + let minY = Infinity; + + for (const name of names) { + const output = outputsData[name]; + if (!output.logical) + continue; + minX = Math.min(minX, output.logical.x); + minY = Math.min(minY, output.logical.y); + } + + if (minX === Infinity || (minX === 0 && minY === 0)) + return outputsData; + + const normalized = JSON.parse(JSON.stringify(outputsData)); + for (const name of names) { + if (!normalized[name].logical) + continue; + normalized[name].logical.x -= minX; + normalized[name].logical.y -= minY; + } + + return normalized; + } + + function buildOutputsWithPendingChanges() { + const result = {}; + + for (const outputName in savedOutputs) { + if (!outputs[outputName]) + result[outputName] = JSON.parse(JSON.stringify(savedOutputs[outputName])); + } + + for (const outputName in outputs) { + result[outputName] = JSON.parse(JSON.stringify(outputs[outputName])); + } + + for (const outputName in pendingChanges) { + if (!result[outputName]) + continue; + const changes = pendingChanges[outputName]; + if (changes.position && result[outputName].logical) { + result[outputName].logical.x = changes.position.x; + result[outputName].logical.y = changes.position.y; + } + if (changes.mode !== undefined && result[outputName].modes) { + for (var i = 0; i < result[outputName].modes.length; i++) { + if (formatMode(result[outputName].modes[i]) === changes.mode) { + result[outputName].current_mode = i; + break; + } + } + } + if (changes.scale !== undefined && result[outputName].logical) + result[outputName].logical.scale = changes.scale; + if (changes.transform !== undefined && result[outputName].logical) + result[outputName].logical.transform = changes.transform; + if (changes.vrr !== undefined) + result[outputName].vrr_enabled = changes.vrr; + if (changes.mirror !== undefined) + result[outputName].mirror = changes.mirror; + } + return normalizeOutputPositions(result); + } + + function backendUpdateOutputPosition(outputName, x, y) { + if (!outputs || !outputs[outputName]) + return; + const updatedOutputs = {}; + for (const name in outputs) { + const output = outputs[name]; + if (name === outputName && output.logical) { + updatedOutputs[name] = JSON.parse(JSON.stringify(output)); + updatedOutputs[name].logical.x = x; + updatedOutputs[name].logical.y = y; + } else { + updatedOutputs[name] = output; + } + } + outputs = updatedOutputs; + } + + function backendUpdateOutputScale(outputName, scale) { + if (!outputs || !outputs[outputName]) + return; + const updatedOutputs = {}; + for (const name in outputs) { + const output = outputs[name]; + if (name === outputName && output.logical) { + updatedOutputs[name] = JSON.parse(JSON.stringify(output)); + updatedOutputs[name].logical.scale = scale; + } else { + updatedOutputs[name] = output; + } + } + outputs = updatedOutputs; + } + + function getOutputDisplayName(output, outputName) { + if (SettingsData.displayNameMode === "model" && output?.make && output?.model) { + if (CompositorService.isNiri) { + const serial = output.serial || "Unknown"; + return output.make + " " + output.model + " " + serial; + } + return output.make + " " + output.model; + } + return outputName; + } + + function getNiriOutputIdentifier(output, outputName) { + if (SettingsData.displayNameMode === "model" && output?.make && output?.model) { + const serial = output.serial || "Unknown"; + return output.make + " " + output.model + " " + serial; + } + return outputName; + } + + function getNiriSetting(output, outputName, key, defaultValue) { + if (!CompositorService.isNiri) + return defaultValue; + const identifier = getNiriOutputIdentifier(output, outputName); + const pending = pendingNiriChanges[identifier]; + if (pending && pending[key] !== undefined) + return pending[key]; + return SettingsData.getNiriOutputSetting(identifier, key, defaultValue); + } + + function setNiriSetting(output, outputName, key, value) { + if (!CompositorService.isNiri) + return; + initOriginalNiriSettings(); + const identifier = getNiriOutputIdentifier(output, outputName); + const newPending = JSON.parse(JSON.stringify(pendingNiriChanges)); + if (!newPending[identifier]) + newPending[identifier] = {}; + newPending[identifier][key] = value; + pendingNiriChanges = newPending; + } + + function initOriginalNiriSettings() { + if (originalNiriSettings) + return; + originalNiriSettings = JSON.parse(JSON.stringify(SettingsData.niriOutputSettings)); + } + + function getHyprlandOutputIdentifier(output, outputName) { + if (SettingsData.displayNameMode === "model" && output?.make && output?.model) + return "desc:" + output.make + " " + output.model + " " + (output?.serial || "Unknown"); + return outputName; + } + + function getHyprlandSetting(output, outputName, key, defaultValue) { + if (!CompositorService.isHyprland) + return defaultValue; + const identifier = getHyprlandOutputIdentifier(output, outputName); + const pending = pendingHyprlandChanges[identifier]; + if (pending && (key in pending)) { + const val = pending[key]; + return (val !== null && val !== undefined) ? val : defaultValue; + } + return SettingsData.getHyprlandOutputSetting(identifier, key, defaultValue); + } + + function setHyprlandSetting(output, outputName, key, value) { + if (!CompositorService.isHyprland) + return; + initOriginalHyprlandSettings(); + const identifier = getHyprlandOutputIdentifier(output, outputName); + const newPending = JSON.parse(JSON.stringify(pendingHyprlandChanges)); + if (!newPending[identifier]) + newPending[identifier] = {}; + newPending[identifier][key] = value; + pendingHyprlandChanges = newPending; + } + + function initOriginalHyprlandSettings() { + if (originalHyprlandSettings) + return; + originalHyprlandSettings = JSON.parse(JSON.stringify(SettingsData.hyprlandOutputSettings)); + } + + function initOriginalOutputs() { + if (!originalOutputs) + originalOutputs = JSON.parse(JSON.stringify(outputs)); + } + + function setPendingChange(outputName, key, value) { + initOriginalOutputs(); + const newPending = JSON.parse(JSON.stringify(pendingChanges)); + if (!newPending[outputName]) + newPending[outputName] = {}; + newPending[outputName][key] = value; + pendingChanges = newPending; + + if (key === "scale") { + recalculateAdjacentPositions(outputName, value); + backendUpdateOutputScale(outputName, value); + } + } + + function recalculateAdjacentPositions(changedOutput, newScale) { + const output = outputs[changedOutput]; + if (!output?.logical) + return; + const oldPhys = getPhysicalSize(output); + const oldLogicalW = Math.round(oldPhys.w / (output.logical.scale || 1.0)); + const newLogicalW = Math.round(oldPhys.w / newScale); + + const changedX = getPendingValue(changedOutput, "position")?.x ?? output.logical.x; + const changedY = getPendingValue(changedOutput, "position")?.y ?? output.logical.y; + + for (const name in outputs) { + if (name === changedOutput) + continue; + const other = outputs[name]; + if (!other?.logical) + continue; + const otherX = getPendingValue(name, "position")?.x ?? other.logical.x; + const otherY = getPendingValue(name, "position")?.y ?? other.logical.y; + const otherSize = getLogicalSize(other); + const otherRight = otherX + otherSize.w; + + if (Math.abs(changedX - otherRight) < 5) { + const newX = otherRight; + const newPending = JSON.parse(JSON.stringify(pendingChanges)); + if (!newPending[changedOutput]) + newPending[changedOutput] = {}; + newPending[changedOutput].position = { + "x": newX, + "y": changedY + }; + pendingChanges = newPending; + backendUpdateOutputPosition(changedOutput, newX, changedY); + return; + } + + const changedRight = changedX + oldLogicalW; + if (Math.abs(otherX - changedRight) < 5) { + const newOtherX = changedX + newLogicalW; + const newPending = JSON.parse(JSON.stringify(pendingChanges)); + if (!newPending[name]) + newPending[name] = {}; + newPending[name].position = { + "x": newOtherX, + "y": otherY + }; + pendingChanges = newPending; + backendUpdateOutputPosition(name, newOtherX, otherY); + } + } + } + + function getPendingValue(outputName, key) { + if (!pendingChanges[outputName]) + return undefined; + return pendingChanges[outputName][key]; + } + + function getEffectiveValue(outputName, key, originalValue) { + const pending = getPendingValue(outputName, key); + return pending !== undefined ? pending : originalValue; + } + + function clearPendingChanges() { + pendingChanges = {}; + pendingNiriChanges = {}; + pendingHyprlandChanges = {}; + originalOutputs = null; + originalNiriSettings = null; + originalHyprlandSettings = null; + originalDisplayNameMode = ""; + } + + function discardChanges() { + if (originalDisplayNameMode !== "") { + SettingsData.displayNameMode = originalDisplayNameMode; + SettingsData.saveSettings(); + } + backendFetchOutputs(); + clearPendingChanges(); + } + + function applyChanges() { + if (!hasPendingChanges) + return; + const changeDescriptions = []; + + if (formatChanged) { + const formatLabel = SettingsData.displayNameMode === "model" ? I18n.tr("Model") : I18n.tr("Name"); + changeDescriptions.push(I18n.tr("Config Format") + " → " + formatLabel); + } + + for (const outputName in pendingChanges) { + const changes = pendingChanges[outputName]; + if (changes.position) + changeDescriptions.push(outputName + ": " + I18n.tr("Position") + " → " + changes.position.x + ", " + changes.position.y); + if (changes.mode) + changeDescriptions.push(outputName + ": " + I18n.tr("Mode") + " → " + changes.mode); + if (changes.scale !== undefined) + changeDescriptions.push(outputName + ": " + I18n.tr("Scale") + " → " + changes.scale); + if (changes.transform) + changeDescriptions.push(outputName + ": " + I18n.tr("Transform") + " → " + getTransformLabel(changes.transform)); + if (changes.vrr !== undefined) + changeDescriptions.push(outputName + ": " + I18n.tr("VRR") + " → " + (changes.vrr ? I18n.tr("Enabled") : I18n.tr("Disabled"))); + } + + for (const outputId in pendingNiriChanges) { + const changes = pendingNiriChanges[outputId]; + if (changes.disabled !== undefined) + changeDescriptions.push(outputId + ": " + I18n.tr("Disabled") + " → " + (changes.disabled ? I18n.tr("Yes") : I18n.tr("No"))); + if (changes.vrrOnDemand !== undefined) + changeDescriptions.push(outputId + ": " + I18n.tr("VRR On-Demand") + " → " + (changes.vrrOnDemand ? I18n.tr("Enabled") : I18n.tr("Disabled"))); + if (changes.focusAtStartup !== undefined) + changeDescriptions.push(outputId + ": " + I18n.tr("Focus at Startup") + " → " + (changes.focusAtStartup ? I18n.tr("Yes") : I18n.tr("No"))); + if (changes.hotCorners !== undefined) + changeDescriptions.push(outputId + ": " + I18n.tr("Hot Corners") + " → " + I18n.tr("Modified")); + if (changes.layout !== undefined) + changeDescriptions.push(outputId + ": " + I18n.tr("Layout") + " → " + I18n.tr("Modified")); + } + + for (const outputId in pendingHyprlandChanges) { + const changes = pendingHyprlandChanges[outputId]; + if (changes.disabled !== undefined) + changeDescriptions.push(outputId + ": " + I18n.tr("Disabled") + " → " + (changes.disabled ? I18n.tr("Yes") : I18n.tr("No"))); + if (changes.bitdepth !== undefined) + changeDescriptions.push(outputId + ": " + I18n.tr("Bit Depth") + " → " + changes.bitdepth); + if (changes.colorManagement !== undefined) + changeDescriptions.push(outputId + ": " + I18n.tr("Color Management") + " → " + changes.colorManagement); + if (changes.sdrBrightness !== undefined) + changeDescriptions.push(outputId + ": " + I18n.tr("SDR Brightness") + " → " + changes.sdrBrightness); + if (changes.sdrSaturation !== undefined) + changeDescriptions.push(outputId + ": " + I18n.tr("SDR Saturation") + " → " + changes.sdrSaturation); + if (changes.supportsHdr !== undefined) + changeDescriptions.push(outputId + ": " + I18n.tr("Force HDR") + " → " + (changes.supportsHdr ? I18n.tr("Yes") : I18n.tr("No"))); + if (changes.supportsWideColor !== undefined) + changeDescriptions.push(outputId + ": " + I18n.tr("Force Wide Color") + " → " + (changes.supportsWideColor ? I18n.tr("Yes") : I18n.tr("No"))); + if (changes.vrrFullscreenOnly !== undefined) + changeDescriptions.push(outputId + ": " + I18n.tr("VRR Fullscreen Only") + " → " + (changes.vrrFullscreenOnly ? I18n.tr("Enabled") : I18n.tr("Disabled"))); + } + + if (CompositorService.isNiri) { + validateAndApplyNiriConfig(changeDescriptions); + return; + } + + changesApplied(changeDescriptions); + + if (formatChanged) + SettingsData.saveSettings(); + + if (CompositorService.isHyprland) + commitHyprlandSettingsChanges(); + + const mergedOutputs = buildOutputsWithPendingChanges(); + backendWriteOutputsConfig(mergedOutputs); + } + + function validateAndApplyNiriConfig(changeDescriptions) { + validatingConfig = true; + validationError = ""; + + const mergedOutputs = buildOutputsWithPendingChanges(); + const mergedNiriSettings = buildMergedNiriSettings(); + const configContent = generateNiriOutputsKdl(mergedOutputs, mergedNiriSettings); + + const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)); + const tempFile = configDir + "/niri/dms/.outputs-validate-tmp.kdl"; + + Proc.runCommand("niri-validate-write-tmp", ["sh", "-c", `mkdir -p "$(dirname "${tempFile}")" && cat > "${tempFile}" << 'EOF'\n${configContent}EOF`], (output, writeExitCode) => { + if (writeExitCode !== 0) { + validatingConfig = false; + validationError = I18n.tr("Failed to write temp file for validation"); + ToastService.showError(I18n.tr("Config validation failed"), validationError, "", "display-config"); + return; + } + Proc.runCommand("niri-validate-config", ["sh", "-c", `niri validate -c "${tempFile}" 2>&1`], (validateOutput, validateExitCode) => { + validatingConfig = false; + Proc.runCommand("niri-validate-cleanup", ["rm", "-f", tempFile], () => {}); + if (validateExitCode !== 0) { + validationError = validateOutput.trim() || I18n.tr("Invalid configuration"); + ToastService.showError(I18n.tr("Config validation failed"), validationError, "", "display-config"); + return; + } + changesApplied(changeDescriptions); + if (formatChanged) + SettingsData.saveSettings(); + commitNiriSettingsChanges(); + backendWriteOutputsConfig(mergedOutputs); + }); + }); + } + + function buildMergedNiriSettings() { + const merged = JSON.parse(JSON.stringify(SettingsData.niriOutputSettings)); + for (const outputId in pendingNiriChanges) { + if (!merged[outputId]) + merged[outputId] = {}; + for (const key in pendingNiriChanges[outputId]) { + merged[outputId][key] = pendingNiriChanges[outputId][key]; + } + } + return merged; + } + + function commitNiriSettingsChanges() { + for (const outputId in pendingNiriChanges) { + for (const key in pendingNiriChanges[outputId]) { + SettingsData.setNiriOutputSetting(outputId, key, pendingNiriChanges[outputId][key]); + } + } + } + + function buildMergedHyprlandSettings() { + const merged = JSON.parse(JSON.stringify(SettingsData.hyprlandOutputSettings)); + for (const outputId in pendingHyprlandChanges) { + if (!merged[outputId]) + merged[outputId] = {}; + for (const key in pendingHyprlandChanges[outputId]) { + const val = pendingHyprlandChanges[outputId][key]; + if (val === null || val === undefined) + delete merged[outputId][key]; + else + merged[outputId][key] = val; + } + } + return merged; + } + + function commitHyprlandSettingsChanges() { + for (const outputId in pendingHyprlandChanges) { + for (const key in pendingHyprlandChanges[outputId]) { + const val = pendingHyprlandChanges[outputId][key]; + if (val === null || val === undefined) + SettingsData.removeHyprlandOutputSetting(outputId, key); + else + SettingsData.setHyprlandOutputSetting(outputId, key, val); + } + } + } + + function generateNiriOutputsKdl(outputsData, niriSettings) { + let kdlContent = `// Auto-generated by DMS - do not edit manually\n\n`; + const sortedNames = Object.keys(outputsData).sort((a, b) => { + const la = outputsData[a].logical || {}; + const lb = outputsData[b].logical || {}; + return (la.x ?? 0) - (lb.x ?? 0) || (la.y ?? 0) - (lb.y ?? 0); + }); + for (const outputName of sortedNames) { + const output = outputsData[outputName]; + const identifier = getNiriOutputIdentifier(output, outputName); + const settings = niriSettings[identifier] || {}; + kdlContent += `output "${identifier}" {\n`; + if (settings.disabled) { + kdlContent += ` off\n}\n\n`; + continue; + } + if (output.current_mode !== undefined && output.modes && output.modes[output.current_mode]) { + const mode = output.modes[output.current_mode]; + kdlContent += ` mode "${mode.width}x${mode.height}@${(mode.refresh_rate / 1000).toFixed(3)}"\n`; + } + if (output.logical) { + kdlContent += ` scale ${output.logical.scale ?? 1.0}\n`; + if (output.logical.transform && output.logical.transform !== "Normal") { + const transformMap = { + "Normal": "normal", + "90": "90", + "180": "180", + "270": "270", + "Flipped": "flipped", + "Flipped90": "flipped-90", + "Flipped180": "flipped-180", + "Flipped270": "flipped-270" + }; + kdlContent += ` transform "${transformMap[output.logical.transform] || "normal"}"\n`; + } + if (output.logical.x !== undefined && output.logical.y !== undefined) + kdlContent += ` position x=${output.logical.x} y=${output.logical.y}\n`; + } + if (settings.vrrOnDemand) { + kdlContent += ` variable-refresh-rate on-demand=true\n`; + } else if (output.vrr_enabled) { + kdlContent += ` variable-refresh-rate\n`; + } + if (settings.focusAtStartup) + kdlContent += ` focus-at-startup\n`; + if (settings.backdropColor) + kdlContent += ` backdrop-color "${settings.backdropColor}"\n`; + kdlContent += generateHotCornersBlock(settings); + kdlContent += generateLayoutBlock(settings); + kdlContent += `}\n\n`; + } + return kdlContent; + } + + function generateHotCornersBlock(settings) { + if (!settings.hotCorners) + return ""; + const hc = settings.hotCorners; + if (hc.off) + return ` hot-corners {\n off\n }\n`; + const corners = hc.corners || []; + if (corners.length === 0) + return ""; + let block = ` hot-corners {\n`; + for (const corner of corners) + block += ` ${corner}\n`; + block += ` }\n`; + return block; + } + + function generateLayoutBlock(settings) { + if (!settings.layout) + return ""; + const layout = settings.layout; + const hasSettings = layout.gaps !== undefined || layout.defaultColumnWidth || layout.presetColumnWidths || layout.alwaysCenterSingleColumn !== undefined; + if (!hasSettings) + return ""; + let block = ` layout {\n`; + if (layout.gaps !== undefined) + block += ` gaps ${layout.gaps}\n`; + if (layout.defaultColumnWidth?.type === "proportion") { + const val = layout.defaultColumnWidth.value; + const formatted = Number.isInteger(val) ? val.toFixed(1) : val.toString(); + block += ` default-column-width { proportion ${formatted}; }\n`; + } + if (layout.presetColumnWidths && layout.presetColumnWidths.length > 0) { + block += ` preset-column-widths {\n`; + for (const preset of layout.presetColumnWidths) { + if (preset.type === "proportion") { + const val = preset.value; + const formatted = Number.isInteger(val) ? val.toFixed(1) : val.toString(); + block += ` proportion ${formatted}\n`; + } + } + block += ` }\n`; + } + if (layout.alwaysCenterSingleColumn !== undefined) + block += layout.alwaysCenterSingleColumn ? ` always-center-single-column\n` : ` always-center-single-column false\n`; + block += ` }\n`; + return block; + } + + function confirmChanges() { + clearPendingChanges(); + changesConfirmed(); + } + + function revertChanges() { + const hadFormatChange = originalDisplayNameMode !== ""; + const hadNiriChanges = originalNiriSettings !== null; + const hadHyprlandChanges = originalHyprlandSettings !== null; + + if (hadFormatChange) { + SettingsData.displayNameMode = originalDisplayNameMode; + SettingsData.saveSettings(); + } + + if (hadNiriChanges) { + SettingsData.niriOutputSettings = JSON.parse(JSON.stringify(originalNiriSettings)); + SettingsData.saveSettings(); + } + + if (hadHyprlandChanges) { + SettingsData.hyprlandOutputSettings = JSON.parse(JSON.stringify(originalHyprlandSettings)); + SettingsData.saveSettings(); + } + + pendingHyprlandChanges = {}; + pendingNiriChanges = {}; + + if (!originalOutputs && !hadNiriChanges && !hadHyprlandChanges) { + if (hadFormatChange) + backendWriteOutputsConfig(buildOutputsWithPendingChanges()); + clearPendingChanges(); + changesReverted(); + return; + } + + const original = originalOutputs ? JSON.parse(JSON.stringify(originalOutputs)) : buildOutputsWithPendingChanges(); + for (const name in savedOutputs) { + if (!original[name]) + original[name] = JSON.parse(JSON.stringify(savedOutputs[name])); + } + backendWriteOutputsConfig(original); + clearPendingChanges(); + if (originalOutputs) + outputs = original; + changesReverted(); + } + + function getOutputBounds() { + if (!allOutputs || Object.keys(allOutputs).length === 0) + return { + "minX": 0, + "minY": 0, + "maxX": 1920, + "maxY": 1080, + "width": 1920, + "height": 1080 + }; + + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + + for (const name in allOutputs) { + const output = allOutputs[name]; + if (!output.logical) + continue; + const x = output.logical.x; + const y = output.logical.y; + const size = getLogicalSize(output); + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x + size.w); + maxY = Math.max(maxY, y + size.h); + } + + if (minX === Infinity) + return { + "minX": 0, + "minY": 0, + "maxX": 1920, + "maxY": 1080, + "width": 1920, + "height": 1080 + }; + return { + "minX": minX, + "minY": minY, + "maxX": maxX, + "maxY": maxY, + "width": maxX - minX, + "height": maxY - minY + }; + } + + function isRotated(transform) { + switch (transform) { + case "90": + case "270": + case "Flipped90": + case "Flipped270": + return true; + default: + return false; + } + } + + function getPhysicalSize(output) { + if (!output) + return { + "w": 1920, + "h": 1080 + }; + + let w = 1920, h = 1080; + if (output.modes && output.current_mode !== undefined) { + const mode = output.modes[output.current_mode]; + if (mode) { + w = mode.width || 1920; + h = mode.height || 1080; + } + } else if (output.logical) { + const scale = output.logical.scale || 1.0; + w = Math.round((output.logical.width || 1920) * scale); + h = Math.round((output.logical.height || 1080) * scale); + } + + if (output.logical && isRotated(output.logical.transform)) + return { + "w": h, + "h": w + }; + return { + "w": w, + "h": h + }; + } + + function getLogicalSize(output) { + if (!output) + return { + "w": 1920, + "h": 1080 + }; + + const phys = getPhysicalSize(output); + const scale = output.logical?.scale || 1.0; + + return { + "w": Math.round(phys.w / scale), + "h": Math.round(phys.h / scale) + }; + } + + function checkOverlap(testName, testX, testY, testW, testH) { + for (const name in outputs) { + if (name === testName) + continue; + const output = outputs[name]; + if (!output.logical) + continue; + const x = output.logical.x; + const y = output.logical.y; + const size = getLogicalSize(output); + if (!(testX + testW <= x || testX >= x + size.w || testY + testH <= y || testY >= y + size.h)) + return true; + } + return false; + } + + function snapToEdges(testName, posX, posY, testW, testH) { + const snapThreshold = 200; + let snappedX = posX; + let snappedY = posY; + let bestXDist = snapThreshold; + let bestYDist = snapThreshold; + + for (const name in outputs) { + if (name === testName) + continue; + const output = outputs[name]; + if (!output.logical) + continue; + const x = output.logical.x; + const y = output.logical.y; + const size = getLogicalSize(output); + + const rightEdge = x + size.w; + const bottomEdge = y + size.h; + const testRight = posX + testW; + const testBottom = posY + testH; + + const xSnaps = [ + { + "val": rightEdge, + "dist": Math.abs(posX - rightEdge) + }, + { + "val": x - testW, + "dist": Math.abs(testRight - x) + }, + { + "val": x, + "dist": Math.abs(posX - x) + }, + { + "val": rightEdge - testW, + "dist": Math.abs(testRight - rightEdge) + } + ]; + + const ySnaps = [ + { + "val": bottomEdge, + "dist": Math.abs(posY - bottomEdge) + }, + { + "val": y - testH, + "dist": Math.abs(testBottom - y) + }, + { + "val": y, + "dist": Math.abs(posY - y) + }, + { + "val": bottomEdge - testH, + "dist": Math.abs(testBottom - bottomEdge) + } + ]; + + for (const snap of xSnaps) { + if (snap.dist < bestXDist) { + bestXDist = snap.dist; + snappedX = snap.val; + } + } + + for (const snap of ySnaps) { + if (snap.dist < bestYDist) { + bestYDist = snap.dist; + snappedY = snap.val; + } + } + } + + if (checkOverlap(testName, snappedX, snappedY, testW, testH)) { + if (!checkOverlap(testName, snappedX, posY, testW, testH)) + return Qt.point(snappedX, posY); + if (!checkOverlap(testName, posX, snappedY, testW, testH)) + return Qt.point(posX, snappedY); + return Qt.point(posX, posY); + } + return Qt.point(snappedX, snappedY); + } + + function findBestSnapPosition(testName, posX, posY, testW, testH) { + const outputNames = Object.keys(outputs).filter(n => n !== testName); + + if (outputNames.length === 0) + return Qt.point(posX, posY); + + let bestPos = null; + let bestDist = Infinity; + + for (const name of outputNames) { + const output = outputs[name]; + if (!output.logical) + continue; + const x = output.logical.x; + const y = output.logical.y; + const size = getLogicalSize(output); + + const candidates = [ + { + "px": x + size.w, + "py": y + }, + { + "px": x - testW, + "py": y + }, + { + "px": x, + "py": y + size.h + }, + { + "px": x, + "py": y - testH + }, + { + "px": x + size.w, + "py": y + size.h - testH + }, + { + "px": x - testW, + "py": y + size.h - testH + }, + { + "px": x + size.w - testW, + "py": y + size.h + }, + { + "px": x + size.w - testW, + "py": y - testH + } + ]; + + for (const c of candidates) { + if (checkOverlap(testName, c.px, c.py, testW, testH)) + continue; + const dist = Math.hypot(c.px - posX, c.py - posY); + if (dist < bestDist) { + bestDist = dist; + bestPos = Qt.point(c.px, c.py); + } + } + } + + return bestPos || Qt.point(posX, posY); + } + + function formatMode(mode) { + if (!mode) + return ""; + return mode.width + "x" + mode.height + "@" + (mode.refresh_rate / 1000).toFixed(3); + } + + function getTransformLabel(transform) { + switch (transform) { + case "Normal": + return I18n.tr("Normal"); + case "90": + return I18n.tr("90°"); + case "180": + return I18n.tr("180°"); + case "270": + return I18n.tr("270°"); + case "Flipped": + return I18n.tr("Flipped"); + case "Flipped90": + return I18n.tr("Flipped 90°"); + case "Flipped180": + return I18n.tr("Flipped 180°"); + case "Flipped270": + return I18n.tr("Flipped 270°"); + default: + return I18n.tr("Normal"); + } + } + + function getTransformValue(label) { + if (label === I18n.tr("Normal")) + return "Normal"; + if (label === I18n.tr("90°")) + return "90"; + if (label === I18n.tr("180°")) + return "180"; + if (label === I18n.tr("270°")) + return "270"; + if (label === I18n.tr("Flipped")) + return "Flipped"; + if (label === I18n.tr("Flipped 90°")) + return "Flipped90"; + if (label === I18n.tr("Flipped 180°")) + return "Flipped180"; + if (label === I18n.tr("Flipped 270°")) + return "Flipped270"; + return "Normal"; + } + + function setOriginalDisplayNameMode(mode) { + if (originalDisplayNameMode === "") + originalDisplayNameMode = mode; + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/HyprlandOutputSettings.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/HyprlandOutputSettings.qml new file mode 100644 index 0000000..7c36e4b --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/HyprlandOutputSettings.qml @@ -0,0 +1,310 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Column { + id: root + + property string outputName: "" + property var outputData: null + property bool expanded: false + + width: parent.width + spacing: 0 + + Rectangle { + width: parent.width + height: headerRow.implicitHeight + Theme.spacingS * 2 + color: headerMouse.containsMouse ? Theme.withAlpha(Theme.primary, 0.1) : "transparent" + radius: Theme.cornerRadius / 2 + + Row { + id: headerRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: root.expanded ? "expand_more" : "chevron_right" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Compositor Settings") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: headerMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.expanded = !root.expanded + } + } + + Column { + id: settingsColumn + width: parent.width + spacing: Theme.spacingS + visible: root.expanded + topPadding: Theme.spacingS + + property int currentBitdepth: { + DisplayConfigState.pendingHyprlandChanges; + return DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "bitdepth", 8); + } + property bool is10Bit: currentBitdepth === 10 + + property string currentCm: { + DisplayConfigState.pendingHyprlandChanges; + return DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "colorManagement", "auto"); + } + property bool isHdrMode: currentCm === "hdr" || currentCm === "hdredid" + + DankToggle { + width: parent.width + text: I18n.tr("Disable Output") + checked: DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "disabled", false) + onToggled: checked => DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "disabled", checked) + } + + DankDropdown { + width: parent.width + text: I18n.tr("Mirror Display") + addHorizontalPadding: true + + property var otherOutputs: { + const list = [I18n.tr("None")]; + for (const name in DisplayConfigState.outputs) { + if (name !== root.outputName) + list.push(name); + } + return list; + } + options: otherOutputs + + currentValue: { + DisplayConfigState.pendingChanges; + const pending = DisplayConfigState.getPendingValue(root.outputName, "mirror"); + const val = pending !== undefined ? pending : (root.outputData.mirror || ""); + return val === "" ? I18n.tr("None") : val; + } + + onValueChanged: value => { + const realVal = value === I18n.tr("None") ? "" : value; + DisplayConfigState.setPendingChange(root.outputName, "mirror", realVal); + } + } + + DankToggle { + width: parent.width + text: I18n.tr("10-bit Color") + description: I18n.tr("Enable 10-bit color depth for wider color gamut and HDR support") + checked: settingsColumn.is10Bit + onToggled: checked => { + if (checked) { + DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "bitdepth", 10); + } else { + DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "bitdepth", null); + if (settingsColumn.isHdrMode) + DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "colorManagement", "auto"); + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: settingsColumn.is10Bit + + Rectangle { + width: parent.width + height: 1 + color: Theme.withAlpha(Theme.outline, 0.15) + } + + DankDropdown { + width: parent.width + text: I18n.tr("Color Gamut") + addHorizontalPadding: true + currentValue: { + DisplayConfigState.pendingHyprlandChanges; + const val = DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "colorManagement", "auto"); + return cmLabelMap[val] || I18n.tr("Auto (Wide)"); + } + options: [I18n.tr("Auto (Wide)"), I18n.tr("Wide (BT2020)"), "DCI-P3", "Apple P3", "Adobe RGB", "EDID", "HDR", I18n.tr("HDR (EDID)")] + + property var cmValueMap: ({ + [I18n.tr("Auto (Wide)")]: "auto", + [I18n.tr("Wide (BT2020)")]: "wide", + "DCI-P3": "dcip3", + "Apple P3": "dp3", + "Adobe RGB": "adobe", + "EDID": "edid", + "HDR": "hdr", + [I18n.tr("HDR (EDID)")]: "hdredid" + }) + + property var cmLabelMap: ({ + "auto": I18n.tr("Auto (Wide)"), + "wide": I18n.tr("Wide (BT2020)"), + "dcip3": "DCI-P3", + "dp3": "Apple P3", + "adobe": "Adobe RGB", + "edid": "EDID", + "hdr": "HDR", + "hdredid": I18n.tr("HDR (EDID)") + }) + + onValueChanged: value => { + const cmValue = cmValueMap[value] || "auto"; + DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "colorManagement", cmValue); + } + } + + Rectangle { + width: parent.width - Theme.spacingM * 2 + anchors.horizontalCenter: parent.horizontalCenter + height: warningColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius / 2 + color: Theme.withAlpha(Theme.warning, 0.15) + border.color: Theme.withAlpha(Theme.warning, 0.3) + border.width: 1 + visible: settingsColumn.isHdrMode + + Column { + id: warningColumn + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingXS + + Row { + spacing: Theme.spacingS + DankIcon { + name: "warning" + size: Theme.iconSize - 4 + color: Theme.warning + anchors.verticalCenter: parent.verticalCenter + } + StyledText { + text: I18n.tr("Experimental Feature") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.warning + anchors.verticalCenter: parent.verticalCenter + } + } + StyledText { + text: I18n.tr("HDR mode is experimental. Verify your monitor supports HDR before enabling.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: settingsColumn.isHdrMode + + Rectangle { + width: parent.width + height: 1 + color: Theme.withAlpha(Theme.outline, 0.15) + } + + StyledText { + text: I18n.tr("HDR Tone Mapping") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceVariantText + leftPadding: Theme.spacingM + } + + Row { + width: parent.width - Theme.spacingM * 2 + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingM + + Column { + width: (parent.width - Theme.spacingM) / 2 + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("SDR Brightness") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + DankTextField { + width: parent.width + height: 40 + placeholderText: "1.0 - 2.0" + text: { + DisplayConfigState.pendingHyprlandChanges; + const val = DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "sdrBrightness", null); + return val !== null ? val.toString() : ""; + } + onEditingFinished: { + const trimmed = text.trim(); + if (!trimmed) { + DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "sdrBrightness", null); + return; + } + const val = parseFloat(trimmed); + if (isNaN(val) || val < 0.1 || val > 5.0) + return; + DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "sdrBrightness", parseFloat(val.toFixed(2))); + } + } + } + + Column { + width: (parent.width - Theme.spacingM) / 2 + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("SDR Saturation") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + DankTextField { + width: parent.width + height: 40 + placeholderText: "0.5 - 1.5" + text: { + DisplayConfigState.pendingHyprlandChanges; + const val = DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "sdrSaturation", null); + return val !== null ? val.toString() : ""; + } + onEditingFinished: { + const trimmed = text.trim(); + if (!trimmed) { + DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "sdrSaturation", null); + return; + } + const val = parseFloat(trimmed); + if (isNaN(val) || val < 0.0 || val > 3.0) + return; + DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "sdrSaturation", parseFloat(val.toFixed(2))); + } + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/IncludeWarningBox.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/IncludeWarningBox.qml new file mode 100644 index 0000000..f5f270b --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/IncludeWarningBox.qml @@ -0,0 +1,94 @@ +import QtQuick +import qs.Common +import qs.Widgets + +StyledRect { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + width: parent.width + height: warningContent.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + + readonly property bool showError: DisplayConfigState.includeStatus.exists && !DisplayConfigState.includeStatus.included + readonly property bool showSetup: !DisplayConfigState.includeStatus.exists && !DisplayConfigState.includeStatus.included + + color: (showError || showSetup) ? Theme.withAlpha(Theme.primary, 0.15) : "transparent" + border.color: (showError || showSetup) ? Theme.withAlpha(Theme.primary, 0.3) : "transparent" + border.width: 1 + visible: (showError || showSetup) && DisplayConfigState.hasOutputBackend && !DisplayConfigState.checkingInclude + + Column { + id: warningContent + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "warning" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - (fixButton.visible ? fixButton.width + Theme.spacingM : 0) - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: { + if (root.showSetup) + return I18n.tr("First Time Setup"); + if (root.showError) + return I18n.tr("Outputs Include Missing"); + return ""; + } + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.primary + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: { + if (root.showSetup) + return I18n.tr("Click 'Setup' to create the outputs config and add include to your compositor config."); + if (root.showError) + return I18n.tr("dms/outputs config exists but is not included in your compositor config. Display changes won't persist."); + return ""; + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + + DankButton { + id: fixButton + visible: root.showError || root.showSetup + text: { + if (DisplayConfigState.fixingInclude) + return I18n.tr("Fixing..."); + if (root.showSetup) + return I18n.tr("Setup"); + return I18n.tr("Fix Now"); + } + backgroundColor: Theme.primary + textColor: Theme.primaryText + enabled: !DisplayConfigState.fixingInclude + anchors.verticalCenter: parent.verticalCenter + onClicked: DisplayConfigState.fixOutputsInclude() + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/MonitorCanvas.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/MonitorCanvas.qml new file mode 100644 index 0000000..accdfd7 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/MonitorCanvas.qml @@ -0,0 +1,82 @@ +import QtQuick +import qs.Common + +Rectangle { + id: root + + property var filteredOutputs: { + const all = DisplayConfigState.allOutputs || {}; + const keys = Object.keys(all); + if (SettingsData.displayShowDisconnected) + return keys; + return keys.filter(k => all[k]?.connected); + } + + property var filteredBounds: { + const all = DisplayConfigState.allOutputs || {}; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const name of filteredOutputs) { + const output = all[name]; + if (!output?.logical) + continue; + const x = output.logical.x; + const y = output.logical.y; + const size = DisplayConfigState.getLogicalSize(output); + const w = size.w || 1920; + const h = size.h || 1080; + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x + w); + maxY = Math.max(maxY, y + h); + } + if (minX === Infinity) + return { + minX: 0, + minY: 0, + width: 1920, + height: 1080 + }; + return { + minX: minX, + minY: minY, + width: maxX - minX, + height: maxY - minY + }; + } + + width: parent.width + height: 280 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHighest + border.color: Theme.outline + border.width: 1 + + Item { + id: canvas + anchors.fill: parent + anchors.margins: Theme.spacingL + + property var bounds: root.filteredBounds + property real scaleFactor: { + if (bounds.width === 0 || bounds.height === 0) + return 0.1; + const padding = Theme.spacingL * 2; + const scaleX = (width - padding) / bounds.width; + const scaleY = (height - padding) / bounds.height; + return Math.min(scaleX, scaleY); + } + property point offset: Qt.point((width - bounds.width * scaleFactor) / 2 - bounds.minX * scaleFactor, (height - bounds.height * scaleFactor) / 2 - bounds.minY * scaleFactor) + + Repeater { + model: root.filteredOutputs + + delegate: MonitorRect { + required property string modelData + outputName: modelData + outputData: DisplayConfigState.allOutputs[modelData] + canvasScaleFactor: canvas.scaleFactor + canvasOffset: canvas.offset + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/MonitorRect.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/MonitorRect.qml new file mode 100644 index 0000000..0209e5f --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/MonitorRect.qml @@ -0,0 +1,159 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + required property string outputName + required property var outputData + required property real canvasScaleFactor + required property point canvasOffset + + property bool isConnected: outputData?.connected ?? false + property bool isDragging: false + property point originalLogical: Qt.point(0, 0) + property point snappedLogical: Qt.point(0, 0) + property bool isValidPosition: true + + property var physSize: DisplayConfigState.getPhysicalSize(outputData) + property var logicalSize: DisplayConfigState.getLogicalSize(outputData) + + x: isDragging ? x : (outputData?.logical?.x ?? 0) * canvasScaleFactor + canvasOffset.x + y: isDragging ? y : (outputData?.logical?.y ?? 0) * canvasScaleFactor + canvasOffset.y + width: logicalSize.w * canvasScaleFactor + height: logicalSize.h * canvasScaleFactor + radius: Theme.cornerRadius + opacity: isConnected ? 1.0 : 0.5 + + color: { + if (!isConnected) + return Theme.surfaceContainerHighest; + if (!isValidPosition) + return Theme.withAlpha(Theme.error, 0.3); + if (isDragging) + return Theme.withAlpha(Theme.primary, 0.4); + if (dragArea.containsMouse) + return Theme.withAlpha(Theme.primary, 0.2); + return Theme.surfaceContainerHigh; + } + + border.color: { + if (!isConnected) + return Theme.outline; + if (!isValidPosition) + return Theme.error; + if (isDragging) + return Theme.primary; + if (CompositorService.getFocusedScreen()?.name === outputName) + return Theme.primary; + return Theme.outline; + } + border.width: isDragging ? 3 : 2 + z: isDragging ? 100 : (isConnected ? 1 : 0) + + Rectangle { + id: snapPreview + visible: root.isDragging && root.isValidPosition && SettingsData.displaySnapToEdge + x: root.snappedLogical.x * root.canvasScaleFactor + root.canvasOffset.x - root.x + y: root.snappedLogical.y * root.canvasScaleFactor + root.canvasOffset.y - root.y + width: parent.width + height: parent.height + radius: Theme.cornerRadius + color: "transparent" + border.color: Theme.primary + border.width: 2 + opacity: 0.6 + } + + Column { + anchors.centerIn: parent + spacing: 2 + + DankIcon { + name: root.isConnected ? "desktop_windows" : "desktop_access_disabled" + size: Math.min(24, Math.min(root.width * 0.3, root.height * 0.25)) + color: root.isConnected ? (root.isValidPosition ? Theme.primary : Theme.error) : Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: DisplayConfigState.getOutputDisplayName(root.outputData, root.outputName) + font.pixelSize: Math.max(10, Math.min(14, root.width * 0.12)) + font.weight: Font.Medium + color: root.isConnected ? Theme.surfaceText : Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + elide: Text.ElideMiddle + width: Math.min(implicitWidth, root.width - 8) + } + + StyledText { + text: root.isConnected ? (root.physSize.w + "x" + root.physSize.h) : I18n.tr("Disconnected") + font.pixelSize: Math.max(8, Math.min(11, root.width * 0.09)) + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + + MouseArea { + id: dragArea + anchors.fill: parent + hoverEnabled: true + enabled: root.isConnected + cursorShape: !root.isConnected ? Qt.ArrowCursor : (root.isDragging ? Qt.ClosedHandCursor : Qt.OpenHandCursor) + drag.target: root.isConnected ? root : null + drag.axis: Drag.XAndYAxis + drag.threshold: 0 + + onPressed: mouse => { + if (!root.isConnected) + return; + root.isDragging = true; + root.originalLogical = Qt.point(root.outputData?.logical?.x ?? 0, root.outputData?.logical?.y ?? 0); + root.snappedLogical = root.originalLogical; + root.isValidPosition = true; + } + + onPositionChanged: mouse => { + if (!root.isDragging || !root.isConnected) + return; + let posX = Math.round((root.x - root.canvasOffset.x) / root.canvasScaleFactor); + let posY = Math.round((root.y - root.canvasOffset.y) / root.canvasScaleFactor); + + const size = DisplayConfigState.getLogicalSize(root.outputData); + + const snapped = SettingsData.displaySnapToEdge ? DisplayConfigState.snapToEdges(root.outputName, posX, posY, size.w, size.h) : Qt.point(posX, posY); + root.snappedLogical = snapped; + root.isValidPosition = SettingsData.displaySnapToEdge ? !DisplayConfigState.checkOverlap(root.outputName, snapped.x, snapped.y, size.w, size.h) : true; + } + + onReleased: { + if (!root.isDragging || !root.isConnected) + return; + root.isDragging = false; + + const size = DisplayConfigState.getLogicalSize(root.outputData); + const finalX = root.snappedLogical.x; + const finalY = root.snappedLogical.y; + + if (SettingsData.displaySnapToEdge && DisplayConfigState.checkOverlap(root.outputName, finalX, finalY, size.w, size.h)) { + root.isValidPosition = true; + return; + } + + if (finalX === root.originalLogical.x && finalY === root.originalLogical.y) + return; + + DisplayConfigState.initOriginalOutputs(); + DisplayConfigState.backendUpdateOutputPosition(root.outputName, finalX, finalY); + DisplayConfigState.setPendingChange(root.outputName, "position", { + "x": finalX, + "y": finalY + }); + } + } + + Drag.active: dragArea.drag.active && root.isConnected +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/NiriOutputSettings.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/NiriOutputSettings.qml new file mode 100644 index 0000000..da9a4a7 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/NiriOutputSettings.qml @@ -0,0 +1,368 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Column { + id: root + + property string outputName: "" + property var outputData: null + property bool expanded: false + + width: parent.width + spacing: 0 + + Rectangle { + width: parent.width + height: headerRow.implicitHeight + Theme.spacingS * 2 + color: headerMouse.containsMouse ? Theme.withAlpha(Theme.primary, 0.1) : "transparent" + radius: Theme.cornerRadius / 2 + + Row { + id: headerRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: root.expanded ? "expand_more" : "chevron_right" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Compositor Settings") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: headerMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.expanded = !root.expanded + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: root.expanded + topPadding: Theme.spacingS + + DankToggle { + width: parent.width + text: I18n.tr("Disable Output") + checked: DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "disabled", false) + onToggled: checked => DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "disabled", checked) + } + + DankToggle { + width: parent.width + text: I18n.tr("Focus at Startup") + checked: DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "focusAtStartup", false) + onToggled: checked => DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "focusAtStartup", checked) + } + + DankDropdown { + width: parent.width + text: I18n.tr("Hot Corners") + addHorizontalPadding: true + + property var hotCornersData: { + void (DisplayConfigState.pendingNiriChanges); + return DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "hotCorners", null); + } + + currentValue: { + if (!hotCornersData) + return I18n.tr("Inherit"); + if (hotCornersData.off) + return I18n.tr("Off"); + const corners = hotCornersData.corners || []; + if (corners.length === 0) + return I18n.tr("Inherit"); + if (corners.length === 4) + return I18n.tr("All"); + return I18n.tr("Select..."); + } + options: [I18n.tr("Inherit"), I18n.tr("Off"), I18n.tr("All"), I18n.tr("Select...")] + + onValueChanged: value => { + switch (value) { + case I18n.tr("Inherit"): + DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "hotCorners", null); + break; + case I18n.tr("Off"): + DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "hotCorners", { + "off": true + }); + break; + case I18n.tr("All"): + DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "hotCorners", { + "corners": ["top-left", "top-right", "bottom-left", "bottom-right"] + }); + break; + case I18n.tr("Select..."): + DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "hotCorners", { + "corners": [] + }); + break; + } + } + } + + Item { + width: parent.width + height: hotCornersGroup.implicitHeight + clip: true + + property var hotCornersData: { + void (DisplayConfigState.pendingNiriChanges); + return DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "hotCorners", null); + } + + visible: hotCornersData && !hotCornersData.off && hotCornersData.corners !== undefined + + DankButtonGroup { + id: hotCornersGroup + anchors.horizontalCenter: parent.horizontalCenter + selectionMode: "multi" + checkEnabled: false + buttonHeight: 32 + buttonPadding: parent.width < 400 ? Theme.spacingXS : Theme.spacingM + minButtonWidth: parent.width < 400 ? 28 : 56 + textSize: parent.width < 400 ? 11 : Theme.fontSizeMedium + model: [I18n.tr("Top Left"), I18n.tr("Top Right"), I18n.tr("Bottom Left"), I18n.tr("Bottom Right")] + + property var cornerKeys: ["top-left", "top-right", "bottom-left", "bottom-right"] + + currentSelection: { + const hcData = parent.hotCornersData; + if (!hcData?.corners) + return []; + return hcData.corners.map(key => { + const idx = cornerKeys.indexOf(key); + return idx >= 0 ? model[idx] : null; + }).filter(v => v !== null); + } + + onSelectionChanged: (index, selected) => { + const corners = currentSelection.map(label => { + const idx = model.indexOf(label); + return idx >= 0 ? cornerKeys[idx] : null; + }).filter(v => v !== null); + DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "hotCorners", { + "corners": corners + }); + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.withAlpha(Theme.outline, 0.15) + } + + Item { + width: parent.width + height: layoutColumn.implicitHeight + + Column { + id: layoutColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingS + + Column { + width: parent.width + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Layout Overrides") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceVariantText + } + + StyledText { + text: I18n.tr("Override global layout settings for this output") + font.pixelSize: Theme.fontSizeSmall + color: Theme.withAlpha(Theme.surfaceVariantText, 0.7) + wrapMode: Text.WordWrap + width: parent.width + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + + Column { + width: (parent.width - Theme.spacingM) / 2 + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Window Gaps (px)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + DankTextField { + width: parent.width + height: 40 + placeholderText: I18n.tr("Inherit") + text: { + const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", null); + if (layout?.gaps === undefined) + return ""; + return layout.gaps.toString(); + } + onEditingFinished: { + const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", {}) || {}; + const trimmed = text.trim(); + if (!trimmed) { + delete layout.gaps; + DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", Object.keys(layout).length > 0 ? layout : null); + return; + } + const val = parseInt(trimmed); + if (isNaN(val) || val < 0) + return; + layout.gaps = val; + DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", layout); + } + } + } + + Column { + width: (parent.width - Theme.spacingM) / 2 + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Default Width (%)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + DankTextField { + width: parent.width + height: 40 + placeholderText: I18n.tr("Inherit") + text: { + const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", null); + if (!layout?.defaultColumnWidth) + return ""; + if (layout.defaultColumnWidth.type !== "proportion") + return ""; + const percent = layout.defaultColumnWidth.value * 100; + return parseFloat(percent.toFixed(4)).toString(); + } + onEditingFinished: { + const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", {}) || {}; + const trimmed = text.trim().replace("%", ""); + if (!trimmed) { + delete layout.defaultColumnWidth; + DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", Object.keys(layout).length > 0 ? layout : null); + return; + } + const val = parseFloat(trimmed); + if (isNaN(val) || val <= 0 || val > 100) + return; + layout.defaultColumnWidth = { + "type": "proportion", + "value": parseFloat((val / 100).toFixed(6)) + }; + DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", layout); + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Preset Widths (%)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: "e.g. 33.33, 50, 66.67" + font.pixelSize: Theme.fontSizeSmall + color: Theme.withAlpha(Theme.surfaceVariantText, 0.7) + } + + DankTextField { + width: parent.width + height: 40 + placeholderText: I18n.tr("Inherit") + text: { + const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", null); + const presets = layout?.presetColumnWidths || []; + if (presets.length === 0) + return ""; + return presets.filter(p => p.type === "proportion").map(p => parseFloat((p.value * 100).toFixed(4))).join(", "); + } + onEditingFinished: { + const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", {}) || {}; + const trimmed = text.trim(); + if (!trimmed) { + delete layout.presetColumnWidths; + DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", Object.keys(layout).length > 0 ? layout : null); + return; + } + const parts = trimmed.split(/[,\s]+/).filter(s => s); + const presets = []; + for (const part of parts) { + const val = parseFloat(part.replace("%", "")); + if (!isNaN(val) && val > 0 && val <= 100) + presets.push({ + "type": "proportion", + "value": parseFloat((val / 100).toFixed(6)) + }); + } + if (presets.length === 0) { + delete layout.presetColumnWidths; + DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", Object.keys(layout).length > 0 ? layout : null); + return; + } + presets.sort((a, b) => a.value - b.value); + layout.presetColumnWidths = presets; + DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", layout); + } + } + } + } + } + + DankToggle { + width: parent.width + text: I18n.tr("Center Single Column") + property var layoutData: DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", null) + checked: layoutData?.alwaysCenterSingleColumn ?? false + onToggled: checked => { + const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", {}) || {}; + if (checked) { + layout.alwaysCenterSingleColumn = true; + } else { + delete layout.alwaysCenterSingleColumn; + } + DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "layout", Object.keys(layout).length > 0 ? layout : null); + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/NoBackendMessage.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/NoBackendMessage.qml new file mode 100644 index 0000000..93ab124 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/NoBackendMessage.qml @@ -0,0 +1,60 @@ +import QtQuick +import qs.Common +import qs.Widgets + +StyledRect { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + width: parent.width + height: messageContent.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 0 + + Column { + id: messageContent + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "monitor" + size: Theme.iconSize + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Monitor Configuration") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: I18n.tr("Display configuration is not available. WLR output management protocol not supported.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml new file mode 100644 index 0000000..0da1175 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml @@ -0,0 +1,352 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +StyledRect { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + required property string outputName + required property var outputData + property bool isConnected: outputData?.connected ?? false + + width: parent.width + height: settingsColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, isConnected ? 0.5 : 0.3) + border.color: Theme.withAlpha(Theme.outline, 0.3) + border.width: 1 + opacity: isConnected ? 1.0 : 0.7 + + Column { + id: settingsColumn + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: root.isConnected ? "desktop_windows" : "desktop_access_disabled" + size: Theme.iconSize - 4 + color: root.isConnected ? Theme.primary : Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - (disconnectedBadge.visible ? disconnectedBadge.width + deleteButton.width + Theme.spacingS * 2 : 0) + spacing: 2 + + StyledText { + text: DisplayConfigState.getOutputDisplayName(root.outputData, root.outputName) + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: root.isConnected ? Theme.surfaceText : Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: (root.outputData?.model ?? "") + (root.outputData?.make ? " - " + root.outputData.make : "") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + + Rectangle { + id: disconnectedBadge + visible: !root.isConnected + width: disconnectedText.implicitWidth + Theme.spacingM + height: disconnectedText.implicitHeight + Theme.spacingXS + radius: height / 2 + color: Theme.withAlpha(Theme.outline, 0.3) + anchors.verticalCenter: parent.verticalCenter + + StyledText { + id: disconnectedText + text: I18n.tr("Disconnected") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.centerIn: parent + } + } + + Rectangle { + id: deleteButton + visible: !root.isConnected + width: 28 + height: 28 + radius: Theme.cornerRadius + color: deleteArea.containsMouse ? Theme.errorHover : "transparent" + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: "delete" + size: 18 + color: deleteArea.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: deleteArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: DisplayConfigState.deleteDisconnectedOutput(root.outputName) + } + } + } + + DankDropdown { + width: parent.width + text: I18n.tr("Resolution & Refresh") + visible: root.isConnected + currentValue: { + const pendingMode = DisplayConfigState.getPendingValue(root.outputName, "mode"); + if (pendingMode) + return pendingMode; + const data = DisplayConfigState.outputs[root.outputName]; + if (!data?.modes || data?.current_mode === undefined) + return "Auto"; + const mode = data.modes[data.current_mode]; + return mode ? DisplayConfigState.formatMode(mode) : "Auto"; + } + options: { + const data = DisplayConfigState.outputs[root.outputName]; + if (!data?.modes) + return ["Auto"]; + const opts = []; + for (var i = 0; i < data.modes.length; i++) { + opts.push(DisplayConfigState.formatMode(data.modes[i])); + } + return opts; + } + onValueChanged: value => DisplayConfigState.setPendingChange(root.outputName, "mode", value) + } + + StyledText { + visible: !root.isConnected + text: I18n.tr("Configuration will be preserved when this display reconnects") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Row { + width: parent.width + spacing: Theme.spacingM + visible: root.isConnected + + Column { + width: (parent.width - Theme.spacingM) / 2 + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Scale") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Item { + id: scaleContainer + width: parent.width + height: scaleDropdown.visible ? scaleDropdown.height : scaleInput.height + + property bool customMode: false + property string currentScale: { + const pendingScale = DisplayConfigState.getPendingValue(root.outputName, "scale"); + if (pendingScale !== undefined) + return parseFloat(pendingScale.toFixed(2)).toString(); + const scale = DisplayConfigState.outputs[root.outputName]?.logical?.scale ?? 1.0; + return parseFloat(scale.toFixed(2)).toString(); + } + + DankDropdown { + id: scaleDropdown + width: parent.width + dropdownWidth: parent.width + visible: !scaleContainer.customMode + currentValue: scaleContainer.currentScale + options: { + const standard = ["0.5", "0.75", "1", "1.25", "1.5", "1.75", "2", "2.5", "3", I18n.tr("Custom...")]; + const current = scaleContainer.currentScale; + if (standard.slice(0, -1).includes(current)) + return standard; + const opts = [...standard.slice(0, -1), current, standard[standard.length - 1]]; + return opts.sort((a, b) => { + if (a === I18n.tr("Custom...")) + return 1; + if (b === I18n.tr("Custom...")) + return -1; + return parseFloat(a) - parseFloat(b); + }); + } + onValueChanged: value => { + if (value === I18n.tr("Custom...")) { + scaleContainer.customMode = true; + scaleInput.text = scaleContainer.currentScale; + scaleInput.forceActiveFocus(); + scaleInput.selectAll(); + return; + } + DisplayConfigState.setPendingChange(root.outputName, "scale", parseFloat(value)); + } + } + + DankTextField { + id: scaleInput + width: parent.width + height: 40 + visible: scaleContainer.customMode + placeholderText: "0.5 - 4.0" + + function applyValue() { + const val = parseFloat(text); + if (isNaN(val) || val < 0.25 || val > 4) { + text = scaleContainer.currentScale; + scaleContainer.customMode = false; + return; + } + DisplayConfigState.setPendingChange(root.outputName, "scale", parseFloat(val.toFixed(2))); + scaleContainer.customMode = false; + } + + onAccepted: applyValue() + onEditingFinished: applyValue() + Keys.onEscapePressed: { + text = scaleContainer.currentScale; + scaleContainer.customMode = false; + } + } + } + } + + Column { + width: (parent.width - Theme.spacingM) / 2 + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Transform") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + DankDropdown { + width: parent.width + dropdownWidth: parent.width + currentValue: { + const pendingTransform = DisplayConfigState.getPendingValue(root.outputName, "transform"); + if (pendingTransform) + return DisplayConfigState.getTransformLabel(pendingTransform); + const data = DisplayConfigState.outputs[root.outputName]; + return DisplayConfigState.getTransformLabel(data?.logical?.transform ?? "Normal"); + } + options: [I18n.tr("Normal"), I18n.tr("90°"), I18n.tr("180°"), I18n.tr("270°"), I18n.tr("Flipped"), I18n.tr("Flipped 90°"), I18n.tr("Flipped 180°"), I18n.tr("Flipped 270°")] + onValueChanged: value => DisplayConfigState.setPendingChange(root.outputName, "transform", DisplayConfigState.getTransformValue(value)) + } + } + } + + DankToggle { + width: parent.width + text: I18n.tr("Variable Refresh Rate") + visible: root.isConnected && !CompositorService.isDwl && !CompositorService.isHyprland && !CompositorService.isNiri && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false) + checked: { + const pendingVrr = DisplayConfigState.getPendingValue(root.outputName, "vrr"); + if (pendingVrr !== undefined) + return pendingVrr; + return DisplayConfigState.outputs[root.outputName]?.vrr_enabled ?? false; + } + onToggled: checked => DisplayConfigState.setPendingChange(root.outputName, "vrr", checked) + } + + DankDropdown { + width: parent.width + text: I18n.tr("Variable Refresh Rate") + visible: root.isConnected && CompositorService.isHyprland && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false) + options: [I18n.tr("Off"), I18n.tr("On"), I18n.tr("Fullscreen Only")] + currentValue: { + DisplayConfigState.pendingHyprlandChanges; + if (DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "vrrFullscreenOnly", false)) + return I18n.tr("Fullscreen Only"); + const pendingVrr = DisplayConfigState.getPendingValue(root.outputName, "vrr"); + const vrrEnabled = pendingVrr !== undefined ? pendingVrr : (DisplayConfigState.outputs[root.outputName]?.vrr_enabled ?? false); + if (vrrEnabled) + return I18n.tr("On"); + return I18n.tr("Off"); + } + onValueChanged: value => { + const off = I18n.tr("Off"); + const fullscreen = I18n.tr("Fullscreen Only"); + DisplayConfigState.setPendingChange(root.outputName, "vrr", value !== off); + DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "vrrFullscreenOnly", value === fullscreen || null); + } + } + + DankDropdown { + width: parent.width + text: I18n.tr("Variable Refresh Rate") + visible: root.isConnected && CompositorService.isNiri && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false) + options: [I18n.tr("Off"), I18n.tr("On"), I18n.tr("On-Demand")] + currentValue: { + DisplayConfigState.pendingNiriChanges; + if (DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "vrrOnDemand", false)) + return I18n.tr("On-Demand"); + const pendingVrr = DisplayConfigState.getPendingValue(root.outputName, "vrr"); + const vrrEnabled = pendingVrr !== undefined ? pendingVrr : (DisplayConfigState.outputs[root.outputName]?.vrr_enabled ?? false); + if (!vrrEnabled) + return I18n.tr("Off"); + return I18n.tr("On"); + } + onValueChanged: value => { + const off = I18n.tr("Off"); + const onDemand = I18n.tr("On-Demand"); + DisplayConfigState.setPendingChange(root.outputName, "vrr", value !== off); + DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "vrrOnDemand", value === onDemand || null); + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.withAlpha(Theme.outline, 0.2) + visible: compositorSettingsLoader.active + } + + Loader { + id: compositorSettingsLoader + width: parent.width + active: root.isConnected && compositorSettingsSource !== "" + source: compositorSettingsSource + + property string compositorSettingsSource: { + switch (CompositorService.compositor) { + case "niri": + return "NiriOutputSettings.qml"; + case "hyprland": + return "HyprlandOutputSettings.qml"; + default: + return ""; + } + } + + onLoaded: { + item.outputName = root.outputName; + item.outputData = root.outputData; + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfigTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfigTab.qml new file mode 100644 index 0000000..61b373d --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfigTab.qml @@ -0,0 +1,540 @@ +import QtQuick +import qs.Common +import qs.Modals +import qs.Services +import qs.Widgets +import qs.Modules.Settings.DisplayConfig + +Item { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property string selectedProfileId: SettingsData.getActiveDisplayProfile(CompositorService.compositor) + property bool showNewProfileDialog: false + property bool showDeleteConfirmDialog: false + property bool showRenameDialog: false + property string newProfileName: "" + property string renameProfileName: "" + + function getProfileOptions() { + const profiles = DisplayConfigState.validatedProfiles; + const options = []; + for (const id in profiles) + options.push(profiles[id].name); + return options; + } + + function getProfileIds() { + return Object.keys(DisplayConfigState.validatedProfiles); + } + + function getProfileIdByName(name) { + const profiles = DisplayConfigState.validatedProfiles; + for (const id in profiles) { + if (profiles[id].name === name) + return id; + } + return ""; + } + + function getProfileNameById(id) { + const profiles = DisplayConfigState.validatedProfiles; + return profiles[id]?.name || ""; + } + + Connections { + target: DisplayConfigState + function onChangesApplied(changeDescriptions) { + confirmationModal.changes = changeDescriptions; + confirmationModal.open(); + } + function onChangesConfirmed() { + } + function onChangesReverted() { + } + function onProfileActivated(profileId, profileName) { + root.selectedProfileId = profileId; + ToastService.showInfo(I18n.tr("Profile activated: %1").arg(profileName)); + } + function onProfileSaved(profileId, profileName) { + root.selectedProfileId = profileId; + ToastService.showInfo(I18n.tr("Profile saved: %1").arg(profileName)); + } + function onProfileDeleted(profileId) { + root.selectedProfileId = SettingsData.getActiveDisplayProfile(CompositorService.compositor); + ToastService.showInfo(I18n.tr("Profile deleted")); + } + function onProfileError(message) { + ToastService.showError(I18n.tr("Profile error"), message); + } + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + IncludeWarningBox { + width: parent.width + } + + StyledRect { + width: parent.width + height: profileSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 0 + visible: DisplayConfigState.hasOutputBackend + + Column { + id: profileSection + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "tune" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - autoSelectColumn.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Display Profiles") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: I18n.tr("Save and switch between display configurations") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + + // ! TODO - auto profile switching is buggy on niri and other compositors + Column { + id: autoSelectColumn + visible: false // disabled for now + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Auto") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + } + + DankToggle { + id: autoSelectToggle + checked: false // disabled for now + enabled: false + onToggled: checked => { + // disabled for now + // SettingsData.displayProfileAutoSelect = checked; + // SettingsData.saveSettings(); + } + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + visible: !root.showNewProfileDialog && !root.showDeleteConfirmDialog && !root.showRenameDialog + + DankDropdown { + id: profileDropdown + width: parent.width - newButton.width - deleteButton.width - Theme.spacingS * 2 + compactMode: true + dropdownWidth: width + options: root.getProfileOptions() + currentValue: root.getProfileNameById(root.selectedProfileId) + emptyText: I18n.tr("No profiles") + onValueChanged: value => { + const profileId = root.getProfileIdByName(value); + if (profileId && profileId !== root.selectedProfileId) + DisplayConfigState.activateProfile(profileId); + } + } + + DankButton { + id: newButton + iconName: "add" + text: "" + buttonHeight: 40 + horizontalPadding: Theme.spacingM + backgroundColor: Theme.surfaceContainer + textColor: Theme.surfaceText + onClicked: { + root.newProfileName = ""; + root.showNewProfileDialog = true; + } + } + + DankButton { + id: deleteButton + iconName: "delete" + text: "" + buttonHeight: 40 + horizontalPadding: Theme.spacingM + backgroundColor: Theme.surfaceContainer + textColor: Theme.error + enabled: root.selectedProfileId !== "" + onClicked: root.showDeleteConfirmDialog = true + } + } + + Rectangle { + width: parent.width + height: newProfileRow.height + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + visible: root.showNewProfileDialog + + Row { + id: newProfileRow + anchors.centerIn: parent + width: parent.width - Theme.spacingM * 2 + spacing: Theme.spacingS + + DankTextField { + id: newProfileField + width: parent.width - createButton.width - cancelNewButton.width - Theme.spacingS * 2 + placeholderText: I18n.tr("Profile name") + text: root.newProfileName + onTextChanged: root.newProfileName = text + onAccepted: { + if (text.trim()) + DisplayConfigState.createProfile(text.trim()); + root.showNewProfileDialog = false; + } + Component.onCompleted: forceActiveFocus() + } + + DankButton { + id: createButton + text: I18n.tr("Create") + enabled: root.newProfileName.trim() !== "" + onClicked: { + DisplayConfigState.createProfile(root.newProfileName.trim()); + root.showNewProfileDialog = false; + } + } + + DankButton { + id: cancelNewButton + text: I18n.tr("Cancel") + backgroundColor: "transparent" + textColor: Theme.surfaceText + onClicked: root.showNewProfileDialog = false + } + } + } + + Rectangle { + width: parent.width + height: deleteConfirmColumn.height + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + visible: root.showDeleteConfirmDialog + + Column { + id: deleteConfirmColumn + anchors.centerIn: parent + width: parent.width - Theme.spacingM * 2 + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Delete profile \"%1\"?").arg(root.getProfileNameById(root.selectedProfileId)) + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignLeft + } + + Row { + spacing: Theme.spacingS + anchors.right: parent.right + + DankButton { + text: I18n.tr("Delete") + backgroundColor: Theme.error + textColor: Theme.primaryText + onClicked: { + DisplayConfigState.deleteProfile(root.selectedProfileId); + root.showDeleteConfirmDialog = false; + } + } + + DankButton { + text: I18n.tr("Cancel") + backgroundColor: "transparent" + textColor: Theme.surfaceText + onClicked: root.showDeleteConfirmDialog = false + } + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + visible: DisplayConfigState.matchedProfile !== "" + + DankIcon { + name: "check_circle" + size: 16 + color: Theme.success + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Matches profile: %1").arg(root.getProfileNameById(DisplayConfigState.matchedProfile)) + font.pixelSize: Theme.fontSizeSmall + color: Theme.success + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + + StyledRect { + width: parent.width + height: monitorConfigSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 0 + visible: DisplayConfigState.hasOutputBackend + + Column { + id: monitorConfigSection + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "monitor" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - (displayFormatColumn.visible ? displayFormatColumn.width + Theme.spacingM : 0) - (snapColumn.visible ? snapColumn.width + Theme.spacingM : 0) + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Monitor Configuration") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: I18n.tr("Arrange displays and configure resolution, refresh rate, and VRR") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + + Column { + id: snapColumn + visible: true + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Snap") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + } + + DankToggle { + id: snapToggle + checked: SettingsData.displaySnapToEdge + onToggled: checked => { + SettingsData.displaySnapToEdge = checked; + SettingsData.saveSettings(); + } + } + } + + Column { + id: displayFormatColumn + visible: !CompositorService.isDwl + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Config Format") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + } + + DankButtonGroup { + id: displayFormatGroup + model: [I18n.tr("Name"), I18n.tr("Model")] + currentIndex: SettingsData.displayNameMode === "model" ? 1 : 0 + onSelectionChanged: (index, selected) => { + if (!selected) + return; + const newMode = index === 1 ? "model" : "system"; + DisplayConfigState.setOriginalDisplayNameMode(SettingsData.displayNameMode); + SettingsData.displayNameMode = newMode; + } + + Connections { + target: SettingsData + function onDisplayNameModeChanged() { + displayFormatGroup.currentIndex = SettingsData.displayNameMode === "model" ? 1 : 0; + } + } + } + } + } + + MonitorCanvas { + width: parent.width + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Row { + width: parent.width + spacing: Theme.spacingS + visible: { + const all = DisplayConfigState.allOutputs || {}; + const disconnected = Object.keys(all).filter(k => !all[k]?.connected); + return disconnected.length > 0; + } + + StyledText { + text: { + const all = DisplayConfigState.allOutputs || {}; + const disconnected = Object.keys(all).filter(k => !all[k]?.connected); + if (SettingsData.displayShowDisconnected) + return I18n.tr("%1 disconnected").arg(disconnected.length); + return I18n.tr("%1 disconnected (hidden)").arg(disconnected.length); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: SettingsData.displayShowDisconnected ? I18n.tr("Hide") : I18n.tr("Show") + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + SettingsData.displayShowDisconnected = !SettingsData.displayShowDisconnected; + SettingsData.saveSettings(); + } + } + } + } + + Repeater { + model: { + const keys = Object.keys(DisplayConfigState.allOutputs || {}); + if (SettingsData.displayShowDisconnected) + return keys; + return keys.filter(k => DisplayConfigState.allOutputs[k]?.connected); + } + + delegate: OutputCard { + required property string modelData + outputName: modelData + outputData: DisplayConfigState.allOutputs[modelData] + } + } + } + + Row { + LayoutMirroring.enabled: false + width: parent.width + spacing: Theme.spacingS + visible: DisplayConfigState.hasPendingChanges + layoutDirection: Qt.RightToLeft + + DankButton { + text: I18n.tr("Apply Changes") + iconName: "check" + onClicked: DisplayConfigState.applyChanges() + } + + DankButton { + text: I18n.tr("Discard") + backgroundColor: "transparent" + textColor: Theme.surfaceText + onClicked: DisplayConfigState.discardChanges() + } + } + } + } + + NoBackendMessage { + width: parent.width + visible: !DisplayConfigState.hasOutputBackend + } + } + } + + DisplayConfirmationModal { + id: confirmationModal + onConfirmed: DisplayConfigState.confirmChanges() + onReverted: DisplayConfigState.revertChanges() + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayWidgetsTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayWidgetsTab.qml new file mode 100644 index 0000000..59f6513 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayWidgetsTab.qml @@ -0,0 +1,522 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + function getBarComponentsFromSettings() { + const bars = SettingsData.barConfigs || []; + return bars.map(bar => ({ + "id": "bar:" + bar.id, + "name": bar.name || "Bar", + "description": I18n.tr("Individual bar configuration"), + "icon": "toolbar", + "barId": bar.id + })); + } + + property var variantComponents: getVariantComponentsList() + + function getVariantComponentsList() { + return [...getBarComponentsFromSettings(), + { + "id": "dock", + "name": I18n.tr("Application Dock"), + "description": I18n.tr("Bottom dock for pinned and running applications"), + "icon": "dock" + }, + { + "id": "notifications", + "name": I18n.tr("Notification Popups"), + "description": I18n.tr("Notification toast popups"), + "icon": "notifications" + }, + { + "id": "wallpaper", + "name": I18n.tr("Wallpaper"), + "description": I18n.tr("Desktop background images"), + "icon": "wallpaper" + }, + { + "id": "osd", + "name": I18n.tr("On-Screen Displays"), + "description": I18n.tr("Volume, brightness, and other system OSDs"), + "icon": "picture_in_picture" + }, + { + "id": "toast", + "name": I18n.tr("Toast Messages"), + "description": I18n.tr("System toast notifications"), + "icon": "campaign" + }, + { + "id": "notepad", + "name": I18n.tr("Notepad Slideout"), + "description": I18n.tr("Quick note-taking slideout panel"), + "icon": "sticky_note_2" + } + ]; + } + + Connections { + target: SettingsData + function onBarConfigsChanged() { + variantComponents = getVariantComponentsList(); + } + } + + function getScreenPreferences(componentId) { + if (componentId.startsWith("bar:")) { + const barId = componentId.substring(4); + const barConfig = SettingsData.getBarConfig(barId); + return barConfig?.screenPreferences || ["all"]; + } + return SettingsData.screenPreferences && SettingsData.screenPreferences[componentId] || ["all"]; + } + + function setScreenPreferences(componentId, screenNames) { + if (componentId.startsWith("bar:")) { + const barId = componentId.substring(4); + SettingsData.updateBarConfig(barId, { + "screenPreferences": screenNames + }); + return; + } + var prefs = SettingsData.screenPreferences || {}; + var newPrefs = Object.assign({}, prefs); + newPrefs[componentId] = screenNames; + SettingsData.set("screenPreferences", newPrefs); + } + + function getShowOnLastDisplay(componentId) { + if (componentId.startsWith("bar:")) { + const barId = componentId.substring(4); + const barConfig = SettingsData.getBarConfig(barId); + return barConfig?.showOnLastDisplay ?? true; + } + return SettingsData.showOnLastDisplay && SettingsData.showOnLastDisplay[componentId] || false; + } + + function setShowOnLastDisplay(componentId, enabled) { + if (componentId.startsWith("bar:")) { + const barId = componentId.substring(4); + SettingsData.updateBarConfig(barId, { + "showOnLastDisplay": enabled + }); + return; + } + var prefs = SettingsData.showOnLastDisplay || {}; + var newPrefs = Object.assign({}, prefs); + newPrefs[componentId] = enabled; + SettingsData.set("showOnLastDisplay", newPrefs); + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + StyledRect { + width: parent.width + height: screensInfoSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 0 + + Column { + id: screensInfoSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "monitor" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Connected Displays") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: I18n.tr("Configure which displays show shell components") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Column { + width: parent.width + spacing: Theme.spacingXS + + Row { + width: parent.width + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Available Screens (%1)").arg(Quickshell.screens.length) + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + horizontalAlignment: Text.AlignLeft + } + + Item { + width: 1 + height: 1 + Layout.fillWidth: true + } + + Column { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Display Name Format") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + + DankButtonGroup { + id: displayModeGroup + model: [I18n.tr("Name"), I18n.tr("Model")] + currentIndex: SettingsData.displayNameMode === "model" ? 1 : 0 + onSelectionChanged: (index, selected) => { + if (!selected) + return; + SettingsData.displayNameMode = index === 1 ? "model" : "system"; + SettingsData.saveSettings(); + } + + Connections { + target: SettingsData + function onDisplayNameModeChanged() { + displayModeGroup.currentIndex = SettingsData.displayNameMode === "model" ? 1 : 0; + } + } + } + } + } + } + + Repeater { + model: Quickshell.screens + + delegate: Rectangle { + width: parent.width + height: screenRow.implicitHeight + Theme.spacingS * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) + border.width: 0 + + Row { + id: screenRow + + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingM + + DankIcon { + name: "desktop_windows" + size: Theme.iconSize - 4 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM * 2 + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS / 2 + + StyledText { + text: SettingsData.getScreenDisplayName(modelData) + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Row { + width: parent.width + spacing: Theme.spacingS + + property var wlrOutput: WlrOutputService.wlrOutputAvailable ? WlrOutputService.getOutput(modelData.name) : null + property var currentMode: wlrOutput?.currentMode + + StyledText { + text: { + if (parent.currentMode) { + return parent.currentMode.width + "×" + parent.currentMode.height + "@" + Math.round(parent.currentMode.refresh / 1000) + "Hz"; + } + return modelData.width + "×" + modelData.height; + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: SettingsData.displayNameMode === "system" ? (modelData.model || "Unknown Model") : modelData.name + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + } + } + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingL + + Repeater { + model: root.variantComponents + + delegate: StyledRect { + width: parent.width + height: componentSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 0 + + Column { + id: componentSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: modelData.icon + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: modelData.name + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: modelData.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Show on screens:") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Column { + property string componentId: modelData.id + + width: parent.width + spacing: Theme.spacingXS + + DankToggle { + width: parent.width + text: I18n.tr("All displays") + description: I18n.tr("Show on all connected displays") + checked: { + var prefs = root.getScreenPreferences(parent.componentId); + return prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all"); + } + onToggled: checked => { + if (checked) { + root.setScreenPreferences(parent.componentId, ["all"]); + } else { + root.setScreenPreferences(parent.componentId, []); + const cid = parent.componentId; + if (["dankBar", "dock", "notifications", "osd", "toast"].includes(cid) || cid.startsWith("bar:")) { + root.setShowOnLastDisplay(cid, true); + } + } + } + } + + DankToggle { + width: parent.width + text: I18n.tr("Focused monitor only") + description: I18n.tr("Show notifications only on the currently focused monitor") + visible: parent.componentId === "notifications" + checked: SettingsData.notificationFocusedMonitor + onToggled: checked => SettingsData.set("notificationFocusedMonitor", checked) + } + + DankToggle { + width: parent.width + text: I18n.tr("Show on Last Display") + description: I18n.tr("Always show when there's only one connected display") + checked: root.getShowOnLastDisplay(parent.componentId) + visible: { + const prefs = root.getScreenPreferences(parent.componentId); + const isAll = prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all"); + const cid = parent.componentId; + const isRelevantComponent = ["dankBar", "dock", "notifications", "osd", "toast", "notepad"].includes(cid) || cid.startsWith("bar:"); + return !isAll && isRelevantComponent; + } + onToggled: checked => { + root.setShowOnLastDisplay(parent.componentId, checked); + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + visible: { + var prefs = root.getScreenPreferences(parent.componentId); + return !prefs.includes("all") && !(typeof prefs[0] === "string" && prefs[0] === "all"); + } + } + + Column { + width: parent.width + spacing: Theme.spacingXS + visible: { + var prefs = root.getScreenPreferences(parent.componentId); + return !prefs.includes("all") && !(typeof prefs[0] === "string" && prefs[0] === "all"); + } + + Repeater { + model: Quickshell.screens + + delegate: DankToggle { + property var screenData: modelData + property string componentId: parent.parent.componentId + + width: parent.width + text: SettingsData.getScreenDisplayName(screenData) + description: screenData.width + "×" + screenData.height + " • " + (SettingsData.displayNameMode === "system" ? (screenData.model || "Unknown Model") : screenData.name) + checked: { + var prefs = root.getScreenPreferences(componentId); + if (typeof prefs[0] === "string" && prefs[0] === "all") + return false; + return SettingsData.isScreenInPreferences(screenData, prefs); + } + onToggled: checked => { + var currentPrefs = root.getScreenPreferences(componentId); + if (typeof currentPrefs[0] === "string" && currentPrefs[0] === "all") { + currentPrefs = []; + } + + const screenModelIndex = SettingsData.getScreenModelIndex(screenData); + + var newPrefs = currentPrefs.filter(pref => { + if (typeof pref === "string") + return false; + if (pref.modelIndex !== undefined && screenModelIndex >= 0) { + return !(pref.model === screenData.model && pref.modelIndex === screenModelIndex); + } + return pref.name !== screenData.name || pref.model !== screenData.model; + }); + + if (checked) { + const prefObj = { + "name": screenData.name, + "model": screenData.model || "" + }; + if (screenModelIndex >= 0) { + prefObj.modelIndex = screenModelIndex; + } + newPrefs.push(prefObj); + } + + root.setScreenPreferences(componentId, newPrefs); + } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DockTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DockTab.qml new file mode 100644 index 0000000..9b281a3 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/DockTab.qml @@ -0,0 +1,657 @@ +import QtQuick +import qs.Common +import qs.Modals.FileBrowser +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + FileBrowserModal { + id: dockLogoFileBrowser + browserTitle: I18n.tr("Select Dock Launcher Logo") + browserIcon: "image" + browserType: "generic" + filterExtensions: ["*.svg", "*.png", "*.jpg", "*.jpeg", "*.webp"] + onFileSelected: path => SettingsData.set("dockLauncherLogoCustomPath", path.replace("file://", "")) + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + width: parent.width + iconName: "dock_to_bottom" + title: I18n.tr("Dock Visibility") + settingKey: "dockVisibility" + + SettingsToggleRow { + settingKey: "showDock" + tags: ["dock", "show", "display", "enable"] + text: I18n.tr("Show Dock") + description: I18n.tr("Display a dock with pinned and running applications") + checked: SettingsData.showDock + onToggled: checked => SettingsData.setShowDock(checked) + } + + SettingsToggleRow { + settingKey: "dockAutoHide" + tags: ["dock", "autohide", "hide", "hover"] + text: I18n.tr("Auto-hide Dock") + description: I18n.tr("Always hide the dock and reveal it when hovering near the dock area") + checked: SettingsData.dockAutoHide + visible: SettingsData.showDock + onToggled: checked => { + if (checked && SettingsData.dockSmartAutoHide) { + SettingsData.set("dockSmartAutoHide", false); + } + SettingsData.set("dockAutoHide", checked); + } + } + + SettingsToggleRow { + settingKey: "dockSmartAutoHide" + tags: ["dock", "smart", "autohide", "windows", "overlap", "intelligent"] + text: I18n.tr("Intelligent Auto-hide") + description: I18n.tr("Show dock when floating windows don't overlap its area") + checked: SettingsData.dockSmartAutoHide + visible: SettingsData.showDock && (CompositorService.isNiri || CompositorService.isHyprland) + onToggled: checked => { + if (checked && SettingsData.dockAutoHide) { + SettingsData.set("dockAutoHide", false); + } + SettingsData.set("dockSmartAutoHide", checked); + } + } + + SettingsToggleRow { + settingKey: "dockOpenOnOverview" + tags: ["dock", "overview", "niri"] + text: I18n.tr("Show on Overview") + description: I18n.tr("Always show the dock when niri's overview is open") + checked: SettingsData.dockOpenOnOverview + visible: CompositorService.isNiri + onToggled: checked => SettingsData.set("dockOpenOnOverview", checked) + } + } + + SettingsCard { + width: parent.width + iconName: "swap_vert" + title: I18n.tr("Position") + settingKey: "dockPosition" + + Item { + width: parent.width + height: dockPositionButtonGroup.height + + DankButtonGroup { + id: dockPositionButtonGroup + anchors.horizontalCenter: parent.horizontalCenter + model: [I18n.tr("Top"), I18n.tr("Bottom"), I18n.tr("Left"), I18n.tr("Right")] + currentIndex: { + switch (SettingsData.dockPosition) { + case SettingsData.Position.Top: + return 0; + case SettingsData.Position.Bottom: + return 1; + case SettingsData.Position.Left: + return 2; + case SettingsData.Position.Right: + return 3; + default: + return 1; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + switch (index) { + case 0: + SettingsData.setDockPosition(SettingsData.Position.Top); + break; + case 1: + SettingsData.setDockPosition(SettingsData.Position.Bottom); + break; + case 2: + SettingsData.setDockPosition(SettingsData.Position.Left); + break; + case 3: + SettingsData.setDockPosition(SettingsData.Position.Right); + break; + } + } + } + } + } + + SettingsCard { + width: parent.width + iconName: "apps" + title: I18n.tr("Behavior") + settingKey: "dockBehavior" + + SettingsToggleRow { + settingKey: "dockIsolateDisplays" + tags: ["dock", "isolate", "monitor", "multi-monitor"] + text: I18n.tr("Isolate Displays") + description: I18n.tr("Only show windows from the current monitor on each dock") + checked: SettingsData.dockIsolateDisplays + onToggled: checked => SettingsData.set("dockIsolateDisplays", checked) + } + + SettingsToggleRow { + settingKey: "dockGroupByApp" + tags: ["dock", "group", "windows", "app"] + text: I18n.tr("Group by App") + description: I18n.tr("Group multiple windows of the same app together with a window count indicator") + checked: SettingsData.dockGroupByApp + onToggled: checked => SettingsData.set("dockGroupByApp", checked) + } + + SettingsToggleRow { + settingKey: "dockRestoreSpecialWorkspaceOnClick" + tags: ["dock", "hyprland", "special", "workspace", "restore"] + text: I18n.tr("Restore Special Workspace Windows") + description: I18n.tr("When clicking a dock window in a Hyprland special workspace, bring that special workspace back before focusing the window") + checked: SettingsData.dockRestoreSpecialWorkspaceOnClick + visible: CompositorService.isHyprland + onToggled: checked => SettingsData.set("dockRestoreSpecialWorkspaceOnClick", checked) + } + + SettingsButtonGroupRow { + settingKey: "dockIndicatorStyle" + tags: ["dock", "indicator", "style", "circle", "line"] + text: I18n.tr("Indicator Style") + model: [I18n.tr("Circle", "dock indicator style option"), I18n.tr("Line", "dock indicator style option")] + buttonPadding: Theme.spacingS + minButtonWidth: 44 + textSize: Theme.fontSizeSmall + currentIndex: SettingsData.dockIndicatorStyle === "circle" ? 0 : 1 + onSelectionChanged: (index, selected) => { + if (selected) { + SettingsData.set("dockIndicatorStyle", index === 0 ? "circle" : "line"); + } + } + } + + SettingsSliderRow { + settingKey: "dockMaxVisibleApps" + tags: ["dock", "overflow", "max", "apps", "limit"] + text: I18n.tr("Max Pinned Apps (0 = Unlimited)") + minimum: 0 + maximum: 30 + value: SettingsData.dockMaxVisibleApps + defaultValue: 0 + unit: "" + onSliderValueChanged: newValue => SettingsData.set("dockMaxVisibleApps", newValue) + } + + SettingsSliderRow { + settingKey: "dockMaxVisibleRunningApps" + tags: ["dock", "overflow", "max", "running", "apps", "limit"] + text: I18n.tr("Max Running Apps (0 = Unlimited)") + minimum: 0 + maximum: 30 + value: SettingsData.dockMaxVisibleRunningApps + defaultValue: 0 + unit: "" + onSliderValueChanged: newValue => SettingsData.set("dockMaxVisibleRunningApps", newValue) + } + + SettingsToggleRow { + settingKey: "dockShowOverflowBadge" + tags: ["dock", "overflow", "badge", "count", "indicator"] + text: I18n.tr("Show Overflow Badge Count") + description: I18n.tr("Displays count when overflow is active") + checked: SettingsData.dockShowOverflowBadge + onToggled: checked => SettingsData.set("dockShowOverflowBadge", checked) + } + } + + SettingsCard { + width: parent.width + iconName: "apps" + title: I18n.tr("Launcher Button") + settingKey: "dockLauncher" + + SettingsToggleRow { + settingKey: "dockLauncherEnabled" + tags: ["dock", "launcher", "button", "apps"] + text: I18n.tr("Show Launcher Button") + checked: SettingsData.dockLauncherEnabled + onToggled: checked => SettingsData.set("dockLauncherEnabled", checked) + } + + Column { + width: parent.width + spacing: Theme.spacingL + visible: SettingsData.dockLauncherEnabled + + Column { + width: parent.width + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Icon") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + Item { + width: parent.width + height: logoModeGroup.implicitHeight + clip: true + + DankButtonGroup { + id: logoModeGroup + anchors.horizontalCenter: parent.horizontalCenter + buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL + minButtonWidth: parent.width < 480 ? 44 : 64 + textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium + model: { + const modes = [I18n.tr("Apps Icon"), I18n.tr("OS Logo"), I18n.tr("Dank")]; + if (CompositorService.isNiri) { + modes.push("niri"); + } else if (CompositorService.isHyprland) { + modes.push("Hyprland"); + } else if (CompositorService.isDwl) { + modes.push("mango"); + } else if (CompositorService.isSway) { + modes.push("Sway"); + } else if (CompositorService.isScroll) { + modes.push("Scroll"); + } else if (CompositorService.isMiracle) { + modes.push("Miracle"); + } else { + modes.push(I18n.tr("Compositor")); + } + modes.push(I18n.tr("Custom")); + return modes; + } + currentIndex: { + if (SettingsData.dockLauncherLogoMode === "apps") + return 0; + if (SettingsData.dockLauncherLogoMode === "os") + return 1; + if (SettingsData.dockLauncherLogoMode === "dank") + return 2; + if (SettingsData.dockLauncherLogoMode === "compositor") + return 3; + if (SettingsData.dockLauncherLogoMode === "custom") + return 4; + return 0; + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + switch (index) { + case 0: + SettingsData.set("dockLauncherLogoMode", "apps"); + break; + case 1: + SettingsData.set("dockLauncherLogoMode", "os"); + break; + case 2: + SettingsData.set("dockLauncherLogoMode", "dank"); + break; + case 3: + SettingsData.set("dockLauncherLogoMode", "compositor"); + break; + case 4: + SettingsData.set("dockLauncherLogoMode", "custom"); + break; + } + } + } + } + } + + Row { + width: parent.width + visible: SettingsData.dockLauncherLogoMode === "custom" + spacing: Theme.spacingM + + StyledRect { + width: parent.width - selectButton.width - Theme.spacingM + height: 36 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9) + border.color: Theme.outlineStrong + border.width: 1 + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + text: SettingsData.dockLauncherLogoCustomPath || I18n.tr("Select an image file...") + font.pixelSize: Theme.fontSizeMedium + color: SettingsData.dockLauncherLogoCustomPath ? Theme.surfaceText : Theme.outlineButton + width: parent.width - Theme.spacingM * 2 + elide: Text.ElideMiddle + } + } + + DankActionButton { + id: selectButton + iconName: "folder_open" + width: 36 + height: 36 + onClicked: dockLogoFileBrowser.open() + } + } + + Column { + width: parent.width + spacing: Theme.spacingL + visible: SettingsData.dockLauncherLogoMode !== "apps" + + Column { + width: parent.width + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Color Override") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + Item { + width: parent.width + height: colorOverrideRow.implicitHeight + clip: true + + Row { + id: colorOverrideRow + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingM + + DankButtonGroup { + id: colorModeGroup + buttonPadding: parent.parent.width < 480 ? Theme.spacingS : Theme.spacingL + minButtonWidth: parent.parent.width < 480 ? 44 : 64 + textSize: parent.parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium + model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")] + currentIndex: { + const override = SettingsData.dockLauncherLogoColorOverride; + if (override === "") + return 0; + if (override === "primary") + return 1; + if (override === "surface") + return 2; + return 3; + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + switch (index) { + case 0: + SettingsData.set("dockLauncherLogoColorOverride", ""); + break; + case 1: + SettingsData.set("dockLauncherLogoColorOverride", "primary"); + break; + case 2: + SettingsData.set("dockLauncherLogoColorOverride", "surface"); + break; + case 3: + const currentOverride = SettingsData.dockLauncherLogoColorOverride; + const isPreset = currentOverride === "" || currentOverride === "primary" || currentOverride === "surface"; + if (isPreset) { + SettingsData.set("dockLauncherLogoColorOverride", "#ffffff"); + } + break; + } + } + } + + Rectangle { + id: colorPickerCircle + visible: { + const override = SettingsData.dockLauncherLogoColorOverride; + return override !== "" && override !== "primary" && override !== "surface"; + } + width: 36 + height: 36 + radius: 18 + color: { + const override = SettingsData.dockLauncherLogoColorOverride; + if (override !== "" && override !== "primary" && override !== "surface") + return override; + return "#ffffff"; + } + border.color: Theme.outline + border.width: 1 + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!PopoutService.colorPickerModal) + return; + PopoutService.colorPickerModal.selectedColor = SettingsData.dockLauncherLogoColorOverride; + PopoutService.colorPickerModal.pickerTitle = I18n.tr("Choose Dock Launcher Logo Color"); + PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) { + SettingsData.set("dockLauncherLogoColorOverride", selectedColor); + }; + PopoutService.colorPickerModal.show(); + } + } + } + } + } + } + + SettingsSliderRow { + settingKey: "dockLauncherLogoSizeOffset" + tags: ["dock", "launcher", "logo", "size", "offset", "scale"] + text: I18n.tr("Size Offset") + minimum: -12 + maximum: 12 + value: SettingsData.dockLauncherLogoSizeOffset + defaultValue: 0 + onSliderValueChanged: newValue => SettingsData.set("dockLauncherLogoSizeOffset", newValue) + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: { + const override = SettingsData.dockLauncherLogoColorOverride; + return override !== "" && override !== "primary" && override !== "surface"; + } + + SettingsSliderRow { + settingKey: "dockLauncherLogoBrightness" + tags: ["dock", "launcher", "logo", "brightness", "color"] + text: I18n.tr("Brightness") + minimum: 0 + maximum: 100 + value: Math.round(SettingsData.dockLauncherLogoBrightness * 100) + unit: "%" + defaultValue: 50 + onSliderValueChanged: newValue => SettingsData.set("dockLauncherLogoBrightness", newValue / 100) + } + + SettingsSliderRow { + settingKey: "dockLauncherLogoContrast" + tags: ["dock", "launcher", "logo", "contrast", "color"] + text: I18n.tr("Contrast") + minimum: 0 + maximum: 200 + value: Math.round(SettingsData.dockLauncherLogoContrast * 100) + unit: "%" + defaultValue: 100 + onSliderValueChanged: newValue => SettingsData.set("dockLauncherLogoContrast", newValue / 100) + } + } + } + } + } + + SettingsCard { + width: parent.width + iconName: "photo_size_select_large" + title: I18n.tr("Sizing") + settingKey: "dockSizing" + + SettingsSliderRow { + settingKey: "dockIconSize" + tags: ["dock", "icon", "size", "scale"] + text: I18n.tr("Icon Size") + value: SettingsData.dockIconSize + minimum: 24 + maximum: 96 + unit: "px" + defaultValue: 48 + onSliderValueChanged: newValue => SettingsData.set("dockIconSize", newValue) + } + } + + SettingsCard { + width: parent.width + iconName: "space_bar" + title: I18n.tr("Spacing") + settingKey: "dockSpacing" + collapsible: true + expanded: false + + SettingsSliderRow { + text: I18n.tr("Padding") + value: SettingsData.dockSpacing + minimum: 0 + maximum: 32 + defaultValue: 8 + onSliderValueChanged: newValue => SettingsData.set("dockSpacing", newValue) + } + + SettingsSliderRow { + text: I18n.tr("Exclusive Zone Offset") + value: SettingsData.dockBottomGap + minimum: -100 + maximum: 100 + defaultValue: 0 + onSliderValueChanged: newValue => SettingsData.set("dockBottomGap", newValue) + } + + SettingsSliderRow { + text: I18n.tr("Margin") + value: SettingsData.dockMargin + minimum: 0 + maximum: 100 + defaultValue: 0 + onSliderValueChanged: newValue => SettingsData.set("dockMargin", newValue) + } + } + + SettingsCard { + width: parent.width + iconName: "opacity" + title: I18n.tr("Transparency") + settingKey: "dockTransparency" + + SettingsSliderRow { + text: I18n.tr("Dock Transparency") + value: Math.round(SettingsData.dockTransparency * 100) + minimum: 0 + maximum: 100 + unit: "%" + defaultValue: 85 + onSliderValueChanged: newValue => SettingsData.set("dockTransparency", newValue / 100) + } + } + + SettingsCard { + width: parent.width + iconName: "border_style" + title: I18n.tr("Border") + settingKey: "dockBorder" + collapsible: true + expanded: false + + SettingsToggleRow { + text: I18n.tr("Border") + description: I18n.tr("Add a border around the dock") + checked: SettingsData.dockBorderEnabled + onToggled: checked => SettingsData.set("dockBorderEnabled", checked) + } + + SettingsButtonGroupRow { + text: I18n.tr("Border Color") + description: I18n.tr("Choose the border accent color") + visible: SettingsData.dockBorderEnabled + model: [I18n.tr("Surface", "color option"), I18n.tr("Secondary", "color option"), I18n.tr("Primary", "color option")] + buttonPadding: Theme.spacingS + minButtonWidth: 44 + textSize: Theme.fontSizeSmall + currentIndex: { + switch (SettingsData.dockBorderColor) { + case "surfaceText": + return 0; + case "secondary": + return 1; + case "primary": + return 2; + default: + return 0; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + switch (index) { + case 0: + SettingsData.set("dockBorderColor", "surfaceText"); + break; + case 1: + SettingsData.set("dockBorderColor", "secondary"); + break; + case 2: + SettingsData.set("dockBorderColor", "primary"); + break; + } + } + } + + SettingsSliderRow { + text: I18n.tr("Border Opacity") + visible: SettingsData.dockBorderEnabled + value: SettingsData.dockBorderOpacity * 100 + minimum: 0 + maximum: 100 + unit: "%" + defaultValue: 100 + onSliderValueChanged: newValue => SettingsData.set("dockBorderOpacity", newValue / 100) + } + + SettingsSliderRow { + text: I18n.tr("Border Thickness") + visible: SettingsData.dockBorderEnabled + value: SettingsData.dockBorderThickness + minimum: 1 + maximum: 10 + unit: "px" + defaultValue: 1 + onSliderValueChanged: newValue => SettingsData.set("dockBorderThickness", newValue) + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/GammaControlTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/GammaControlTab.qml new file mode 100644 index 0000000..7d08c5a --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/GammaControlTab.qml @@ -0,0 +1,663 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + function formatGammaTime(isoString) { + if (!isoString) + return ""; + try { + const date = new Date(isoString); + if (isNaN(date.getTime())) + return ""; + return date.toLocaleTimeString(Qt.locale(), "HH:mm"); + } catch (e) { + return ""; + } + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + StyledRect { + width: parent.width + height: gammaSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 0 + + Column { + id: gammaSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "brightness_6" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Gamma Control") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: nightModeToggle + + width: parent.width + text: I18n.tr("Night Mode") + description: DisplayService.gammaControlAvailable ? I18n.tr("Apply warm color temperature to reduce eye strain. Use automation settings below to control when it activates.") : I18n.tr("Gamma control not available. Requires DMS API v6+.") + checked: DisplayService.nightModeEnabled + enabled: DisplayService.gammaControlAvailable + onToggled: checked => { + DisplayService.toggleNightMode(); + } + + Connections { + function onNightModeEnabledChanged() { + nightModeToggle.checked = DisplayService.nightModeEnabled; + } + + target: DisplayService + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + leftPadding: Theme.spacingM + rightPadding: Theme.spacingM + visible: DisplayService.gammaControlAvailable + + SettingsSliderRow { + id: nightTempSlider + settingKey: "nightModeTemperature" + tags: ["gamma", "night", "temperature", "kelvin", "warm", "color", "blue light"] + width: parent.width - parent.leftPadding - parent.rightPadding + text: SessionData.nightModeAutoEnabled ? I18n.tr("Night Temperature") : I18n.tr("Color Temperature") + description: SessionData.nightModeAutoEnabled ? I18n.tr("Color temperature for night mode") : I18n.tr("Warm color temperature to apply") + minimum: 2500 + maximum: 6000 + step: 100 + unit: "K" + value: SessionData.nightModeTemperature + onSliderValueChanged: newValue => { + SessionData.setNightModeTemperature(newValue); + if (SessionData.nightModeHighTemperature < newValue) + SessionData.setNightModeHighTemperature(newValue); + } + } + + SettingsSliderRow { + id: dayTempSlider + settingKey: "nightModeHighTemperature" + tags: ["gamma", "day", "temperature", "kelvin", "color"] + width: parent.width - parent.leftPadding - parent.rightPadding + text: I18n.tr("Day Temperature") + description: I18n.tr("Color temperature for day time") + minimum: SessionData.nightModeTemperature + maximum: 10000 + step: 100 + unit: "K" + value: Math.max(SessionData.nightModeHighTemperature, SessionData.nightModeTemperature) + visible: SessionData.nightModeAutoEnabled + onSliderValueChanged: newValue => SessionData.setNightModeHighTemperature(newValue) + } + } + + DankToggle { + id: automaticToggle + width: parent.width + text: I18n.tr("Automatic Control") + description: I18n.tr("Only adjust gamma based on time or location rules.") + checked: SessionData.nightModeAutoEnabled + visible: DisplayService.gammaControlAvailable + onToggled: checked => { + if (checked && !DisplayService.nightModeEnabled) { + DisplayService.toggleNightMode(); + } else if (!checked && DisplayService.nightModeEnabled) { + DisplayService.toggleNightMode(); + } + SessionData.setNightModeAutoEnabled(checked); + } + + Connections { + target: SessionData + function onNightModeAutoEnabledChanged() { + automaticToggle.checked = SessionData.nightModeAutoEnabled; + } + } + } + + Column { + id: automaticSettings + width: parent.width + spacing: Theme.spacingS + visible: SessionData.nightModeAutoEnabled && DisplayService.gammaControlAvailable + + Connections { + target: SessionData + function onNightModeAutoEnabledChanged() { + automaticSettings.visible = SessionData.nightModeAutoEnabled; + } + } + + Item { + width: parent.width + height: 45 + Theme.spacingM + + DankTabBar { + id: modeTabBarNight + width: 200 + height: 45 + anchors.horizontalCenter: parent.horizontalCenter + model: [ + { + "text": I18n.tr("Time"), + "icon": "access_time" + }, + { + "text": I18n.tr("Location"), + "icon": "place" + } + ] + + Component.onCompleted: { + currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0; + Qt.callLater(updateIndicator); + } + + onTabClicked: index => { + DisplayService.setNightModeAutomationMode(index === 1 ? "location" : "time"); + currentIndex = index; + } + + Connections { + target: SessionData + function onNightModeAutoModeChanged() { + modeTabBarNight.currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0; + Qt.callLater(modeTabBarNight.updateIndicator); + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: SessionData.nightModeAutoMode === "time" + + Column { + spacing: Theme.spacingXS + anchors.horizontalCenter: parent.horizontalCenter + + Row { + spacing: Theme.spacingM + + StyledText { + text: "" + width: 50 + height: 20 + } + + StyledText { + text: I18n.tr("Hour") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: 70 + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + text: I18n.tr("Minute") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: 70 + horizontalAlignment: Text.AlignHCenter + } + } + + Row { + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Start") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: 50 + height: 40 + verticalAlignment: Text.AlignVCenter + } + + DankDropdown { + dropdownWidth: 70 + currentValue: SessionData.nightModeStartHour.toString() + options: { + var hours = []; + for (var i = 0; i < 24; i++) { + hours.push(i.toString()); + } + return hours; + } + onValueChanged: value => { + SessionData.setNightModeStartHour(parseInt(value)); + } + } + + DankDropdown { + dropdownWidth: 70 + currentValue: SessionData.nightModeStartMinute.toString().padStart(2, '0') + options: { + var minutes = []; + for (var i = 0; i < 60; i += 5) { + minutes.push(i.toString().padStart(2, '0')); + } + return minutes; + } + onValueChanged: value => { + SessionData.setNightModeStartMinute(parseInt(value)); + } + } + } + + Row { + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("End") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: 50 + height: 40 + verticalAlignment: Text.AlignVCenter + } + + DankDropdown { + dropdownWidth: 70 + currentValue: SessionData.nightModeEndHour.toString() + options: { + var hours = []; + for (var i = 0; i < 24; i++) { + hours.push(i.toString()); + } + return hours; + } + onValueChanged: value => { + SessionData.setNightModeEndHour(parseInt(value)); + } + } + + DankDropdown { + dropdownWidth: 70 + currentValue: SessionData.nightModeEndMinute.toString().padStart(2, '0') + options: { + var minutes = []; + for (var i = 0; i < 60; i += 5) { + minutes.push(i.toString().padStart(2, '0')); + } + return minutes; + } + onValueChanged: value => { + SessionData.setNightModeEndMinute(parseInt(value)); + } + } + } + } + } + + Column { + property bool isLocationMode: SessionData.nightModeAutoMode === "location" + visible: isLocationMode + spacing: Theme.spacingM + width: parent.width + + DankToggle { + id: ipLocationToggle + width: parent.width + text: I18n.tr("Use IP Location") + description: I18n.tr("Automatically detect location based on IP address") + checked: SessionData.nightModeUseIPLocation || false + onToggled: checked => { + SessionData.setNightModeUseIPLocation(checked); + } + + Connections { + target: SessionData + function onNightModeUseIPLocationChanged() { + ipLocationToggle.checked = SessionData.nightModeUseIPLocation; + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + leftPadding: Theme.spacingM + visible: !SessionData.nightModeUseIPLocation + + StyledText { + text: I18n.tr("Manual Coordinates") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + } + + Row { + spacing: Theme.spacingL + + Column { + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Latitude") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + DankTextField { + width: 120 + height: 40 + text: SessionData.latitude.toString() + placeholderText: "0.0" + onEditingFinished: { + const lat = parseFloat(text); + if (!isNaN(lat) && lat >= -90 && lat <= 90 && lat !== SessionData.latitude) { + SessionData.setLatitude(lat); + } + } + } + } + + Column { + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Longitude") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + DankTextField { + width: 120 + height: 40 + text: SessionData.longitude.toString() + placeholderText: "0.0" + onEditingFinished: { + const lon = parseFloat(text); + if (!isNaN(lon) && lon >= -180 && lon <= 180 && lon !== SessionData.longitude) { + SessionData.setLongitude(lon); + } + } + } + } + } + + StyledText { + text: I18n.tr("Uses sunrise/sunset times to automatically adjust night mode based on your location.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width - parent.leftPadding + wrapMode: Text.WordWrap + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + visible: gammaStatusSection.visible + } + + Column { + id: gammaStatusSection + width: parent.width + spacing: Theme.spacingM + visible: DisplayService.nightModeEnabled && DisplayService.gammaCurrentTemp > 0 + + Row { + width: parent.width + spacing: Theme.spacingS + + DankIcon { + name: DisplayService.gammaIsDay ? "light_mode" : "dark_mode" + size: Theme.iconSizeSmall + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Current Status") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: tempColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + id: tempColumn + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "device_thermostat" + size: Theme.iconSize + color: Theme.primary + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: DisplayService.gammaCurrentTemp + "K" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("Current Temp") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: periodColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + id: periodColumn + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: DisplayService.gammaIsDay ? "wb_sunny" : "nightlight" + size: Theme.iconSize + color: DisplayService.gammaIsDay ? "#FFA726" : "#7E57C2" + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: DisplayService.gammaIsDay ? I18n.tr("Daytime") : I18n.tr("Night") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("Current Period") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + visible: SessionData.nightModeAutoMode === "location" && (DisplayService.gammaSunriseTime || DisplayService.gammaSunsetTime) + + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: sunriseColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + visible: DisplayService.gammaSunriseTime + + Column { + id: sunriseColumn + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "wb_twilight" + size: Theme.iconSize + color: "#FF7043" + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: root.formatGammaTime(DisplayService.gammaSunriseTime) + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("Sunrise") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: sunsetColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + visible: DisplayService.gammaSunsetTime + + Column { + id: sunsetColumn + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "wb_twilight" + size: Theme.iconSize + color: "#5C6BC0" + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: root.formatGammaTime(DisplayService.gammaSunsetTime) + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("Sunset") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + + Rectangle { + width: parent.width + height: nextChangeRow.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + visible: DisplayService.gammaNextTransition + + Row { + id: nextChangeRow + anchors.centerIn: parent + spacing: Theme.spacingM + + DankIcon { + name: "schedule" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: 2 + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Next Transition") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: root.formatGammaTime(DisplayService.gammaNextTransition) + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + } + } + } + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/GreeterTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/GreeterTab.qml new file mode 100644 index 0000000..b3d3a41 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/GreeterTab.qml @@ -0,0 +1,784 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Common +import qs.Modals.Common +import qs.Modals.FileBrowser +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + readonly property bool greeterFprintToggleAvailable: SettingsData.greeterFingerprintCanEnable || SettingsData.greeterEnableFprint + readonly property bool greeterU2fToggleAvailable: SettingsData.greeterU2fCanEnable || SettingsData.greeterEnableU2f + + function greeterFingerprintDescription() { + const source = SettingsData.greeterFingerprintSource; + const reason = SettingsData.greeterFingerprintReason; + + if (source === "pam") { + switch (reason) { + case "configured_externally": + return SettingsData.greeterEnableFprint ? I18n.tr("Enabled. PAM already provides fingerprint auth.") : I18n.tr("PAM already provides fingerprint auth. Enable this to show it at login."); + case "missing_enrollment": + return SettingsData.greeterEnableFprint ? I18n.tr("Enabled. PAM provides fingerprint auth, but no prints are enrolled yet.") : I18n.tr("PAM provides fingerprint auth, but no prints are enrolled yet."); + case "missing_reader": + return I18n.tr("PAM provides fingerprint auth, but no reader was detected."); + default: + return I18n.tr("PAM provides fingerprint auth, but availability could not be confirmed."); + } + } + + switch (reason) { + case "ready": + return SettingsData.greeterEnableFprint ? I18n.tr("Authentication changes apply automatically. Fingerprint-only login may not unlock Keyring.") : I18n.tr("Only affects DMS-managed PAM. If greetd already includes pam_fprintd, fingerprint stays enabled."); + case "missing_enrollment": + if (SettingsData.greeterEnableFprint) + return I18n.tr("Enabled, but no prints are enrolled yet. Enroll fingerprints and run Sync."); + return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later."); + case "missing_reader": + return SettingsData.greeterEnableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected."); + case "missing_pam_support": + return I18n.tr("Not available — install fprintd and pam_fprintd, or configure greetd PAM."); + default: + return SettingsData.greeterEnableFprint ? I18n.tr("Enabled, but fingerprint availability could not be confirmed.") : I18n.tr("Fingerprint availability could not be confirmed."); + } + } + + function greeterU2fDescription() { + const source = SettingsData.greeterU2fSource; + const reason = SettingsData.greeterU2fReason; + + if (source === "pam") { + return SettingsData.greeterEnableU2f ? I18n.tr("Enabled. PAM already provides security-key auth.") : I18n.tr("PAM already provides security-key auth. Enable this to show it at login."); + } + + switch (reason) { + case "ready": + return SettingsData.greeterEnableU2f ? I18n.tr("Authentication changes apply automatically.") : I18n.tr("Available."); + case "missing_key_registration": + if (SettingsData.greeterEnableU2f) + return I18n.tr("Enabled, but no registered security key was found yet. Register a key and run Sync."); + return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later."); + case "missing_pam_support": + return I18n.tr("Not available — install or configure pam_u2f, or configure greetd PAM."); + default: + return SettingsData.greeterEnableU2f ? I18n.tr("Enabled, but security-key availability could not be confirmed.") : I18n.tr("Security-key availability could not be confirmed."); + } + } + + function refreshAuthDetection() { + SettingsData.refreshAuthAvailability(); + } + + onVisibleChanged: { + if (visible) + refreshAuthDetection(); + } + + ConfirmModal { + id: greeterActionConfirm + } + + FileBrowserModal { + id: greeterWallpaperBrowserModal + browserTitle: I18n.tr("Select greeter background image") + browserIcon: "wallpaper" + browserType: "wallpaper" + showHiddenFiles: true + fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif"] + onFileSelected: path => { + SettingsData.set("greeterWallpaperPath", path); + close(); + } + } + + property string greeterStatusText: "" + property bool greeterStatusRunning: false + property bool greeterSyncRunning: false + property bool greeterInstallActionRunning: false + property string greeterStatusStdout: "" + property string greeterStatusStderr: "" + property string greeterSyncStdout: "" + property string greeterSyncStderr: "" + property string greeterSudoProbeStderr: "" + property string greeterTerminalFallbackStderr: "" + property bool greeterTerminalFallbackFromPrecheck: false + property var cachedFontFamilies: [] + property bool fontsEnumerated: false + property bool greeterBinaryExists: false + property bool greeterEnabled: false + readonly property bool greeterInstalled: greeterBinaryExists || greeterEnabled + + readonly property string greeterActionLabel: { + if (!root.greeterInstalled) + return I18n.tr("Install"); + if (!root.greeterEnabled) + return I18n.tr("Activate"); + return I18n.tr("Uninstall"); + } + readonly property string greeterActionIcon: { + if (!root.greeterInstalled) + return "download"; + if (!root.greeterEnabled) + return "login"; + return "delete"; + } + readonly property var greeterActionCommand: { + if (!root.greeterInstalled) + return ["dms", "greeter", "install", "--terminal"]; + if (!root.greeterEnabled) + return ["dms", "greeter", "enable", "--terminal"]; + return ["dms", "greeter", "uninstall", "--terminal", "--yes"]; + } + property string greeterPendingAction: "" + + function checkGreeterInstallState() { + greetdEnabledCheckProcess.running = true; + greeterBinaryCheckProcess.running = true; + } + + function runGreeterStatus() { + greeterStatusText = ""; + greeterStatusStdout = ""; + greeterStatusStderr = ""; + greeterStatusRunning = true; + greeterStatusProcess.running = true; + } + + function runGreeterInstallAction() { + root.greeterPendingAction = !root.greeterInstalled ? "install" : !root.greeterEnabled ? "activate" : "uninstall"; + greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + "…"; + greeterInstallActionRunning = true; + greeterInstallActionProcess.running = true; + } + + function promptGreeterActionConfirm() { + var title, message, confirmText; + if (!root.greeterInstalled) { + title = I18n.tr("Install Greeter", "greeter action confirmation"); + message = I18n.tr("Install the DMS greeter? A terminal will open for sudo authentication."); + confirmText = I18n.tr("Install"); + } else if (!root.greeterEnabled) { + title = I18n.tr("Activate Greeter", "greeter action confirmation"); + message = I18n.tr("Activate the DMS greeter? A terminal will open for sudo authentication. Run Sync after activation to apply your settings."); + confirmText = I18n.tr("Activate"); + } else { + title = I18n.tr("Uninstall Greeter", "greeter action confirmation"); + message = I18n.tr("Uninstall the DMS greeter? This will remove configuration and restore your previous display manager. A terminal will open for sudo authentication."); + confirmText = I18n.tr("Uninstall"); + } + greeterActionConfirm.showWithOptions({ + "title": title, + "message": message, + "confirmText": confirmText, + "cancelText": I18n.tr("Cancel"), + "confirmColor": Theme.primary, + "onConfirm": () => root.runGreeterInstallAction(), + "onCancel": () => {} + }); + } + + function runGreeterSync() { + greeterSyncStdout = ""; + greeterSyncStderr = ""; + greeterSudoProbeStderr = ""; + greeterTerminalFallbackStderr = ""; + greeterTerminalFallbackFromPrecheck = false; + greeterStatusText = I18n.tr("Checking whether sudo authentication is needed…"); + greeterSyncRunning = true; + greeterSudoProbeProcess.running = true; + } + + function launchGreeterSyncTerminalFallback(fromPrecheck, statusText) { + greeterTerminalFallbackFromPrecheck = fromPrecheck; + if (statusText && statusText !== "") + greeterStatusText = statusText; + greeterTerminalFallbackStderr = ""; + greeterTerminalFallbackProcess.running = true; + } + + function enumerateFonts() { + if (fontsEnumerated) + return; + var fonts = []; + var availableFonts = Qt.fontFamilies(); + for (var i = 0; i < availableFonts.length; i++) { + var fontName = availableFonts[i]; + if (fontName.startsWith(".")) + continue; + fonts.push(fontName); + } + fonts.sort(); + fonts.unshift("Default"); + cachedFontFamilies = fonts; + fontsEnumerated = true; + } + + Component.onCompleted: { + refreshAuthDetection(); + Qt.callLater(enumerateFonts); + Qt.callLater(checkGreeterInstallState); + } + + Process { + id: greetdEnabledCheckProcess + command: ["systemctl", "is-enabled", "greetd"] + running: false + + stdout: StdioCollector { + onStreamFinished: root.greeterEnabled = text.trim() === "enabled" + } + } + + Process { + id: greeterBinaryCheckProcess + command: ["sh", "-c", "test -f /usr/bin/dms-greeter || test -f /usr/local/bin/dms-greeter"] + running: false + + onExited: exitCode => { + root.greeterBinaryExists = (exitCode === 0); + } + } + + Process { + id: greeterStatusProcess + command: ["dms", "greeter", "status"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + root.greeterStatusStdout = text || ""; + } + } + + stderr: StdioCollector { + onStreamFinished: root.greeterStatusStderr = text || "" + } + + onExited: exitCode => { + root.greeterStatusRunning = false; + const out = (root.greeterStatusStdout || "").trim(); + const err = (root.greeterStatusStderr || "").trim(); + if (exitCode === 0) { + root.greeterStatusText = out !== "" ? out : I18n.tr("No status output."); + if (err !== "") + root.greeterStatusText = root.greeterStatusText + "\n\nstderr:\n" + err; + return; + } + var failure = I18n.tr("Failed to run 'dms greeter status'. Ensure DMS is installed and dms is in PATH.", "greeter status error") + " (exit " + exitCode + ")"; + if (out !== "") + failure = failure + "\n\n" + out; + if (err !== "") + failure = failure + "\n\nstderr:\n" + err; + root.greeterStatusText = failure; + } + } + + Process { + id: greeterSyncProcess + command: ["dms", "greeter", "sync", "--yes"] + running: false + + stdout: StdioCollector { + onStreamFinished: root.greeterSyncStdout = text || "" + } + + stderr: StdioCollector { + onStreamFinished: root.greeterSyncStderr = text || "" + } + + onExited: exitCode => { + root.greeterSyncRunning = false; + const out = (root.greeterSyncStdout || "").trim(); + const err = (root.greeterSyncStderr || "").trim(); + if (exitCode === 0) { + var success = I18n.tr("Sync completed successfully."); + if (out !== "") + success = success + "\n\n" + out; + if (err !== "") + success = success + "\n\nstderr:\n" + err; + root.greeterStatusText = success; + } else { + var failure = I18n.tr("Sync failed in background mode. Trying terminal mode so you can authenticate interactively.") + " (exit " + exitCode + ")"; + if (out !== "") + failure = failure + "\n\n" + out; + if (err !== "") + failure = failure + "\n\nstderr:\n" + err; + root.greeterStatusText = failure; + root.launchGreeterSyncTerminalFallback(false, ""); + } + root.checkGreeterInstallState(); + } + } + + Process { + id: greeterSudoProbeProcess + command: ["sudo", "-n", "true"] + running: false + + stderr: StdioCollector { + onStreamFinished: root.greeterSudoProbeStderr = text || "" + } + + onExited: exitCode => { + const err = (root.greeterSudoProbeStderr || "").trim(); + if (exitCode === 0) { + root.greeterStatusText = I18n.tr("Running greeter sync…"); + greeterSyncProcess.running = true; + return; + } + + var authNeeded = I18n.tr("Sync needs sudo authentication. Opening terminal so you can use password or fingerprint."); + if (err !== "") + authNeeded = authNeeded + "\n\n" + err; + root.launchGreeterSyncTerminalFallback(true, authNeeded); + } + } + + Process { + id: greeterTerminalFallbackProcess + command: ["dms", "greeter", "sync", "--terminal", "--yes"] + running: false + + stderr: StdioCollector { + onStreamFinished: root.greeterTerminalFallbackStderr = text || "" + } + + onExited: exitCode => { + root.greeterSyncRunning = false; + if (exitCode === 0) { + var launched = root.greeterTerminalFallbackFromPrecheck ? I18n.tr("Terminal opened. Complete sync authentication there; it will close automatically when done.") : I18n.tr("Terminal fallback opened. Complete sync there; it will close automatically when done."); + root.greeterStatusText = root.greeterStatusText ? root.greeterStatusText + "\n\n" + launched : launched; + return; + } + var fallback = I18n.tr("Terminal fallback failed. Install one of the supported terminal emulators or run 'dms greeter sync' manually.") + " (exit " + exitCode + ")"; + const err = (root.greeterTerminalFallbackStderr || "").trim(); + if (err !== "") + fallback = fallback + "\n\nstderr:\n" + err; + root.greeterStatusText = root.greeterStatusText ? root.greeterStatusText + "\n\n" + fallback : fallback; + } + } + + Process { + id: greeterInstallActionProcess + command: root.greeterActionCommand + running: false + + onExited: exitCode => { + root.greeterInstallActionRunning = false; + const pending = root.greeterPendingAction; + root.greeterPendingAction = ""; + if (exitCode === 0) { + if (pending === "install") + root.greeterStatusText = I18n.tr("Install complete. Greeter has been installed."); + else if (pending === "activate") + root.greeterStatusText = I18n.tr("Greeter activated. greetd is now enabled."); + else + root.greeterStatusText = I18n.tr("Uninstall complete. Greeter has been removed."); + } else { + root.greeterStatusText = I18n.tr("Action failed or terminal was closed.") + " (exit " + exitCode + ")"; + } + root.checkGreeterInstallState(); + } + } + + readonly property var _lockDateFormatPresets: [ + { + format: "", + label: I18n.tr("System Default", "date format option") + }, + { + format: "ddd d", + label: I18n.tr("Day Date", "date format option") + }, + { + format: "ddd MMM d", + label: I18n.tr("Day Month Date", "date format option") + }, + { + format: "MMM d", + label: I18n.tr("Month Date", "date format option") + }, + { + format: "M/d", + label: I18n.tr("Numeric (M/D)", "date format option") + }, + { + format: "d/M", + label: I18n.tr("Numeric (D/M)", "date format option") + }, + { + format: "ddd d MMM yyyy", + label: I18n.tr("Full with Year", "date format option") + }, + { + format: "yyyy-MM-dd", + label: I18n.tr("ISO Date", "date format option") + }, + { + format: "dddd, MMMM d", + label: I18n.tr("Full Day & Month", "date format option") + } + ] + readonly property var _wallpaperFillModes: ["Stretch", "Fit", "Fill", "Tile", "TileVertically", "TileHorizontally", "Pad"] + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + width: parent.width + iconName: "info" + title: I18n.tr("Greeter Status") + settingKey: "greeterStatus" + + StyledText { + text: I18n.tr("Check sync status on demand. Sync copies your theme, settings, and wallpaper configuration to the login screen. Authentication changes apply automatically.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.Wrap + } + + Item { + width: 1 + height: Theme.spacingS + } + + Rectangle { + width: parent.width + height: Math.min(180, statusTextArea.implicitHeight + Theme.spacingM * 2) + radius: Theme.cornerRadius + color: Theme.surfaceContainerHighest + + StyledText { + id: statusTextArea + anchors.fill: parent + anchors.margins: Theme.spacingM + text: root.greeterStatusRunning ? I18n.tr("Checking…", "greeter status loading") : (root.greeterStatusText || I18n.tr("Click Refresh to check status.", "greeter status placeholder")) + font.pixelSize: Theme.fontSizeSmall + font.family: "monospace" + color: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText + wrapMode: Text.Wrap + verticalAlignment: Text.AlignTop + } + } + + Item { + width: 1 + height: Theme.spacingM + } + + RowLayout { + width: parent.width + spacing: Theme.spacingS + + DankButton { + text: root.greeterActionLabel + iconName: root.greeterActionIcon + horizontalPadding: Theme.spacingL + onClicked: root.promptGreeterActionConfirm() + enabled: !root.greeterInstallActionRunning && !root.greeterSyncRunning + } + + Item { + Layout.fillWidth: true + } + + DankButton { + text: I18n.tr("Refresh") + iconName: "refresh" + horizontalPadding: Theme.spacingL + onClicked: root.runGreeterStatus() + enabled: !root.greeterStatusRunning + } + + DankButton { + text: I18n.tr("Sync") + iconName: "sync" + horizontalPadding: Theme.spacingL + onClicked: root.runGreeterSync() + enabled: root.greeterInstalled && !root.greeterSyncRunning && !root.greeterInstallActionRunning + } + } + } + + SettingsCard { + width: parent.width + iconName: "fingerprint" + title: I18n.tr("Login Authentication") + settingKey: "greeterAuth" + + StyledText { + text: I18n.tr("Enable fingerprint or security key for DMS Greeter. Authentication changes apply automatically.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.Wrap + } + + SettingsToggleRow { + settingKey: "greeterEnableFprint" + tags: ["greeter", "fingerprint", "fprintd", "login", "auth"] + text: I18n.tr("Enable fingerprint at login") + description: root.greeterFingerprintDescription() + descriptionColor: (SettingsData.greeterFingerprintReason === "ready" || SettingsData.greeterFingerprintReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning + checked: SettingsData.greeterEnableFprint + enabled: root.greeterFprintToggleAvailable + onToggled: checked => SettingsData.set("greeterEnableFprint", checked) + } + + SettingsToggleRow { + settingKey: "greeterEnableU2f" + tags: ["greeter", "u2f", "security", "key", "login", "auth"] + text: I18n.tr("Enable security key at login") + description: root.greeterU2fDescription() + descriptionColor: (SettingsData.greeterU2fReason === "ready" || SettingsData.greeterU2fReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning + checked: SettingsData.greeterEnableU2f + enabled: root.greeterU2fToggleAvailable + onToggled: checked => SettingsData.set("greeterEnableU2f", checked) + } + } + + SettingsCard { + width: parent.width + iconName: "palette" + title: I18n.tr("Greeter Appearance") + settingKey: "greeterAppearance" + + StyledText { + text: I18n.tr("Font") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + topPadding: Theme.spacingM + } + + SettingsDropdownRow { + settingKey: "greeterFontFamily" + tags: ["greeter", "font", "typography"] + text: I18n.tr("Greeter font") + description: I18n.tr("Font used on the login screen") + options: root.fontsEnumerated ? root.cachedFontFamilies : ["Default"] + currentValue: (!SettingsData.greeterFontFamily || SettingsData.greeterFontFamily === "" || SettingsData.greeterFontFamily === Theme.defaultFontFamily) ? "Default" : (SettingsData.greeterFontFamily || "Default") + enableFuzzySearch: true + popupWidthOffset: 100 + maxPopupHeight: 400 + onValueChanged: value => { + if (value === "Default") + SettingsData.set("greeterFontFamily", ""); + else + SettingsData.set("greeterFontFamily", value); + } + } + + StyledText { + text: I18n.tr("Time format") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + topPadding: Theme.spacingM + } + + SettingsToggleRow { + settingKey: "greeterUse24Hour" + tags: ["greeter", "time", "24hour"] + text: I18n.tr("24-hour clock") + description: I18n.tr("Greeter only — does not affect main clock") + checked: SettingsData.greeterUse24HourClock + onToggled: checked => SettingsData.set("greeterUse24HourClock", checked) + } + + SettingsToggleRow { + settingKey: "greeterShowSeconds" + tags: ["greeter", "time", "seconds"] + text: I18n.tr("Show seconds") + checked: SettingsData.greeterShowSeconds + onToggled: checked => SettingsData.set("greeterShowSeconds", checked) + } + + SettingsToggleRow { + settingKey: "greeterPadHours" + tags: ["greeter", "time", "12hour"] + text: I18n.tr("Pad hours (02:00 vs 2:00)") + visible: !SettingsData.greeterUse24HourClock + checked: SettingsData.greeterPadHours12Hour + onToggled: checked => SettingsData.set("greeterPadHours12Hour", checked) + } + + StyledText { + text: I18n.tr("Date format on greeter") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + topPadding: Theme.spacingM + } + + SettingsDropdownRow { + settingKey: "greeterLockDateFormat" + tags: ["greeter", "date", "format"] + text: I18n.tr("Date format") + description: I18n.tr("Greeter only — format for the date on the login screen") + options: root._lockDateFormatPresets.map(p => p.label) + currentValue: { + var current = (SettingsData.greeterLockDateFormat !== undefined && SettingsData.greeterLockDateFormat !== "") ? SettingsData.greeterLockDateFormat : SettingsData.lockDateFormat || ""; + var match = root._lockDateFormatPresets.find(p => p.format === current); + return match ? match.label : (current ? I18n.tr("Custom: ") + current : root._lockDateFormatPresets[0].label); + } + onValueChanged: value => { + var preset = root._lockDateFormatPresets.find(p => p.label === value); + SettingsData.set("greeterLockDateFormat", preset ? preset.format : ""); + } + } + + StyledText { + text: I18n.tr("Background") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + topPadding: Theme.spacingM + } + + StyledText { + text: I18n.tr("Use a custom image for the login screen, or leave empty to use your desktop wallpaper.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.Wrap + } + + Row { + width: parent.width + spacing: Theme.spacingS + + DankTextField { + id: greeterWallpaperPathField + width: parent.width - browseGreeterWallpaperButton.width - Theme.spacingS + placeholderText: I18n.tr("Use desktop wallpaper") + text: SettingsData.greeterWallpaperPath + backgroundColor: Theme.surfaceContainerHighest + onTextChanged: { + if (text !== SettingsData.greeterWallpaperPath) + SettingsData.set("greeterWallpaperPath", text); + } + } + + DankButton { + id: browseGreeterWallpaperButton + text: I18n.tr("Browse") + horizontalPadding: Theme.spacingL + onClicked: greeterWallpaperBrowserModal.open() + } + } + + SettingsDropdownRow { + settingKey: "greeterWallpaperFillMode" + tags: ["greeter", "wallpaper", "background", "fill"] + text: I18n.tr("Wallpaper fill mode") + description: I18n.tr("How the background image is scaled") + options: root._wallpaperFillModes.map(m => I18n.tr(m, "wallpaper fill mode")) + currentValue: { + var mode = (SettingsData.greeterWallpaperFillMode && SettingsData.greeterWallpaperFillMode !== "") ? SettingsData.greeterWallpaperFillMode : (SettingsData.wallpaperFillMode || "Fill"); + var idx = root._wallpaperFillModes.indexOf(mode); + return idx >= 0 ? I18n.tr(root._wallpaperFillModes[idx], "wallpaper fill mode") : I18n.tr("Fill", "wallpaper fill mode"); + } + onValueChanged: value => { + var idx = root._wallpaperFillModes.map(m => I18n.tr(m, "wallpaper fill mode")).indexOf(value); + if (idx >= 0) + SettingsData.set("greeterWallpaperFillMode", root._wallpaperFillModes[idx]); + } + } + + StyledText { + text: I18n.tr("Layout and module positions on the greeter are synced from your shell (e.g. bar config). Run Sync to apply.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.Wrap + topPadding: Theme.spacingS + } + } + + SettingsCard { + width: parent.width + iconName: "history" + title: I18n.tr("Greeter Behavior") + settingKey: "greeterBehavior" + + StyledText { + text: I18n.tr("Convenience options for the login screen. Sync to apply.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.Wrap + } + + SettingsToggleRow { + settingKey: "greeterRememberLastSession" + tags: ["greeter", "session", "remember", "login"] + text: I18n.tr("Remember last session") + description: I18n.tr("Pre-select the last used session on the greeter") + checked: SettingsData.greeterRememberLastSession + onToggled: checked => SettingsData.set("greeterRememberLastSession", checked) + } + + SettingsToggleRow { + settingKey: "greeterRememberLastUser" + tags: ["greeter", "user", "remember", "login", "username"] + text: I18n.tr("Remember last user") + description: I18n.tr("Pre-fill the last successful username on the greeter") + checked: SettingsData.greeterRememberLastUser + onToggled: checked => SettingsData.set("greeterRememberLastUser", checked) + } + } + + SettingsCard { + width: parent.width + iconName: "extension" + title: I18n.tr("Dependencies & documentation") + settingKey: "greeterDeps" + + StyledText { + text: I18n.tr("DMS greeter needs: greetd, dms-greeter. Fingerprint: fprintd, pam_fprintd. Security keys: pam_u2f. Add your user to the greeter group. Authentication changes apply automatically and may open a terminal when sudo authentication is required.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.Wrap + } + + StyledText { + text: I18n.tr("Installation and PAM setup: see the ") + "<a href=\"https://danklinux.com/docs/dankgreeter/installation\" style=\"text-decoration:none; color:" + Theme.primary + ";\">DankGreeter docs</a> " + I18n.tr("or run ") + "'dms greeter install'." + textFormat: Text.RichText + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + linkColor: Theme.primary + width: parent.width + wrapMode: Text.Wrap + onLinkActivated: url => Qt.openUrlExternally(url) + + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/KeybindsTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/KeybindsTab.qml new file mode 100644 index 0000000..6bf5d54 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/KeybindsTab.qml @@ -0,0 +1,672 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: keybindsTab + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property var parentModal: null + property string selectedCategory: "" + property string searchQuery: "" + property string expandedKey: "" + property bool showingNewBind: false + + property int _lastDataVersion: -1 + property var _cachedCategories: [] + property var _filteredBinds: [] + property real _savedScrollY: 0 + property bool _preserveScroll: false + property string _editingKey: "" + + function _updateFiltered() { + const allBinds = KeybindsService.getFlatBinds(); + if (!searchQuery && !selectedCategory) { + _filteredBinds = allBinds; + return; + } + + const q = searchQuery.toLowerCase(); + const isOverrideFilter = selectedCategory === "__overrides__"; + const result = []; + + for (let i = 0; i < allBinds.length; i++) { + const group = allBinds[i]; + if (q) { + let keyMatch = false; + for (let k = 0; k < group.keys.length; k++) { + if (group.keys[k].key.toLowerCase().indexOf(q) !== -1) { + keyMatch = true; + break; + } + } + if (!keyMatch && group.desc.toLowerCase().indexOf(q) === -1 && group.action.toLowerCase().indexOf(q) === -1) + continue; + } + if (isOverrideFilter) { + let hasOverride = false; + for (let k = 0; k < group.keys.length; k++) { + if (group.keys[k].isOverride) { + hasOverride = true; + break; + } + } + if (!hasOverride) + continue; + } else if (selectedCategory && group.category !== selectedCategory) { + continue; + } + result.push(group); + } + _filteredBinds = result; + } + + function _updateCategories() { + _cachedCategories = ["__overrides__"].concat(KeybindsService.getCategories()); + } + + function getCategoryLabel(cat) { + if (cat === "__overrides__") + return I18n.tr("Overrides"); + return cat; + } + + function toggleExpanded(action) { + expandedKey = expandedKey === action ? "" : action; + } + + function startNewBind() { + showingNewBind = true; + expandedKey = ""; + } + + function cancelNewBind() { + showingNewBind = false; + } + + function saveNewBind(bindData) { + KeybindsService.saveBind("", bindData); + _editingKey = bindData.key; + expandedKey = bindData.action; + } + + function _onSaveSuccess() { + if (showingNewBind) { + showingNewBind = false; + selectedCategory = ""; + } + } + + function scrollToTop() { + flickable.contentY = 0; + } + + function _scrollToExpandedItem() { + for (let i = 0; i < bindsRepeater.count; i++) { + const item = bindsRepeater.itemAt(i); + if (item && item.modelData.action === expandedKey) { + const itemY = item.mapToItem(flickable.contentItem, 0, 0).y; + const itemH = item.height; + const viewH = flickable.height; + if (itemY >= flickable.contentY && itemY + itemH <= flickable.contentY + viewH) + return; + flickable.contentY = Math.max(0, Math.min(itemY - viewH / 4, flickable.contentHeight - viewH)); + return; + } + } + flickable.contentY = _savedScrollY; + } + + Timer { + id: searchDebounce + interval: 150 + onTriggered: keybindsTab._updateFiltered() + } + + Connections { + target: KeybindsService + function onBindsLoaded() { + const savedY = keybindsTab._savedScrollY; + const wasPreserving = keybindsTab._preserveScroll; + keybindsTab._lastDataVersion = KeybindsService._dataVersion; + keybindsTab._updateCategories(); + keybindsTab._updateFiltered(); + keybindsTab._preserveScroll = false; + if (wasPreserving) { + if (keybindsTab.expandedKey) + Qt.callLater(keybindsTab._scrollToExpandedItem); + else + Qt.callLater(() => flickable.contentY = savedY); + } + } + function onBindSaved(key) { + keybindsTab._savedScrollY = flickable.contentY; + keybindsTab._preserveScroll = true; + } + function onBindSaveCompleted(success) { + if (success) + keybindsTab._onSaveSuccess(); + } + function onBindRemoved(key) { + keybindsTab._savedScrollY = flickable.contentY; + keybindsTab._preserveScroll = true; + } + } + + function _ensureCurrentProvider() { + if (!KeybindsService.available) + return; + const cachedProvider = KeybindsService.keybinds?.provider; + const targetProvider = KeybindsService.currentProvider; + if (cachedProvider !== targetProvider || KeybindsService._dataVersion === 0) { + KeybindsService.loadBinds(); + return; + } + if (_lastDataVersion !== KeybindsService._dataVersion) { + _lastDataVersion = KeybindsService._dataVersion; + _updateCategories(); + _updateFiltered(); + } + } + + Component.onCompleted: _ensureCurrentProvider() + + onVisibleChanged: { + if (!visible) + return; + Qt.callLater(scrollToTop); + _ensureCurrentProvider(); + } + + DankFlickable { + id: flickable + anchors.fill: parent + clip: true + contentWidth: width + contentHeight: contentColumn.implicitHeight + + Column { + id: contentColumn + width: flickable.width + spacing: Theme.spacingL + topPadding: Theme.spacingXL + bottomPadding: Theme.spacingXL + + StyledRect { + width: Math.min(650, parent.width - Theme.spacingL * 2) + height: headerSection.implicitHeight + Theme.spacingL * 2 + anchors.horizontalCenter: parent.horizontalCenter + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.width: 0 + + Column { + id: headerSection + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "keyboard" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM * 2 + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Keyboard Shortcuts") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + readonly property string bindsFile: KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : "dms/binds.conf" + text: I18n.tr("Click any shortcut to edit. Changes save to %1").arg(bindsFile) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + + DankTextField { + id: searchField + width: parent.width - addButton.width - Theme.spacingM + placeholderText: I18n.tr("Search keybinds...") + leftIconName: "search" + onTextChanged: { + keybindsTab.searchQuery = text; + searchDebounce.restart(); + } + } + + DankActionButton { + id: addButton + width: searchField.height + height: searchField.height + circular: false + iconName: "add" + iconSize: Theme.iconSize + iconColor: Theme.primary + anchors.verticalCenter: parent.verticalCenter + enabled: !keybindsTab.showingNewBind + opacity: enabled ? 1 : 0.5 + onClicked: keybindsTab.startNewBind() + } + } + } + } + + StyledRect { + id: warningBox + width: Math.min(650, parent.width - Theme.spacingL * 2) + height: warningSection.implicitHeight + Theme.spacingL * 2 + anchors.horizontalCenter: parent.horizontalCenter + radius: Theme.cornerRadius + + readonly property var status: KeybindsService.dmsStatus + readonly property bool showError: !status.included && status.exists + readonly property bool showWarning: status.included && status.overriddenBy > 0 + readonly property bool showSetup: !status.exists + + color: (showError || showWarning || showSetup) ? Theme.withAlpha(Theme.primary, 0.15) : "transparent" + border.color: (showError || showWarning || showSetup) ? Theme.withAlpha(Theme.primary, 0.3) : "transparent" + border.width: 1 + visible: (showError || showWarning || showSetup) && !KeybindsService.loading + + Column { + id: warningSection + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: warningBox.showWarning ? "info" : "warning" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - (fixButton.visible ? fixButton.width + Theme.spacingM : 0) - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: { + if (warningBox.showSetup) + return I18n.tr("First Time Setup"); + if (warningBox.showError) + return I18n.tr("Binds Include Missing"); + if (warningBox.showWarning) + return I18n.tr("Possible Override Conflicts"); + return ""; + } + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.primary + } + + StyledText { + readonly property string bindsFile: KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : "dms/binds.conf" + text: { + if (warningBox.showSetup) + return I18n.tr("Click 'Setup' to create %1 and add include to config.").arg(bindsFile); + if (warningBox.showError) + return I18n.tr("%1 exists but is not included in config. Custom keybinds will not work until this is fixed.").arg(bindsFile); + if (warningBox.showWarning) { + const count = warningBox.status.overriddenBy; + return I18n.ntr( + "%1 DMS bind may be overridden by config binds that come after the include.", + "%1 DMS binds may be overridden by config binds that come after the include.", + count + ).arg(count); + } + return ""; + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + + DankButton { + id: fixButton + visible: warningBox.showError || warningBox.showSetup + text: { + if (KeybindsService.fixing) + return I18n.tr("Fixing..."); + if (warningBox.showSetup) + return I18n.tr("Setup"); + return I18n.tr("Fix Now"); + } + backgroundColor: Theme.primary + textColor: Theme.primaryText + enabled: !KeybindsService.fixing + anchors.verticalCenter: parent.verticalCenter + onClicked: KeybindsService.fixDmsBindsInclude() + } + } + } + } + + StyledRect { + width: Math.min(650, parent.width - Theme.spacingL * 2) + height: categorySection.implicitHeight + Theme.spacingL * 2 + anchors.horizontalCenter: parent.horizontalCenter + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.width: 0 + + Column { + id: categorySection + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Flow { + width: parent.width + spacing: Theme.spacingS + + Rectangle { + readonly property real chipHeight: allChip.implicitHeight + Theme.spacingM + width: allChip.implicitWidth + Theme.spacingL + height: chipHeight + radius: chipHeight / 2 + color: !keybindsTab.selectedCategory ? Theme.primary : Theme.surfaceContainerHighest + + StyledText { + id: allChip + text: I18n.tr("All") + font.pixelSize: Theme.fontSizeSmall + color: !keybindsTab.selectedCategory ? Theme.primaryText : Theme.surfaceVariantText + anchors.centerIn: parent + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + keybindsTab.selectedCategory = ""; + keybindsTab._updateFiltered(); + } + } + } + + Repeater { + model: keybindsTab._cachedCategories + + delegate: Rectangle { + required property string modelData + required property int index + + readonly property real chipHeight: catText.implicitHeight + Theme.spacingM + width: catText.implicitWidth + Theme.spacingL + height: chipHeight + radius: chipHeight / 2 + color: keybindsTab.selectedCategory === modelData ? Theme.primary : (modelData === "__overrides__" ? Theme.withAlpha(Theme.primary, 0.15) : Theme.surfaceContainerHighest) + + StyledText { + id: catText + text: keybindsTab.getCategoryLabel(modelData) + font.pixelSize: Theme.fontSizeSmall + color: keybindsTab.selectedCategory === modelData ? Theme.primaryText : (modelData === "__overrides__" ? Theme.primary : Theme.surfaceVariantText) + anchors.centerIn: parent + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + keybindsTab.selectedCategory = modelData; + keybindsTab._updateFiltered(); + } + } + } + } + } + } + } + + StyledRect { + width: Math.min(650, parent.width - Theme.spacingL * 2) + height: newBindSection.implicitHeight + Theme.spacingL * 2 + anchors.horizontalCenter: parent.horizontalCenter + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.color: Theme.outlineVariant + border.width: 1 + visible: keybindsTab.showingNewBind + + Column { + id: newBindSection + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "add" + size: Theme.iconSize + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("New Keybind") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + KeybindItem { + width: parent.width + isNew: true + isExpanded: true + bindData: ({ + keys: [ + { + key: "", + source: "dms", + isOverride: true + } + ], + action: "", + desc: "" + }) + panelWindow: keybindsTab.parentModal + onSaveBind: (originalKey, newData) => keybindsTab.saveNewBind(newData) + onCancelEdit: keybindsTab.cancelNewBind() + } + } + } + + StyledRect { + width: Math.min(650, parent.width - Theme.spacingL * 2) + height: bindsListHeader.implicitHeight + Theme.spacingL * 2 + anchors.horizontalCenter: parent.horizontalCenter + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.width: 0 + + Column { + id: bindsListHeader + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "list" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: { + if (KeybindsService.loading) + return I18n.tr("Shortcuts"); + const count = keybindsTab._filteredBinds.length; + return count === 1 + ? I18n.tr("Shortcut (%1)").arg(count) + : I18n.tr("Shortcuts (%1)").arg(count); + } + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + visible: KeybindsService.loading + + DankIcon { + id: loadingIcon + name: "sync" + size: 20 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + + RotationAnimation on rotation { + from: 0 + to: 360 + duration: 1000 + loops: Animation.Infinite + running: KeybindsService.loading + } + } + + StyledText { + text: I18n.tr("Loading keybinds...") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + + StyledText { + text: I18n.tr("No keybinds found") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + visible: !KeybindsService.loading && keybindsTab._filteredBinds.length === 0 + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingXS + + Repeater { + id: bindsRepeater + model: ScriptModel { + values: keybindsTab._filteredBinds + objectProp: "action" + } + + delegate: Item { + required property var modelData + required property int index + + width: parent.width + height: bindItem.height + + KeybindItem { + id: bindItem + width: Math.min(650, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + bindData: modelData + isExpanded: keybindsTab.expandedKey === modelData.action + panelWindow: keybindsTab.parentModal + onToggleExpand: keybindsTab.toggleExpanded(modelData.action) + onSaveBind: (originalKey, newData) => { + KeybindsService.saveBind(originalKey, newData); + keybindsTab._editingKey = newData.key; + keybindsTab.expandedKey = newData.action; + } + onRemoveBind: key => { + const remainingKey = bindItem.keys.find(k => k.key !== key)?.key ?? ""; + KeybindsService.removeBind(key); + keybindsTab._editingKey = remainingKey; + } + onIsExpandedChanged: { + if (!isExpanded || !keybindsTab._editingKey) + return; + const keyExists = keys.some(k => k.key === keybindsTab._editingKey); + if (keyExists) { + restoreKey = keybindsTab._editingKey; + keybindsTab._editingKey = ""; + } + } + + onKeysChanged: { + if (!isExpanded || !keybindsTab._editingKey) + return; + const keyExists = keys.some(k => k.key === keybindsTab._editingKey); + if (keyExists) { + restoreKey = keybindsTab._editingKey; + keybindsTab._editingKey = ""; + } + } + + Connections { + target: keybindsTab + function on_EditingKeyChanged() { + if (!bindItem.isExpanded || !keybindsTab._editingKey) + return; + const keyExists = bindItem.keys.some(k => k.key === keybindsTab._editingKey); + if (keyExists) { + bindItem.restoreKey = keybindsTab._editingKey; + keybindsTab._editingKey = ""; + } + } + } + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/LauncherTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/LauncherTab.qml new file mode 100644 index 0000000..8705d42 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/LauncherTab.qml @@ -0,0 +1,1306 @@ +import QtQuick +import qs.Common +import qs.Modals.FileBrowser +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + FileBrowserModal { + id: logoFileBrowser + browserTitle: I18n.tr("Select Launcher Logo") + browserIcon: "image" + browserType: "generic" + filterExtensions: ["*.svg", "*.png", "*.jpg", "*.jpeg", "*.webp"] + onFileSelected: path => SettingsData.set("launcherLogoCustomPath", path.replace("file://", "")) + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + width: parent.width + iconName: "apps" + title: I18n.tr("Launcher Button Logo") + settingKey: "launcherLogo" + + StyledText { + width: parent.width + text: I18n.tr("Choose the logo displayed on the launcher button in DankBar") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + + Item { + width: parent.width + height: logoModeGroup.implicitHeight + clip: true + + DankButtonGroup { + id: logoModeGroup + anchors.horizontalCenter: parent.horizontalCenter + buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL + minButtonWidth: parent.width < 480 ? 44 : 64 + textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium + model: { + const modes = [I18n.tr("Apps Icon"), I18n.tr("OS Logo"), I18n.tr("Dank")]; + if (CompositorService.isNiri) { + modes.push("niri"); + } else if (CompositorService.isHyprland) { + modes.push("Hyprland"); + } else if (CompositorService.isDwl) { + modes.push("mango"); + } else if (CompositorService.isSway) { + modes.push("Sway"); + } else if (CompositorService.isScroll) { + modes.push("Scroll"); + } else if (CompositorService.isMiracle) { + modes.push("Miracle"); + } else { + modes.push(I18n.tr("Compositor")); + } + modes.push(I18n.tr("Custom")); + return modes; + } + currentIndex: { + if (SettingsData.launcherLogoMode === "apps") + return 0; + if (SettingsData.launcherLogoMode === "os") + return 1; + if (SettingsData.launcherLogoMode === "dank") + return 2; + if (SettingsData.launcherLogoMode === "compositor") + return 3; + if (SettingsData.launcherLogoMode === "custom") + return 4; + return 0; + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + switch (index) { + case 0: + SettingsData.set("launcherLogoMode", "apps"); + break; + case 1: + SettingsData.set("launcherLogoMode", "os"); + break; + case 2: + SettingsData.set("launcherLogoMode", "dank"); + break; + case 3: + SettingsData.set("launcherLogoMode", "compositor"); + break; + case 4: + SettingsData.set("launcherLogoMode", "custom"); + break; + } + } + } + } + + Row { + width: parent.width + visible: SettingsData.launcherLogoMode === "custom" + spacing: Theme.spacingM + + StyledRect { + width: parent.width - selectButton.width - Theme.spacingM + height: 36 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9) + border.color: Theme.outlineStrong + border.width: 1 + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + text: SettingsData.launcherLogoCustomPath || I18n.tr("Select an image file...") + font.pixelSize: Theme.fontSizeMedium + color: SettingsData.launcherLogoCustomPath ? Theme.surfaceText : Theme.outlineButton + width: parent.width - Theme.spacingM * 2 + elide: Text.ElideMiddle + } + } + + DankActionButton { + id: selectButton + iconName: "folder_open" + width: 36 + height: 36 + onClicked: logoFileBrowser.open() + } + } + + Column { + width: parent.width + spacing: Theme.spacingL + visible: SettingsData.launcherLogoMode !== "apps" + + Column { + width: parent.width + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Color Override") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + Item { + width: parent.width + height: colorOverrideRow.implicitHeight + clip: true + + Row { + id: colorOverrideRow + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingM + + DankButtonGroup { + id: colorModeGroup + buttonPadding: parent.parent.width < 480 ? Theme.spacingS : Theme.spacingL + minButtonWidth: parent.parent.width < 480 ? 44 : 64 + textSize: parent.parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium + model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")] + currentIndex: { + const override = SettingsData.launcherLogoColorOverride; + if (override === "") + return 0; + if (override === "primary") + return 1; + if (override === "surface") + return 2; + return 3; + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + switch (index) { + case 0: + SettingsData.set("launcherLogoColorOverride", ""); + break; + case 1: + SettingsData.set("launcherLogoColorOverride", "primary"); + break; + case 2: + SettingsData.set("launcherLogoColorOverride", "surface"); + break; + case 3: + const currentOverride = SettingsData.launcherLogoColorOverride; + const isPreset = currentOverride === "" || currentOverride === "primary" || currentOverride === "surface"; + if (isPreset) { + SettingsData.set("launcherLogoColorOverride", "#ffffff"); + } + break; + } + } + } + + Rectangle { + id: colorPickerCircle + visible: { + const override = SettingsData.launcherLogoColorOverride; + return override !== "" && override !== "primary" && override !== "surface"; + } + width: 36 + height: 36 + radius: 18 + color: { + const override = SettingsData.launcherLogoColorOverride; + if (override !== "" && override !== "primary" && override !== "surface") + return override; + return "#ffffff"; + } + border.color: Theme.outline + border.width: 1 + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!PopoutService.colorPickerModal) + return; + PopoutService.colorPickerModal.selectedColor = SettingsData.launcherLogoColorOverride; + PopoutService.colorPickerModal.pickerTitle = I18n.tr("Choose Launcher Logo Color"); + PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) { + SettingsData.set("launcherLogoColorOverride", selectedColor); + }; + PopoutService.colorPickerModal.show(); + } + } + } + } + } + } + + SettingsSliderRow { + settingKey: "launcherLogoSizeOffset" + tags: ["launcher", "logo", "size", "offset", "scale"] + text: I18n.tr("Size Offset") + minimum: -12 + maximum: 12 + value: SettingsData.launcherLogoSizeOffset + defaultValue: 0 + onSliderValueChanged: newValue => SettingsData.set("launcherLogoSizeOffset", newValue) + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: { + const override = SettingsData.launcherLogoColorOverride; + return override !== "" && override !== "primary" && override !== "surface"; + } + + SettingsSliderRow { + settingKey: "launcherLogoBrightness" + tags: ["launcher", "logo", "brightness", "color"] + text: I18n.tr("Brightness") + minimum: 0 + maximum: 100 + value: Math.round(SettingsData.launcherLogoBrightness * 100) + unit: "%" + defaultValue: 100 + onSliderValueChanged: newValue => SettingsData.set("launcherLogoBrightness", newValue / 100) + } + + SettingsSliderRow { + settingKey: "launcherLogoContrast" + tags: ["launcher", "logo", "contrast", "color"] + text: I18n.tr("Contrast") + minimum: 0 + maximum: 200 + value: Math.round(SettingsData.launcherLogoContrast * 100) + unit: "%" + defaultValue: 100 + onSliderValueChanged: newValue => SettingsData.set("launcherLogoContrast", newValue / 100) + } + + SettingsToggleRow { + settingKey: "launcherLogoColorInvertOnMode" + tags: ["launcher", "logo", "invert", "mode", "color"] + text: I18n.tr("Invert on mode change") + checked: SettingsData.launcherLogoColorInvertOnMode + onToggled: checked => SettingsData.set("launcherLogoColorInvertOnMode", checked) + } + } + } + } + + SettingsCard { + width: parent.width + iconName: "terminal" + title: I18n.tr("Launch Prefix") + settingKey: "launchPrefix" + + StyledText { + width: parent.width + text: I18n.tr("Add a custom prefix to all application launches. This can be used for things like 'uwsm-app', 'systemd-run', or other command wrappers.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + + DankTextField { + width: parent.width + text: SettingsData.launchPrefix + placeholderText: I18n.tr("Enter launch prefix (e.g., 'uwsm-app')") + onTextEdited: SettingsData.set("launchPrefix", text) + } + } + + SettingsCard { + width: parent.width + iconName: "sort_by_alpha" + title: I18n.tr("Sorting & Layout") + settingKey: "launcherSorting" + + SettingsToggleRow { + settingKey: "sortAppsAlphabetically" + tags: ["launcher", "sort", "alphabetically", "apps", "order"] + text: I18n.tr("Sort Alphabetically") + description: I18n.tr("When enabled, apps are sorted alphabetically. When disabled, apps are sorted by usage frequency.") + checked: SettingsData.sortAppsAlphabetically + onToggled: checked => SettingsData.set("sortAppsAlphabetically", checked) + } + + SettingsSliderRow { + settingKey: "appLauncherGridColumns" + tags: ["launcher", "grid", "columns", "layout"] + text: I18n.tr("Grid Columns") + description: I18n.tr("Adjust the number of columns in grid view mode.") + minimum: 2 + maximum: 8 + value: SettingsData.appLauncherGridColumns + defaultValue: 5 + onSliderValueChanged: newValue => SettingsData.set("appLauncherGridColumns", newValue) + } + } + + SettingsCard { + width: parent.width + iconName: "tune" + title: I18n.tr("Appearance", "launcher appearance settings") + settingKey: "dankLauncherV2Appearance" + + Column { + width: parent.width + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Size", "launcher size option") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + Item { + width: parent.width + height: sizeGroup.implicitHeight + clip: true + + DankButtonGroup { + id: sizeGroup + anchors.horizontalCenter: parent.horizontalCenter + buttonPadding: parent.width < 400 ? Theme.spacingS : Theme.spacingL + minButtonWidth: parent.width < 400 ? 60 : 80 + textSize: parent.width < 400 ? Theme.fontSizeSmall : Theme.fontSizeMedium + model: ["1", "2", "3", "4"] + currentIndex: { + switch (SettingsData.dankLauncherV2Size) { + case "micro": + return 0; + case "compact": + return 1; + case "large": + return 3; + default: + return 2; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + var sizes = ["micro", "compact", "medium", "large"]; + SettingsData.set("dankLauncherV2Size", sizes[index]); + } + } + } + } + + SettingsToggleRow { + settingKey: "dankLauncherV2ShowFooter" + tags: ["launcher", "footer", "hints", "shortcuts"] + text: I18n.tr("Show Footer", "launcher footer visibility") + description: I18n.tr("Show mode tabs and keyboard hints at the bottom.", "launcher footer description") + checked: SettingsData.dankLauncherV2ShowFooter + enabled: SettingsData.dankLauncherV2Size !== "micro" + onToggled: checked => SettingsData.set("dankLauncherV2ShowFooter", checked) + } + + SettingsToggleRow { + settingKey: "dankLauncherV2UnloadOnClose" + tags: ["launcher", "unload", "close", "memory", "vram"] + text: I18n.tr("Unload on Close") + description: I18n.tr("Free VRAM/memory when the launcher is closed. May cause a slight delay when reopening.") + checked: SettingsData.dankLauncherV2UnloadOnClose + onToggled: checked => SettingsData.set("dankLauncherV2UnloadOnClose", checked) + } + + SettingsToggleRow { + settingKey: "dankLauncherV2BorderEnabled" + tags: ["launcher", "border", "outline"] + text: I18n.tr("Border", "launcher border option") + checked: SettingsData.dankLauncherV2BorderEnabled + onToggled: checked => SettingsData.set("dankLauncherV2BorderEnabled", checked) + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: SettingsData.dankLauncherV2BorderEnabled + + SettingsSliderRow { + settingKey: "dankLauncherV2BorderThickness" + tags: ["launcher", "border", "thickness"] + text: I18n.tr("Thickness", "border thickness") + minimum: 1 + maximum: 6 + value: SettingsData.dankLauncherV2BorderThickness + defaultValue: 2 + unit: "px" + onSliderValueChanged: newValue => SettingsData.set("dankLauncherV2BorderThickness", newValue) + } + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Color", "border color") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + Item { + width: parent.width + height: borderColorGroup.implicitHeight + clip: true + + DankButtonGroup { + id: borderColorGroup + anchors.horizontalCenter: parent.horizontalCenter + buttonPadding: parent.width < 400 ? Theme.spacingS : Theme.spacingL + minButtonWidth: parent.width < 400 ? 50 : 70 + textSize: parent.width < 400 ? Theme.fontSizeSmall : Theme.fontSizeMedium + model: [I18n.tr("Primary", "primary color"), I18n.tr("Secondary", "secondary color"), I18n.tr("Outline", "outline color"), I18n.tr("Text", "text color")] + currentIndex: SettingsData.dankLauncherV2BorderColor === "secondary" ? 1 : SettingsData.dankLauncherV2BorderColor === "outline" ? 2 : SettingsData.dankLauncherV2BorderColor === "surfaceText" ? 3 : 0 + onSelectionChanged: (index, selected) => { + if (!selected) + return; + SettingsData.set("dankLauncherV2BorderColor", index === 1 ? "secondary" : index === 2 ? "outline" : index === 3 ? "surfaceText" : "primary"); + } + } + } + } + } + } + + SettingsCard { + width: parent.width + iconName: "open_in_new" + title: I18n.tr("Niri Integration").replace("Niri", "niri") + visible: CompositorService.isNiri + + SettingsToggleRow { + settingKey: "spotlightCloseNiriOverview" + tags: ["launcher", "niri", "overview", "close", "launch"] + text: I18n.tr("Close Overview on Launch") + description: I18n.tr("Auto-close Niri overview when launching apps.") + checked: SettingsData.spotlightCloseNiriOverview + onToggled: checked => SettingsData.set("spotlightCloseNiriOverview", checked) + } + + SettingsToggleRow { + settingKey: "niriOverviewOverlayEnabled" + tags: ["launcher", "niri", "overview", "overlay", "enable"] + text: I18n.tr("Enable Overview Overlay") + description: I18n.tr("Show launcher overlay when typing in Niri overview. Disable to use another launcher.") + checked: SettingsData.niriOverviewOverlayEnabled + onToggled: checked => SettingsData.set("niriOverviewOverlayEnabled", checked) + } + } + + SettingsCard { + id: builtInPluginsCard + width: parent.width + iconName: "extension" + title: "DMS" + settingKey: "builtInPlugins" + + Column { + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: ["dms_settings", "dms_notepad", "dms_sysmon", "dms_settings_search"] + + delegate: Rectangle { + id: pluginDelegate + required property string modelData + required property int index + readonly property var plugin: AppSearchService.builtInPlugins[modelData] + + width: parent.width + height: 56 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3) + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: pluginDelegate.plugin?.cornerIcon ?? "extension" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: pluginDelegate.plugin?.name ?? pluginDelegate.modelData + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + } + + StyledText { + text: pluginDelegate.plugin?.comment ?? "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + + Row { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankTextField { + id: triggerField + width: 60 + visible: pluginDelegate.plugin?.isLauncher === true + anchors.verticalCenter: parent.verticalCenter + placeholderText: I18n.tr("Trigger") + onTextEdited: SettingsData.setBuiltInPluginSetting(pluginDelegate.modelData, "trigger", text) + Component.onCompleted: text = SettingsData.getBuiltInPluginSetting(pluginDelegate.modelData, "trigger", pluginDelegate.plugin?.defaultTrigger ?? "") + } + + DankToggle { + id: enableToggle + anchors.verticalCenter: parent.verticalCenter + checked: SettingsData.getBuiltInPluginSetting(pluginDelegate.modelData, "enabled", true) + onToggled: SettingsData.setBuiltInPluginSetting(pluginDelegate.modelData, "enabled", checked) + } + } + } + } + } + } + + SettingsCard { + id: pluginVisibilityCard + width: parent.width + iconName: "filter_list" + title: I18n.tr("Plugin Visibility") + settingKey: "pluginVisibility" + + property var allLauncherPlugins: { + SettingsData.launcherPluginVisibility; + SettingsData.launcherPluginOrder; + SettingsData.dankLauncherV2IncludeFilesInAll; + SettingsData.dankLauncherV2IncludeFoldersInAll; + var plugins = []; + var builtIn = AppSearchService.getBuiltInLauncherPlugins() || {}; + for (var pluginId in builtIn) { + var plugin = builtIn[pluginId]; + plugins.push({ + id: pluginId, + name: plugin.name || pluginId, + icon: plugin.cornerIcon || "extension", + iconType: "material", + isBuiltIn: true, + isVirtual: false, + trigger: AppSearchService.getBuiltInPluginTrigger(pluginId) || "" + }); + } + var thirdParty = PluginService.getLauncherPlugins() || {}; + for (var pluginId in thirdParty) { + var plugin = thirdParty[pluginId]; + var rawIcon = plugin.icon || "extension"; + plugins.push({ + id: pluginId, + name: plugin.name || pluginId, + icon: rawIcon.startsWith("material:") ? rawIcon.substring(9) : rawIcon.startsWith("unicode:") ? rawIcon.substring(8) : rawIcon, + iconType: rawIcon.startsWith("unicode:") ? "unicode" : "material", + isBuiltIn: false, + isVirtual: false, + trigger: PluginService.getPluginTrigger(pluginId) || "" + }); + } + if (SettingsData.dankLauncherV2IncludeFilesInAll) { + plugins.push({ + id: "__files", + name: I18n.tr("Files"), + icon: "insert_drive_file", + iconType: "material", + isBuiltIn: false, + isVirtual: true, + trigger: "/" + }); + } + if (SettingsData.dankLauncherV2IncludeFoldersInAll) { + plugins.push({ + id: "__folders", + name: I18n.tr("Folders"), + icon: "folder", + iconType: "material", + isBuiltIn: false, + isVirtual: true, + trigger: "/" + }); + } + return SettingsData.getOrderedLauncherPlugins(plugins); + } + + function reorderPlugin(fromIndex, toIndex) { + if (fromIndex === toIndex) + return; + var currentOrder = allLauncherPlugins.map(p => p.id); + var item = currentOrder.splice(fromIndex, 1)[0]; + currentOrder.splice(toIndex, 0, item); + SettingsData.setLauncherPluginOrder(currentOrder); + } + + StyledText { + width: parent.width + text: I18n.tr("Control which plugins appear in 'All' mode without requiring a trigger prefix. Drag to reorder.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + + Column { + id: pluginVisibilityColumn + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: pluginVisibilityCard.allLauncherPlugins + + delegate: Item { + id: visibilityDelegateItem + required property var modelData + required property int index + + property bool held: pluginDragArea.pressed + property real originalY: y + + width: pluginVisibilityColumn.width + height: 52 + z: held ? 2 : 1 + + Rectangle { + id: visibilityDelegate + width: parent.width + height: 52 + radius: Theme.cornerRadius + color: visibilityDelegateItem.held ? Theme.surfaceHover : Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3) + + Row { + anchors.left: parent.left + anchors.leftMargin: 28 + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + Item { + width: Theme.iconSize + height: Theme.iconSize + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + visible: visibilityDelegateItem.modelData.iconType !== "unicode" + name: visibilityDelegateItem.modelData.icon + size: Theme.iconSize + color: Theme.primary + } + + StyledText { + anchors.centerIn: parent + visible: visibilityDelegateItem.modelData.iconType === "unicode" + text: visibilityDelegateItem.modelData.icon + font.pixelSize: Theme.iconSize + color: Theme.primary + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + Row { + spacing: Theme.spacingS + + StyledText { + text: visibilityDelegateItem.modelData.name + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + } + + Rectangle { + visible: visibilityDelegateItem.modelData.isBuiltIn + width: dmsBadgeLabel.implicitWidth + Theme.spacingS + height: 16 + radius: 8 + color: Theme.primaryContainer + anchors.verticalCenter: parent.verticalCenter + + StyledText { + id: dmsBadgeLabel + anchors.centerIn: parent + text: "DMS" + font.pixelSize: Theme.fontSizeSmall - 2 + color: Theme.primary + } + } + } + + StyledText { + text: visibilityDelegateItem.modelData.trigger ? I18n.tr("Trigger: %1").arg(visibilityDelegateItem.modelData.trigger) : I18n.tr("No trigger") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + + DankToggle { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + checked: { + switch (visibilityDelegateItem.modelData.id) { + case "__files": + return SettingsData.dankLauncherV2IncludeFilesInAll; + case "__folders": + return SettingsData.dankLauncherV2IncludeFoldersInAll; + default: + return SettingsData.getPluginAllowWithoutTrigger(visibilityDelegateItem.modelData.id); + } + } + onToggled: function (isChecked) { + switch (visibilityDelegateItem.modelData.id) { + case "__files": + SettingsData.set("dankLauncherV2IncludeFilesInAll", isChecked); + break; + case "__folders": + SettingsData.set("dankLauncherV2IncludeFoldersInAll", isChecked); + break; + default: + SettingsData.setPluginAllowWithoutTrigger(visibilityDelegateItem.modelData.id, isChecked); + } + } + } + } + + MouseArea { + id: pluginDragArea + anchors.left: parent.left + anchors.top: parent.top + width: 28 + height: parent.height + hoverEnabled: true + cursorShape: Qt.SizeVerCursor + drag.target: visibilityDelegateItem.held ? visibilityDelegateItem : undefined + drag.axis: Drag.YAxis + preventStealing: true + + onPressed: { + visibilityDelegateItem.originalY = visibilityDelegateItem.y; + } + + onReleased: { + if (!drag.active) { + visibilityDelegateItem.y = visibilityDelegateItem.originalY; + return; + } + const spacing = Theme.spacingS; + const itemH = visibilityDelegateItem.height + spacing; + var newIndex = Math.round(visibilityDelegateItem.y / itemH); + newIndex = Math.max(0, Math.min(newIndex, pluginVisibilityCard.allLauncherPlugins.length - 1)); + pluginVisibilityCard.reorderPlugin(visibilityDelegateItem.index, newIndex); + visibilityDelegateItem.y = visibilityDelegateItem.originalY; + } + } + + DankIcon { + x: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + name: "drag_indicator" + size: 18 + color: Theme.outline + opacity: pluginDragArea.containsMouse || pluginDragArea.pressed ? 1 : 0.5 + } + + Behavior on y { + enabled: !pluginDragArea.pressed && !pluginDragArea.drag.active + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + StyledText { + width: parent.width + text: I18n.tr("No launcher plugins installed.") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + visible: pluginVisibilityCard.allLauncherPlugins.length === 0 + } + } + } + + SettingsCard { + width: parent.width + iconName: "search" + title: I18n.tr("Search Options") + settingKey: "searchOptions" + + SettingsToggleRow { + settingKey: "searchAppActions" + tags: ["launcher", "search", "actions", "shortcuts"] + text: I18n.tr("Search App Actions") + description: I18n.tr("Include desktop actions (shortcuts) in search results.") + checked: SessionData.searchAppActions + onToggled: checked => SessionData.setSearchAppActions(checked) + } + + SettingsToggleRow { + settingKey: "rememberLastQuery" + tags: ["launcher", "remember", "last", "search", "query"] + text: I18n.tr("Remember Last Query") + description: I18n.tr("Autofill last remembered query when opened") + checked: SettingsData.rememberLastQuery + onToggled: checked => SettingsData.set("rememberLastQuery", checked) + } + + SettingsToggleRow { + settingKey: "dankLauncherV2IncludeFilesInAll" + tags: ["launcher", "files", "dsearch", "all", "results", "indexed"] + text: I18n.tr("Include Files in All Tab") + description: I18n.tr("Merge indexed file results into the All tab (requires dsearch).") + checked: SettingsData.dankLauncherV2IncludeFilesInAll + onToggled: checked => SettingsData.set("dankLauncherV2IncludeFilesInAll", checked) + } + + SettingsToggleRow { + settingKey: "dankLauncherV2IncludeFoldersInAll" + tags: ["launcher", "folders", "dirs", "dsearch", "all", "results", "indexed"] + text: I18n.tr("Include Folders in All Tab") + description: I18n.tr("Merge indexed folder results into the All tab (requires dsearch).") + checked: SettingsData.dankLauncherV2IncludeFoldersInAll + onToggled: checked => SettingsData.set("dankLauncherV2IncludeFoldersInAll", checked) + } + } + + SettingsCard { + id: hiddenAppsCard + width: parent.width + iconName: "visibility_off" + title: I18n.tr("Hidden Apps") + settingKey: "hiddenApps" + + property var hiddenAppsModel: { + SessionData.hiddenApps; + const apps = []; + const allApps = AppSearchService.applications || []; + for (const hiddenId of SessionData.hiddenApps) { + const app = allApps.find(a => (a.id || a.execString || a.exec) === hiddenId); + if (app) { + apps.push({ + id: hiddenId, + name: app.name || hiddenId, + icon: app.icon || "", + comment: app.comment || "" + }); + } else { + apps.push({ + id: hiddenId, + name: hiddenId, + icon: "", + comment: "" + }); + } + } + return apps.sort((a, b) => a.name.localeCompare(b.name)); + } + + StyledText { + width: parent.width + text: I18n.tr("Hidden apps won't appear in the launcher. Right-click an app and select 'Hide App' to hide it.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + + Column { + id: hiddenAppsList + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: hiddenAppsCard.hiddenAppsModel + + delegate: Rectangle { + width: hiddenAppsList.width + height: 48 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3) + border.width: 0 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + Image { + width: 24 + height: 24 + source: Paths.resolveIconUrl(modelData.icon || "application-x-executable") + sourceSize.width: 24 + sourceSize.height: 24 + fillMode: Image.PreserveAspectFit + anchors.verticalCenter: parent.verticalCenter + onStatusChanged: { + if (status === Image.Error) + source = "image://icon/application-x-executable"; + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: modelData.name + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: modelData.comment || modelData.id + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: text.length > 0 + } + } + } + + DankActionButton { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + iconName: "visibility" + iconSize: 18 + iconColor: Theme.primary + onClicked: SessionData.showApp(modelData.id) + } + } + } + + StyledText { + width: parent.width + text: I18n.tr("No hidden apps.") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + visible: hiddenAppsCard.hiddenAppsModel.length === 0 + } + } + } + + SettingsCard { + id: appOverridesCard + width: parent.width + iconName: "edit" + title: I18n.tr("App Customizations") + settingKey: "appOverrides" + + property var overridesModel: { + SessionData.appOverrides; + const items = []; + const allApps = AppSearchService.applications || []; + for (const appId in SessionData.appOverrides) { + const override = SessionData.appOverrides[appId]; + const app = allApps.find(a => (a.id || a.execString || a.exec) === appId); + items.push({ + id: appId, + name: override.name || app?.name || appId, + originalName: app?.name || appId, + icon: override.icon || app?.icon || "", + hasOverride: true + }); + } + return items.sort((a, b) => a.name.localeCompare(b.name)); + } + + StyledText { + width: parent.width + text: I18n.tr("Apps with custom display name, icon, or launch options. Right-click an app and select 'Edit App' to customize.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + + Column { + id: overridesList + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: appOverridesCard.overridesModel + + delegate: Rectangle { + width: overridesList.width + height: 48 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3) + border.width: 0 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + Image { + width: 24 + height: 24 + source: Paths.resolveIconUrl(modelData.icon || "application-x-executable") + sourceSize.width: 24 + sourceSize.height: 24 + fillMode: Image.PreserveAspectFit + anchors.verticalCenter: parent.verticalCenter + onStatusChanged: { + if (status === Image.Error) + source = "image://icon/application-x-executable"; + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: modelData.name + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: modelData.originalName !== modelData.name ? modelData.originalName : modelData.id + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + + DankActionButton { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + iconName: "delete" + iconSize: 18 + iconColor: Theme.error + onClicked: SessionData.clearAppOverride(modelData.id) + } + } + } + + StyledText { + width: parent.width + text: I18n.tr("No app customizations.") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + visible: appOverridesCard.overridesModel.length === 0 + } + } + } + + SettingsCard { + id: recentAppsCard + width: parent.width + iconName: "history" + title: I18n.tr("Recently Used Apps") + settingKey: "recentApps" + collapsible: true + expanded: false + + property var rankedAppsModel: { + var ranking = AppUsageHistoryData.appUsageRanking; + if (!ranking) + return []; + var apps = []; + for (var appId in ranking) { + var appData = ranking[appId]; + apps.push({ + "id": appId, + "name": appData.name, + "exec": appData.exec, + "icon": appData.icon, + "comment": appData.comment, + "usageCount": appData.usageCount, + "lastUsed": appData.lastUsed + }); + } + apps.sort(function (a, b) { + if (a.usageCount !== b.usageCount) + return b.usageCount - a.usageCount; + return a.name.localeCompare(b.name); + }); + return apps.slice(0, 20); + } + + Row { + width: parent.width + spacing: Theme.spacingM + + StyledText { + width: parent.width - clearAllButton.width - Theme.spacingM + text: I18n.tr("Apps are ordered by usage frequency, then last used, then alphabetically.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + anchors.verticalCenter: parent.verticalCenter + } + + DankActionButton { + id: clearAllButton + iconName: "delete_sweep" + iconSize: Theme.iconSize - 2 + iconColor: Theme.error + anchors.verticalCenter: parent.verticalCenter + onClicked: { + AppUsageHistoryData.appUsageRanking = {}; + AppUsageHistoryData.saveSettings(); + } + } + } + + Column { + id: rankedAppsList + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: recentAppsCard.rankedAppsModel + + delegate: Rectangle { + width: rankedAppsList.width + height: 48 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3) + border.width: 0 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + StyledText { + text: (index + 1).toString() + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.primary + width: 20 + anchors.verticalCenter: parent.verticalCenter + } + + Image { + width: 24 + height: 24 + source: Paths.resolveIconUrl(modelData.icon || "application-x-executable") + sourceSize.width: 24 + sourceSize.height: 24 + fillMode: Image.PreserveAspectFit + anchors.verticalCenter: parent.verticalCenter + onStatusChanged: { + if (status === Image.Error) + source = "image://icon/application-x-executable"; + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: modelData.name || I18n.tr("Unknown App") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: { + if (!modelData.lastUsed) + return I18n.tr("Never used"); + var date = new Date(modelData.lastUsed); + var now = new Date(); + var diffMs = now - date; + var diffMins = Math.floor(diffMs / (1000 * 60)); + var diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + var diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + if (diffMins < 1) + return I18n.tr("Last launched just now"); + if (diffMins < 60) + return diffMins === 1 ? I18n.tr("Last launched %1 minute ago").arg(diffMins) : I18n.tr("Last launched %1 minutes ago").arg(diffMins); + if (diffHours < 24) + return diffHours === 1 ? I18n.tr("Last launched %1 hour ago").arg(diffHours) : I18n.tr("Last launched %1 hours ago").arg(diffHours); + if (diffDays < 7) + return diffDays === 1 ? I18n.tr("Last launched %1 day ago").arg(diffDays) : I18n.tr("Last launched %1 days ago").arg(diffDays); + return I18n.tr("Last launched %1").arg(date.toLocaleDateString()); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + + DankActionButton { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + circular: true + iconName: "close" + iconSize: 16 + iconColor: Theme.error + onClicked: { + var currentRanking = Object.assign({}, AppUsageHistoryData.appUsageRanking || {}); + delete currentRanking[modelData.id]; + AppUsageHistoryData.appUsageRanking = currentRanking; + AppUsageHistoryData.saveSettings(); + } + } + } + } + + StyledText { + width: parent.width + text: I18n.tr("No apps have been launched yet.") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + visible: recentAppsCard.rankedAppsModel.length === 0 + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/LocaleTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/LocaleTab.qml new file mode 100644 index 0000000..d4f7f5e --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/LocaleTab.qml @@ -0,0 +1,91 @@ +import QtQuick +import qs.Common +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: localeTab + + readonly property string _systemDefaultLabel: I18n.tr("System Default") + + function _localeDisplayName(localeCode) { + if (!I18n.presentLocales[localeCode]) + return; + const nativeName = I18n.presentLocales[localeCode].nativeLanguageName; + return nativeName[0].toUpperCase() + nativeName.slice(1); + } + + function _allLocaleOptions() { + return [_systemDefaultLabel].concat(Object.keys(I18n.presentLocales).map(_localeDisplayName)); + } + + function _codeForDisplayName(displayName) { + if (displayName === _systemDefaultLabel) + return ""; + for (const code of Object.keys(I18n.presentLocales)) { + if (_localeDisplayName(code) === displayName) + return code; + } + return ""; + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + tab: "locale" + tags: ["locale", "language", "country"] + title: I18n.tr("Locale Settings") + iconName: "language" + + SettingsDropdownRow { + id: localeDropdown + tab: "locale" + tags: ["locale", "language", "country"] + settingKey: "locale" + text: I18n.tr("Current Locale") + description: I18n.tr("Change the locale used by the DMS interface.") + options: localeTab._allLocaleOptions() + enableFuzzySearch: true + + Component.onCompleted: { + currentValue = SessionData.locale ? localeTab._localeDisplayName(SessionData.locale) : localeTab._systemDefaultLabel; + } + + onValueChanged: value => { + SessionData.set("locale", localeTab._codeForDisplayName(value)); + } + } + + SettingsDropdownRow { + id: timeLocaleDropdown + tab: "locale" + tags: ["locale", "time", "date", "format", "region"] + settingKey: "timeLocale" + text: I18n.tr("Time & Date Locale") + description: I18n.tr("Change the locale used for date and time formatting, independent of the interface language.") + options: localeTab._allLocaleOptions() + enableFuzzySearch: true + + Component.onCompleted: { + currentValue = SessionData.timeLocale ? localeTab._localeDisplayName(SessionData.timeLocale) : localeTab._systemDefaultLabel; + } + + onValueChanged: value => { + SessionData.set("timeLocale", localeTab._codeForDisplayName(value)); + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/LockScreenTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/LockScreenTab.qml new file mode 100644 index 0000000..d86589c --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/LockScreenTab.qml @@ -0,0 +1,458 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Modals.FileBrowser +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + readonly property bool lockFprintToggleAvailable: SettingsData.lockFingerprintCanEnable || SettingsData.enableFprint + readonly property bool lockU2fToggleAvailable: SettingsData.lockU2fCanEnable || SettingsData.enableU2f + + function lockFingerprintDescription() { + switch (SettingsData.lockFingerprintReason) { + case "ready": + return SettingsData.enableFprint ? I18n.tr("Authentication changes apply automatically.") : I18n.tr("Use fingerprint authentication for the lock screen."); + case "missing_enrollment": + if (SettingsData.enableFprint) + return I18n.tr("Enabled, but no prints are enrolled yet. Authentication changes apply automatically once you enroll fingerprints."); + return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later."); + case "missing_reader": + return SettingsData.enableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected."); + case "missing_pam_support": + return I18n.tr("Not available — install fprintd and pam_fprintd."); + default: + return SettingsData.enableFprint ? I18n.tr("Enabled, but fingerprint availability could not be confirmed.") : I18n.tr("Fingerprint availability could not be confirmed."); + } + } + + function lockU2fDescription() { + switch (SettingsData.lockU2fReason) { + case "ready": + return SettingsData.enableU2f ? I18n.tr("Authentication changes apply automatically.") : I18n.tr("Use a security key for lock screen authentication.", "lock screen U2F security key setting"); + case "missing_key_registration": + if (SettingsData.enableU2f) + return I18n.tr("Enabled, but no registered security key was found yet. Authentication changes apply automatically once your key is registered or your U2F config is updated."); + return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later."); + case "missing_pam_support": + return I18n.tr("Not available — install or configure pam_u2f."); + default: + return SettingsData.enableU2f ? I18n.tr("Enabled, but security-key availability could not be confirmed.") : I18n.tr("Security-key availability could not be confirmed."); + } + } + + function refreshAuthDetection() { + SettingsData.refreshAuthAvailability(); + } + + Component.onCompleted: refreshAuthDetection() + onVisibleChanged: { + if (visible) + refreshAuthDetection(); + } + + FileBrowserModal { + id: videoBrowserModal + browserTitle: I18n.tr("Select Video or Folder") + browserIcon: "movie" + browserType: "video" + showHiddenFiles: false + fileExtensions: ["*.mp4", "*.mkv", "*.webm", "*.mov", "*.avi", "*.m4v"] + onFileSelected: path => SettingsData.set("lockScreenVideoPath", path) + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + width: parent.width + iconName: "lock" + title: I18n.tr("Lock Screen layout") + settingKey: "lockLayout" + + SettingsToggleRow { + settingKey: "lockScreenShowPowerActions" + tags: ["lock", "screen", "power", "actions", "shutdown", "reboot"] + text: I18n.tr("Show Power Actions", "Enable power action icon on the lock screen window") + checked: SettingsData.lockScreenShowPowerActions + onToggled: checked => SettingsData.set("lockScreenShowPowerActions", checked) + } + + SettingsToggleRow { + settingKey: "lockScreenShowSystemIcons" + tags: ["lock", "screen", "system", "icons", "status"] + text: I18n.tr("Show System Icons", "Enable system status icons on the lock screen window") + checked: SettingsData.lockScreenShowSystemIcons + onToggled: checked => SettingsData.set("lockScreenShowSystemIcons", checked) + } + + SettingsToggleRow { + settingKey: "lockScreenShowTime" + tags: ["lock", "screen", "time", "clock", "display"] + text: I18n.tr("Show System Time", "Enable system time display on the lock screen window") + checked: SettingsData.lockScreenShowTime + onToggled: checked => SettingsData.set("lockScreenShowTime", checked) + } + + SettingsToggleRow { + settingKey: "lockScreenShowDate" + tags: ["lock", "screen", "date", "calendar", "display"] + text: I18n.tr("Show System Date", "Enable system date display on the lock screen window") + checked: SettingsData.lockScreenShowDate + onToggled: checked => SettingsData.set("lockScreenShowDate", checked) + } + + SettingsToggleRow { + settingKey: "lockScreenShowProfileImage" + tags: ["lock", "screen", "profile", "image", "avatar", "picture"] + text: I18n.tr("Show Profile Image", "Enable profile image display on the lock screen window") + checked: SettingsData.lockScreenShowProfileImage + onToggled: checked => SettingsData.set("lockScreenShowProfileImage", checked) + } + + SettingsToggleRow { + settingKey: "lockScreenShowPasswordField" + tags: ["lock", "screen", "password", "field", "input", "visible"] + text: I18n.tr("Show Password Field", "Enable password field display on the lock screen window") + description: I18n.tr("If the field is hidden, it will appear as soon as a key is pressed.") + checked: SettingsData.lockScreenShowPasswordField + onToggled: checked => SettingsData.set("lockScreenShowPasswordField", checked) + } + + SettingsToggleRow { + settingKey: "lockScreenShowMediaPlayer" + tags: ["lock", "screen", "media", "player", "music", "mpris"] + text: I18n.tr("Show Media Player", "Enable media player controls on the lock screen window") + checked: SettingsData.lockScreenShowMediaPlayer + onToggled: checked => SettingsData.set("lockScreenShowMediaPlayer", checked) + } + + SettingsDropdownRow { + settingKey: "lockScreenNotificationMode" + tags: ["lock", "screen", "notification", "notifications", "privacy"] + text: I18n.tr("Notification Display", "lock screen notification privacy setting") + description: I18n.tr("Control what notification information is shown on the lock screen", "lock screen notification privacy setting") + options: [I18n.tr("Disabled", "lock screen notification mode option"), I18n.tr("Count Only", "lock screen notification mode option"), I18n.tr("App Names", "lock screen notification mode option"), I18n.tr("Full Content", "lock screen notification mode option")] + currentValue: options[SettingsData.lockScreenNotificationMode] || options[0] + onValueChanged: value => { + const idx = options.indexOf(value); + if (idx >= 0) { + SettingsData.set("lockScreenNotificationMode", idx); + } + } + } + } + + SettingsCard { + width: parent.width + iconName: "lock" + title: I18n.tr("Lock Screen behaviour") + settingKey: "lockBehavior" + + StyledText { + text: I18n.tr("loginctl not available - lock integration requires DMS socket connection") + font.pixelSize: Theme.fontSizeSmall + color: Theme.warning + visible: !SessionService.loginctlAvailable + width: parent.width + wrapMode: Text.Wrap + } + + SettingsToggleRow { + settingKey: "loginctlLockIntegration" + tags: ["lock", "screen", "loginctl", "dbus", "integration", "external"] + text: I18n.tr("Enable loginctl lock integration") + description: I18n.tr("Bind lock screen to dbus signals from loginctl. Disable if using an external lock screen") + checked: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration + enabled: SessionService.loginctlAvailable + onToggled: checked => { + if (!SessionService.loginctlAvailable) + return; + SettingsData.set("loginctlLockIntegration", checked); + } + } + + SettingsToggleRow { + settingKey: "lockBeforeSuspend" + tags: ["lock", "screen", "suspend", "sleep", "automatic"] + text: I18n.tr("Lock before suspend") + description: I18n.tr("Automatically lock the screen when the system prepares to suspend") + checked: SettingsData.lockBeforeSuspend + visible: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration + onToggled: checked => SettingsData.set("lockBeforeSuspend", checked) + } + + SettingsToggleRow { + settingKey: "lockScreenPowerOffMonitorsOnLock" + tags: ["lock", "screen", "monitor", "display", "dpms", "power"] + text: I18n.tr("Power off monitors on lock") + description: I18n.tr("Turn off all displays immediately when the lock screen activates") + checked: SettingsData.lockScreenPowerOffMonitorsOnLock + onToggled: checked => SettingsData.set("lockScreenPowerOffMonitorsOnLock", checked) + } + + SettingsToggleRow { + settingKey: "lockAtStartup" + tags: ["lock", "screen", "startup", "start", "boot", "login", "automatic"] + text: I18n.tr("Lock at startup") + description: I18n.tr("Automatically lock the screen when DMS starts") + checked: SettingsData.lockAtStartup + onToggled: checked => SettingsData.set("lockAtStartup", checked) + } + + StyledText { + text: I18n.tr("Lock screen authentication changes apply automatically and may open a terminal when sudo authentication is required.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.Wrap + topPadding: Theme.spacingS + } + + SettingsToggleRow { + settingKey: "enableFprint" + tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"] + text: I18n.tr("Enable fingerprint authentication") + description: root.lockFingerprintDescription() + descriptionColor: SettingsData.lockFingerprintReason === "ready" ? Theme.surfaceVariantText : Theme.warning + checked: SettingsData.enableFprint + enabled: root.lockFprintToggleAvailable + onToggled: checked => SettingsData.set("enableFprint", checked) + } + + SettingsToggleRow { + settingKey: "enableU2f" + tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "fido", "authentication", "hardware"] + text: I18n.tr("Enable security key authentication", "Enable FIDO2/U2F hardware security key for lock screen") + description: root.lockU2fDescription() + descriptionColor: SettingsData.lockU2fReason === "ready" ? Theme.surfaceVariantText : Theme.warning + checked: SettingsData.enableU2f + enabled: root.lockU2fToggleAvailable + onToggled: checked => SettingsData.set("enableU2f", checked) + } + + SettingsDropdownRow { + settingKey: "u2fMode" + tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"] + text: I18n.tr("Security key mode", "lock screen U2F security key mode setting") + description: I18n.tr("'Alternative' lets the key unlock on its own. 'Second factor' requires password or fingerprint first, then the key.", "lock screen U2F security key mode setting") + visible: SettingsData.enableU2f + options: [I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method"), I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint")] + currentValue: SettingsData.u2fMode === "and" ? I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint") : I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method") + onValueChanged: value => { + if (value === I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint")) + SettingsData.set("u2fMode", "and"); + else + SettingsData.set("u2fMode", "or"); + } + } + } + + SettingsCard { + width: parent.width + iconName: "movie" + title: I18n.tr("Video Screensaver") + settingKey: "videoScreensaver" + + StyledText { + visible: !MultimediaService.available + text: I18n.tr("QtMultimedia is not available - video screensaver requires qt multimedia services") + font.pixelSize: Theme.fontSizeSmall + color: Theme.warning + width: parent.width + wrapMode: Text.WordWrap + } + + SettingsToggleRow { + settingKey: "lockScreenVideoEnabled" + tags: ["lock", "screen", "video", "screensaver", "animation", "movie"] + text: I18n.tr("Enable Video Screensaver") + description: I18n.tr("Play a video when the screen locks.") + enabled: MultimediaService.available + checked: SettingsData.lockScreenVideoEnabled + onToggled: checked => SettingsData.set("lockScreenVideoEnabled", checked) + } + + Column { + width: parent.width + spacing: Theme.spacingXS + visible: SettingsData.lockScreenVideoEnabled && MultimediaService.available + + StyledText { + text: I18n.tr("Video Path") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: I18n.tr("Path to a video file or folder containing videos") + font.pixelSize: Theme.fontSizeSmall + color: Theme.outlineVariant + wrapMode: Text.WordWrap + width: parent.width + } + + Row { + width: parent.width + spacing: Theme.spacingS + + DankTextField { + id: videoPathField + width: parent.width - browseVideoButton.width - Theme.spacingS + placeholderText: I18n.tr("/path/to/videos") + text: SettingsData.lockScreenVideoPath + backgroundColor: Theme.surfaceContainerHighest + onTextChanged: { + if (text !== SettingsData.lockScreenVideoPath) { + SettingsData.set("lockScreenVideoPath", text); + } + } + } + + DankButton { + id: browseVideoButton + text: I18n.tr("Browse") + onClicked: videoBrowserModal.open() + } + } + } + + SettingsToggleRow { + settingKey: "lockScreenVideoCycling" + tags: ["lock", "screen", "video", "screensaver", "cycling", "random", "shuffle"] + text: I18n.tr("Automatic Cycling") + description: I18n.tr("Pick a different random video each time from the same folder") + visible: SettingsData.lockScreenVideoEnabled && MultimediaService.available + enabled: MultimediaService.available + checked: SettingsData.lockScreenVideoCycling + onToggled: checked => SettingsData.set("lockScreenVideoCycling", checked) + } + } + + SettingsCard { + width: parent.width + iconName: "monitor" + title: I18n.tr("Lock Screen Display") + settingKey: "lockDisplay" + visible: Quickshell.screens.length > 1 + + StyledText { + text: I18n.tr("Choose which monitor shows the lock screen interface. Other monitors will display a solid color for OLED burn-in protection.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.Wrap + } + + SettingsDropdownRow { + id: lockScreenMonitorDropdown + settingKey: "lockScreenActiveMonitor" + tags: ["lock", "screen", "monitor", "display", "active"] + text: I18n.tr("Active Lock Screen Monitor") + options: { + var opts = [I18n.tr("All Monitors")]; + var screens = Quickshell.screens; + for (var i = 0; i < screens.length; i++) { + opts.push(SettingsData.getScreenDisplayName(screens[i])); + } + return opts; + } + + Component.onCompleted: { + if (SettingsData.lockScreenActiveMonitor === "all") { + currentValue = I18n.tr("All Monitors"); + return; + } + var screens = Quickshell.screens; + for (var i = 0; i < screens.length; i++) { + if (screens[i].name === SettingsData.lockScreenActiveMonitor) { + currentValue = SettingsData.getScreenDisplayName(screens[i]); + return; + } + } + currentValue = I18n.tr("All Monitors"); + } + + onValueChanged: value => { + if (value === I18n.tr("All Monitors")) { + SettingsData.set("lockScreenActiveMonitor", "all"); + return; + } + var screens = Quickshell.screens; + for (var i = 0; i < screens.length; i++) { + if (SettingsData.getScreenDisplayName(screens[i]) === value) { + SettingsData.set("lockScreenActiveMonitor", screens[i].name); + return; + } + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + visible: SettingsData.lockScreenActiveMonitor !== "all" + + Column { + width: parent.width - inactiveColorPreview.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Inactive Monitor Color") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + } + + StyledText { + text: I18n.tr("Color displayed on monitors without the lock screen") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.Wrap + } + } + + Rectangle { + id: inactiveColorPreview + width: 48 + height: 48 + radius: Theme.cornerRadius + color: SettingsData.lockScreenInactiveColor + border.color: Theme.outline + border.width: 1 + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!PopoutService.colorPickerModal) + return; + PopoutService.colorPickerModal.selectedColor = SettingsData.lockScreenInactiveColor; + PopoutService.colorPickerModal.pickerTitle = I18n.tr("Inactive Monitor Color"); + PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) { + SettingsData.set("lockScreenInactiveColor", selectedColor); + }; + PopoutService.colorPickerModal.show(); + } + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/MediaPlayerTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/MediaPlayerTab.qml new file mode 100644 index 0000000..a844a86 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/MediaPlayerTab.qml @@ -0,0 +1,119 @@ +import QtQuick +import qs.Common +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + width: parent.width + iconName: "music_note" + title: I18n.tr("Media Player Settings") + settingKey: "mediaPlayer" + + SettingsToggleRow { + text: I18n.tr("Wave Progress Bars") + description: I18n.tr("Use animated wave progress bars for media playback") + checked: SettingsData.waveProgressEnabled + onToggled: checked => SettingsData.set("waveProgressEnabled", checked) + } + + SettingsToggleRow { + text: I18n.tr("Scroll song title") + description: I18n.tr("Scroll title if it doesn't fit in widget") + checked: SettingsData.scrollTitleEnabled + onToggled: checked => SettingsData.set("scrollTitleEnabled", checked) + } + + SettingsToggleRow { + text: I18n.tr("Audio Visualizer") + description: I18n.tr("Show cava audio visualizer in media widget") + checked: SettingsData.audioVisualizerEnabled + onToggled: checked => SettingsData.set("audioVisualizerEnabled", checked) + } + + SettingsToggleRow { + text: I18n.tr("Adaptive Media Width") + description: I18n.tr("Shrink the media widget to fit shorter song titles while still respecting the configured maximum size") + checked: SettingsData.mediaAdaptiveWidthEnabled + onToggled: checked => SettingsData.set("mediaAdaptiveWidthEnabled", checked) + } + + SettingsDropdownRow { + property var scrollOptsInternal: ["volume", "song", "nothing"] + property var scrollOptsDisplay: [I18n.tr("Change Volume", "media scroll wheel option"), I18n.tr("Change Song", "media scroll wheel option"), I18n.tr("Nothing", "media scroll wheel option")] + + text: I18n.tr("Scroll Wheel") + description: I18n.tr("Scroll wheel behavior on media widget") + settingKey: "audioScrollMode" + tags: ["media", "music", "scroll"] + options: scrollOptsDisplay + currentValue: { + const idx = scrollOptsInternal.indexOf(SettingsData.audioScrollMode); + return idx >= 0 ? scrollOptsDisplay[idx] : scrollOptsDisplay[0]; + } + onValueChanged: value => { + const idx = scrollOptsDisplay.indexOf(value); + if (idx >= 0) + SettingsData.set("audioScrollMode", scrollOptsInternal[idx]); + } + } + + Item { + width: parent.width + height: audioWheelScrollAmountColumn.height + visible: SettingsData.audioScrollMode == "volume" + opacity: visible ? 1 : 0 + + Column { + id: audioWheelScrollAmountColumn + x: Theme.spacingL + width: 120 + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Adjust volume per scroll indent") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + font.weight: Font.Medium + horizontalAlignment: Text.AlignLeft + } + + DankTextField { + width: 100 + height: 28 + placeholderText: "5" + text: SettingsData.audioWheelScrollAmount + maximumLength: 2 + font.pixelSize: Theme.fontSizeSmall + topPadding: Theme.spacingXS + bottomPadding: Theme.spacingXS + onEditingFinished: SettingsData.set("audioWheelScrollAmount", parseInt(text, 10)) + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/MuxTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/MuxTab.qml new file mode 100644 index 0000000..a82cdbe --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/MuxTab.qml @@ -0,0 +1,112 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + readonly property var muxTypeOptions: [ + "tmux", + "zellij" + ] + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + tab: "mux" + tags: ["mux", "multiplexer", "tmux", "zellij", "type"] + title: I18n.tr("Multiplexer") + iconName: "terminal" + + SettingsDropdownRow { + tab: "mux" + tags: ["mux", "multiplexer", "tmux", "zellij", "type", "backend"] + settingKey: "muxType" + text: I18n.tr("Multiplexer Type") + description: I18n.tr("Terminal multiplexer backend to use") + options: root.muxTypeOptions + currentValue: SettingsData.muxType + onValueChanged: value => SettingsData.set("muxType", value) + } + } + + SettingsCard { + tab: "mux" + tags: ["mux", "terminal", "custom", "command", "script"] + title: I18n.tr("Terminal") + iconName: "desktop_windows" + + SettingsToggleRow { + tab: "mux" + tags: ["mux", "custom", "command", "override"] + settingKey: "muxUseCustomCommand" + text: I18n.tr("Use Custom Command") + description: I18n.tr("Override terminal with a custom command or script") + checked: SettingsData.muxUseCustomCommand + onToggled: checked => SettingsData.set("muxUseCustomCommand", checked) + } + + Column { + width: parent?.width ?? 0 + spacing: Theme.spacingS + visible: SettingsData.muxUseCustomCommand + + StyledText { + width: parent.width + text: I18n.tr("The custom command used when attaching to sessions (receives the session name as the first argument)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + + DankTextField { + width: parent.width + text: SettingsData.muxCustomCommand + placeholderText: I18n.tr("Enter command or script path") + onTextEdited: SettingsData.set("muxCustomCommand", text) + } + } + } + + SettingsCard { + tab: "mux" + tags: ["mux", "session", "filter", "exclude", "hide"] + title: I18n.tr("Session Filter") + iconName: "filter_list" + + Column { + width: parent?.width ?? 0 + spacing: Theme.spacingS + + StyledText { + width: parent.width + text: I18n.tr("Comma-separated list of session names to hide. Wrap in slashes for regex (e.g., /^_.*/).") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + + DankTextField { + width: parent.width + text: SettingsData.muxSessionFilter + placeholderText: I18n.tr("e.g., scratch, /^tmp_.*/, build") + onTextEdited: SettingsData.set("muxSessionFilter", text) + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/NetworkTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/NetworkTab.qml new file mode 100644 index 0000000..b9b2566 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/NetworkTab.qml @@ -0,0 +1,1999 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Common +import qs.Modals.Common +import qs.Modals.FileBrowser +import qs.Services +import qs.Widgets + +Item { + id: networkTab + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property string expandedVpnUuid: "" + property string expandedWifiSsid: "" + property string expandedEthDevice: "" + property int maxPinnedWifiNetworks: 3 + + Component.onCompleted: { + NetworkService.addRef(); + } + + Component.onDestruction: { + NetworkService.removeRef(); + } + + function openVpnFileBrowser() { + vpnFileBrowserLoader.active = true; + if (vpnFileBrowserLoader.item) + vpnFileBrowserLoader.item.open(); + } + + function normalizePinList(value) { + if (Array.isArray(value)) + return value.filter(v => v) + if (typeof value === "string" && value.length > 0) + return [value] + return [] + } + + function getPinnedWifiNetworks() { + const pins = SettingsData.wifiNetworkPins || {} + return normalizePinList(pins["preferredWifi"]) + } + + function toggleWifiPin(ssid) { + const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {})) + let pinnedList = normalizePinList(pins["preferredWifi"]) + const pinIndex = pinnedList.indexOf(ssid) + + if (pinIndex !== -1) { + pinnedList.splice(pinIndex, 1) + } else { + pinnedList.unshift(ssid) + if (pinnedList.length > maxPinnedWifiNetworks) + pinnedList = pinnedList.slice(0, maxPinnedWifiNetworks) + } + + if (pinnedList.length > 0) + pins["preferredWifi"] = pinnedList + else + delete pins["preferredWifi"] + + SettingsData.set("wifiNetworkPins", pins) + } + + LazyLoader { + id: vpnFileBrowserLoader + active: false + + FileBrowserModal { + browserTitle: I18n.tr("Import VPN") + browserIcon: "vpn_key" + browserType: "vpn" + fileExtensions: VPNService.getFileFilter() + + onFileSelected: path => { + VPNService.importVpn(path.replace("file://", "")); + } + } + } + + ConfirmModal { + id: deleteVpnConfirm + } + + ConfirmModal { + id: forgetNetworkConfirm + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + + width: Math.min(600, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingL + + StyledRect { + width: parent.width + height: overviewSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + id: overviewSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "lan" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Network Status") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: I18n.tr("Overview of your network connections") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + Grid { + columns: 2 + columnSpacing: Theme.spacingL + rowSpacing: Theme.spacingS + width: parent.width + + StyledText { + text: I18n.tr("Backend") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + StyledText { + text: NetworkService.backend || I18n.tr("Unknown") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: I18n.tr("Status") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + Row { + spacing: Theme.spacingS + + Rectangle { + width: 8 + height: 8 + radius: 4 + anchors.verticalCenter: parent.verticalCenter + color: { + switch (NetworkService.networkStatus) { + case "ethernet": + case "wifi": + return Theme.success; + case "disconnected": + return Theme.error; + default: + return Theme.warning; + } + } + } + + StyledText { + text: { + switch (NetworkService.networkStatus) { + case "ethernet": + return I18n.tr("Ethernet"); + case "wifi": + return I18n.tr("WiFi"); + case "disconnected": + return I18n.tr("Disconnected"); + default: + return NetworkService.networkStatus || I18n.tr("Unknown"); + } + } + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + } + + StyledText { + text: I18n.tr("Primary") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + visible: NetworkService.primaryConnection.length > 0 + } + StyledText { + text: NetworkService.primaryConnection || "-" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + elide: Text.ElideRight + visible: NetworkService.primaryConnection.length > 0 + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + visible: NetworkService.backend === "networkmanager" && NetworkService.ethernetConnected && NetworkService.wifiConnected + + StyledText { + text: I18n.tr("Preference") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - preferenceLabel.width - preferenceButtons.width - Theme.spacingM * 2 + height: 1 + } + + DankButtonGroup { + id: preferenceButtons + model: [I18n.tr("Auto"), I18n.tr("Ethernet"), I18n.tr("WiFi")] + currentIndex: { + switch (NetworkService.userPreference) { + case "ethernet": + return 1; + case "wifi": + return 2; + default: + return 0; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + switch (index) { + case 0: + NetworkService.setNetworkPreference("auto"); + break; + case 1: + NetworkService.setNetworkPreference("ethernet"); + break; + case 2: + NetworkService.setNetworkPreference("wifi"); + break; + } + } + } + } + + StyledText { + id: preferenceLabel + visible: false + text: I18n.tr("Preference") + } + } + } + + StyledRect { + width: parent.width + height: ethernetSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + visible: NetworkService.ethernetConnected || (NetworkService.ethernetDevices?.length ?? 0) > 0 + + Column { + id: ethernetSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "settings_ethernet" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Ethernet") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: { + const devices = NetworkService.ethernetDevices; + const connected = devices.filter(d => d.connected).length; + if (devices.length === 0) + return I18n.tr("No adapters"); + if (connected === 0) + return devices.length === 1 + ? I18n.tr("%1 adapter, none connected").arg(devices.length) + : I18n.tr("%1 adapters, none connected").arg(devices.length); + return I18n.tr("%1 connected").arg(connected); + } + font.pixelSize: Theme.fontSizeSmall + color: NetworkService.ethernetConnected ? Theme.primary : Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + Column { + width: parent.width + spacing: 4 + visible: NetworkService.ethernetDevices.length > 0 + + StyledText { + text: I18n.tr("Adapters") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Repeater { + model: NetworkService.ethernetDevices + + delegate: Rectangle { + id: ethDeviceDelegate + required property var modelData + required property int index + + readonly property bool isConnected: modelData.connected || false + readonly property bool isExpanded: networkTab.expandedEthDevice === modelData.name + + width: parent.width + height: isExpanded ? 56 + ethExpandedContent.height : 56 + radius: Theme.cornerRadius + color: ethDeviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + border.width: isConnected ? 2 : 0 + border.color: Theme.primary + clip: true + + Behavior on height { + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + } + } + + Column { + anchors.fill: parent + spacing: 0 + + Item { + width: parent.width + height: 56 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + anchors.right: ethDeviceActions.left + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: "lan" + size: 20 + color: isConnected ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + width: parent.width - 20 - Theme.spacingS + + StyledText { + text: modelData.name || I18n.tr("Unknown") + font.pixelSize: Theme.fontSizeMedium + color: isConnected ? Theme.primary : Theme.surfaceText + font.weight: isConnected ? Font.Medium : Font.Normal + elide: Text.ElideRight + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Row { + anchors.left: parent.left + spacing: Theme.spacingXS + + StyledText { + text: { + switch (modelData.state) { + case "activated": + return I18n.tr("Connected"); + case "disconnected": + return I18n.tr("Disconnected"); + case "unavailable": + return I18n.tr("Unavailable"); + default: + return modelData.state || I18n.tr("Unknown"); + } + } + font.pixelSize: Theme.fontSizeSmall + color: isConnected ? Theme.primary : Theme.surfaceVariantText + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: (modelData.ip || "").length > 0 + } + + StyledText { + text: modelData.ip || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: (modelData.ip || "").length > 0 + } + } + } + } + + Row { + id: ethDeviceActions + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: ethExpandBtn.containsMouse ? Theme.surfacePressed : "transparent" + visible: isConnected + + DankIcon { + anchors.centerIn: parent + name: isExpanded ? "expand_less" : "expand_more" + size: 18 + color: Theme.surfaceText + } + + MouseArea { + id: ethExpandBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (isExpanded) { + networkTab.expandedEthDevice = ""; + } else { + networkTab.expandedEthDevice = modelData.name; + NetworkService.fetchWiredNetworkInfo(NetworkService.ethernetConnectionUuid); + } + } + } + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: ethDisconnectBtn.containsMouse ? Theme.errorHover : "transparent" + visible: isConnected + + DankIcon { + anchors.centerIn: parent + name: "link_off" + size: 18 + color: ethDisconnectBtn.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: ethDisconnectBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: NetworkService.disconnectEthernetDevice(modelData.name) + } + } + } + + MouseArea { + id: ethDeviceMouseArea + anchors.fill: parent + anchors.rightMargin: ethDeviceActions.width + Theme.spacingM + hoverEnabled: true + } + } + + Column { + id: ethExpandedContent + width: parent.width + visible: isExpanded + + Rectangle { + width: parent.width - Theme.spacingM * 2 + height: 1 + x: Theme.spacingM + color: Theme.outlineLight + } + + Item { + width: parent.width + height: ethDetailsColumn.implicitHeight + Theme.spacingM * 2 + + Column { + id: ethDetailsColumn + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + Flow { + width: parent.width + spacing: Theme.spacingXS + + Repeater { + model: { + const fields = []; + const dev = modelData; + if (!dev) + return fields; + + if (dev.ip) + fields.push({ + label: I18n.tr("IP"), + value: dev.ip + }); + if (dev.speed && dev.speed > 0) + fields.push({ + label: I18n.tr("Speed"), + value: dev.speed + " Mbps" + }); + if (dev.hwAddress) + fields.push({ + label: I18n.tr("MAC"), + value: dev.hwAddress + }); + if (dev.driver) + fields.push({ + label: I18n.tr("Driver"), + value: dev.driver + }); + fields.push({ + label: I18n.tr("State"), + value: dev.state || I18n.tr("Unknown") + }); + + return fields; + } + + delegate: Rectangle { + required property var modelData + required property int index + + width: ethFieldContent.width + Theme.spacingM * 2 + height: 32 + radius: Theme.cornerRadius - 2 + color: Theme.surfaceContainerHigh + border.width: 1 + border.color: Theme.outlineLight + + Row { + id: ethFieldContent + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: modelData.label + ":" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData.value + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + Item { + width: parent.width + height: NetworkService.networkWiredInfoLoading ? 40 : 0 + visible: NetworkService.networkWiredInfoLoading + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + id: wiredLoadIcon + name: "sync" + size: 16 + color: Theme.surfaceVariantText + + SequentialAnimation { + running: NetworkService.networkWiredInfoLoading + loops: Animation.Infinite + NumberAnimation { + target: wiredLoadIcon + property: "opacity" + to: 0.3 + duration: 400 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: wiredLoadIcon + property: "opacity" + to: 1.0 + duration: 400 + easing.type: Easing.InOutQuad + } + onRunningChanged: if (!running) + wiredLoadIcon.opacity = 1.0 + } + } + + StyledText { + text: I18n.tr("Loading...") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + } + } + } + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: NetworkService.wiredConnections.length > 0 + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + StyledText { + text: I18n.tr("Saved Configurations") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Repeater { + model: NetworkService.wiredConnections + + delegate: Rectangle { + required property var modelData + required property int index + + width: parent.width + height: 48 + radius: Theme.cornerRadius + color: wiredMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + border.width: modelData.isActive ? 2 : 0 + border.color: Theme.primary + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "lan" + size: 20 + color: modelData.isActive ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: modelData.id || I18n.tr("Unknown") + font.pixelSize: Theme.fontSizeMedium + color: modelData.isActive ? Theme.primary : Theme.surfaceText + font.weight: modelData.isActive ? Font.Medium : Font.Normal + } + + StyledText { + text: modelData.isActive ? I18n.tr("Active") : "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + visible: modelData.isActive + } + } + } + + MouseArea { + id: wiredMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!modelData.isActive) { + NetworkService.connectToSpecificWiredConfig(modelData.uuid); + } + } + } + } + } + } + } + } + + StyledRect { + width: parent.width + height: wifiSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + id: wifiSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: NetworkService.wifiEnabled ? "wifi" : "wifi_off" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - wifiControls.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("WiFi") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: { + if (NetworkService.wifiToggling) + return I18n.tr("Toggling..."); + if (!NetworkService.wifiEnabled) + return I18n.tr("Disabled"); + if (NetworkService.wifiConnected) + return NetworkService.currentWifiSSID; + return I18n.tr("Not connected"); + } + font.pixelSize: Theme.fontSizeSmall + color: NetworkService.wifiConnected ? Theme.primary : Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + + Row { + id: wifiControls + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankActionButton { + iconName: "wifi_find" + buttonSize: 32 + visible: NetworkService.backend === "networkmanager" && NetworkService.wifiEnabled && !NetworkService.wifiToggling + onClicked: PopoutService.showHiddenNetworkModal() + } + + DankActionButton { + iconName: "refresh" + buttonSize: 32 + visible: NetworkService.wifiEnabled && !NetworkService.wifiToggling && !NetworkService.isScanning + onClicked: NetworkService.scanWifi() + } + + DankToggle { + checked: NetworkService.wifiEnabled + enabled: !NetworkService.wifiToggling + onToggled: NetworkService.toggleWifiRadio() + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + visible: NetworkService.wifiEnabled && (NetworkService.wifiDevices?.length ?? 0) > 1 + + StyledText { + text: I18n.tr("WiFi Device") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - wifiDeviceLabel.width - wifiDeviceDropdown.width - Theme.spacingM * 2 + height: 1 + } + + DankDropdown { + id: wifiDeviceDropdown + dropdownWidth: 150 + popupWidth: 180 + currentValue: NetworkService.wifiDeviceOverride || I18n.tr("Auto") + options: { + const devices = NetworkService.wifiDevices; + if (!devices || devices.length === 0) + return [I18n.tr("Auto")]; + return [I18n.tr("Auto")].concat(devices.map(d => d.name)); + } + onValueChanged: value => { + const deviceName = value === I18n.tr("Auto") ? "" : value; + NetworkService.setWifiDeviceOverride(deviceName); + } + } + } + + StyledText { + id: wifiDeviceLabel + visible: false + text: I18n.tr("WiFi Device") + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + visible: NetworkService.wifiEnabled + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: NetworkService.wifiEnabled && !NetworkService.wifiToggling + + Column { + width: parent.width + spacing: Theme.spacingS + visible: NetworkService.wifiInterface.length > 0 + + Row { + width: parent.width + height: 24 + + StyledText { + text: I18n.tr("Interface:") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + width: 100 + anchors.verticalCenter: parent.verticalCenter + } + StyledText { + text: NetworkService.wifiInterface || "-" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + width: parent.width + height: 24 + visible: NetworkService.wifiIP.length > 0 + + StyledText { + text: I18n.tr("IP Address:") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + width: 100 + anchors.verticalCenter: parent.verticalCenter + } + StyledText { + text: NetworkService.wifiIP || "-" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + width: parent.width + height: 24 + visible: NetworkService.wifiConnected + + StyledText { + text: I18n.tr("Signal:") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + width: 100 + anchors.verticalCenter: parent.verticalCenter + } + + Row { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: { + const s = NetworkService.wifiSignalStrength; + if (s >= 50) + return "wifi"; + if (s >= 25) + return "wifi_2_bar"; + return "wifi_1_bar"; + } + size: 18 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: NetworkService.wifiSignalStrength + "%" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + } + } + } + } + + Item { + width: parent.width + height: Theme.spacingS + } + + Row { + width: parent.width + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Available Networks") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: 1 + height: 1 + Layout.fillWidth: true + } + + StyledText { + text: NetworkService.wifiNetworks?.length ?? 0 + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + + Item { + width: parent.width + height: 80 + visible: NetworkService.isScanning && (NetworkService.wifiNetworks?.length ?? 0) === 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + id: scanningIcon + name: "wifi_find" + size: 32 + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + + SequentialAnimation { + running: NetworkService.isScanning + loops: Animation.Infinite + NumberAnimation { + target: scanningIcon + property: "opacity" + to: 0.3 + duration: 400 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: scanningIcon + property: "opacity" + to: 1.0 + duration: 400 + easing.type: Easing.InOutQuad + } + onRunningChanged: if (!running) + scanningIcon.opacity = 1.0 + } + } + + StyledText { + text: I18n.tr("Scanning...") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + Column { + width: parent.width + spacing: 4 + visible: (NetworkService.wifiNetworks?.length ?? 0) > 0 + + Repeater { + model: { + const ssid = NetworkService.currentWifiSSID; + const networks = NetworkService.wifiNetworks || []; + const pinnedList = networkTab.getPinnedWifiNetworks(); + + let sorted = [...networks]; + sorted.sort((a, b) => { + const aPinnedIndex = pinnedList.indexOf(a.ssid) + const bPinnedIndex = pinnedList.indexOf(b.ssid) + if (aPinnedIndex !== -1 || bPinnedIndex !== -1) { + if (aPinnedIndex === -1) + return 1 + if (bPinnedIndex === -1) + return -1 + return aPinnedIndex - bPinnedIndex + } + if (a.ssid === ssid) + return -1; + if (b.ssid === ssid) + return 1; + return b.signal - a.signal; + }); + return sorted; + } + + delegate: Rectangle { + id: wifiNetworkDelegate + required property var modelData + required property int index + + readonly property bool isConnected: modelData.ssid === NetworkService.currentWifiSSID + readonly property bool isPinned: networkTab.getPinnedWifiNetworks().includes(modelData.ssid) + readonly property bool isExpanded: networkTab.expandedWifiSsid === modelData.ssid + + width: parent.width + height: isExpanded ? 56 + wifiExpandedContent.height : 56 + radius: Theme.cornerRadius + color: wifiNetworkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + border.width: isConnected ? 2 : 0 + border.color: Theme.primary + clip: true + + Behavior on height { + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + } + } + + Column { + anchors.fill: parent + spacing: 0 + + Item { + width: parent.width + height: 56 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + anchors.right: wifiNetworkActions.left + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: { + const s = modelData.signal || 0; + if (s >= 50) + return "wifi"; + if (s >= 25) + return "wifi_2_bar"; + return "wifi_1_bar"; + } + size: 20 + color: isConnected ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + width: parent.width - 20 - Theme.spacingS + + Row { + anchors.left: parent.left + spacing: Theme.spacingXS + + StyledText { + text: modelData.ssid || I18n.tr("Unknown") + font.pixelSize: Theme.fontSizeMedium + color: isConnected ? Theme.primary : Theme.surfaceText + font.weight: isConnected ? Font.Medium : Font.Normal + elide: Text.ElideRight + } + + DankIcon { + name: "push_pin" + size: 14 + color: Theme.primary + visible: isPinned + anchors.verticalCenter: parent.verticalCenter + } + + DankIcon { + name: "visibility_off" + size: 14 + color: Theme.surfaceVariantText + visible: modelData.hidden || false + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + anchors.left: parent.left + spacing: Theme.spacingXS + + StyledText { + text: isConnected ? I18n.tr("Connected") : (modelData.secured ? I18n.tr("Secured") : I18n.tr("Open")) + font.pixelSize: Theme.fontSizeSmall + color: isConnected ? Theme.primary : Theme.surfaceVariantText + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: modelData.saved + } + + StyledText { + text: I18n.tr("Saved") + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + visible: modelData.saved + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: modelData.hidden || false + } + + StyledText { + text: I18n.tr("Hidden") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: modelData.hidden || false + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: modelData.signal + "%" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + } + + Row { + id: wifiNetworkActions + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: wifiExpandBtn.containsMouse ? Theme.surfacePressed : "transparent" + visible: isConnected || modelData.saved + + DankIcon { + anchors.centerIn: parent + name: isExpanded ? "expand_less" : "expand_more" + size: 18 + color: Theme.surfaceText + } + + MouseArea { + id: wifiExpandBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (isExpanded) { + networkTab.expandedWifiSsid = ""; + } else { + networkTab.expandedWifiSsid = modelData.ssid; + NetworkService.fetchNetworkInfo(modelData.ssid); + } + } + } + } + + DankActionButton { + iconName: "qr_code" + buttonSize: 28 + visible: modelData.secured && modelData.saved + onClicked: { + PopoutService.showWifiQRCodeModal(modelData.ssid); + } + } + + DankActionButton { + iconName: isPinned ? "push_pin" : "push_pin" + buttonSize: 28 + iconColor: isPinned ? Theme.primary : Theme.surfaceVariantText + onClicked: { + networkTab.toggleWifiPin(modelData.ssid) + } + } + + DankActionButton { + iconName: "delete" + buttonSize: 28 + iconColor: Theme.error + visible: modelData.saved || isConnected + onClicked: { + forgetNetworkConfirm.showWithOptions({ + title: I18n.tr("Forget Network"), + message: I18n.tr("Forget \"%1\"?").arg(modelData.ssid), + confirmText: I18n.tr("Forget"), + confirmColor: Theme.error, + onConfirm: () => NetworkService.forgetWifiNetwork(modelData.ssid) + }); + } + } + } + + MouseArea { + id: wifiNetworkMouseArea + + anchors.fill: parent + anchors.rightMargin: wifiNetworkActions.width + Theme.spacingM + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (isConnected) { + NetworkService.disconnectWifi(); + return; + } + NetworkService.connectToWifi(modelData.ssid); + } + } + } + + Column { + id: wifiExpandedContent + width: parent.width + visible: isExpanded + + Rectangle { + width: parent.width - Theme.spacingM * 2 + height: 1 + x: Theme.spacingM + color: Theme.outlineLight + } + + Item { + width: parent.width + height: wifiDetailsColumn.implicitHeight + Theme.spacingM * 2 + + Column { + id: wifiDetailsColumn + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + Item { + width: parent.width + height: NetworkService.networkInfoLoading ? 40 : 0 + visible: NetworkService.networkInfoLoading + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + id: wifiInfoLoadIcon + name: "sync" + size: 16 + color: Theme.surfaceVariantText + + SequentialAnimation { + running: NetworkService.networkInfoLoading + loops: Animation.Infinite + NumberAnimation { + target: wifiInfoLoadIcon + property: "opacity" + to: 0.3 + duration: 400 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: wifiInfoLoadIcon + property: "opacity" + to: 1.0 + duration: 400 + easing.type: Easing.InOutQuad + } + onRunningChanged: if (!running) + wifiInfoLoadIcon.opacity = 1.0 + } + } + + StyledText { + text: I18n.tr("Loading...") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + + Flow { + width: parent.width + spacing: Theme.spacingXS + visible: !NetworkService.networkInfoLoading + + Repeater { + model: { + const fields = []; + const net = modelData; + if (!net) + return fields; + + fields.push({ + label: I18n.tr("Signal"), + value: net.signal + "%" + }); + if (net.frequency) + fields.push({ + label: I18n.tr("Frequency"), + value: (net.frequency / 1000).toFixed(1) + " GHz" + }); + if (net.channel) + fields.push({ + label: I18n.tr("Channel"), + value: String(net.channel) + }); + if (net.rate) + fields.push({ + label: I18n.tr("Rate"), + value: net.rate + " Mbps" + }); + if (net.mode) + fields.push({ + label: I18n.tr("Mode"), + value: net.mode + }); + if (net.bssid) + fields.push({ + label: I18n.tr("BSSID"), + value: net.bssid + }); + fields.push({ + label: I18n.tr("Security"), + value: net.secured ? (net.enterprise ? I18n.tr("Enterprise") : I18n.tr("WPA/WPA2")) : I18n.tr("Open") + }); + + return fields; + } + + delegate: Rectangle { + required property var modelData + required property int index + + width: wifiFieldContent.width + Theme.spacingM * 2 + height: 32 + radius: Theme.cornerRadius - 2 + color: Theme.surfaceContainerHigh + border.width: 1 + border.color: Theme.outlineLight + + Row { + id: wifiFieldContent + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: modelData.label + ":" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData.value + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + Row { + spacing: Theme.spacingS + visible: (modelData.saved || isConnected) && DMSService.apiVersion > 13 + + DankToggle { + id: autoconnectToggle + text: I18n.tr("Autoconnect") + checked: modelData.autoconnect || false + onToggled: checked => { + NetworkService.setWifiAutoconnect(modelData.ssid, checked); + } + } + } + } + } + } + } + } + } + } + } + } + } + + StyledRect { + width: parent.width + height: vpnSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + visible: DMSNetworkService.vpnAvailable + + Column { + id: vpnSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - vpnHeaderControls.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("VPN") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: { + if (!DMSNetworkService.connected) + return I18n.tr("Disconnected"); + const names = DMSNetworkService.activeNames || []; + if (names.length <= 1) + return names[0] || I18n.tr("Connected"); + return names[0] + " +" + (names.length - 1); + } + font.pixelSize: Theme.fontSizeSmall + color: DMSNetworkService.connected ? Theme.primary : Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + + Row { + id: vpnHeaderControls + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + Rectangle { + height: 28 + radius: 14 + width: importVpnRow.width + Theme.spacingM * 2 + color: importVpnArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + opacity: VPNService.importing ? 0.5 : 1.0 + + Row { + id: importVpnRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: VPNService.importing ? "sync" : "add" + size: Theme.fontSizeSmall + color: Theme.primary + } + + StyledText { + text: I18n.tr("Import") + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + font.weight: Font.Medium + } + } + + MouseArea { + id: importVpnArea + anchors.fill: parent + hoverEnabled: true + cursorShape: VPNService.importing ? Qt.BusyCursor : Qt.PointingHandCursor + enabled: !VPNService.importing + onClicked: networkTab.openVpnFileBrowser() + } + } + + Rectangle { + height: 28 + radius: 14 + width: disconnectAllRow.width + Theme.spacingM * 2 + color: disconnectAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight + visible: DMSNetworkService.connected + opacity: DMSNetworkService.isBusy ? 0.5 : 1.0 + + Row { + id: disconnectAllRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "link_off" + size: Theme.fontSizeSmall + color: Theme.surfaceText + } + + StyledText { + text: I18n.tr("Disconnect") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + } + + MouseArea { + id: disconnectAllArea + anchors.fill: parent + hoverEnabled: true + cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor + enabled: !DMSNetworkService.isBusy + onClicked: DMSNetworkService.disconnectAllActive() + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + Item { + width: parent.width + height: 100 + visible: DMSNetworkService.profiles.length === 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "vpn_key_off" + size: 36 + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("No VPN profiles") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("Click Import to add a .ovpn or .conf") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + Column { + width: parent.width + spacing: 4 + visible: DMSNetworkService.profiles.length > 0 + + Repeater { + model: DMSNetworkService.profiles + + delegate: Rectangle { + id: vpnProfileRow + required property var modelData + required property int index + + readonly property bool isActive: DMSNetworkService.isActiveUuid(modelData.uuid) + readonly property bool isExpanded: networkTab.expandedVpnUuid === modelData.uuid + readonly property var configData: isExpanded ? VPNService.editConfig : null + + width: parent.width + height: isExpanded ? 56 + vpnExpandedContent.height : 56 + radius: Theme.cornerRadius + color: vpnRowArea.containsMouse ? Theme.primaryHoverLight : (isActive ? Theme.primaryPressed : Theme.surfaceLight) + border.width: isActive ? 2 : 0 + border.color: Theme.primary + opacity: DMSNetworkService.isBusy ? 0.6 : 1.0 + clip: true + + Behavior on height { + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + } + } + + MouseArea { + id: vpnRowArea + anchors.fill: parent + hoverEnabled: true + cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor + enabled: !DMSNetworkService.isBusy + onClicked: DMSNetworkService.toggle(modelData.uuid) + } + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingS + + Row { + width: parent.width + height: 56 - Theme.spacingS * 2 + spacing: Theme.spacingS + + DankIcon { + name: isActive ? "vpn_lock" : "vpn_key_off" + size: 20 + color: isActive ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: 2 + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 20 - 28 - 28 - Theme.spacingS * 4 + + StyledText { + text: modelData.name + font.pixelSize: Theme.fontSizeMedium + color: isActive ? Theme.primary : Theme.surfaceText + elide: Text.ElideRight + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: VPNService.getVpnTypeFromProfile(modelData) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.left: parent.left + } + } + + Item { + width: Theme.spacingXS + height: 1 + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: vpnExpandBtn.containsMouse ? Theme.surfacePressed : "transparent" + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: isExpanded ? "expand_less" : "expand_more" + size: 18 + color: Theme.surfaceText + } + + MouseArea { + id: vpnExpandBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (isExpanded) { + networkTab.expandedVpnUuid = ""; + } else { + networkTab.expandedVpnUuid = modelData.uuid; + VPNService.getConfig(modelData.uuid); + } + } + } + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: vpnDeleteBtn.containsMouse ? Theme.errorHover : "transparent" + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: "delete" + size: 18 + color: vpnDeleteBtn.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: vpnDeleteBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + deleteVpnConfirm.showWithOptions({ + title: I18n.tr("Delete VPN"), + message: I18n.tr("Delete \"%1\"?").arg(modelData.name), + confirmText: I18n.tr("Delete"), + confirmColor: Theme.error, + onConfirm: () => VPNService.deleteVpn(modelData.uuid) + }); + } + } + } + } + + Column { + id: vpnExpandedContent + width: parent.width + spacing: Theme.spacingXS + visible: isExpanded + + Rectangle { + width: parent.width + height: 1 + color: Theme.outlineLight + } + + Item { + width: parent.width + height: VPNService.configLoading ? 40 : 0 + visible: VPNService.configLoading + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + id: vpnLoadIcon + name: "sync" + size: 16 + color: Theme.surfaceVariantText + + SequentialAnimation { + running: VPNService.configLoading + loops: Animation.Infinite + NumberAnimation { + target: vpnLoadIcon + property: "opacity" + to: 0.3 + duration: 400 + easing.type: Easing.InOutQuad + } + NumberAnimation { + target: vpnLoadIcon + property: "opacity" + to: 1.0 + duration: 400 + easing.type: Easing.InOutQuad + } + onRunningChanged: if (!running) + vpnLoadIcon.opacity = 1.0 + } + } + + StyledText { + text: I18n.tr("Loading...") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + + Flow { + width: parent.width + spacing: Theme.spacingXS + visible: !VPNService.configLoading && configData + + Repeater { + model: { + if (!configData) + return []; + const fields = []; + const data = configData.data || {}; + + if (data.remote) + fields.push({ + label: I18n.tr("Server"), + value: data.remote + }); + if (configData.username || data.username) + fields.push({ + label: I18n.tr("Username"), + value: configData.username || data.username + }); + if (data.cipher) + fields.push({ + label: I18n.tr("Cipher"), + value: data.cipher + }); + if (data.auth) + fields.push({ + label: I18n.tr("Auth"), + value: data.auth + }); + if (data["proto-tcp"] === "yes" || data["proto-tcp"] === "no") + fields.push({ + label: I18n.tr("Protocol"), + value: data["proto-tcp"] === "yes" ? "TCP" : "UDP" + }); + if (data["tunnel-mtu"]) + fields.push({ + label: I18n.tr("MTU"), + value: data["tunnel-mtu"] + }); + if (data["connection-type"]) + fields.push({ + label: I18n.tr("Auth Type"), + value: data["connection-type"] + }); + return fields; + } + + delegate: Rectangle { + required property var modelData + required property int index + + width: vpnFieldContent.width + Theme.spacingM * 2 + height: 32 + radius: Theme.cornerRadius - 2 + color: Theme.surfaceContainerHigh + border.width: 1 + border.color: Theme.outlineLight + + Row { + id: vpnFieldContent + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: modelData.label + ":" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData.value + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + DankToggle { + width: parent.width + text: I18n.tr("Autoconnect") + checked: configData ? (configData.autoconnect || false) : false + visible: !VPNService.configLoading && configData !== null + onToggled: checked => { + VPNService.updateConfig(modelData.uuid, {autoconnect: checked}); + } + } + + Item { + width: 1 + height: Theme.spacingXS + } + } + } + } + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/NotificationsTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/NotificationsTab.qml new file mode 100644 index 0000000..c43a5e2 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/NotificationsTab.qml @@ -0,0 +1,900 @@ +import QtQuick +import qs.Common +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + Component.onCompleted: { + if (SettingsData._pendingExpandNotificationRules) { + SettingsData._pendingExpandNotificationRules = false; + notificationRulesCard.userToggledCollapse = true; + notificationRulesCard.expanded = true; + SettingsData._pendingNotificationRuleIndex = -1; + } + } + + readonly property var mutedRules: { + var rules = SettingsData.notificationRules || []; + var out = []; + for (var i = 0; i < rules.length; i++) { + if ((rules[i].action || "").toString().toLowerCase() === "mute") + out.push({ + rule: rules[i], + index: i + }); + } + return out; + } + + readonly property var timeoutOptions: [ + { + text: I18n.tr("Never"), + value: 0 + }, + { + text: I18n.tr("1 second"), + value: 1000 + }, + { + text: I18n.tr("3 seconds"), + value: 3000 + }, + { + text: I18n.tr("5 seconds"), + value: 5000 + }, + { + text: I18n.tr("8 seconds"), + value: 8000 + }, + { + text: I18n.tr("10 seconds"), + value: 10000 + }, + { + text: I18n.tr("15 seconds"), + value: 15000 + }, + { + text: I18n.tr("30 seconds"), + value: 30000 + }, + { + text: I18n.tr("1 minute"), + value: 60000 + }, + { + text: I18n.tr("2 minutes"), + value: 120000 + }, + { + text: I18n.tr("5 minutes"), + value: 300000 + }, + { + text: I18n.tr("10 minutes"), + value: 600000 + } + ] + + readonly property var notificationRuleFieldOptions: [ + { + value: "appName", + label: I18n.tr("App Names", "notification rule match field option") + }, + { + value: "desktopEntry", + label: I18n.tr("Desktop Entry", "notification rule match field option") + }, + { + value: "summary", + label: I18n.tr("Summary", "notification rule match field option") + }, + { + value: "body", + label: I18n.tr("Body", "notification rule match field option") + } + ] + + readonly property var notificationRuleMatchTypeOptions: [ + { + value: "contains", + label: I18n.tr("Contains", "notification rule match type option") + }, + { + value: "exact", + label: I18n.tr("Exact", "notification rule match type option") + }, + { + value: "regex", + label: I18n.tr("Regex", "notification rule match type option") + } + ] + + readonly property var notificationRuleActionOptions: [ + { + value: "default", + label: I18n.tr("Default", "notification rule action option") + }, + { + value: "mute", + label: I18n.tr("Mute Popups", "notification rule action option") + }, + { + value: "ignore", + label: I18n.tr("Ignore Completely", "notification rule action option") + }, + { + value: "popup_only", + label: I18n.tr("Popup Only", "notification rule action option") + }, + { + value: "no_history", + label: I18n.tr("No History", "notification rule action option") + } + ] + + readonly property var notificationRuleUrgencyOptions: [ + { + value: "default", + label: I18n.tr("Default", "notification rule urgency option") + }, + { + value: "low", + label: I18n.tr("Low Priority", "notification rule urgency option") + }, + { + value: "normal", + label: I18n.tr("Normal Priority", "notification rule urgency option") + }, + { + value: "critical", + label: I18n.tr("Critical Priority", "notification rule urgency option") + } + ] + + function getTimeoutText(value) { + if (value === undefined || value === null || isNaN(value)) + return I18n.tr("5 seconds"); + for (let i = 0; i < timeoutOptions.length; i++) { + if (timeoutOptions[i].value === value) + return timeoutOptions[i].text; + } + if (value === 0) + return I18n.tr("Never"); + if (value < 1000) + return value + "ms"; + if (value < 60000) + return Math.round(value / 1000) + " " + I18n.tr("seconds"); + return Math.round(value / 60000) + " " + I18n.tr("minutes"); + } + + function getRuleOptionLabel(options, value, fallback) { + for (let i = 0; i < options.length; i++) { + if (options[i].value === value) + return options[i].label; + } + return fallback; + } + + function getRuleOptionValue(options, label, fallback) { + for (let i = 0; i < options.length; i++) { + if (options[i].label === label) + return options[i].value; + } + return fallback; + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + width: parent.width + iconName: "notifications" + title: I18n.tr("Notification Popups") + settingKey: "notificationPopups" + + SettingsDropdownRow { + settingKey: "notificationPopupPosition" + tags: ["notification", "popup", "position", "screen", "location"] + text: I18n.tr("Popup Position") + description: I18n.tr("Choose where notification popups appear on screen") + currentValue: { + if (SettingsData.notificationPopupPosition === -1) + return I18n.tr("Top Center", "screen position option"); + switch (SettingsData.notificationPopupPosition) { + case SettingsData.Position.Top: + return I18n.tr("Top Right", "screen position option"); + case SettingsData.Position.Bottom: + return I18n.tr("Bottom Left", "screen position option"); + case SettingsData.Position.Left: + return I18n.tr("Top Left", "screen position option"); + case SettingsData.Position.Right: + return I18n.tr("Bottom Right", "screen position option"); + case SettingsData.Position.BottomCenter: + return I18n.tr("Bottom Center", "screen position option"); + default: + return I18n.tr("Top Right", "screen position option"); + } + } + options: [I18n.tr("Top Right", "screen position option"), I18n.tr("Top Left", "screen position option"), I18n.tr("Top Center", "screen position option"), I18n.tr("Bottom Center", "screen position option"), I18n.tr("Bottom Right", "screen position option"), I18n.tr("Bottom Left", "screen position option")] + onValueChanged: value => { + switch (value) { + case I18n.tr("Top Right", "screen position option"): + SettingsData.set("notificationPopupPosition", SettingsData.Position.Top); + break; + case I18n.tr("Top Left", "screen position option"): + SettingsData.set("notificationPopupPosition", SettingsData.Position.Left); + break; + case I18n.tr("Top Center", "screen position option"): + SettingsData.set("notificationPopupPosition", -1); + break; + case I18n.tr("Bottom Center", "screen position option"): + SettingsData.set("notificationPopupPosition", SettingsData.Position.BottomCenter); + break; + case I18n.tr("Bottom Right", "screen position option"): + SettingsData.set("notificationPopupPosition", SettingsData.Position.Right); + break; + case I18n.tr("Bottom Left", "screen position option"): + SettingsData.set("notificationPopupPosition", SettingsData.Position.Bottom); + break; + } + SettingsData.sendTestNotifications(); + } + } + + SettingsToggleRow { + settingKey: "notificationOverlayEnabled" + tags: ["notification", "overlay", "fullscreen", "priority"] + text: I18n.tr("Notification Overlay") + description: I18n.tr("Display all priorities over fullscreen apps") + checked: SettingsData.notificationOverlayEnabled + onToggled: checked => SettingsData.set("notificationOverlayEnabled", checked) + } + + SettingsToggleRow { + settingKey: "notificationCompactMode" + tags: ["notification", "compact", "size", "display", "mode"] + text: I18n.tr("Compact") + description: I18n.tr("Use smaller notification cards") + checked: SettingsData.notificationCompactMode + onToggled: checked => SettingsData.set("notificationCompactMode", checked) + } + + SettingsToggleRow { + settingKey: "notificationPopupShadowEnabled" + tags: ["notification", "popup", "shadow", "radius", "rounded"] + text: I18n.tr("Popup Shadow") + description: I18n.tr("Show drop shadow on notification popups. Requires M3 Elevation to be enabled in Theme & Colors.") + checked: SettingsData.notificationPopupShadowEnabled + onToggled: checked => SettingsData.set("notificationPopupShadowEnabled", checked) + } + + SettingsToggleRow { + settingKey: "notificationPopupPrivacyMode" + tags: ["notification", "popup", "privacy", "body", "content", "hide"] + text: I18n.tr("Privacy Mode") + description: I18n.tr("Hide notification content until expanded; popups show collapsed by default") + checked: SettingsData.notificationPopupPrivacyMode + onToggled: checked => SettingsData.set("notificationPopupPrivacyMode", checked) + } + + SettingsToggleRow { + settingKey: "notificationFocusedMonitor" + tags: ["notification", "popup", "focused", "monitor", "display", "screen", "active"] + text: I18n.tr("Focused Monitor Only") + description: I18n.tr("Show notification popups only on the currently focused monitor") + checked: SettingsData.notificationFocusedMonitor + onToggled: checked => SettingsData.set("notificationFocusedMonitor", checked) + } + + Item { + width: parent.width + height: notificationAnimationColumn.implicitHeight + Theme.spacingM * 2 + + Column { + id: notificationAnimationColumn + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + anchors.top: parent.top + anchors.topMargin: Theme.spacingM + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Animation Speed") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + } + + StyledText { + text: I18n.tr("Control animation duration for notification popups and history") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + + DankButtonGroup { + id: notificationSpeedGroup + anchors.horizontalCenter: parent.horizontalCenter + buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingM + minButtonWidth: parent.width < 480 ? 44 : 56 + textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium + model: [I18n.tr("None"), I18n.tr("Short"), I18n.tr("Medium"), I18n.tr("Long"), I18n.tr("Custom")] + selectionMode: "single" + currentIndex: SettingsData.notificationAnimationSpeed + onSelectionChanged: (index, selected) => { + if (!selected) + return; + SettingsData.set("notificationAnimationSpeed", index); + } + + Connections { + target: SettingsData + function onNotificationAnimationSpeedChanged() { + notificationSpeedGroup.currentIndex = SettingsData.notificationAnimationSpeed; + } + } + } + + SettingsSliderRow { + id: animationDurationSlider + settingKey: "notificationCustomAnimationDuration" + tags: ["notification", "animation", "duration", "custom", "speed"] + text: I18n.tr("Duration") + description: I18n.tr("Base duration for animations (drag to use Custom)") + minimum: 100 + maximum: 800 + value: Theme.notificationAnimationBaseDuration + unit: "ms" + defaultValue: 400 + onSliderValueChanged: newValue => { + if (SettingsData.notificationAnimationSpeed !== SettingsData.AnimationSpeed.Custom) { + SettingsData.set("notificationAnimationSpeed", SettingsData.AnimationSpeed.Custom); + } + SettingsData.set("notificationCustomAnimationDuration", newValue); + } + + Connections { + target: Theme + function onNotificationAnimationBaseDurationChanged() { + animationDurationSlider.value = Theme.notificationAnimationBaseDuration; + } + } + } + } + } + } + + SettingsCard { + width: parent.width + iconName: "notifications_off" + title: I18n.tr("Do Not Disturb") + settingKey: "doNotDisturb" + + SettingsToggleRow { + settingKey: "doNotDisturb" + tags: ["notification", "dnd", "mute", "silent", "suppress"] + text: I18n.tr("Enable Do Not Disturb") + description: I18n.tr("Suppress notification popups while enabled") + checked: SessionData.doNotDisturb + onToggled: checked => SessionData.setDoNotDisturb(checked) + } + } + + SettingsCard { + id: notificationRulesCard + width: parent.width + iconName: "rule_settings" + title: I18n.tr("Notification Rules") + settingKey: "notificationRules" + tags: ["notification", "rules", "mute", "ignore", "priority", "regex", "history"] + collapsible: true + expanded: false + + headerActions: [ + DankActionButton { + buttonSize: 36 + iconName: "restart_alt" + iconSize: 20 + visible: JSON.stringify(SettingsData.notificationRules) !== JSON.stringify(SettingsData.getDefaultNotificationRules()) + backgroundColor: Theme.surfaceContainer + iconColor: Theme.surfaceVariantText + onClicked: SettingsData.resetNotificationRules() + }, + DankActionButton { + buttonSize: 36 + iconName: "add" + iconSize: 20 + backgroundColor: Theme.surfaceContainer + iconColor: Theme.primary + onClicked: { + SettingsData.addNotificationRule(); + notificationRulesCard.userToggledCollapse = true; + notificationRulesCard.expanded = true; + } + } + ] + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Create rules to mute, ignore, hide from history, or override notification priority. Default only overrides priority; notifications still show normally.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + bottomPadding: Theme.spacingS + } + + Repeater { + model: SettingsData.notificationRules + + delegate: Rectangle { + id: ruleItem + width: parent.width + height: ruleColumn.implicitHeight + Theme.spacingM + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainer, 0.5) + + Column { + id: ruleColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingS + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + id: ruleLabel + text: I18n.tr("Rule") + " " + (index + 1) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: Math.max(0, parent.width - ruleLabel.implicitWidth - enableToggle.width - deleteBtn.width - Theme.spacingS * 3) + height: 1 + } + + DankToggle { + id: enableToggle + width: 40 + height: 24 + hideText: true + checked: modelData.enabled !== false + onToggled: checked => SettingsData.updateNotificationRuleField(index, "enabled", checked) + } + + Item { + id: deleteBtn + width: 28 + height: 28 + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: deleteArea.containsMouse ? Theme.withAlpha(Theme.error, 0.2) : "transparent" + } + + DankIcon { + anchors.centerIn: parent + name: "delete" + size: 18 + color: deleteArea.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: deleteArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: SettingsData.removeNotificationRule(index) + } + } + } + + Column { + width: parent.width + spacing: 2 + + StyledText { + text: I18n.tr("Pattern") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + } + + DankTextField { + width: parent.width + text: modelData.pattern || "" + font.pixelSize: Theme.fontSizeSmall + placeholderText: I18n.tr("Pattern") + onEditingFinished: SettingsData.updateNotificationRuleField(index, "pattern", text) + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + Column { + width: (parent.width - Theme.spacingS * 3) / 4 + spacing: 2 + + StyledText { + text: I18n.tr("Field") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + } + + DankDropdown { + width: parent.width + compactMode: true + dropdownWidth: parent.width + popupWidth: 165 + currentValue: root.getRuleOptionLabel(root.notificationRuleFieldOptions, modelData.field, root.notificationRuleFieldOptions[0].label) + options: root.notificationRuleFieldOptions.map(o => o.label) + onValueChanged: value => SettingsData.updateNotificationRuleField(index, "field", root.getRuleOptionValue(root.notificationRuleFieldOptions, value, "appName")) + } + } + + Column { + width: (parent.width - Theme.spacingS * 3) / 4 + spacing: 2 + + StyledText { + text: I18n.tr("Type") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + } + + DankDropdown { + width: parent.width + compactMode: true + dropdownWidth: parent.width + currentValue: root.getRuleOptionLabel(root.notificationRuleMatchTypeOptions, modelData.matchType, root.notificationRuleMatchTypeOptions[0].label) + options: root.notificationRuleMatchTypeOptions.map(o => o.label) + onValueChanged: value => SettingsData.updateNotificationRuleField(index, "matchType", root.getRuleOptionValue(root.notificationRuleMatchTypeOptions, value, "contains")) + } + } + + Column { + width: (parent.width - Theme.spacingS * 3) / 4 + spacing: 2 + + StyledText { + text: I18n.tr("Action") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + } + + DankDropdown { + width: parent.width + compactMode: true + dropdownWidth: parent.width + popupWidth: 170 + currentValue: root.getRuleOptionLabel(root.notificationRuleActionOptions, modelData.action, root.notificationRuleActionOptions[0].label) + options: root.notificationRuleActionOptions.map(o => o.label) + onValueChanged: value => SettingsData.updateNotificationRuleField(index, "action", root.getRuleOptionValue(root.notificationRuleActionOptions, value, "default")) + } + } + + Column { + width: (parent.width - Theme.spacingS * 3) / 4 + spacing: 2 + + StyledText { + text: I18n.tr("Priority") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + } + + DankDropdown { + width: parent.width + compactMode: true + dropdownWidth: parent.width + popupWidth: 165 + currentValue: root.getRuleOptionLabel(root.notificationRuleUrgencyOptions, modelData.urgency, root.notificationRuleUrgencyOptions[0].label) + options: root.notificationRuleUrgencyOptions.map(o => o.label) + onValueChanged: value => SettingsData.updateNotificationRuleField(index, "urgency", root.getRuleOptionValue(root.notificationRuleUrgencyOptions, value, "default")) + } + } + } + } + } + } + } + } + + SettingsCard { + width: parent.width + iconName: "volume_off" + title: I18n.tr("Muted Apps") + settingKey: "mutedApps" + tags: ["notification", "mute", "unmute", "popup"] + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: mutedRules.length > 0 ? I18n.tr("Apps with notification popups muted. Unmute or delete to remove.") : I18n.tr("No apps muted. Right-click a notification and choose \"Mute popups\" to add one here.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + bottomPadding: Theme.spacingS + } + + Repeater { + model: mutedRules + + delegate: Rectangle { + width: parent.width + height: mutedRow.implicitHeight + Theme.spacingS * 2 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainer, 0.5) + + Row { + id: mutedRow + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingM + + StyledText { + id: mutedAppLabel + text: (modelData.rule && modelData.rule.pattern) ? modelData.rule.pattern : I18n.tr("Unknown") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: Math.max(0, parent.width - parent.spacing - mutedAppLabel.width - unmuteBtn.width - deleteBtn.width - Theme.spacingS * 5) + height: 1 + } + + DankButton { + id: unmuteBtn + text: I18n.tr("Unmute") + backgroundColor: Theme.surfaceContainer + textColor: Theme.primary + onClicked: SettingsData.removeNotificationRule(modelData.index) + } + + Item { + id: deleteBtn + width: 28 + height: 28 + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: deleteArea.containsMouse ? Theme.withAlpha(Theme.error, 0.2) : "transparent" + } + + DankIcon { + anchors.centerIn: parent + name: "delete" + size: 18 + color: deleteArea.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: deleteArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: SettingsData.removeNotificationRule(modelData.index) + } + } + } + } + } + } + } + + SettingsCard { + width: parent.width + iconName: "lock" + title: I18n.tr("Lock Screen", "lock screen notifications settings card") + settingKey: "lockScreenNotifications" + + SettingsDropdownRow { + settingKey: "lockScreenNotificationMode" + tags: ["lock", "screen", "notification", "notifications", "privacy"] + text: I18n.tr("Notification Display", "lock screen notification privacy setting") + description: I18n.tr("Control what notification information is shown on the lock screen", "lock screen notification privacy setting") + options: [I18n.tr("Disabled", "lock screen notification mode option"), I18n.tr("Count Only", "lock screen notification mode option"), I18n.tr("App Names", "lock screen notification mode option"), I18n.tr("Full Content", "lock screen notification mode option")] + currentValue: options[SettingsData.lockScreenNotificationMode] || options[0] + onValueChanged: value => { + const idx = options.indexOf(value); + if (idx >= 0) { + SettingsData.set("lockScreenNotificationMode", idx); + } + } + } + } + + SettingsCard { + width: parent.width + iconName: "timer" + title: I18n.tr("Notification Timeouts") + settingKey: "notificationTimeouts" + collapsible: true + expanded: false + + SettingsDropdownRow { + settingKey: "notificationTimeoutLow" + tags: ["notification", "timeout", "low", "priority", "duration"] + text: I18n.tr("Low Priority") + description: I18n.tr("Timeout for low priority notifications") + currentValue: root.getTimeoutText(SettingsData.notificationTimeoutLow) + options: root.timeoutOptions.map(opt => opt.text) + onValueChanged: value => { + for (let i = 0; i < root.timeoutOptions.length; i++) { + if (root.timeoutOptions[i].text === value) { + SettingsData.set("notificationTimeoutLow", root.timeoutOptions[i].value); + break; + } + } + } + } + + SettingsDropdownRow { + settingKey: "notificationTimeoutNormal" + tags: ["notification", "timeout", "normal", "priority", "duration"] + text: I18n.tr("Normal Priority") + description: I18n.tr("Timeout for normal priority notifications") + currentValue: root.getTimeoutText(SettingsData.notificationTimeoutNormal) + options: root.timeoutOptions.map(opt => opt.text) + onValueChanged: value => { + for (let i = 0; i < root.timeoutOptions.length; i++) { + if (root.timeoutOptions[i].text === value) { + SettingsData.set("notificationTimeoutNormal", root.timeoutOptions[i].value); + break; + } + } + } + } + + SettingsDropdownRow { + settingKey: "notificationTimeoutCritical" + tags: ["notification", "timeout", "critical", "priority", "duration"] + text: I18n.tr("Critical Priority") + description: I18n.tr("Timeout for critical priority notifications") + currentValue: root.getTimeoutText(SettingsData.notificationTimeoutCritical) + options: root.timeoutOptions.map(opt => opt.text) + onValueChanged: value => { + for (let i = 0; i < root.timeoutOptions.length; i++) { + if (root.timeoutOptions[i].text === value) { + SettingsData.set("notificationTimeoutCritical", root.timeoutOptions[i].value); + break; + } + } + } + } + } + + SettingsCard { + width: parent.width + iconName: "history" + title: I18n.tr("History Settings") + settingKey: "notificationHistory" + + SettingsToggleRow { + settingKey: "notificationHistoryEnabled" + tags: ["notification", "history", "enable", "disable", "save"] + text: I18n.tr("Enable History", "notification history toggle label") + description: I18n.tr("Save dismissed notifications to history", "notification history toggle description") + checked: SettingsData.notificationHistoryEnabled + onToggled: checked => SettingsData.set("notificationHistoryEnabled", checked) + } + + SettingsSliderRow { + settingKey: "notificationHistoryMaxCount" + tags: ["notification", "history", "max", "count", "limit"] + text: I18n.tr("Maximum History") + description: I18n.tr("Maximum number of notifications to keep", "notification history limit") + value: SettingsData.notificationHistoryMaxCount + minimum: 10 + maximum: 200 + step: 10 + unit: "" + defaultValue: 50 + onSliderValueChanged: newValue => SettingsData.set("notificationHistoryMaxCount", newValue) + } + + SettingsDropdownRow { + settingKey: "notificationHistoryMaxAgeDays" + tags: ["notification", "history", "max", "age", "days", "retention"] + text: I18n.tr("History Retention", "notification history retention settings label") + description: I18n.tr("Auto-delete notifications older than this", "notification history setting") + currentValue: { + switch (SettingsData.notificationHistoryMaxAgeDays) { + case 0: + return I18n.tr("Forever", "notification history retention option"); + case 1: + return I18n.tr("1 day", "notification history retention option"); + case 3: + return I18n.tr("3 days", "notification history retention option"); + case 7: + return I18n.tr("7 days", "notification history retention option"); + case 14: + return I18n.tr("14 days", "notification history retention option"); + case 30: + return I18n.tr("30 days", "notification history retention option"); + default: + return SettingsData.notificationHistoryMaxAgeDays + " " + I18n.tr("days"); + } + } + options: [I18n.tr("Forever", "notification history retention option"), I18n.tr("1 day", "notification history retention option"), I18n.tr("3 days", "notification history retention option"), I18n.tr("7 days", "notification history retention option"), I18n.tr("14 days", "notification history retention option"), I18n.tr("30 days", "notification history retention option")] + onValueChanged: value => { + let days = 7; + if (value === I18n.tr("Forever", "notification history retention option")) + days = 0; + else if (value === I18n.tr("1 day", "notification history retention option")) + days = 1; + else if (value === I18n.tr("3 days", "notification history retention option")) + days = 3; + else if (value === I18n.tr("7 days", "notification history retention option")) + days = 7; + else if (value === I18n.tr("14 days", "notification history retention option")) + days = 14; + else if (value === I18n.tr("30 days", "notification history retention option")) + days = 30; + SettingsData.set("notificationHistoryMaxAgeDays", days); + } + } + + SettingsToggleRow { + settingKey: "notificationHistorySaveLow" + tags: ["notification", "history", "save", "low", "priority"] + text: I18n.tr("Low Priority") + description: I18n.tr("Save low priority notifications to history", "notification history setting") + checked: SettingsData.notificationHistorySaveLow + onToggled: checked => SettingsData.set("notificationHistorySaveLow", checked) + } + + SettingsToggleRow { + settingKey: "notificationHistorySaveNormal" + tags: ["notification", "history", "save", "normal", "priority"] + text: I18n.tr("Normal Priority") + description: I18n.tr("Save normal priority notifications to history", "notification history setting") + checked: SettingsData.notificationHistorySaveNormal + onToggled: checked => SettingsData.set("notificationHistorySaveNormal", checked) + } + + SettingsToggleRow { + settingKey: "notificationHistorySaveCritical" + tags: ["notification", "history", "save", "critical", "priority"] + text: I18n.tr("Critical Priority") + description: I18n.tr("Save critical priority notifications to history", "notification history setting") + checked: SettingsData.notificationHistorySaveCritical + onToggled: checked => SettingsData.set("notificationHistorySaveCritical", checked) + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/OSDTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/OSDTab.qml new file mode 100644 index 0000000..4bf26d8 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/OSDTab.qml @@ -0,0 +1,165 @@ +import QtQuick +import qs.Common +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + width: parent.width + iconName: "tune" + title: I18n.tr("On-screen Displays") + settingKey: "osd" + + SettingsDropdownRow { + settingKey: "osdPosition" + text: I18n.tr("OSD Position") + description: I18n.tr("Choose where on-screen displays appear on screen") + currentValue: { + switch (SettingsData.osdPosition) { + case SettingsData.Position.Top: + return I18n.tr("Top Right", "screen position option"); + case SettingsData.Position.Left: + return I18n.tr("Top Left", "screen position option"); + case SettingsData.Position.TopCenter: + return I18n.tr("Top Center", "screen position option"); + case SettingsData.Position.Right: + return I18n.tr("Bottom Right", "screen position option"); + case SettingsData.Position.Bottom: + return I18n.tr("Bottom Left", "screen position option"); + case SettingsData.Position.BottomCenter: + return I18n.tr("Bottom Center", "screen position option"); + case SettingsData.Position.LeftCenter: + return I18n.tr("Left Center", "screen position option"); + case SettingsData.Position.RightCenter: + return I18n.tr("Right Center", "screen position option"); + default: + return I18n.tr("Bottom Center", "screen position option"); + } + } + options: [I18n.tr("Top Right", "screen position option"), I18n.tr("Top Left", "screen position option"), I18n.tr("Top Center", "screen position option"), I18n.tr("Bottom Right", "screen position option"), I18n.tr("Bottom Left", "screen position option"), I18n.tr("Bottom Center", "screen position option"), I18n.tr("Left Center", "screen position option"), I18n.tr("Right Center", "screen position option")] + onValueChanged: value => { + if (value === I18n.tr("Top Right", "screen position option")) { + SettingsData.set("osdPosition", SettingsData.Position.Top); + } else if (value === I18n.tr("Top Left", "screen position option")) { + SettingsData.set("osdPosition", SettingsData.Position.Left); + } else if (value === I18n.tr("Top Center", "screen position option")) { + SettingsData.set("osdPosition", SettingsData.Position.TopCenter); + } else if (value === I18n.tr("Bottom Right", "screen position option")) { + SettingsData.set("osdPosition", SettingsData.Position.Right); + } else if (value === I18n.tr("Bottom Left", "screen position option")) { + SettingsData.set("osdPosition", SettingsData.Position.Bottom); + } else if (value === I18n.tr("Bottom Center", "screen position option")) { + SettingsData.set("osdPosition", SettingsData.Position.BottomCenter); + } else if (value === I18n.tr("Left Center", "screen position option")) { + SettingsData.set("osdPosition", SettingsData.Position.LeftCenter); + } else if (value === I18n.tr("Right Center", "screen position option")) { + SettingsData.set("osdPosition", SettingsData.Position.RightCenter); + } + } + } + + SettingsToggleRow { + settingKey: "osdAlwaysShowValue" + text: I18n.tr("Always Show Percentage") + description: I18n.tr("Display volume and brightness percentage values in OSD popups") + checked: SettingsData.osdAlwaysShowValue + onToggled: checked => SettingsData.set("osdAlwaysShowValue", checked) + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsToggleRow { + settingKey: "osdVolumeEnabled" + text: I18n.tr("Volume") + description: I18n.tr("Show on-screen display when volume changes") + checked: SettingsData.osdVolumeEnabled + onToggled: checked => SettingsData.set("osdVolumeEnabled", checked) + } + + SettingsToggleRow { + settingKey: "osdMediaVolumeEnabled" + text: I18n.tr("Media Volume") + description: I18n.tr("Show on-screen display when media player volume changes") + checked: SettingsData.osdMediaVolumeEnabled + onToggled: checked => SettingsData.set("osdMediaVolumeEnabled", checked) + } + + SettingsToggleRow { + settingKey: "osdMediaPlaybackEnabled" + text: I18n.tr("Media Playback") + description: I18n.tr("Show on-screen display when media player status changes") + checked: SettingsData.osdMediaPlaybackEnabled + onToggled: checked => SettingsData.set("osdMediaPlaybackEnabled", checked) + } + + SettingsToggleRow { + settingKey: "osdBrightnessEnabled" + text: I18n.tr("Brightness") + description: I18n.tr("Show on-screen display when brightness changes") + checked: SettingsData.osdBrightnessEnabled + onToggled: checked => SettingsData.set("osdBrightnessEnabled", checked) + } + + SettingsToggleRow { + settingKey: "osdIdleInhibitorEnabled" + text: I18n.tr("Idle Inhibitor") + description: I18n.tr("Show on-screen display when idle inhibitor state changes") + checked: SettingsData.osdIdleInhibitorEnabled + onToggled: checked => SettingsData.set("osdIdleInhibitorEnabled", checked) + } + + SettingsToggleRow { + settingKey: "osdMicMuteEnabled" + text: I18n.tr("Microphone Mute") + description: I18n.tr("Show on-screen display when microphone is muted/unmuted") + checked: SettingsData.osdMicMuteEnabled + onToggled: checked => SettingsData.set("osdMicMuteEnabled", checked) + } + + SettingsToggleRow { + settingKey: "osdCapsLockEnabled" + text: I18n.tr("Caps Lock") + description: I18n.tr("Show on-screen display when caps lock state changes") + checked: SettingsData.osdCapsLockEnabled + onToggled: checked => SettingsData.set("osdCapsLockEnabled", checked) + } + + SettingsToggleRow { + settingKey: "osdPowerProfileEnabled" + text: I18n.tr("Power Profile") + description: I18n.tr("Show on-screen display when power profile changes") + checked: SettingsData.osdPowerProfileEnabled + onToggled: checked => SettingsData.set("osdPowerProfileEnabled", checked) + } + + SettingsToggleRow { + settingKey: "osdAudioOutputEnabled" + text: I18n.tr("Audio Output Switch") + description: I18n.tr("Show on-screen display when cycling audio output devices") + checked: SettingsData.osdAudioOutputEnabled + onToggled: checked => SettingsData.set("osdAudioOutputEnabled", checked) + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PluginBrowser.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PluginBrowser.qml new file mode 100644 index 0000000..b1f0a6f --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PluginBrowser.qml @@ -0,0 +1,856 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Modals.Common +import qs.Services +import qs.Widgets + +FloatingWindow { + id: root + + property bool disablePopupTransparency: true + property var allPlugins: [] + property string searchQuery: "" + property var filteredPlugins: [] + property int selectedIndex: -1 + property bool keyboardNavigationActive: false + property bool isLoading: false + property var parentModal: null + property bool pendingInstallHandled: false + property string typeFilter: "" + + function updateFilteredPlugins() { + var filtered = []; + var query = searchQuery ? searchQuery.toLowerCase() : ""; + + for (var i = 0; i < allPlugins.length; i++) { + var plugin = allPlugins[i]; + var isFirstParty = plugin.firstParty || false; + + if (!SessionData.showThirdPartyPlugins && !isFirstParty) + continue; + if (typeFilter !== "") { + var hasCapability = plugin.capabilities && plugin.capabilities.includes(typeFilter); + if (!hasCapability) + continue; + } + + if (query.length === 0) { + filtered.push(plugin); + continue; + } + + var name = plugin.name ? plugin.name.toLowerCase() : ""; + var description = plugin.description ? plugin.description.toLowerCase() : ""; + var author = plugin.author ? plugin.author.toLowerCase() : ""; + + if (name.indexOf(query) !== -1 || description.indexOf(query) !== -1 || author.indexOf(query) !== -1) + filtered.push(plugin); + } + + filtered.sort((a, b) => { + if (a.featured !== b.featured) + return a.featured ? -1 : 1; + if (a.firstParty !== b.firstParty) + return a.firstParty ? -1 : 1; + return 0; + }); + + filteredPlugins = filtered; + selectedIndex = -1; + keyboardNavigationActive = false; + } + + function selectNext() { + if (filteredPlugins.length === 0) + return; + keyboardNavigationActive = true; + selectedIndex = Math.min(selectedIndex + 1, filteredPlugins.length - 1); + } + + function selectPrevious() { + if (filteredPlugins.length === 0) + return; + keyboardNavigationActive = true; + selectedIndex = Math.max(selectedIndex - 1, -1); + if (selectedIndex === -1) + keyboardNavigationActive = false; + } + + function installPlugin(pluginName, enableAfterInstall) { + ToastService.showInfo(I18n.tr("Installing: %1", "installation progress").arg(pluginName)); + DMSService.install(pluginName, response => { + if (response.error) { + ToastService.showError(I18n.tr("Install failed: %1", "installation error").arg(response.error)); + return; + } + ToastService.showInfo(I18n.tr("Installed: %1", "installation success").arg(pluginName)); + PluginService.scanPlugins(); + refreshPlugins(); + if (enableAfterInstall) { + Qt.callLater(() => { + PluginService.enablePlugin(pluginName); + const plugin = PluginService.availablePlugins[pluginName]; + if (plugin?.type === "desktop") { + const defaultConfig = DesktopWidgetRegistry.getDefaultConfig(pluginName); + SettingsData.createDesktopWidgetInstance(pluginName, plugin.name || pluginName, defaultConfig); + } + hide(); + }); + } + }); + } + + function refreshPlugins() { + isLoading = true; + DMSService.listPlugins(); + if (DMSService.apiVersion >= 8) + DMSService.listInstalled(); + } + + function checkPendingInstall() { + if (!PopoutService.pendingPluginInstall || pendingInstallHandled) + return; + pendingInstallHandled = true; + var pluginId = PopoutService.pendingPluginInstall; + PopoutService.pendingPluginInstall = ""; + urlInstallConfirm.showWithOptions({ + "title": I18n.tr("Install Plugin", "plugin installation dialog title"), + "message": I18n.tr("Install plugin '%1' from the DMS registry?", "plugin installation confirmation").arg(pluginId), + "confirmText": I18n.tr("Install", "install action button"), + "cancelText": I18n.tr("Cancel"), + "onConfirm": () => installPlugin(pluginId, true), + "onCancel": () => hide() + }); + } + + function show() { + if (parentModal) + parentModal.shouldHaveFocus = false; + visible = true; + Qt.callLater(() => browserSearchField.forceActiveFocus()); + } + + function hide() { + visible = false; + if (!parentModal) + return; + parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible); + Qt.callLater(() => { + if (parentModal.modalFocusScope) + parentModal.modalFocusScope.forceActiveFocus(); + }); + } + + objectName: "pluginBrowser" + title: I18n.tr("Browse Plugins", "plugin browser window title") + minimumSize: Qt.size(450, 400) + implicitWidth: 600 + implicitHeight: 650 + color: Theme.surfaceContainer + visible: false + + onVisibleChanged: { + if (visible) { + pendingInstallHandled = false; + refreshPlugins(); + Qt.callLater(() => { + browserSearchField.forceActiveFocus(); + checkPendingInstall(); + }); + return; + } + allPlugins = []; + searchQuery = ""; + filteredPlugins = []; + selectedIndex = -1; + keyboardNavigationActive = false; + isLoading = false; + } + + Connections { + target: DMSService + + function onPluginsListReceived(plugins) { + root.isLoading = false; + root.allPlugins = plugins; + root.updateFilteredPlugins(); + } + + function onInstalledPluginsReceived(plugins) { + var pluginMap = {}; + for (var i = 0; i < plugins.length; i++) { + var plugin = plugins[i]; + if (plugin.id) + pluginMap[plugin.id] = true; + if (plugin.name) + pluginMap[plugin.name] = true; + } + var updated = root.allPlugins.map(p => { + var isInstalled = pluginMap[p.name] || pluginMap[p.id] || false; + return Object.assign({}, p, { + "installed": isInstalled + }); + }); + root.allPlugins = updated; + root.updateFilteredPlugins(); + } + } + + ConfirmModal { + id: urlInstallConfirm + } + + FocusScope { + id: browserKeyHandler + + anchors.fill: parent + focus: true + + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Escape: + root.hide(); + event.accepted = true; + return; + case Qt.Key_Down: + root.selectNext(); + event.accepted = true; + return; + case Qt.Key_Up: + root.selectPrevious(); + event.accepted = true; + return; + } + } + + Item { + id: browserContent + anchors.fill: parent + anchors.margins: Theme.spacingL + + Item { + id: headerArea + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: Math.max(headerIcon.height, headerText.height, refreshButton.height, closeButton.height) + + MouseArea { + anchors.fill: parent + onPressed: windowControls.tryStartMove() + onDoubleClicked: windowControls.tryToggleMaximize() + } + + DankIcon { + id: headerIcon + name: "store" + size: Theme.iconSize + color: Theme.primary + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: headerText + text: I18n.tr("Browse Plugins", "plugin browser header") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.left: headerIcon.right + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + } + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + DankButton { + id: thirdPartyButton + text: SessionData.showThirdPartyPlugins ? I18n.tr("Hide 3rd Party") : I18n.tr("Show 3rd Party") + iconName: SessionData.showThirdPartyPlugins ? "visibility_off" : "visibility" + height: 28 + onClicked: { + if (SessionData.showThirdPartyPlugins) { + SessionData.setShowThirdPartyPlugins(false); + root.updateFilteredPlugins(); + return; + } + thirdPartyConfirmLoader.active = true; + if (thirdPartyConfirmLoader.item) + thirdPartyConfirmLoader.item.show(); + } + } + + DankActionButton { + id: refreshButton + iconName: "refresh" + iconSize: 18 + iconColor: Theme.primary + visible: !root.isLoading + onClicked: root.refreshPlugins() + } + + DankActionButton { + visible: windowControls.supported + iconName: root.maximized ? "fullscreen_exit" : "fullscreen" + iconSize: Theme.iconSize - 2 + iconColor: Theme.outline + onClicked: windowControls.tryToggleMaximize() + } + + DankActionButton { + id: closeButton + iconName: "close" + iconSize: Theme.iconSize - 2 + iconColor: Theme.outline + onClicked: root.hide() + } + } + } + + StyledText { + id: descriptionText + anchors.left: parent.left + anchors.right: parent.right + anchors.top: headerArea.bottom + anchors.topMargin: Theme.spacingM + text: I18n.tr("Install plugins from the DMS plugin registry", "plugin browser description") + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + wrapMode: Text.WordWrap + } + + DankTextField { + id: browserSearchField + anchors.left: parent.left + anchors.right: parent.right + anchors.top: descriptionText.bottom + anchors.topMargin: Theme.spacingM + height: 48 + cornerRadius: Theme.cornerRadius + backgroundColor: Theme.surfaceContainerHigh + normalBorderColor: Theme.outlineMedium + focusedBorderColor: Theme.primary + leftIconName: "search" + leftIconSize: Theme.iconSize + leftIconColor: Theme.surfaceVariantText + leftIconFocusedColor: Theme.primary + showClearButton: true + textColor: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + placeholderText: I18n.tr("Search plugins...", "plugin search placeholder") + text: root.searchQuery + focus: true + ignoreLeftRightKeys: true + keyForwardTargets: [browserKeyHandler] + onTextEdited: { + root.searchQuery = text; + root.updateFilteredPlugins(); + } + } + + Item { + id: listArea + anchors.left: parent.left + anchors.right: parent.right + anchors.top: browserSearchField.bottom + anchors.topMargin: Theme.spacingM + anchors.bottom: parent.bottom + anchors.bottomMargin: Theme.spacingM + + Item { + anchors.fill: parent + visible: root.isLoading + + Column { + anchors.centerIn: parent + spacing: Theme.spacingM + + DankIcon { + name: "sync" + size: 48 + color: Theme.primary + anchors.horizontalCenter: parent.horizontalCenter + + RotationAnimator on rotation { + from: 0 + to: -360 + duration: 1000 + loops: Animation.Infinite + running: root.isLoading + } + } + + StyledText { + text: I18n.tr("Loading...", "loading indicator") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + DankListView { + id: pluginBrowserList + + anchors.fill: parent + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + anchors.topMargin: Theme.spacingS + anchors.bottomMargin: Theme.spacingS + spacing: Theme.spacingS + model: ScriptModel { + values: root.filteredPlugins + } + clip: true + visible: !root.isLoading + add: null + displaced: null + + ScrollBar.vertical: DankScrollbar { + id: browserScrollbar + } + + delegate: Rectangle { + width: pluginBrowserList.width + height: pluginDelegateColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex + property bool isInstalled: modelData.installed || false + property bool isFirstParty: modelData.firstParty || false + property bool isFeatured: modelData.featured || false + property bool isCompatible: PluginService.checkPluginCompatibility(modelData.requires_dms) + color: isSelected ? Theme.primarySelected : Theme.withAlpha(Theme.surfaceVariant, 0.3) + border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.outline, 0.2) + border.width: isSelected ? 2 : 1 + + Column { + id: pluginDelegateColumn + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingXS + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: modelData.icon || "extension" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - installButton.width - Theme.spacingM + spacing: 2 + + Row { + spacing: Theme.spacingXS + + StyledText { + text: modelData.name + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + height: 16 + width: featuredRow.implicitWidth + Theme.spacingXS * 2 + radius: 8 + color: Theme.withAlpha(Theme.secondary, 0.15) + border.color: Theme.withAlpha(Theme.secondary, 0.4) + border.width: 1 + visible: isFeatured + anchors.verticalCenter: parent.verticalCenter + + Row { + id: featuredRow + anchors.centerIn: parent + spacing: 2 + + DankIcon { + name: "star" + size: 10 + color: Theme.secondary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("featured") + font.pixelSize: Theme.fontSizeSmall - 2 + color: Theme.secondary + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + } + + Rectangle { + height: 16 + width: firstPartyText.implicitWidth + Theme.spacingXS * 2 + radius: 8 + color: Theme.withAlpha(Theme.primary, 0.15) + border.color: Theme.withAlpha(Theme.primary, 0.4) + border.width: 1 + visible: isFirstParty + anchors.verticalCenter: parent.verticalCenter + + StyledText { + id: firstPartyText + anchors.centerIn: parent + text: I18n.tr("official") + font.pixelSize: Theme.fontSizeSmall - 2 + color: Theme.primary + font.weight: Font.Medium + } + } + + Rectangle { + height: 16 + width: thirdPartyText.implicitWidth + Theme.spacingXS * 2 + radius: 8 + color: Theme.withAlpha(Theme.warning, 0.15) + border.color: Theme.withAlpha(Theme.warning, 0.4) + border.width: 1 + visible: !isFirstParty + anchors.verticalCenter: parent.verticalCenter + + StyledText { + id: thirdPartyText + anchors.centerIn: parent + text: I18n.tr("3rd party") + font.pixelSize: Theme.fontSizeSmall - 2 + color: Theme.warning + font.weight: Font.Medium + } + } + } + + StyledText { + text: { + const author = I18n.tr("by %1", "author attribution").arg(modelData.author || I18n.tr("Unknown", "unknown author")); + const source = modelData.repo ? ` • <a href="${modelData.repo}" style="text-decoration:none; color:${Theme.primary};">${I18n.tr("source", "source code link")}</a>` : ""; + return author + source; + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + linkColor: Theme.primary + textFormat: Text.RichText + elide: Text.ElideRight + width: parent.width + onLinkActivated: url => Qt.openUrlExternally(url) + + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + } + } + } + + Rectangle { + id: installButton + + property string buttonState: { + if (isInstalled) + return "installed"; + if (!isCompatible) + return "incompatible"; + return "available"; + } + + width: buttonState === "incompatible" ? incompatRow.implicitWidth + Theme.spacingM * 2 : 80 + height: 32 + radius: Theme.cornerRadius + anchors.verticalCenter: parent.verticalCenter + color: { + switch (buttonState) { + case "installed": + return Theme.surfaceVariant; + case "incompatible": + return Theme.withAlpha(Theme.warning, 0.15); + default: + return Theme.primary; + } + } + opacity: buttonState === "available" && installMouseArea.containsMouse ? 0.9 : 1 + border.width: buttonState !== "available" ? 1 : 0 + border.color: buttonState === "incompatible" ? Theme.warning : Theme.outline + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Row { + id: incompatRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: { + switch (installButton.buttonState) { + case "installed": + return "check"; + case "incompatible": + return "warning"; + default: + return "download"; + } + } + size: 14 + color: { + switch (installButton.buttonState) { + case "installed": + return Theme.surfaceText; + case "incompatible": + return Theme.warning; + default: + return Theme.surface; + } + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: { + switch (installButton.buttonState) { + case "installed": + return I18n.tr("Installed", "installed status"); + case "incompatible": + return I18n.tr("Requires %1", "version requirement").arg(modelData.requires_dms); + default: + return I18n.tr("Install", "install action button"); + } + } + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: { + switch (installButton.buttonState) { + case "installed": + return Theme.surfaceText; + case "incompatible": + return Theme.warning; + default: + return Theme.surface; + } + } + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: installMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: installButton.buttonState === "available" ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: installButton.buttonState === "available" + onClicked: { + const isDesktop = modelData.type === "desktop"; + root.installPlugin(modelData.name, isDesktop); + } + } + } + } + + StyledText { + text: modelData.description || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + width: parent.width + wrapMode: Text.WordWrap + visible: modelData.description && modelData.description.length > 0 + } + + Flow { + width: parent.width + spacing: Theme.spacingXS + visible: modelData.capabilities && modelData.capabilities.length > 0 + + Repeater { + model: modelData.capabilities || [] + + Rectangle { + height: 18 + width: capabilityText.implicitWidth + Theme.spacingXS * 2 + radius: 9 + color: Theme.withAlpha(Theme.primary, 0.1) + border.color: Theme.withAlpha(Theme.primary, 0.3) + border.width: 1 + + StyledText { + id: capabilityText + anchors.centerIn: parent + text: modelData + font.pixelSize: Theme.fontSizeSmall - 2 + color: Theme.primary + } + } + } + } + } + } + } + + StyledText { + anchors.centerIn: listArea + text: I18n.tr("No plugins found", "empty plugin list") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + visible: !root.isLoading && root.filteredPlugins.length === 0 + } + } + } + } + + LazyLoader { + id: thirdPartyConfirmLoader + active: false + + FloatingWindow { + id: thirdPartyConfirmModal + + property bool disablePopupTransparency: true + + function show() { + visible = true; + } + + function hide() { + visible = false; + } + + objectName: "thirdPartyConfirm" + title: I18n.tr("Third-Party Plugin Warning") + implicitWidth: 500 + implicitHeight: 350 + color: Theme.surfaceContainer + visible: false + + FocusScope { + anchors.fill: parent + focus: true + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + thirdPartyConfirmModal.hide(); + event.accepted = true; + } + } + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingL + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "warning" + size: Theme.iconSize + color: Theme.warning + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Third-Party Plugin Warning") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - parent.spacing * 2 - Theme.iconSize - parent.children[1].implicitWidth - closeConfirmBtn.width + height: 1 + } + + DankActionButton { + id: closeConfirmBtn + iconName: "close" + iconSize: Theme.iconSize - 2 + iconColor: Theme.outline + anchors.verticalCenter: parent.verticalCenter + onClicked: thirdPartyConfirmModal.hide() + } + } + + StyledText { + width: parent.width + text: I18n.tr("Third-party plugins are created by the community and are not officially supported by DankMaterialShell.\n\nThese plugins may pose security and privacy risks - install at your own risk.") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + wrapMode: Text.WordWrap + } + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("• Plugins may contain bugs or security issues") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: I18n.tr("• Review code before installation when possible") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: I18n.tr("• Install only from trusted sources") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + + Item { + width: parent.width + height: parent.height - parent.spacing * 3 - y + } + + Row { + anchors.right: parent.right + spacing: Theme.spacingM + + DankButton { + text: I18n.tr("Cancel") + iconName: "close" + onClicked: thirdPartyConfirmModal.hide() + } + + DankButton { + text: I18n.tr("I Understand") + iconName: "check" + onClicked: { + SessionData.setShowThirdPartyPlugins(true); + root.updateFilteredPlugins(); + thirdPartyConfirmModal.hide(); + } + } + } + } + } + } + } + + FloatingWindowControls { + id: windowControls + targetWindow: root + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PluginListItem.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PluginListItem.qml new file mode 100644 index 0000000..39a5d1e --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PluginListItem.qml @@ -0,0 +1,439 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +StyledRect { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property var pluginData: null + property string expandedPluginId: "" + property bool hasUpdate: false + property bool isReloading: false + property var sharedTooltip: null + + property string pluginId: pluginData ? pluginData.id : "" + property string pluginDirectoryName: { + if (pluginData && pluginData.pluginDirectory) { + var path = pluginData.pluginDirectory; + return path.substring(path.lastIndexOf('/') + 1); + } + return pluginId; + } + property string pluginName: pluginData ? (pluginData.name || pluginData.id) : "" + property string pluginVersion: pluginData ? (pluginData.version || "1.0.0") : "" + property string pluginAuthor: pluginData ? (pluginData.author || "Unknown") : "" + property string pluginDescription: pluginData ? (pluginData.description || "") : "" + property string pluginIcon: pluginData ? (pluginData.icon || "extension") : "extension" + property string pluginSettingsPath: pluginData ? (pluginData.settingsPath || "") : "" + property var pluginPermissions: pluginData ? (pluginData.permissions || []) : [] + property bool hasSettings: pluginData && pluginData.settings !== undefined && pluginData.settings !== "" + property bool isDesktopPlugin: pluginData ? (pluginData.type === "desktop") : false + property bool showSettings: hasSettings && !isDesktopPlugin + property bool isSystemPlugin: pluginData ? (pluginData.source === "system") : false + property string requiresDms: pluginData ? (pluginData.requires_dms || "") : "" + property bool meetsRequirements: requiresDms ? PluginService.checkPluginCompatibility(requiresDms) : true + + Connections { + target: SystemUpdateService + function onSemverVersionChanged() { + root.meetsRequirementsChanged(); + } + } + property bool isExpanded: expandedPluginId === pluginId + property bool isLoaded: { + PluginService.loadedPlugins; + return PluginService.loadedPlugins[pluginId] !== undefined; + } + + width: parent.width + height: pluginItemColumn.implicitHeight + Theme.spacingM * 2 + settingsContainer.height + radius: Theme.cornerRadius + color: (pluginMouseArea.containsMouse || updateArea.containsMouse || uninstallArea.containsMouse || reloadArea.containsMouse) ? Theme.surfacePressed : (isExpanded ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh) + border.width: 0 + + MouseArea { + id: pluginMouseArea + anchors.fill: parent + anchors.bottomMargin: root.isExpanded ? settingsContainer.height : 0 + hoverEnabled: true + cursorShape: root.showSettings ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: root.showSettings + onClicked: { + root.expandedPluginId = root.expandedPluginId === root.pluginId ? "" : root.pluginId; + } + } + + Column { + id: pluginItemColumn + width: parent.width + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: root.pluginIcon + size: Theme.iconSize + color: root.isLoaded ? Theme.primary : Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - toggleRow.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + Row { + spacing: Theme.spacingXS + width: parent.width + + StyledText { + text: root.pluginName + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: incompatIcon.width + Theme.spacingXS * 2 + height: 18 + radius: 9 + color: Theme.withAlpha(Theme.error, 0.15) + visible: !root.meetsRequirements + anchors.verticalCenter: parent.verticalCenter + + Row { + anchors.centerIn: parent + spacing: 2 + + DankIcon { + id: incompatIcon + name: "warning" + size: 12 + color: Theme.error + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + if (root.sharedTooltip) + root.sharedTooltip.show(I18n.tr("Requires DMS %1").arg(root.requiresDms), parent, 0, 0, "top"); + } + onExited: { + if (root.sharedTooltip) + root.sharedTooltip.hide(); + } + } + } + + DankIcon { + name: root.showSettings ? (root.isExpanded ? "expand_less" : "expand_more") : "" + size: 16 + color: root.showSettings ? Theme.primary : "transparent" + anchors.verticalCenter: parent.verticalCenter + visible: root.showSettings + } + + Rectangle { + width: desktopLabel.implicitWidth + Theme.spacingXS * 2 + height: 18 + radius: 9 + color: Theme.withAlpha(Theme.secondary, 0.15) + visible: root.isDesktopPlugin + anchors.verticalCenter: parent.verticalCenter + + StyledText { + id: desktopLabel + anchors.centerIn: parent + text: I18n.tr("Desktop Widget") + font.pixelSize: Theme.fontSizeSmall - 2 + color: Theme.secondary + } + } + } + + StyledText { + text: I18n.tr("v%1 by %2").arg(root.pluginVersion).arg(root.pluginAuthor) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + + Row { + id: toggleRow + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: updateArea.containsMouse ? Theme.surfaceContainerHighest : "transparent" + visible: DMSService.dmsAvailable && root.isLoaded && root.hasUpdate && !root.isSystemPlugin + + DankIcon { + anchors.centerIn: parent + name: "download" + size: 16 + color: updateArea.containsMouse ? Theme.primary : Theme.surfaceVariantText + } + + MouseArea { + id: updateArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + const currentPluginName = root.pluginName; + const currentPluginId = root.pluginId; + DMSService.update(currentPluginName, response => { + if (response.error) { + ToastService.showError("Update failed: " + response.error); + return; + } + ToastService.showInfo("Plugin updated: " + currentPluginName); + PluginService.forceRescanPlugin(currentPluginId); + if (DMSService.apiVersion >= 8) + DMSService.listInstalled(); + }); + } + onEntered: { + if (root.sharedTooltip) + root.sharedTooltip.show(I18n.tr("Update Plugin"), parent, 0, 0, "top"); + } + onExited: { + if (root.sharedTooltip) + root.sharedTooltip.hide(); + } + } + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: uninstallArea.containsMouse ? Theme.surfaceContainerHighest : "transparent" + visible: DMSService.dmsAvailable && !root.isSystemPlugin + + DankIcon { + anchors.centerIn: parent + name: "delete" + size: 16 + color: uninstallArea.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: uninstallArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + const currentPluginName = root.pluginName; + DMSService.uninstall(currentPluginName, response => { + if (response.error) { + ToastService.showError("Uninstall failed: " + response.error); + return; + } + ToastService.showInfo("Plugin uninstalled: " + currentPluginName); + PluginService.scanPlugins(); + if (root.isExpanded) + root.expandedPluginId = ""; + }); + } + onEntered: { + if (root.sharedTooltip) + root.sharedTooltip.show(I18n.tr("Uninstall Plugin"), parent, 0, 0, "top"); + } + onExited: { + if (root.sharedTooltip) + root.sharedTooltip.hide(); + } + } + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: reloadArea.containsMouse ? Theme.surfaceContainerHighest : "transparent" + visible: root.isLoaded + + DankIcon { + anchors.centerIn: parent + name: "refresh" + size: 16 + color: reloadArea.containsMouse ? Theme.primary : Theme.surfaceVariantText + } + + MouseArea { + id: reloadArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + const currentPluginId = root.pluginId; + const currentPluginName = root.pluginName; + root.isReloading = true; + if (PluginService.reloadPlugin(currentPluginId)) { + ToastService.showInfo("Plugin reloaded: " + currentPluginName); + return; + } + ToastService.showError("Failed to reload plugin: " + currentPluginName); + root.isReloading = false; + } + onEntered: { + if (root.sharedTooltip) + root.sharedTooltip.show(I18n.tr("Reload Plugin"), parent, 0, 0, "top"); + } + onExited: { + if (root.sharedTooltip) + root.sharedTooltip.hide(); + } + } + } + + DankToggle { + id: pluginToggle + anchors.verticalCenter: parent.verticalCenter + checked: root.isLoaded + onToggled: isChecked => { + const currentPluginId = root.pluginId; + const currentPluginName = root.pluginName; + + if (isChecked) { + if (PluginService.enablePlugin(currentPluginId)) { + ToastService.showInfo("Plugin enabled: " + currentPluginName); + return; + } + ToastService.showError("Failed to enable plugin: " + currentPluginName); + return; + } + if (PluginService.disablePlugin(currentPluginId)) { + ToastService.showInfo("Plugin disabled: " + currentPluginName); + if (root.isExpanded) + root.expandedPluginId = ""; + return; + } + ToastService.showError("Failed to disable plugin: " + currentPluginName); + } + } + } + } + + StyledText { + width: parent.width + text: root.pluginDescription + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + visible: root.pluginDescription !== "" + horizontalAlignment: Text.AlignLeft + } + + Flow { + width: parent.width + spacing: Theme.spacingXS + visible: root.pluginPermissions && Array.isArray(root.pluginPermissions) && root.pluginPermissions.length > 0 + + Repeater { + model: root.pluginPermissions + + Rectangle { + height: 20 + width: permissionText.implicitWidth + Theme.spacingXS * 2 + radius: 10 + color: Theme.withAlpha(Theme.primary, 0.1) + border.color: Theme.withAlpha(Theme.primary, 0.3) + border.width: 1 + + StyledText { + id: permissionText + anchors.centerIn: parent + text: modelData + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.primary + } + } + } + } + } + + FocusScope { + id: settingsContainer + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: root.isExpanded && root.showSettings ? (settingsLoader.item ? settingsLoader.item.implicitHeight + Theme.spacingL * 2 : 0) : 0 + clip: true + focus: root.isExpanded && root.showSettings + + Keys.onPressed: event => { + event.accepted = true; + } + + Rectangle { + anchors.fill: parent + color: Theme.surfaceContainerHighest + radius: Theme.cornerRadius + anchors.topMargin: Theme.spacingXS + border.width: 0 + } + + Loader { + id: settingsLoader + anchors.fill: parent + anchors.margins: Theme.spacingL + active: root.isExpanded && root.showSettings && root.isLoaded + asynchronous: false + + source: { + if (active && root.pluginSettingsPath) { + var path = root.pluginSettingsPath; + if (!path.startsWith("file://")) { + path = "file://" + path; + } + return path; + } + return ""; + } + + onLoaded: { + if (item && typeof PluginService !== "undefined") { + item.pluginService = PluginService; + } + if (item && typeof PopoutService !== "undefined" && "popoutService" in item) { + item.popoutService = PopoutService; + } + if (item) { + Qt.callLater(() => { + settingsContainer.focus = true; + item.forceActiveFocus(); + }); + } + } + } + + StyledText { + anchors.centerIn: parent + text: !root.isLoaded ? "Enable plugin to access settings" : (settingsLoader.status === Loader.Error ? "Failed to load settings" : "No configurable settings") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: root.isExpanded && (!settingsLoader.active || settingsLoader.status === Loader.Error) + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PluginsTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PluginsTab.qml new file mode 100644 index 0000000..1f83a0f --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PluginsTab.qml @@ -0,0 +1,450 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +FocusScope { + id: pluginsTab + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property string expandedPluginId: "" + property bool isRefreshingPlugins: false + property var parentModal: null + property var installedPluginsData: ({}) + property bool isReloading: false + property alias sharedTooltip: sharedTooltip + + focus: true + + DankTooltipV2 { + id: sharedTooltip + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + StyledRect { + width: parent.width + height: headerColumn.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.width: 0 + + Column { + id: headerColumn + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "extension" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + width: parent.width - Theme.iconSize - Theme.spacingM + + StyledText { + text: I18n.tr("Plugin Management") + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: I18n.tr("Manage and configure plugins for extending DMS functionality") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + } + + StyledRect { + width: parent.width + height: dmsWarningColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.warning, 0.1) + border.color: Theme.warning + border.width: 1 + visible: !DMSService.dmsAvailable + + Column { + id: dmsWarningColumn + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingXS + + Row { + spacing: Theme.spacingXS + + DankIcon { + name: "warning" + size: 16 + color: Theme.warning + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("DMS Plugin Manager Unavailable") + font.pixelSize: Theme.fontSizeSmall + color: Theme.warning + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + StyledText { + text: I18n.tr("The DMS_SOCKET environment variable is not set or the socket is unavailable. Automated plugin management requires the DMS_SOCKET.") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + } + + StyledRect { + id: incompatWarning + property var incompatPlugins: [] + width: parent.width + height: incompatWarningColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.error, 0.1) + border.color: Theme.error + border.width: 1 + visible: incompatPlugins.length > 0 + + function refresh() { + incompatPlugins = PluginService.getIncompatiblePlugins(); + } + + Component.onCompleted: Qt.callLater(refresh) + + Column { + id: incompatWarningColumn + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingXS + + Row { + spacing: Theme.spacingXS + + DankIcon { + name: "error" + size: 16 + color: Theme.error + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Incompatible Plugins Loaded") + font.pixelSize: Theme.fontSizeSmall + color: Theme.error + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + StyledText { + text: I18n.tr("Some plugins require a newer version of DMS:") + " " + incompatWarning.incompatPlugins.map(p => p.name + " (" + p.requires_dms + ")").join(", ") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + + Connections { + target: PluginService + function onPluginLoaded() { + incompatWarning.refresh(); + } + function onPluginUnloaded() { + incompatWarning.refresh(); + } + } + + Connections { + target: SystemUpdateService + function onSemverVersionChanged() { + incompatWarning.refresh(); + } + } + } + + Flow { + width: parent.width + spacing: Theme.spacingM + + DankButton { + text: I18n.tr("Browse") + iconName: "store" + enabled: DMSService.dmsAvailable + onClicked: { + showPluginBrowser(); + } + } + + DankButton { + text: I18n.tr("Scan") + iconName: "refresh" + onClicked: { + pluginsTab.isRefreshingPlugins = true; + PluginService.scanPlugins(); + if (DMSService.dmsAvailable) { + DMSService.listInstalled(); + } + pluginsTab.refreshPluginList(); + } + } + + DankButton { + text: I18n.tr("Create Dir") + iconName: "create_new_folder" + onClicked: { + PluginService.createPluginDirectory(); + ToastService.showInfo("Created plugin directory: " + PluginService.pluginDirectory); + } + } + } + } + } + + StyledRect { + width: parent.width + height: directoryColumn.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.width: 0 + + Column { + id: directoryColumn + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Plugin Directory") + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: PluginService.pluginDirectory + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + font.family: "monospace" + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: I18n.tr("Place plugin directories here. Each plugin should have a plugin.json manifest file.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + } + + StyledRect { + width: parent.width + height: Math.max(200, availableColumn.implicitHeight + Theme.spacingL * 2) + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.width: 0 + + Column { + id: availableColumn + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Available Plugins") + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Column { + width: parent.width + spacing: Theme.spacingM + + Repeater { + id: pluginRepeater + model: PluginService.availablePluginsList + + PluginListItem { + pluginData: modelData + expandedPluginId: pluginsTab.expandedPluginId + hasUpdate: { + if (DMSService.apiVersion < 8) + return false; + return pluginsTab.installedPluginsData[pluginId] || pluginsTab.installedPluginsData[pluginName] || false; + } + isReloading: pluginsTab.isReloading + sharedTooltip: pluginsTab.sharedTooltip + onExpandedPluginIdChanged: { + pluginsTab.expandedPluginId = expandedPluginId; + } + onIsReloadingChanged: { + pluginsTab.isReloading = isReloading; + } + } + } + + StyledText { + width: parent.width + text: I18n.tr("No plugins found.") + "\n" + I18n.tr("Place plugins in %1").arg(PluginService.pluginDirectory) + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + visible: pluginRepeater.model && pluginRepeater.model.length === 0 + } + } + } + } + } + } + + function refreshPluginList() { + pluginsTab.isRefreshingPlugins = false; + } + + Connections { + target: PluginService + function onPluginLoaded() { + refreshPluginList(); + if (isReloading) { + isReloading = false; + } + } + function onPluginUnloaded() { + refreshPluginList(); + if (!isReloading && pluginsTab.expandedPluginId !== "" && !PluginService.isPluginLoaded(pluginsTab.expandedPluginId)) { + pluginsTab.expandedPluginId = ""; + } + } + function onPluginListUpdated() { + if (DMSService.apiVersion >= 8) { + DMSService.listInstalled(); + } + refreshPluginList(); + } + function onPluginDataChanged(pluginId) { + var plugin = PluginService.availablePlugins[pluginId]; + if (!plugin || !PluginService.isPluginLoaded(pluginId)) + return; + var isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher")); + if (isLauncher) { + pluginsTab.isReloading = true; + PluginService.reloadPlugin(pluginId); + } + } + } + + Connections { + target: DMSService + function onPluginsListReceived(plugins) { + if (!pluginBrowserLoader.item) + return; + pluginBrowserLoader.item.isLoading = false; + pluginBrowserLoader.item.allPlugins = plugins; + pluginBrowserLoader.item.updateFilteredPlugins(); + } + function onInstalledPluginsReceived(plugins) { + var pluginMap = {}; + for (var i = 0; i < plugins.length; i++) { + var plugin = plugins[i]; + var hasUpdate = plugin.hasUpdate || false; + if (plugin.id) { + pluginMap[plugin.id] = hasUpdate; + } + if (plugin.name) { + pluginMap[plugin.name] = hasUpdate; + } + } + installedPluginsData = pluginMap; + Qt.callLater(refreshPluginList); + } + function onOperationSuccess(message) { + ToastService.showInfo(message); + } + function onOperationError(error) { + ToastService.showError(error); + } + } + + Component.onCompleted: { + if (DMSService.dmsAvailable && DMSService.apiVersion >= 8) + DMSService.listInstalled(); + if (PopoutService.pendingPluginInstall) + Qt.callLater(showPluginBrowser); + } + + Connections { + target: PopoutService + function onPendingPluginInstallChanged() { + if (PopoutService.pendingPluginInstall) + showPluginBrowser(); + } + } + + LazyLoader { + id: pluginBrowserLoader + active: false + + PluginBrowser { + id: pluginBrowserItem + + Component.onCompleted: { + pluginBrowserItem.parentModal = pluginsTab.parentModal; + } + } + } + + function showPluginBrowser() { + pluginBrowserLoader.active = true; + if (pluginBrowserLoader.item) + pluginBrowserLoader.item.show(); + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PowerSleepTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PowerSleepTab.qml new file mode 100644 index 0000000..7282d34 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PowerSleepTab.qml @@ -0,0 +1,598 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + readonly property var timeoutOptions: [I18n.tr("Never"), I18n.tr("1 minute"), I18n.tr("2 minutes"), I18n.tr("3 minutes"), I18n.tr("5 minutes"), I18n.tr("10 minutes"), I18n.tr("15 minutes"), I18n.tr("20 minutes"), I18n.tr("30 minutes"), I18n.tr("1 hour"), I18n.tr("1 hour 30 minutes"), I18n.tr("2 hours"), I18n.tr("3 hours")] + readonly property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800] + + function getTimeoutIndex(timeout) { + var idx = timeoutValues.indexOf(timeout); + return idx >= 0 ? idx : 0; + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + width: parent.width + iconName: "schedule" + title: I18n.tr("Idle Settings") + settingKey: "idleSettings" + + Row { + width: parent.width + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Power source") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + visible: BatteryService.batteryAvailable + } + + Item { + width: Theme.spacingS + height: 1 + visible: BatteryService.batteryAvailable + } + + DankButtonGroup { + id: powerCategory + anchors.verticalCenter: parent.verticalCenter + visible: BatteryService.batteryAvailable + model: [I18n.tr("AC Power"), I18n.tr("Battery")] + currentIndex: 0 + selectionMode: "single" + checkEnabled: false + onSelectionChanged: (index, selected) => { + if (!selected) + return; + currentIndex = index; + } + } + } + + SettingsToggleRow { + settingKey: "fadeToLockEnabled" + tags: ["fade", "lock", "screen", "idle", "grace period"] + text: I18n.tr("Fade to lock screen") + description: I18n.tr("Gradually fade the screen before locking with a configurable grace period") + checked: SettingsData.fadeToLockEnabled + onToggled: checked => SettingsData.set("fadeToLockEnabled", checked) + } + + SettingsToggleRow { + settingKey: "fadeToDpmsEnabled" + tags: ["fade", "dpms", "monitor", "screen", "idle", "grace period"] + text: I18n.tr("Fade to monitor off") + description: I18n.tr("Gradually fade the screen before turning off monitors with a configurable grace period") + checked: SettingsData.fadeToDpmsEnabled + onToggled: checked => SettingsData.set("fadeToDpmsEnabled", checked) + } + + SettingsToggleRow { + settingKey: "lockBeforeSuspend" + tags: ["lock", "suspend", "sleep", "security"] + text: I18n.tr("Lock before suspend") + description: I18n.tr("Automatically lock the screen when the system prepares to suspend") + checked: SettingsData.lockBeforeSuspend + visible: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration + onToggled: checked => SettingsData.set("lockBeforeSuspend", checked) + } + + SettingsDropdownRow { + id: fadeGracePeriodDropdown + settingKey: "fadeToLockGracePeriod" + tags: ["fade", "grace", "period", "timeout", "lock"] + property var periodOptions: [I18n.tr("1 second"), I18n.tr("2 seconds"), I18n.tr("3 seconds"), I18n.tr("4 seconds"), I18n.tr("5 seconds"), I18n.tr("10 seconds"), I18n.tr("15 seconds"), I18n.tr("20 seconds"), I18n.tr("30 seconds")] + property var periodValues: [1, 2, 3, 4, 5, 10, 15, 20, 30] + + text: I18n.tr("Lock fade grace period") + options: periodOptions + visible: SettingsData.fadeToLockEnabled + enabled: SettingsData.fadeToLockEnabled + + Component.onCompleted: { + const currentPeriod = SettingsData.fadeToLockGracePeriod; + const index = periodValues.indexOf(currentPeriod); + currentValue = index >= 0 ? periodOptions[index] : I18n.tr("5 seconds"); + } + + onValueChanged: value => { + const index = periodOptions.indexOf(value); + if (index < 0) + return; + SettingsData.set("fadeToLockGracePeriod", periodValues[index]); + } + } + + SettingsDropdownRow { + id: fadeDpmsGracePeriodDropdown + settingKey: "fadeToDpmsGracePeriod" + tags: ["fade", "grace", "period", "timeout", "dpms", "monitor"] + property var periodOptions: [I18n.tr("1 second"), I18n.tr("2 seconds"), I18n.tr("3 seconds"), I18n.tr("4 seconds"), I18n.tr("5 seconds"), I18n.tr("10 seconds"), I18n.tr("15 seconds"), I18n.tr("20 seconds"), I18n.tr("30 seconds")] + property var periodValues: [1, 2, 3, 4, 5, 10, 15, 20, 30] + + text: I18n.tr("Monitor fade grace period") + options: periodOptions + visible: SettingsData.fadeToDpmsEnabled + enabled: SettingsData.fadeToDpmsEnabled + + Component.onCompleted: { + const currentPeriod = SettingsData.fadeToDpmsGracePeriod; + const index = periodValues.indexOf(currentPeriod); + currentValue = index >= 0 ? periodOptions[index] : I18n.tr("5 seconds"); + } + + onValueChanged: value => { + const index = periodOptions.indexOf(value); + if (index < 0) + return; + SettingsData.set("fadeToDpmsGracePeriod", periodValues[index]); + } + } + SettingsDropdownRow { + id: powerProfileDropdown + settingKey: "powerProfile" + tags: ["power", "profile", "performance", "balanced", "saver", "battery"] + property var profileOptions: [I18n.tr("Don't Change"), Theme.getPowerProfileLabel(0), Theme.getPowerProfileLabel(1), Theme.getPowerProfileLabel(2)] + property var profileValues: ["", "0", "1", "2"] + + width: parent.width + addHorizontalPadding: true + text: I18n.tr("Switch to power profile") + options: profileOptions + + Connections { + target: powerCategory + function onCurrentIndexChanged() { + const currentProfile = powerCategory.currentIndex === 0 ? SettingsData.acProfileName : SettingsData.batteryProfileName; + const index = powerProfileDropdown.profileValues.indexOf(currentProfile); + powerProfileDropdown.currentValue = powerProfileDropdown.profileOptions[index]; + } + } + + Component.onCompleted: { + const currentProfile = powerCategory.currentIndex === 0 ? SettingsData.acProfileName : SettingsData.batteryProfileName; + const index = profileValues.indexOf(currentProfile); + currentValue = profileOptions[index]; + } + + onValueChanged: value => { + const index = profileOptions.indexOf(value); + if (index >= 0) { + const profileValue = profileValues[index]; + if (powerCategory.currentIndex === 0) { + SettingsData.set("acProfileName", profileValue); + } else { + SettingsData.set("batteryProfileName", profileValue); + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsDropdownRow { + id: lockDropdown + settingKey: "lockTimeout" + tags: ["lock", "timeout", "idle", "automatic", "security"] + text: I18n.tr("Automatically lock after") + options: root.timeoutOptions + + Connections { + target: powerCategory + function onCurrentIndexChanged() { + const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acLockTimeout : SettingsData.batteryLockTimeout; + lockDropdown.currentValue = root.timeoutOptions[root.getTimeoutIndex(currentTimeout)]; + } + } + + Component.onCompleted: { + const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acLockTimeout : SettingsData.batteryLockTimeout; + currentValue = root.timeoutOptions[root.getTimeoutIndex(currentTimeout)]; + } + + onValueChanged: value => { + const index = root.timeoutOptions.indexOf(value); + if (index < 0) + return; + const timeout = root.timeoutValues[index]; + if (powerCategory.currentIndex === 0) { + SettingsData.set("acLockTimeout", timeout); + } else { + SettingsData.set("batteryLockTimeout", timeout); + } + } + } + + SettingsDropdownRow { + id: monitorDropdown + settingKey: "monitorTimeout" + tags: ["monitor", "display", "screen", "timeout", "off", "idle"] + text: I18n.tr("Turn off monitors after") + options: root.timeoutOptions + + Connections { + target: powerCategory + function onCurrentIndexChanged() { + const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acMonitorTimeout : SettingsData.batteryMonitorTimeout; + monitorDropdown.currentValue = root.timeoutOptions[root.getTimeoutIndex(currentTimeout)]; + } + } + + Component.onCompleted: { + const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acMonitorTimeout : SettingsData.batteryMonitorTimeout; + currentValue = root.timeoutOptions[root.getTimeoutIndex(currentTimeout)]; + } + + onValueChanged: value => { + const index = root.timeoutOptions.indexOf(value); + if (index < 0) + return; + const timeout = root.timeoutValues[index]; + if (powerCategory.currentIndex === 0) { + SettingsData.set("acMonitorTimeout", timeout); + } else { + SettingsData.set("batteryMonitorTimeout", timeout); + } + } + } + + SettingsDropdownRow { + id: suspendDropdown + settingKey: "suspendTimeout" + tags: ["suspend", "sleep", "timeout", "idle", "system"] + text: I18n.tr("Suspend system after") + options: root.timeoutOptions + + Connections { + target: powerCategory + function onCurrentIndexChanged() { + const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acSuspendTimeout : SettingsData.batterySuspendTimeout; + suspendDropdown.currentValue = root.timeoutOptions[root.getTimeoutIndex(currentTimeout)]; + } + } + + Component.onCompleted: { + const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acSuspendTimeout : SettingsData.batterySuspendTimeout; + currentValue = root.timeoutOptions[root.getTimeoutIndex(currentTimeout)]; + } + + onValueChanged: value => { + const index = root.timeoutOptions.indexOf(value); + if (index < 0) + return; + const timeout = root.timeoutValues[index]; + if (powerCategory.currentIndex === 0) { + SettingsData.set("acSuspendTimeout", timeout); + } else { + SettingsData.set("batterySuspendTimeout", timeout); + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: SessionService.hibernateSupported + + StyledText { + text: I18n.tr("Suspend behavior") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + leftPadding: Theme.spacingM + } + + DankButtonGroup { + id: suspendBehaviorSelector + anchors.horizontalCenter: parent.horizontalCenter + model: [I18n.tr("Suspend"), I18n.tr("Hibernate"), I18n.tr("Suspend then Hibernate")] + selectionMode: "single" + checkEnabled: false + + Connections { + target: powerCategory + function onCurrentIndexChanged() { + const behavior = powerCategory.currentIndex === 0 ? SettingsData.acSuspendBehavior : SettingsData.batterySuspendBehavior; + suspendBehaviorSelector.currentIndex = behavior; + } + } + + Component.onCompleted: { + const behavior = powerCategory.currentIndex === 0 ? SettingsData.acSuspendBehavior : SettingsData.batterySuspendBehavior; + currentIndex = behavior; + } + + onSelectionChanged: (index, selected) => { + if (!selected) + return; + currentIndex = index; + if (powerCategory.currentIndex === 0) { + SettingsData.set("acSuspendBehavior", index); + } else { + SettingsData.set("batterySuspendBehavior", index); + } + } + } + } + + StyledText { + text: I18n.tr("Idle monitoring not supported - requires newer Quickshell version") + font.pixelSize: Theme.fontSizeSmall + color: Theme.error + anchors.horizontalCenter: parent.horizontalCenter + visible: !IdleService.idleMonitorAvailable + } + } + + SettingsCard { + width: parent.width + iconName: "tune" + title: I18n.tr("Power Menu Customization") + settingKey: "powerMenu" + + StyledText { + text: I18n.tr("Customize which actions appear in the power menu") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.Wrap + } + + SettingsToggleRow { + settingKey: "powerMenuGridLayout" + tags: ["power", "menu", "grid", "layout", "list"] + text: I18n.tr("Use Grid Layout") + description: I18n.tr("Display power menu actions in a grid instead of a list") + checked: SettingsData.powerMenuGridLayout + onToggled: checked => SettingsData.set("powerMenuGridLayout", checked) + } + + SettingsDropdownRow { + id: defaultActionDropdown + settingKey: "powerMenuDefaultAction" + tags: ["power", "menu", "default", "action", "reboot", "logout", "shutdown"] + text: I18n.tr("Default selected action") + options: [I18n.tr("Reboot"), I18n.tr("Log Out"), I18n.tr("Power Off"), I18n.tr("Lock"), I18n.tr("Suspend"), I18n.tr("Restart DMS"), I18n.tr("Hibernate")] + property var actionValues: ["reboot", "logout", "poweroff", "lock", "suspend", "restart", "hibernate"] + + Component.onCompleted: { + const currentAction = SettingsData.powerMenuDefaultAction || "logout"; + const index = actionValues.indexOf(currentAction); + currentValue = index >= 0 ? options[index] : I18n.tr("Log Out"); + } + + onValueChanged: value => { + const index = options.indexOf(value); + if (index < 0) + return; + SettingsData.set("powerMenuDefaultAction", actionValues[index]); + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: [ + { + key: "reboot", + label: I18n.tr("Show Reboot") + }, + { + key: "logout", + label: I18n.tr("Show Log Out") + }, + { + key: "poweroff", + label: I18n.tr("Show Power Off") + }, + { + key: "lock", + label: I18n.tr("Show Lock") + }, + { + key: "suspend", + label: I18n.tr("Show Suspend") + }, + { + key: "restart", + label: I18n.tr("Show Restart DMS"), + desc: I18n.tr("Restart the DankMaterialShell") + }, + { + key: "hibernate", + label: I18n.tr("Show Hibernate"), + desc: I18n.tr("Only visible if hibernate is supported by your system"), + hibernate: true + } + ] + + SettingsToggleRow { + required property var modelData + settingKey: "powerMenuAction_" + modelData.key + tags: ["power", "menu", "action", "show", modelData.key] + text: modelData.label + description: modelData.desc || "" + visible: !modelData.hibernate || SessionService.hibernateSupported + checked: SettingsData.powerMenuActions.includes(modelData.key) + onToggled: checked => { + let actions = [...SettingsData.powerMenuActions]; + if (checked && !actions.includes(modelData.key)) { + actions.push(modelData.key); + } else if (!checked) { + actions = actions.filter(a => a !== modelData.key); + } + SettingsData.set("powerMenuActions", actions); + } + } + } + } + } + + SettingsCard { + width: parent.width + iconName: "check_circle" + title: I18n.tr("Power Action Confirmation") + settingKey: "powerConfirmation" + + SettingsToggleRow { + settingKey: "powerActionConfirm" + tags: ["power", "confirm", "hold", "button", "safety"] + text: I18n.tr("Hold to Confirm Power Actions") + description: I18n.tr("Require holding button/key to confirm power off, restart, suspend, hibernate and logout") + checked: SettingsData.powerActionConfirm + onToggled: checked => SettingsData.set("powerActionConfirm", checked) + } + + SettingsDropdownRow { + id: holdDurationDropdown + settingKey: "powerActionHoldDuration" + tags: ["power", "hold", "duration", "confirm", "time"] + property var durationOptions: [I18n.tr("250 ms"), I18n.tr("500 ms"), I18n.tr("750 ms"), I18n.tr("1 second"), I18n.tr("2 seconds"), I18n.tr("3 seconds"), I18n.tr("5 seconds"), I18n.tr("10 seconds")] + property var durationValues: [0.25, 0.5, 0.75, 1, 2, 3, 5, 10] + + text: I18n.tr("Hold Duration") + options: durationOptions + visible: SettingsData.powerActionConfirm + + Component.onCompleted: { + const currentDuration = SettingsData.powerActionHoldDuration; + const index = durationValues.indexOf(currentDuration); + currentValue = index >= 0 ? durationOptions[index] : I18n.tr("500 ms"); + } + + onValueChanged: value => { + const index = durationOptions.indexOf(value); + if (index < 0) + return; + SettingsData.set("powerActionHoldDuration", durationValues[index]); + } + } + } + + SettingsCard { + width: parent.width + iconName: "developer_mode" + title: I18n.tr("Custom Power Actions") + settingKey: "customPowerActions" + + Repeater { + model: [ + { + key: "customPowerActionLock", + label: I18n.tr("Custom Lock Command"), + placeholder: "/usr/bin/myLock.sh" + }, + { + key: "customPowerActionLogout", + label: I18n.tr("Custom Logout Command"), + placeholder: "/usr/bin/myLogout.sh" + }, + { + key: "customPowerActionSuspend", + label: I18n.tr("Custom Suspend Command"), + placeholder: "/usr/bin/mySuspend.sh" + }, + { + key: "customPowerActionHibernate", + label: I18n.tr("Custom Hibernate Command"), + placeholder: "/usr/bin/myHibernate.sh" + }, + { + key: "customPowerActionReboot", + label: I18n.tr("Custom Reboot Command"), + placeholder: "/usr/bin/myReboot.sh" + }, + { + key: "customPowerActionPowerOff", + label: I18n.tr("Custom Power Off Command"), + placeholder: "/usr/bin/myPowerOff.sh" + } + ] + + Column { + required property var modelData + width: parent.width + spacing: Theme.spacingXS + + StyledText { + text: modelData.label + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + DankTextField { + width: parent.width + placeholderText: modelData.placeholder + backgroundColor: Theme.surfaceContainerHighest + normalBorderColor: Theme.outlineMedium + focusedBorderColor: Theme.primary + + Component.onCompleted: { + var val = SettingsData[modelData.key]; + if (val) + text = val; + } + + onTextEdited: { + SettingsData.set(modelData.key, text.trim()); + } + } + } + } + } + + SettingsCard { + width: parent.width + iconName: "tune" + title: I18n.tr("Advanced") + settingKey: "powerAdvanced" + collapsible: true + expanded: false + + SettingsSliderRow { + settingKey: "batteryChargeLimit" + tags: ["battery", "charge", "limit", "percentage", "power"] + text: I18n.tr("Battery Charge Limit") + description: I18n.tr("Note: this only changes the percentage, it does not actually limit charging.") + value: SettingsData.batteryChargeLimit + minimum: 50 + maximum: 100 + defaultValue: 100 + onSliderValueChanged: newValue => SettingsData.set("batteryChargeLimit", newValue) + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PrinterTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PrinterTab.qml new file mode 100644 index 0000000..437dc47 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PrinterTab.qml @@ -0,0 +1,1702 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Common +import qs.Modals.Common +import qs.Services +import qs.Widgets + +Item { + id: printerTab + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property bool showAddPrinter: false + property bool manualEntryMode: false + property string manualHost: "" + property string manualPort: "631" + property string manualProtocol: "ipp" + property bool testingConnection: false + property var testConnectionResult: null + property string newPrinterName: "" + property string selectedDeviceUri: "" + property var selectedDevice: null + property string selectedPpd: "" + property string newPrinterLocation: "" + property string newPrinterInfo: "" + property var suggestedPPDs: [] + + function resetAddPrinterForm() { + manualEntryMode = false; + manualHost = ""; + manualPort = "631"; + manualProtocol = "ipp"; + testingConnection = false; + testConnectionResult = null; + newPrinterName = ""; + selectedDeviceUri = ""; + selectedDevice = null; + selectedPpd = ""; + newPrinterLocation = ""; + newPrinterInfo = ""; + suggestedPPDs = []; + } + + Connections { + target: CupsService + function onPpdsChanged() { + if (printerTab.manualEntryMode && printerTab.testConnectionResult?.success) + printerTab.selectDriverlessPPD(); + } + } + + function selectDriverlessPPD() { + if (printerTab.selectedPpd || CupsService.ppds.length === 0) + return; + + const probeModel = printerTab.testConnectionResult?.data?.makeModel || ""; + let suggested = []; + + // Try to find a model-specific PPD match + if (probeModel) { + const normalizedModel = probeModel.toLowerCase().replace(/[^a-z0-9]/g, ""); + const modelMatches = CupsService.ppds.filter(p => { + const normalizedPPD = (p.makeModel || "").toLowerCase().replace(/[^a-z0-9]/g, ""); + return normalizedPPD.includes(normalizedModel) || normalizedModel.includes(normalizedPPD); + }); + if (modelMatches.length > 0) + suggested = suggested.concat(modelMatches); + } + + // Always include driverless as an option + const driverless = CupsService.ppds.filter(p => p.name === "driverless" || p.name === "everywhere"); + for (const d of driverless) { + if (!suggested.find(s => s.name === d.name)) + suggested.push(d); + } + + if (suggested.length > 0) { + printerTab.selectedPpd = suggested[0].name; + printerTab.suggestedPPDs = suggested; + } + } + + function selectDevice(device) { + if (!device) + return; + selectedDevice = device; + selectedDeviceUri = device.uri; + if (!newPrinterName) { + newPrinterName = CupsService.suggestPrinterName(device); + } + if (device.location && !newPrinterLocation) { + newPrinterLocation = CupsService.decodeUri(device.location); + } + suggestedPPDs = CupsService.getMatchingPPDs(device); + if (suggestedPPDs.length > 0 && !selectedPpd) { + selectedPpd = suggestedPPDs[0].name; + } + } + + Component.onCompleted: { + CupsService.getClasses(); + } + + ConfirmModal { + id: deletePrinterConfirm + } + + ConfirmModal { + id: purgeJobsConfirm + } + + ConfirmModal { + id: deleteClassConfirm + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + + width: Math.min(600, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingL + + StyledRect { + width: parent.width + height: overviewSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + id: overviewSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "print" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("CUPS Print Server") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + Grid { + columns: 2 + columnSpacing: Theme.spacingL + rowSpacing: Theme.spacingS + width: parent.width + + StyledText { + text: I18n.tr("Status") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + Row { + spacing: Theme.spacingS + + Rectangle { + width: 8 + height: 8 + radius: 4 + anchors.verticalCenter: parent.verticalCenter + color: CupsService.cupsAvailable ? Theme.success : Theme.error + } + + StyledText { + text: CupsService.cupsAvailable ? I18n.tr("Available") : I18n.tr("Unavailable") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + } + + StyledText { + text: I18n.tr("Printers") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + StyledText { + text: CupsService.printerNames.length.toString() + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: I18n.tr("Total Jobs") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + StyledText { + text: CupsService.getTotalJobsNum().toString() + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + } + } + } + + StyledRect { + width: parent.width + height: addPrinterSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + visible: CupsService.cupsAvailable + + Column { + id: addPrinterSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "add_circle" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - addPrinterToggleBtn.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Add Printer") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: I18n.tr("Configure a new printer") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + + Rectangle { + id: addPrinterToggleBtn + width: 28 + height: 28 + radius: 14 + color: addPrinterToggleArea.containsMouse ? Theme.surfacePressed : "transparent" + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: printerTab.showAddPrinter ? "expand_less" : "expand_more" + size: 18 + color: Theme.surfaceText + } + + MouseArea { + id: addPrinterToggleArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + printerTab.showAddPrinter = !printerTab.showAddPrinter; + if (printerTab.showAddPrinter) { + if (CupsService.devices.length === 0) { + CupsService.getDevices(); + CupsService.getPPDs(); + } + } else { + printerTab.resetAddPrinterForm(); + } + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: printerTab.showAddPrinter + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + Row { + width: parent.width + spacing: Theme.spacingS + + Rectangle { + width: discoverRow.width + Theme.spacingM * 2 + height: 32 + radius: Theme.cornerRadius + color: !printerTab.manualEntryMode ? Theme.primary : (discoverArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight) + + Row { + id: discoverRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "search" + size: 16 + color: !printerTab.manualEntryMode ? Theme.onPrimary : Theme.surfaceText + } + + StyledText { + text: I18n.tr("Discover Devices", "Toggle button to scan for printers via mDNS/Avahi") + font.pixelSize: Theme.fontSizeSmall + color: !printerTab.manualEntryMode ? Theme.onPrimary : Theme.surfaceText + font.weight: Font.Medium + } + } + + MouseArea { + id: discoverArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + printerTab.manualEntryMode = false; + printerTab.testConnectionResult = null; + printerTab.testingConnection = false; + } + } + } + + Rectangle { + width: manualRow.width + Theme.spacingM * 2 + height: 32 + radius: Theme.cornerRadius + color: printerTab.manualEntryMode ? Theme.primary : (manualArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight) + + Row { + id: manualRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "edit" + size: 16 + color: printerTab.manualEntryMode ? Theme.onPrimary : Theme.surfaceText + } + + StyledText { + text: I18n.tr("Add by Address", "Toggle button to manually add a printer by IP or hostname") + font.pixelSize: Theme.fontSizeSmall + color: printerTab.manualEntryMode ? Theme.onPrimary : Theme.surfaceText + font.weight: Font.Medium + } + } + + MouseArea { + id: manualArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + printerTab.manualEntryMode = true; + printerTab.selectedDevice = null; + printerTab.selectedDeviceUri = ""; + if (CupsService.ppds.length === 0) + CupsService.getPPDs(); + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: !printerTab.manualEntryMode + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Device") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: 80 + anchors.verticalCenter: parent.verticalCenter + } + + DankDropdown { + id: deviceDropdown + dropdownWidth: parent.width - 80 - scanDevicesBtn.width - Theme.spacingS * 2 + popupWidth: parent.width - 80 - scanDevicesBtn.width - Theme.spacingS * 2 + enableFuzzySearch: true + emptyText: I18n.tr("No devices found") + currentValue: { + if (CupsService.loadingDevices) + return I18n.tr("Scanning..."); + if (printerTab.selectedDevice) + return CupsService.getDeviceDisplayName(printerTab.selectedDevice); + return I18n.tr("Select device..."); + } + options: CupsService.filteredDevices.map(d => CupsService.getDeviceDisplayName(d)) + onValueChanged: value => { + const filtered = CupsService.filteredDevices; + const device = filtered.find(d => CupsService.getDeviceDisplayName(d) === value); + if (device) + printerTab.selectDevice(device); + } + } + + DankActionButton { + id: scanDevicesBtn + iconName: "refresh" + buttonSize: 32 + anchors.verticalCenter: parent.verticalCenter + enabled: !CupsService.loadingDevices + onClicked: CupsService.getDevices() + + RotationAnimation on rotation { + running: CupsService.loadingDevices + loops: Animation.Infinite + from: 0 + to: 360 + duration: 1000 + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + visible: printerTab.selectedDevice !== null + + Item { + width: 80 + height: 1 + } + + StyledText { + text: CupsService.getDeviceSubtitle(printerTab.selectedDevice) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width - 80 - Theme.spacingS + elide: Text.ElideRight + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: printerTab.manualEntryMode + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Host", "Label for printer IP address or hostname input field") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: 80 + anchors.verticalCenter: parent.verticalCenter + } + + DankTextField { + width: parent.width - 80 - Theme.spacingS + placeholderText: I18n.tr("IP address or hostname", "Placeholder text for manual printer address input") + text: printerTab.manualHost + onTextEdited: { + printerTab.manualHost = text; + printerTab.testConnectionResult = null; + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Port", "Label for printer port number input field") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: 80 + anchors.verticalCenter: parent.verticalCenter + } + + DankTextField { + width: 80 + placeholderText: "631" + text: printerTab.manualPort + onTextEdited: { + printerTab.manualPort = text; + printerTab.testConnectionResult = null; + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Protocol", "Label for printer protocol selector, e.g. ipp, ipps, lpd, socket") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: 80 + anchors.verticalCenter: parent.verticalCenter + } + + DankDropdown { + id: protocolDropdown + dropdownWidth: 120 + popupWidth: 120 + currentValue: printerTab.manualProtocol + options: ["ipp", "ipps", "lpd", "socket"] + onValueChanged: value => { + printerTab.manualProtocol = value; + printerTab.testConnectionResult = null; + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + Item { + width: 80 + height: 1 + } + + DankButton { + text: printerTab.testingConnection ? I18n.tr("Testing...", "Button state while testing printer connection") : I18n.tr("Test Connection", "Button to test connection to a printer by IP address") + iconName: printerTab.testingConnection ? "sync" : "lan" + buttonHeight: 36 + enabled: printerTab.manualHost.length > 0 && !printerTab.testingConnection + onClicked: { + printerTab.testingConnection = true; + printerTab.testConnectionResult = null; + const port = parseInt(printerTab.manualPort) || 631; + CupsService.testConnection(printerTab.manualHost, port, printerTab.manualProtocol, response => { + printerTab.testingConnection = false; + if (response.error) { + printerTab.testConnectionResult = { + "success": false, + "error": response.error + }; + } else if (response.result) { + printerTab.testConnectionResult = { + "success": response.result.reachable === true, + "data": response.result + }; + if (response.result.reachable) { + if (response.result.uri) + printerTab.selectedDeviceUri = response.result.uri; + if (response.result.name && !printerTab.newPrinterName) + printerTab.newPrinterName = response.result.name.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").substring(0, 32) || "Printer"; + // Load PPDs if not loaded yet, then select driverless + if (CupsService.ppds.length === 0) { + CupsService.getPPDs(); + } + selectDriverlessPPD(); + } + } + }); + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingXS + visible: printerTab.testConnectionResult !== null + + Row { + spacing: Theme.spacingS + + Item { + width: 80 + height: 1 + } + + Rectangle { + width: 8 + height: 8 + radius: 4 + anchors.verticalCenter: parent.verticalCenter + color: printerTab.testConnectionResult?.success ? Theme.success : Theme.error + } + + StyledText { + text: printerTab.testConnectionResult?.success ? I18n.tr("Printer reachable", "Status message when test connection to printer succeeds") : I18n.tr("Connection failed", "Status message when test connection to printer fails") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: printerTab.testConnectionResult?.success ? Theme.success : Theme.error + } + } + + Row { + spacing: Theme.spacingS + visible: printerTab.testConnectionResult?.success && (printerTab.testConnectionResult?.data?.makeModel || printerTab.testConnectionResult?.data?.info) + + Item { + width: 80 + height: 1 + } + + StyledText { + text: printerTab.testConnectionResult?.data?.makeModel || printerTab.testConnectionResult?.data?.info || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + + Row { + spacing: Theme.spacingS + visible: !printerTab.testConnectionResult?.success && printerTab.testConnectionResult?.data?.error + + Item { + width: 80 + height: 1 + } + + StyledText { + text: printerTab.testConnectionResult?.data?.error || printerTab.testConnectionResult?.error || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.parent.width - 80 - Theme.spacingS + wrapMode: Text.WordWrap + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Driver") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: 80 + anchors.verticalCenter: parent.verticalCenter + } + + DankDropdown { + id: ppdDropdown + dropdownWidth: parent.width - 80 - refreshPpdsBtn.width - Theme.spacingS * 2 + popupWidth: parent.width - 80 - refreshPpdsBtn.width - Theme.spacingS * 2 + enableFuzzySearch: true + emptyText: I18n.tr("No drivers found") + currentValue: { + if (CupsService.loadingPPDs) + return I18n.tr("Loading..."); + if (printerTab.selectedPpd) { + const ppd = CupsService.ppds.find(p => p.name === printerTab.selectedPpd); + if (ppd) { + const isSuggested = printerTab.suggestedPPDs.some(s => s.name === ppd.name); + return (isSuggested ? "★ " : "") + (ppd.makeModel || ppd.name); + } + return printerTab.selectedPpd; + } + return printerTab.suggestedPPDs.length > 0 ? I18n.tr("Recommended available") : I18n.tr("Select driver..."); + } + options: { + const suggested = printerTab.suggestedPPDs.map(p => "★ " + (p.makeModel || p.name)); + const others = CupsService.ppds.filter(p => !printerTab.suggestedPPDs.some(s => s.name === p.name)).map(p => p.makeModel || p.name); + return suggested.concat(others); + } + onValueChanged: value => { + const cleanValue = value.replace(/^★ /, ""); + const ppd = CupsService.ppds.find(p => (p.makeModel || p.name) === cleanValue); + if (ppd) + printerTab.selectedPpd = ppd.name; + } + } + + DankActionButton { + id: refreshPpdsBtn + iconName: "refresh" + buttonSize: 32 + anchors.verticalCenter: parent.verticalCenter + enabled: !CupsService.loadingPPDs + onClicked: CupsService.getPPDs() + + RotationAnimation on rotation { + running: CupsService.loadingPPDs + loops: Animation.Infinite + from: 0 + to: 360 + duration: 1000 + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Name") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: 80 + anchors.verticalCenter: parent.verticalCenter + } + + DankTextField { + width: parent.width - 80 - Theme.spacingS + placeholderText: I18n.tr("Printer name (no spaces)") + text: printerTab.newPrinterName + onTextEdited: printerTab.newPrinterName = text.replace(/\s/g, "-") + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Location") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: 80 + anchors.verticalCenter: parent.verticalCenter + } + + DankTextField { + width: parent.width - 80 - Theme.spacingS + placeholderText: I18n.tr("Optional location") + text: printerTab.newPrinterLocation + onTextEdited: printerTab.newPrinterLocation = text + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Description") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: 80 + anchors.verticalCenter: parent.verticalCenter + } + + DankTextField { + width: parent.width - 80 - Theme.spacingS + placeholderText: I18n.tr("Optional description") + text: printerTab.newPrinterInfo + onTextEdited: printerTab.newPrinterInfo = text + } + } + } + + Row { + LayoutMirroring.enabled: false + width: parent.width + spacing: Theme.spacingS + layoutDirection: Qt.RightToLeft + + DankButton { + text: CupsService.creatingPrinter ? I18n.tr("Creating...") : I18n.tr("Create Printer") + iconName: CupsService.creatingPrinter ? "sync" : "add" + buttonHeight: 36 + enabled: printerTab.newPrinterName.length > 0 && printerTab.selectedDeviceUri.length > 0 && printerTab.selectedPpd.length > 0 && !CupsService.creatingPrinter + onClicked: { + CupsService.createPrinter(printerTab.newPrinterName, printerTab.selectedDeviceUri, printerTab.selectedPpd, { + location: printerTab.newPrinterLocation, + information: printerTab.newPrinterInfo + }); + printerTab.resetAddPrinterForm(); + printerTab.showAddPrinter = false; + } + } + } + } + } + } + + StyledRect { + width: parent.width + height: printersSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + visible: CupsService.cupsAvailable + + Column { + id: printersSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "print" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - refreshBtn.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Printers") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: { + const count = CupsService.printerNames.length; + if (count === 0) + return I18n.tr("No printers configured"); + return I18n.ntr("%1 printer", "%1 printers", count).arg(count); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + + DankActionButton { + id: refreshBtn + iconName: "refresh" + buttonSize: 32 + anchors.verticalCenter: parent.verticalCenter + onClicked: CupsService.getState() + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + Item { + width: parent.width + height: 80 + visible: CupsService.printerNames.length === 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "print_disabled" + size: 32 + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("No printers found") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + Column { + width: parent.width + spacing: 4 + visible: CupsService.printerNames.length > 0 + + Repeater { + model: CupsService.printerNames + + delegate: Rectangle { + id: printerDelegate + required property string modelData + required property int index + + readonly property var printerData: CupsService.getPrinterData(modelData) + readonly property bool isExpanded: CupsService.expandedPrinter === modelData || hasJobs + readonly property bool hasJobs: (printerData?.jobs?.length ?? 0) > 0 + readonly property bool isIdle: printerData?.state === "idle" + readonly property bool isStopped: printerData?.state === "stopped" + + width: parent.width + height: isExpanded ? 56 + expandedContent.height : 56 + radius: Theme.cornerRadius + color: printerMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + border.width: CupsService.selectedPrinter === modelData ? 2 : 0 + border.color: Theme.primary + clip: true + + Behavior on height { + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + } + } + + Column { + anchors.fill: parent + spacing: 0 + + Item { + width: parent.width + height: 56 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + anchors.right: printerActions.left + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: isStopped ? "print_disabled" : "print" + size: 20 + color: isStopped ? Theme.error : (isIdle ? Theme.primary : Theme.warning) + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + width: parent.width - 20 - Theme.spacingS + + StyledText { + text: modelData + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: CupsService.selectedPrinter === modelData ? Font.Medium : Font.Normal + elide: Text.ElideRight + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + Row { + anchors.left: parent.left + spacing: Theme.spacingXS + + StyledText { + text: CupsService.getPrinterStateTranslation(printerData?.state || "") + font.pixelSize: Theme.fontSizeSmall + color: { + switch (printerData?.state) { + case "idle": + return Theme.primary; + case "stopped": + return Theme.error; + case "processing": + return Theme.warning; + default: + return Theme.surfaceVariantText; + } + } + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: (printerData?.jobs?.length ?? 0) > 0 + } + + StyledText { + text: I18n.ntr("%1 job", "%1 jobs", printerData?.jobs?.length ?? 0).arg(printerData?.jobs?.length ?? 0) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: (printerData?.jobs?.length ?? 0) > 0 + } + } + } + } + + Row { + id: printerActions + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: expandBtn.containsMouse ? Theme.surfacePressed : "transparent" + + DankIcon { + anchors.centerIn: parent + name: isExpanded ? "expand_less" : "expand_more" + size: 18 + color: Theme.surfaceText + } + + MouseArea { + id: expandBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + CupsService.expandedPrinter = isExpanded ? "" : modelData; + } + } + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: deleteBtn.containsMouse ? Theme.errorHover : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "delete" + size: 18 + color: deleteBtn.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: deleteBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + deletePrinterConfirm.showWithOptions({ + title: I18n.tr("Delete Printer"), + message: I18n.tr("Delete \"%1\"?").arg(modelData), + confirmText: I18n.tr("Delete"), + confirmColor: Theme.error, + onConfirm: () => CupsService.deletePrinter(modelData) + }); + } + } + } + } + + MouseArea { + id: printerMouseArea + anchors.fill: parent + anchors.rightMargin: printerActions.width + Theme.spacingM + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + CupsService.setSelectedPrinter(modelData); + } + } + } + + Column { + id: expandedContent + width: parent.width + visible: isExpanded + + Rectangle { + width: parent.width - Theme.spacingM * 2 + height: 1 + x: Theme.spacingM + color: Theme.outlineLight + } + + Item { + width: parent.width + height: detailsColumn.implicitHeight + Theme.spacingM * 2 + + Column { + id: detailsColumn + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + Flow { + width: parent.width + spacing: Theme.spacingXS + + Repeater { + model: { + const fields = []; + const p = printerData; + if (!p) + return fields; + + fields.push({ + label: I18n.tr("State"), + value: CupsService.getPrinterStateTranslation(p.state) + }); + if (p.stateReason && p.stateReason !== "none") + fields.push({ + label: I18n.tr("Reason"), + value: CupsService.getPrinterStateReasonTranslation(p.stateReason) + }); + if (p.makeModel) + fields.push({ + label: I18n.tr("Model"), + value: p.makeModel + }); + if (p.location) + fields.push({ + label: I18n.tr("Location"), + value: p.location + }); + fields.push({ + label: I18n.tr("Accepting"), + value: p.accepting ? I18n.tr("Yes") : I18n.tr("No") + }); + + return fields; + } + + delegate: Rectangle { + required property var modelData + required property int index + + width: fieldContent.width + Theme.spacingM * 2 + height: 32 + radius: Theme.cornerRadius - 2 + color: Theme.surfaceContainerHigh + border.width: 1 + border.color: Theme.outlineLight + + Row { + id: fieldContent + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: modelData.label + ":" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData.value + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + Rectangle { + height: 28 + width: pauseResumeRow.width + Theme.spacingM * 2 + radius: 14 + color: pauseResumeArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + + Row { + id: pauseResumeRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: isStopped ? "play_arrow" : "pause" + size: 16 + color: Theme.surfaceText + } + + StyledText { + text: isStopped ? I18n.tr("Resume") : I18n.tr("Pause") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + } + + MouseArea { + id: pauseResumeArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (isStopped) { + CupsService.resumePrinter(modelData); + } else { + CupsService.pausePrinter(modelData); + } + } + } + } + + Rectangle { + height: 28 + width: testPageRow.width + Theme.spacingM * 2 + radius: 14 + color: testPageArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + + Row { + id: testPageRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "description" + size: 16 + color: Theme.surfaceText + } + + StyledText { + text: I18n.tr("Test Page") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + } + + MouseArea { + id: testPageArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: CupsService.printTestPage(modelData) + } + } + + Rectangle { + height: 28 + width: acceptRejectRow.width + Theme.spacingM * 2 + radius: 14 + color: acceptRejectArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + + Row { + id: acceptRejectRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: printerData?.accepting ? "block" : "check_circle" + size: 16 + color: Theme.surfaceText + } + + StyledText { + text: printerData?.accepting ? I18n.tr("Reject Jobs") : I18n.tr("Accept Jobs") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + } + + MouseArea { + id: acceptRejectArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (printerData?.accepting) { + CupsService.rejectJobs(modelData); + } else { + CupsService.acceptJobs(modelData); + } + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingXS + visible: (printerData?.jobs?.length ?? 0) > 0 + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Jobs") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: 1 + height: 1 + Layout.fillWidth: true + } + + Rectangle { + height: 24 + width: purgeRow.width + Theme.spacingM * 2 + radius: 12 + color: purgeArea.containsMouse ? Theme.errorHover : Theme.surfaceLight + + Row { + id: purgeRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "delete_sweep" + size: 14 + color: purgeArea.containsMouse ? Theme.error : Theme.surfaceText + } + + StyledText { + text: I18n.tr("Clear All") + font.pixelSize: Theme.fontSizeSmall - 1 + color: purgeArea.containsMouse ? Theme.error : Theme.surfaceText + font.weight: Font.Medium + } + } + + MouseArea { + id: purgeArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + purgeJobsConfirm.showWithOptions({ + title: I18n.tr("Clear All Jobs"), + message: I18n.tr("Cancel all jobs for \"%1\"?").arg(modelData), + confirmText: I18n.tr("Clear"), + confirmColor: Theme.error, + onConfirm: () => CupsService.purgeJobs(modelData) + }); + } + } + } + } + + Repeater { + model: printerData?.jobs ?? [] + + delegate: Rectangle { + required property var modelData + required property int index + + width: parent.width + height: 44 + radius: Theme.cornerRadius - 2 + color: Theme.surfaceContainerHighest + border.width: 1 + border.color: Theme.outlineLight + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: jobActions.left + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "description" + size: 18 + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 1 + width: parent.width - 18 - Theme.spacingS + + StyledText { + text: "[" + modelData.id + "] " + CupsService.getJobStateTranslation(modelData.state) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + elide: Text.ElideRight + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: { + const size = Math.round((modelData.size || 0) / 1024); + const date = new Date(modelData.timeCreated); + return size + " KB • " + date.toLocaleString(Qt.locale(), Locale.ShortFormat); + } + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + anchors.left: parent.left + } + } + } + + Row { + id: jobActions + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + + Rectangle { + width: 24 + height: 24 + radius: 12 + color: holdJobBtn.containsMouse ? Theme.surfacePressed : "transparent" + visible: modelData.state === "pending" + + DankIcon { + anchors.centerIn: parent + name: "pause" + size: 14 + color: Theme.surfaceVariantText + } + + MouseArea { + id: holdJobBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: CupsService.holdJob(modelData.id) + } + } + + Rectangle { + width: 24 + height: 24 + radius: 12 + color: restartJobBtn.containsMouse ? Theme.surfacePressed : "transparent" + visible: modelData.state === "pending-held" || modelData.state === "completed" || modelData.state === "aborted" + + DankIcon { + anchors.centerIn: parent + name: "replay" + size: 14 + color: Theme.surfaceVariantText + } + + MouseArea { + id: restartJobBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: CupsService.restartJob(modelData.id) + } + } + + Rectangle { + width: 24 + height: 24 + radius: 12 + color: cancelJobBtn.containsMouse ? Theme.errorHover : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "close" + size: 14 + color: cancelJobBtn.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: cancelJobBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: CupsService.cancelJob(printerDelegate.modelData, modelData.id) + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + + StyledRect { + width: parent.width + height: classesSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + visible: CupsService.cupsAvailable && CupsService.printerClasses.length > 0 + + Column { + id: classesSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "workspaces" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - refreshClassesBtn.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Printer Classes") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: I18n.ntr("%1 class", "%1 classes", CupsService.printerClasses.length).arg(CupsService.printerClasses.length) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + + DankActionButton { + id: refreshClassesBtn + iconName: "refresh" + buttonSize: 32 + anchors.verticalCenter: parent.verticalCenter + onClicked: CupsService.getClasses() + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + Column { + width: parent.width + spacing: 4 + + Repeater { + model: CupsService.printerClasses + + delegate: Rectangle { + required property var modelData + required property int index + + width: parent.width + height: 48 + radius: Theme.cornerRadius + color: classMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.right: classActions.left + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "workspaces" + size: 20 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: modelData.name || I18n.tr("Unknown") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + } + + StyledText { + text: I18n.ntr("%1 printer", "%1 printers", modelData.members?.length ?? 0).arg(modelData.members?.length ?? 0) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + + Row { + id: classActions + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: deleteClassBtn.containsMouse ? Theme.errorHover : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "delete" + size: 18 + color: deleteClassBtn.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: deleteClassBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + deleteClassConfirm.showWithOptions({ + title: I18n.tr("Delete Class"), + message: I18n.tr("Delete class \"%1\"?").arg(modelData.name), + confirmText: I18n.tr("Delete"), + confirmColor: Theme.error, + onConfirm: () => CupsService.deleteClass(modelData.name) + }); + } + } + } + } + + MouseArea { + id: classMouseArea + anchors.fill: parent + anchors.rightMargin: classActions.width + Theme.spacingM + hoverEnabled: true + } + } + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/RunningAppsTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/RunningAppsTab.qml new file mode 100644 index 0000000..022e131 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/RunningAppsTab.qml @@ -0,0 +1,175 @@ +import QtQuick +import qs.Common +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + width: parent.width + iconName: "find_replace" + title: I18n.tr("App ID Substitutions") + settingKey: "appIdSubstitutions" + tags: ["app", "icon", "substitution", "replacement", "pattern", "window", "class", "regex"] + + headerActions: [ + DankActionButton { + buttonSize: 36 + iconName: "restart_alt" + iconSize: 20 + visible: JSON.stringify(SettingsData.appIdSubstitutions) !== JSON.stringify(SettingsData.getDefaultAppIdSubstitutions()) + backgroundColor: Theme.surfaceContainer + iconColor: Theme.surfaceVariantText + onClicked: SettingsData.resetAppIdSubstitutions() + }, + DankActionButton { + buttonSize: 36 + iconName: "add" + iconSize: 20 + backgroundColor: Theme.surfaceContainer + iconColor: Theme.primary + onClicked: SettingsData.addAppIdSubstitution("", "", "exact") + } + ] + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Map window class names to icon names for proper icon display") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + bottomPadding: Theme.spacingS + } + + Repeater { + model: SettingsData.appIdSubstitutions + + delegate: Rectangle { + id: subItem + width: parent.width + height: subColumn.implicitHeight + Theme.spacingM + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainer, 0.5) + + Column { + id: subColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingS + + Row { + width: parent.width + spacing: Theme.spacingS + + Column { + width: (parent.width - deleteBtn.width - Theme.spacingS) / 2 + spacing: 2 + + StyledText { + text: I18n.tr("Pattern") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + } + + DankTextField { + id: patternField + width: parent.width + text: modelData.pattern + font.pixelSize: Theme.fontSizeSmall + onEditingFinished: SettingsData.updateAppIdSubstitution(index, text, replacementField.text, modelData.type) + } + } + + Column { + width: (parent.width - deleteBtn.width - Theme.spacingS) / 2 + spacing: 2 + + StyledText { + text: I18n.tr("Replacement") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + } + + DankTextField { + id: replacementField + width: parent.width + text: modelData.replacement + font.pixelSize: Theme.fontSizeSmall + onEditingFinished: SettingsData.updateAppIdSubstitution(index, patternField.text, text, modelData.type) + } + } + + Item { + id: deleteBtn + width: 32 + height: 40 + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: deleteArea.containsMouse ? Theme.withAlpha(Theme.error, 0.2) : "transparent" + } + + DankIcon { + anchors.centerIn: parent + name: "delete" + size: 18 + color: deleteArea.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: deleteArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: SettingsData.removeAppIdSubstitution(index) + } + } + } + + Column { + width: 120 + spacing: 2 + + StyledText { + text: I18n.tr("Type") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + } + + DankDropdown { + width: parent.width + compactMode: true + dropdownWidth: 120 + currentValue: modelData.type + options: ["exact", "contains", "regex"] + onValueChanged: value => SettingsData.updateAppIdSubstitution(index, modelData.pattern, modelData.replacement, value) + } + } + } + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/SoundsTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/SoundsTab.qml new file mode 100644 index 0000000..a8e9958 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/SoundsTab.qml @@ -0,0 +1,168 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + tab: "sounds" + tags: ["sound", "audio", "notification", "volume"] + title: I18n.tr("System Sounds") + settingKey: "systemSounds" + iconName: SettingsData.soundsEnabled ? "volume_up" : "volume_off" + visible: AudioService.soundsAvailable + + SettingsToggleRow { + tab: "sounds" + tags: ["sound", "enable", "system"] + settingKey: "soundsEnabled" + text: I18n.tr("Enable System Sounds") + description: I18n.tr("Play sounds for system events") + checked: SettingsData.soundsEnabled + onToggled: checked => SettingsData.set("soundsEnabled", checked) + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: SettingsData.soundsEnabled + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + } + + SettingsToggleRow { + tab: "sounds" + tags: ["sound", "theme", "system"] + settingKey: "useSystemSoundTheme" + visible: AudioService.gsettingsAvailable + text: I18n.tr("Use System Theme") + description: I18n.tr("Use sound theme from system settings") + checked: SettingsData.useSystemSoundTheme + onToggled: checked => SettingsData.set("useSystemSoundTheme", checked) + } + + SettingsDropdownRow { + tab: "sounds" + tags: ["sound", "theme", "select"] + settingKey: "soundTheme" + visible: SettingsData.useSystemSoundTheme && AudioService.availableSoundThemes.length > 0 + enabled: SettingsData.useSystemSoundTheme && AudioService.availableSoundThemes.length > 0 + text: I18n.tr("Sound Theme") + description: I18n.tr("Select system sound theme") + options: AudioService.availableSoundThemes + currentValue: { + const theme = AudioService.currentSoundTheme; + if (theme && AudioService.availableSoundThemes.includes(theme)) + return theme; + return AudioService.availableSoundThemes.length > 0 ? AudioService.availableSoundThemes[0] : ""; + } + onValueChanged: value => { + if (value && value !== AudioService.currentSoundTheme) + AudioService.setSoundTheme(value); + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + visible: AudioService.gsettingsAvailable + } + + SettingsToggleRow { + tab: "sounds" + tags: ["sound", "login", "startup", "boot"] + settingKey: "soundLogin" + text: I18n.tr("Login") + description: I18n.tr("Play sound after logging in") + checked: SettingsData.soundLogin + onToggled: checked => SettingsData.set("soundLogin", checked) + } + + SettingsToggleRow { + tab: "sounds" + tags: ["sound", "notification", "new"] + settingKey: "soundNewNotification" + text: I18n.tr("New Notification") + description: I18n.tr("Play sound when new notification arrives") + checked: SettingsData.soundNewNotification + onToggled: checked => SettingsData.set("soundNewNotification", checked) + } + + SettingsToggleRow { + tab: "sounds" + tags: ["sound", "volume", "changed"] + settingKey: "soundVolumeChanged" + text: I18n.tr("Volume Changed") + description: I18n.tr("Play sound when volume is adjusted") + checked: SettingsData.soundVolumeChanged + onToggled: checked => SettingsData.set("soundVolumeChanged", checked) + } + + SettingsToggleRow { + tab: "sounds" + tags: ["sound", "power", "plugged"] + settingKey: "soundPluggedIn" + visible: BatteryService.batteryAvailable + text: I18n.tr("Plugged In") + description: I18n.tr("Play sound when power cable is connected") + checked: SettingsData.soundPluggedIn + onToggled: checked => SettingsData.set("soundPluggedIn", checked) + } + } + } + + Rectangle { + width: parent.width + height: notAvailableText.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) + visible: !AudioService.soundsAvailable + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + DankIcon { + name: "info" + size: Theme.iconSizeSmall + color: Theme.warning + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: notAvailableText + font.pixelSize: Theme.fontSizeSmall + text: I18n.tr("System sounds are not available. Install %1 for sound support.").arg("qt6-multimedia") + wrapMode: Text.WordWrap + width: parent.width - Theme.iconSizeSmall - Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/SystemUpdaterTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/SystemUpdaterTab.qml new file mode 100644 index 0000000..5a61dbb --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/SystemUpdaterTab.qml @@ -0,0 +1,142 @@ +import QtQuick +import qs.Common +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + width: parent.width + iconName: "refresh" + title: I18n.tr("System Updater") + settingKey: "systemUpdater" + + SettingsToggleRow { + text: I18n.tr("Hide Updater Widget", "When updater widget is used, then hide it if no update found") + description: I18n.tr("When updater widget is used, then hide it if no update found") + checked: SettingsData.updaterHideWidget + onToggled: checked => { + SettingsData.set("updaterHideWidget", checked); + } + } + + SettingsToggleRow { + text: I18n.tr("Use Custom Command") + description: I18n.tr("Use custom command for update your system") + checked: SettingsData.updaterUseCustomCommand + onToggled: checked => { + if (!checked) { + updaterCustomCommand.text = ""; + updaterTerminalCustomClass.text = ""; + SettingsData.set("updaterCustomCommand", ""); + SettingsData.set("updaterTerminalAdditionalParams", ""); + } + SettingsData.set("updaterUseCustomCommand", checked); + } + } + + FocusScope { + width: parent.width - Theme.spacingM * 2 + height: customCommandColumn.implicitHeight + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + + Column { + id: customCommandColumn + width: parent.width + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("System update custom command") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + DankTextField { + id: updaterCustomCommand + width: parent.width + placeholderText: "myPkgMngr --sysupdate" + backgroundColor: Theme.surfaceContainerHighest + normalBorderColor: Theme.outlineMedium + focusedBorderColor: Theme.primary + + Component.onCompleted: { + if (SettingsData.updaterCustomCommand) { + text = SettingsData.updaterCustomCommand; + } + } + + onTextEdited: SettingsData.set("updaterCustomCommand", text.trim()) + + MouseArea { + anchors.fill: parent + onPressed: mouse => { + updaterCustomCommand.forceActiveFocus(); + mouse.accepted = false; + } + } + } + } + } + + FocusScope { + width: parent.width - Theme.spacingM * 2 + height: terminalParamsColumn.implicitHeight + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + + Column { + id: terminalParamsColumn + width: parent.width + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Terminal custom additional parameters") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + DankTextField { + id: updaterTerminalCustomClass + width: parent.width + placeholderText: "-T udpClass" + backgroundColor: Theme.surfaceContainerHighest + normalBorderColor: Theme.outlineMedium + focusedBorderColor: Theme.primary + + Component.onCompleted: { + if (SettingsData.updaterTerminalAdditionalParams) { + text = SettingsData.updaterTerminalAdditionalParams; + } + } + + onTextEdited: SettingsData.set("updaterTerminalAdditionalParams", text.trim()) + + MouseArea { + anchors.fill: parent + onPressed: mouse => { + updaterTerminalCustomClass.forceActiveFocus(); + mouse.accepted = false; + } + } + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/ThemeBrowser.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/ThemeBrowser.qml new file mode 100644 index 0000000..1df32c7 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/ThemeBrowser.qml @@ -0,0 +1,697 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Modals.Common +import qs.Services +import qs.Widgets + +FloatingWindow { + id: root + + property bool disablePopupTransparency: true + property var allThemes: [] + property string searchQuery: "" + property var filteredThemes: [] + property int selectedIndex: -1 + property bool keyboardNavigationActive: false + property bool isLoading: false + property var parentModal: null + property bool pendingInstallHandled: false + property string pendingApplyThemeId: "" + + function updateFilteredThemes() { + var filtered = []; + var query = searchQuery ? searchQuery.toLowerCase() : ""; + + for (var i = 0; i < allThemes.length; i++) { + var theme = allThemes[i]; + + if (query.length === 0) { + filtered.push(theme); + continue; + } + + var name = theme.name ? theme.name.toLowerCase() : ""; + var description = theme.description ? theme.description.toLowerCase() : ""; + var author = theme.author ? theme.author.toLowerCase() : ""; + + if (name.indexOf(query) !== -1 || description.indexOf(query) !== -1 || author.indexOf(query) !== -1) + filtered.push(theme); + } + + filteredThemes = filtered; + selectedIndex = -1; + keyboardNavigationActive = false; + } + + function selectNext() { + if (filteredThemes.length === 0) + return; + keyboardNavigationActive = true; + selectedIndex = Math.min(selectedIndex + 1, filteredThemes.length - 1); + } + + function selectPrevious() { + if (filteredThemes.length === 0) + return; + keyboardNavigationActive = true; + selectedIndex = Math.max(selectedIndex - 1, -1); + if (selectedIndex === -1) + keyboardNavigationActive = false; + } + + function installTheme(themeId, themeName, applyAfterInstall) { + ToastService.showInfo(I18n.tr("Installing: %1", "installation progress").arg(themeName)); + DMSService.installTheme(themeId, response => { + if (response.error) { + ToastService.showError(I18n.tr("Install failed: %1", "installation error").arg(response.error)); + return; + } + ToastService.showInfo(I18n.tr("Installed: %1", "installation success").arg(themeName)); + if (applyAfterInstall) + pendingApplyThemeId = themeId; + refreshThemes(); + }); + } + + function applyInstalledTheme(themeId, installedThemes) { + for (var i = 0; i < installedThemes.length; i++) { + var theme = installedThemes[i]; + if (theme.id === themeId) { + var sourceDir = theme.sourceDir || theme.id; + var themePath = Quickshell.env("HOME") + "/.config/DankMaterialShell/themes/" + sourceDir + "/theme.json"; + SettingsData.set("customThemeFile", themePath); + Theme.switchThemeCategory("registry", "custom"); + Theme.switchTheme("custom", true, true); + hide(); + return; + } + } + } + + function uninstallTheme(themeId, themeName) { + ToastService.showInfo(I18n.tr("Uninstalling: %1", "uninstallation progress").arg(themeName)); + DMSService.uninstallTheme(themeId, response => { + if (response.error) { + ToastService.showError(I18n.tr("Uninstall failed: %1", "uninstallation error").arg(response.error)); + return; + } + ToastService.showInfo(I18n.tr("Uninstalled: %1", "uninstallation success").arg(themeName)); + refreshThemes(); + }); + } + + function refreshThemes() { + isLoading = true; + DMSService.listThemes(); + DMSService.listInstalledThemes(); + } + + function checkPendingInstall() { + if (!PopoutService.pendingThemeInstall || pendingInstallHandled) + return; + pendingInstallHandled = true; + var themeId = PopoutService.pendingThemeInstall; + PopoutService.pendingThemeInstall = ""; + urlInstallConfirm.showWithOptions({ + "title": I18n.tr("Install Theme", "theme installation dialog title"), + "message": I18n.tr("Install theme '%1' from the DMS registry?", "theme installation confirmation").arg(themeId), + "confirmText": I18n.tr("Install", "install action button"), + "cancelText": I18n.tr("Cancel"), + "onConfirm": () => installTheme(themeId, themeId, true), + "onCancel": () => hide() + }); + } + + function show() { + if (parentModal) + parentModal.shouldHaveFocus = false; + visible = true; + Qt.callLater(() => browserSearchField.forceActiveFocus()); + } + + function hide() { + visible = false; + if (!parentModal) + return; + parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible); + Qt.callLater(() => { + if (parentModal.modalFocusScope) + parentModal.modalFocusScope.forceActiveFocus(); + }); + } + + objectName: "themeBrowser" + title: I18n.tr("Browse Themes", "theme browser window title") + minimumSize: Qt.size(550, 450) + implicitWidth: 700 + implicitHeight: 700 + color: Theme.surfaceContainer + visible: false + + onVisibleChanged: { + if (visible) { + pendingInstallHandled = false; + refreshThemes(); + Qt.callLater(() => { + browserSearchField.forceActiveFocus(); + checkPendingInstall(); + }); + return; + } + allThemes = []; + searchQuery = ""; + filteredThemes = []; + selectedIndex = -1; + keyboardNavigationActive = false; + isLoading = false; + } + + ConfirmModal { + id: urlInstallConfirm + } + + Connections { + target: DMSService + function onThemesListReceived(themes) { + isLoading = false; + allThemes = themes; + updateFilteredThemes(); + } + function onInstalledThemesReceived(themes) { + if (!pendingApplyThemeId) + return; + var themeId = pendingApplyThemeId; + pendingApplyThemeId = ""; + applyInstalledTheme(themeId, themes); + } + } + + FocusScope { + id: browserKeyHandler + + anchors.fill: parent + focus: true + + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Escape: + root.hide(); + event.accepted = true; + return; + case Qt.Key_Down: + root.selectNext(); + event.accepted = true; + return; + case Qt.Key_Up: + root.selectPrevious(); + event.accepted = true; + return; + } + } + + Item { + id: browserContent + anchors.fill: parent + anchors.margins: Theme.spacingL + + Item { + id: headerArea + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: Math.max(headerIcon.height, headerText.height, refreshButton.height, closeButton.height) + + MouseArea { + anchors.fill: parent + onPressed: windowControls.tryStartMove() + onDoubleClicked: windowControls.tryToggleMaximize() + } + + DankIcon { + id: headerIcon + name: "palette" + size: Theme.iconSize + color: Theme.primary + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: headerText + text: I18n.tr("Browse Themes", "theme browser header") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.left: headerIcon.right + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + } + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + DankActionButton { + id: refreshButton + iconName: "refresh" + iconSize: 18 + iconColor: Theme.primary + visible: !root.isLoading + onClicked: root.refreshThemes() + } + + DankActionButton { + visible: windowControls.supported + iconName: root.maximized ? "fullscreen_exit" : "fullscreen" + iconSize: Theme.iconSize - 2 + iconColor: Theme.outline + onClicked: windowControls.tryToggleMaximize() + } + + DankActionButton { + id: closeButton + iconName: "close" + iconSize: Theme.iconSize - 2 + iconColor: Theme.outline + onClicked: root.hide() + } + } + } + + StyledText { + id: descriptionText + anchors.left: parent.left + anchors.right: parent.right + anchors.top: headerArea.bottom + anchors.topMargin: Theme.spacingM + text: I18n.tr("Install color themes from the DMS theme registry", "theme browser description") + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + wrapMode: Text.WordWrap + } + + DankTextField { + id: browserSearchField + anchors.left: parent.left + anchors.right: parent.right + anchors.top: descriptionText.bottom + anchors.topMargin: Theme.spacingM + height: 48 + cornerRadius: Theme.cornerRadius + backgroundColor: Theme.surfaceContainerHigh + normalBorderColor: Theme.outlineMedium + focusedBorderColor: Theme.primary + leftIconName: "search" + leftIconSize: Theme.iconSize + leftIconColor: Theme.surfaceVariantText + leftIconFocusedColor: Theme.primary + showClearButton: true + textColor: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + placeholderText: I18n.tr("Search themes...", "theme search placeholder") + text: root.searchQuery + focus: true + ignoreLeftRightKeys: true + keyForwardTargets: [browserKeyHandler] + onTextEdited: { + root.searchQuery = text; + root.updateFilteredThemes(); + } + } + + Item { + id: listArea + anchors.left: parent.left + anchors.right: parent.right + anchors.top: browserSearchField.bottom + anchors.topMargin: Theme.spacingM + anchors.bottom: parent.bottom + + Item { + anchors.fill: parent + visible: root.isLoading + + Column { + anchors.centerIn: parent + spacing: Theme.spacingM + + DankIcon { + name: "sync" + size: 48 + color: Theme.primary + anchors.horizontalCenter: parent.horizontalCenter + + RotationAnimator on rotation { + from: 0 + to: 360 + duration: 1000 + loops: Animation.Infinite + running: root.isLoading + } + } + + StyledText { + text: I18n.tr("Loading...", "loading indicator") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + DankListView { + id: themeBrowserList + + anchors.fill: parent + spacing: Theme.spacingS + model: ScriptModel { + values: root.filteredThemes + } + clip: true + visible: !root.isLoading + + ScrollBar.vertical: DankScrollbar { + id: browserScrollbar + } + + delegate: Rectangle { + id: themeDelegate + width: themeBrowserList.width + height: hasPreview ? 140 : themeDelegateContent.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex + property bool isInstalled: modelData.installed || false + property bool isFirstParty: modelData.firstParty || false + property bool hasVariants: modelData.hasVariants || false + property var variants: modelData.variants || null + property string selectedVariantId: { + if (!hasVariants || !variants) + return ""; + if (variants.type === "multi") { + const mode = Theme.isLightMode ? "light" : "dark"; + const defaults = variants.defaults?.[mode] || variants.defaults?.dark || {}; + return (defaults.flavor || "") + (defaults.accent ? "-" + defaults.accent : ""); + } + return variants.default || (variants.options?.[0]?.id ?? ""); + } + property string previewPath: { + const baseDir = "/tmp/dankdots-plugin-registry/themes/" + (modelData.sourceDir || modelData.id); + const mode = Theme.isLightMode ? "light" : "dark"; + if (hasVariants && selectedVariantId) { + if (variants?.type === "multi") + return baseDir + "/preview-" + selectedVariantId + ".svg"; + return baseDir + "/preview-" + selectedVariantId + "-" + mode + ".svg"; + } + return baseDir + "/preview-" + mode + ".svg"; + } + property bool hasPreview: previewImage.status === Image.Ready + color: isSelected ? Theme.primarySelected : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: isSelected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: isSelected ? 2 : 1 + + Row { + id: themeDelegateContent + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + Rectangle { + width: hasPreview ? 180 : 0 + height: parent.height + radius: Theme.cornerRadius - 2 + color: Theme.surfaceContainerHigh + visible: hasPreview + + Image { + id: previewImage + anchors.fill: parent + anchors.margins: 2 + source: "file://" + previewPath + fillMode: Image.PreserveAspectFit + smooth: true + mipmap: true + } + } + + DankIcon { + name: "palette" + size: 48 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + visible: !hasPreview + } + + Column { + width: parent.width - (hasPreview ? 180 : 48) - Theme.spacingM - installButton.width - Theme.spacingM + spacing: 6 + anchors.verticalCenter: parent.verticalCenter + + Row { + spacing: Theme.spacingXS + width: parent.width + + StyledText { + text: modelData.name + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + height: 18 + width: versionText.implicitWidth + Theme.spacingS + radius: 9 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.15) + anchors.verticalCenter: parent.verticalCenter + + StyledText { + id: versionText + anchors.centerIn: parent + text: modelData.version || "1.0.0" + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + } + } + + Rectangle { + height: 18 + width: firstPartyText.implicitWidth + Theme.spacingS + radius: 9 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) + border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) + border.width: 1 + visible: isFirstParty + anchors.verticalCenter: parent.verticalCenter + + StyledText { + id: firstPartyText + anchors.centerIn: parent + text: I18n.tr("official") + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + font.weight: Font.Medium + } + } + + Rectangle { + height: 18 + width: variantsText.implicitWidth + Theme.spacingS + radius: 9 + color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.15) + border.color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.4) + border.width: 1 + visible: themeDelegate.hasVariants + anchors.verticalCenter: parent.verticalCenter + + StyledText { + id: variantsText + anchors.centerIn: parent + text: { + if (themeDelegate.variants?.type === "multi") + return I18n.tr("%1 variants").arg(themeDelegate.variants?.accents?.length ?? 0); + return I18n.tr("%1 variants").arg(themeDelegate.variants?.options?.length ?? 0); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.secondary + font.weight: Font.Medium + } + } + } + + StyledText { + text: I18n.tr("by %1", "author attribution").arg(modelData.author || I18n.tr("Unknown", "unknown author")) + font.pixelSize: Theme.fontSizeMedium + color: Theme.outline + elide: Text.ElideRight + width: parent.width + } + + StyledText { + text: modelData.description || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.WordWrap + maximumLineCount: themeDelegate.hasVariants ? 2 : 3 + elide: Text.ElideRight + visible: modelData.description && modelData.description.length > 0 + } + + Flow { + width: parent.width + spacing: Theme.spacingXS + visible: themeDelegate.hasVariants && themeDelegate.variants?.type !== "multi" + + Repeater { + model: themeDelegate.variants?.options ?? [] + + Rectangle { + property bool isActive: themeDelegate.selectedVariantId === modelData.id + height: 22 + width: variantChipText.implicitWidth + Theme.spacingS * 2 + radius: 11 + color: isActive ? Theme.primary : Theme.surfaceContainerHigh + border.color: isActive ? Theme.primary : Theme.outline + border.width: 1 + + StyledText { + id: variantChipText + anchors.centerIn: parent + text: modelData.name + font.pixelSize: Theme.fontSizeSmall + color: isActive ? Theme.primaryText : Theme.surfaceText + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: themeDelegate.selectedVariantId = modelData.id + } + } + } + } + + Flow { + width: parent.width + spacing: Theme.spacingXS + visible: themeDelegate.hasVariants && themeDelegate.variants?.type === "multi" + + Repeater { + model: themeDelegate.variants?.accents ?? [] + + Rectangle { + width: 18 + height: 18 + radius: 9 + color: modelData.color || Theme.primary + border.color: Theme.outline + border.width: 1 + + DankTooltipV2 { + id: accentTooltip + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: accentTooltip.show(modelData.name || modelData.id, parent, 0, 0, "top") + onExited: accentTooltip.hide() + } + } + } + } + } + + Rectangle { + id: installButton + width: 90 + height: 36 + radius: Theme.cornerRadius + anchors.verticalCenter: parent.verticalCenter + color: isInstalled ? (uninstallMouseArea.containsMouse ? Theme.error : Theme.surfaceVariant) : Theme.primary + opacity: installMouseArea.containsMouse || uninstallMouseArea.containsMouse ? 0.9 : 1 + border.width: isInstalled ? 1 : 0 + border.color: isInstalled ? (uninstallMouseArea.containsMouse ? Theme.error : Theme.outline) : "transparent" + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + } + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: isInstalled ? (uninstallMouseArea.containsMouse ? "delete" : "check") : "download" + size: 16 + color: isInstalled ? (uninstallMouseArea.containsMouse ? "white" : Theme.surfaceText) : Theme.surface + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: { + if (!isInstalled) + return I18n.tr("Install", "install action button"); + if (uninstallMouseArea.containsMouse) + return I18n.tr("Uninstall", "uninstall action button"); + return I18n.tr("Installed", "installed status"); + } + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: isInstalled ? (uninstallMouseArea.containsMouse ? "white" : Theme.surfaceText) : Theme.surface + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: installMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + visible: !isInstalled + onClicked: root.installTheme(modelData.id, modelData.name, false) + } + + MouseArea { + id: uninstallMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + visible: isInstalled + onClicked: root.uninstallTheme(modelData.id, modelData.name) + } + } + } + } + } + + StyledText { + anchors.centerIn: listArea + text: I18n.tr("No themes found", "empty theme list") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + visible: !root.isLoading && root.filteredThemes.length === 0 + } + } + } + } + + FloatingWindowControls { + id: windowControls + targetWindow: root + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/ThemeColorsTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/ThemeColorsTab.qml new file mode 100644 index 0000000..ce080c4 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/ThemeColorsTab.qml @@ -0,0 +1,3016 @@ +import QtCore +import QtQuick +import QtQuick.Effects +import Quickshell +import qs.Common +import qs.Modals.FileBrowser +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: themeColorsTab + + property var cachedIconThemes: SettingsData.availableIconThemes + property var cachedCursorThemes: SettingsData.availableCursorThemes + property var cachedMatugenSchemes: Theme.availableMatugenSchemes.map(option => option.label) + property var installedRegistryThemes: [] + property var templateDetection: [] + + property var cursorIncludeStatus: ({ + "exists": false, + "included": false + }) + property bool checkingCursorInclude: false + property bool fixingCursorInclude: false + + function getCursorConfigPaths() { + const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)); + switch (CompositorService.compositor) { + case "niri": + return { + "configFile": configDir + "/niri/config.kdl", + "cursorFile": configDir + "/niri/dms/cursor.kdl", + "grepPattern": 'include.*"dms/cursor.kdl"', + "includeLine": 'include "dms/cursor.kdl"' + }; + case "hyprland": + return { + "configFile": configDir + "/hypr/hyprland.conf", + "cursorFile": configDir + "/hypr/dms/cursor.conf", + "grepPattern": 'source.*dms/cursor.conf', + "includeLine": "source = ./dms/cursor.conf" + }; + case "dwl": + return { + "configFile": configDir + "/mango/config.conf", + "cursorFile": configDir + "/mango/dms/cursor.conf", + "grepPattern": 'source.*dms/cursor.conf', + "includeLine": "source=./dms/cursor.conf" + }; + default: + return null; + } + } + + function checkCursorIncludeStatus() { + const compositor = CompositorService.compositor; + if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") { + cursorIncludeStatus = { + "exists": false, + "included": false + }; + return; + } + + const filename = (compositor === "niri") ? "cursor.kdl" : "cursor.conf"; + const compositorArg = (compositor === "dwl") ? "mangowc" : compositor; + + checkingCursorInclude = true; + Proc.runCommand("check-cursor-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => { + checkingCursorInclude = false; + if (exitCode !== 0) { + cursorIncludeStatus = { + "exists": false, + "included": false + }; + return; + } + try { + cursorIncludeStatus = JSON.parse(output.trim()); + } catch (e) { + cursorIncludeStatus = { + "exists": false, + "included": false + }; + } + }); + } + + function fixCursorInclude() { + const paths = getCursorConfigPaths(); + if (!paths) + return; + fixingCursorInclude = true; + const cursorDir = paths.cursorFile.substring(0, paths.cursorFile.lastIndexOf("/")); + const unixTime = Math.floor(Date.now() / 1000); + const backupFile = paths.configFile + ".backup" + unixTime; + Proc.runCommand("fix-cursor-include", ["sh", "-c", `cp "${paths.configFile}" "${backupFile}" 2>/dev/null; ` + `mkdir -p "${cursorDir}" && ` + `touch "${paths.cursorFile}" && ` + `if ! grep -v '^[[:space:]]*\\(//\\|#\\)' "${paths.configFile}" 2>/dev/null | grep -q '${paths.grepPattern}'; then ` + `echo '' >> "${paths.configFile}" && ` + `echo '${paths.includeLine}' >> "${paths.configFile}"; fi`], (output, exitCode) => { + fixingCursorInclude = false; + if (exitCode !== 0) + return; + checkCursorIncludeStatus(); + SettingsData.updateCompositorCursor(); + }); + } + + function isTemplateDetected(templateId) { + if (!templateDetection || templateDetection.length === 0) + return true; + var item = templateDetection.find(i => i.id === templateId); + return !item || item.detected !== false; + } + + function getTemplateDescription(templateId, baseDescription) { + if (isTemplateDetected(templateId)) + return baseDescription; + if (baseDescription) + return baseDescription + " · " + I18n.tr("Not detected"); + return I18n.tr("Not detected"); + } + + function getTemplateDescriptionColor(templateId) { + if (isTemplateDetected(templateId)) + return Theme.surfaceVariantText; + return Theme.warning; + } + + function openBlurBorderColorPicker() { + PopoutService.colorPickerModal.selectedColor = SettingsData.blurBorderCustomColor ?? "#ffffff"; + PopoutService.colorPickerModal.pickerTitle = I18n.tr("Blur Border Color"); + PopoutService.colorPickerModal.onColorSelectedCallback = function (color) { + SettingsData.set("blurBorderCustomColor", color.toString()); + }; + PopoutService.colorPickerModal.open(); + } + + function openM3ShadowColorPicker() { + PopoutService.colorPickerModal.selectedColor = SettingsData.m3ElevationCustomColor ?? "#000000"; + PopoutService.colorPickerModal.pickerTitle = I18n.tr("Shadow Color"); + PopoutService.colorPickerModal.onColorSelectedCallback = function (color) { + SettingsData.set("m3ElevationCustomColor", color.toString()); + }; + PopoutService.colorPickerModal.show(); + } + + function formatThemeAutoTime(isoString) { + if (!isoString) + return ""; + try { + const date = new Date(isoString); + if (isNaN(date.getTime())) + return ""; + return date.toLocaleTimeString(Qt.locale(), "HH:mm"); + } catch (e) { + return ""; + } + } + + Component.onCompleted: { + SettingsData.detectAvailableIconThemes(); + SettingsData.detectAvailableCursorThemes(); + if (DMSService.dmsAvailable) + DMSService.listInstalledThemes(); + if (PopoutService.pendingThemeInstall) + Qt.callLater(() => showThemeBrowser()); + Proc.runCommand("template-check", ["dms", "matugen", "check"], (output, exitCode) => { + if (exitCode !== 0) + return; + try { + themeColorsTab.templateDetection = JSON.parse(output.trim()); + } catch (e) {} + }); + if (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl) + checkCursorIncludeStatus(); + } + + Connections { + target: DMSService + function onInstalledThemesReceived(themes) { + themeColorsTab.installedRegistryThemes = themes; + } + } + + Connections { + target: PopoutService + function onPendingThemeInstallChanged() { + if (PopoutService.pendingThemeInstall) + showThemeBrowser(); + } + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + tab: "theme" + tags: ["color", "palette", "theme", "appearance"] + title: I18n.tr("Theme Color") + settingKey: "themeColor" + iconName: "palette" + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + property string registryThemeName: { + if (Theme.currentThemeCategory !== "registry") + return ""; + for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) { + var t = themeColorsTab.installedRegistryThemes[i]; + if (SettingsData.customThemeFile && SettingsData.customThemeFile.endsWith((t.sourceDir || t.id) + "/theme.json")) + return t.name; + } + return ""; + } + text: { + if (Theme.currentTheme === Theme.dynamic) + return I18n.tr("Current Theme: %1", "current theme label").arg(I18n.tr("Dynamic", "dynamic theme name")); + if (Theme.currentThemeCategory === "registry" && registryThemeName) + return I18n.tr("Current Theme: %1", "current theme label").arg(registryThemeName); + return I18n.tr("Current Theme: %1", "current theme label").arg(Theme.getThemeColors(Theme.currentThemeName).name); + } + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: { + if (Theme.currentTheme === Theme.dynamic) + return I18n.tr("Material colors generated from wallpaper", "dynamic theme description"); + if (Theme.currentThemeCategory === "registry") + return I18n.tr("Color theme from DMS registry", "registry theme description"); + if (Theme.currentTheme === Theme.custom) + return I18n.tr("Custom theme loaded from JSON file", "custom theme description"); + return I18n.tr("Material Design inspired color themes", "generic theme description"); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + wrapMode: Text.WordWrap + width: Math.min(parent.width, 400) + horizontalAlignment: Text.AlignHCenter + } + } + + Column { + id: themeCategoryColumn + spacing: Theme.spacingM + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + + Item { + width: parent.width + height: themeCategoryGroup.implicitHeight + clip: true + + DankButtonGroup { + id: themeCategoryGroup + anchors.horizontalCenter: parent.horizontalCenter + buttonPadding: parent.width < 420 ? Theme.spacingS : Theme.spacingL + minButtonWidth: parent.width < 420 ? 44 : 64 + textSize: parent.width < 420 ? Theme.fontSizeSmall : Theme.fontSizeMedium + property bool isRegistryTheme: Theme.currentThemeCategory === "registry" + property int pendingIndex: -1 + property int computedIndex: { + if (isRegistryTheme) + return 3; + if (Theme.currentTheme === Theme.dynamic) + return 1; + if (Theme.currentThemeName === "custom") + return 2; + return 0; + } + + model: DMSService.dmsAvailable ? [I18n.tr("Generic", "theme category option"), I18n.tr("Auto", "theme category option"), I18n.tr("Custom", "theme category option"), I18n.tr("Browse", "theme category option")] : [I18n.tr("Generic", "theme category option"), I18n.tr("Auto", "theme category option"), I18n.tr("Custom", "theme category option")] + currentIndex: pendingIndex >= 0 ? pendingIndex : computedIndex + selectionMode: "single" + onSelectionChanged: (index, selected) => { + if (!selected) + return; + pendingIndex = index; + } + onAnimationCompleted: { + if (pendingIndex < 0) + return; + const idx = pendingIndex; + pendingIndex = -1; + switch (idx) { + case 0: + Theme.switchThemeCategory("generic", "blue"); + break; + case 1: + if (ToastService.wallpaperErrorStatus === "matugen_missing") + ToastService.showError(I18n.tr("matugen not found - install matugen package for dynamic theming", "matugen error")); + else if (ToastService.wallpaperErrorStatus === "error") + ToastService.showError(I18n.tr("Wallpaper processing failed - check wallpaper path", "wallpaper error")); + else + Theme.switchThemeCategory("dynamic", Theme.dynamic); + break; + case 2: + Theme.switchThemeCategory("custom", "custom"); + break; + case 3: + Theme.switchThemeCategory("registry", ""); + break; + } + } + } + } + + Item { + width: parent.width + height: genericColorGrid.implicitHeight + Math.ceil(genericColorGrid.dotSize * 0.05) + visible: Theme.currentThemeCategory === "generic" && Theme.currentTheme !== Theme.dynamic && Theme.currentThemeName !== "custom" + + Grid { + id: genericColorGrid + property var colorList: ["blue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral", "monochrome"] + property int dotSize: parent.width < 300 ? 28 : 32 + columns: Math.ceil(colorList.length / 2) + rowSpacing: Theme.spacingS + columnSpacing: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + + Repeater { + model: genericColorGrid.colorList + + Rectangle { + required property string modelData + property string themeName: modelData + width: genericColorGrid.dotSize + height: genericColorGrid.dotSize + radius: width / 2 + color: Theme.getThemeColors(themeName).primary + border.color: Theme.outline + border.width: (Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic) ? 2 : 1 + scale: (Theme.currentThemeName === themeName && Theme.currentTheme !== Theme.dynamic) ? 1.1 : 1 + + Rectangle { + width: nameText.contentWidth + Theme.spacingS * 2 + height: nameText.contentHeight + Theme.spacingXS * 2 + color: Theme.surfaceContainer + radius: Theme.cornerRadius + anchors.bottom: parent.top + anchors.bottomMargin: Theme.spacingXS + anchors.horizontalCenter: parent.horizontalCenter + visible: mouseArea.containsMouse + + StyledText { + id: nameText + text: Theme.getThemeColors(parent.parent.themeName).name + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.centerIn: parent + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Theme.switchTheme(parent.themeName) + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: Theme.currentTheme === Theme.dynamic && Theme.currentThemeCategory !== "registry" + + Row { + width: parent.width + spacing: Theme.spacingM + + StyledRect { + width: 120 + height: 90 + radius: Theme.cornerRadius + color: Theme.surfaceVariant + + Image { + anchors.fill: parent + anchors.margins: 1 + source: { + var wp = Theme.wallpaperPath; + if (!wp || wp === "" || wp.startsWith("#")) + return ""; + if (wp.startsWith("file://")) + wp = wp.substring(7); + return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/'); + } + fillMode: Image.PreserveAspectCrop + visible: Theme.wallpaperPath && !Theme.wallpaperPath.startsWith("#") + sourceSize.width: 120 + sourceSize.height: 120 + asynchronous: true + layer.enabled: true + layer.effect: MultiEffect { + maskEnabled: true + maskSource: autoWallpaperMask + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: Theme.cornerRadius - 1 + color: Theme.wallpaperPath && Theme.wallpaperPath.startsWith("#") ? Theme.wallpaperPath : "transparent" + visible: Theme.wallpaperPath && Theme.wallpaperPath.startsWith("#") + } + + Rectangle { + id: autoWallpaperMask + anchors.fill: parent + anchors.margins: 1 + radius: Theme.cornerRadius - 1 + color: "black" + visible: false + layer.enabled: true + } + + DankIcon { + anchors.centerIn: parent + name: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? "error" : "palette" + size: Theme.iconSizeLarge + color: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? Theme.error : Theme.surfaceVariantText + visible: !Theme.wallpaperPath + } + } + + Column { + width: parent.width - 120 - Theme.spacingM + spacing: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: { + if (ToastService.wallpaperErrorStatus === "error") + return I18n.tr("Wallpaper Error", "wallpaper error status"); + if (ToastService.wallpaperErrorStatus === "matugen_missing") + return I18n.tr("Matugen Missing", "matugen not found status"); + if (Theme.wallpaperPath) + return Theme.wallpaperPath.split('/').pop(); + return I18n.tr("No wallpaper selected", "no wallpaper status"); + } + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + elide: Text.ElideMiddle + maximumLineCount: 1 + width: parent.width + } + + StyledText { + text: { + if (ToastService.wallpaperErrorStatus === "error") + return I18n.tr("Wallpaper processing failed", "wallpaper processing error"); + if (ToastService.wallpaperErrorStatus === "matugen_missing") + return I18n.tr("Install matugen package for dynamic theming", "matugen installation hint"); + if (Theme.wallpaperPath) + return Theme.wallpaperPath; + return I18n.tr("Dynamic colors from wallpaper", "dynamic colors description"); + } + font.pixelSize: Theme.fontSizeSmall + color: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? Theme.error : Theme.surfaceVariantText + elide: Text.ElideMiddle + maximumLineCount: 2 + width: parent.width + wrapMode: Text.WordWrap + } + } + } + + SettingsDropdownRow { + tab: "theme" + tags: ["matugen", "palette", "algorithm", "dynamic"] + settingKey: "matugenScheme" + text: I18n.tr("Matugen Palette") + description: I18n.tr("Select the palette algorithm used for wallpaper-based colors") + options: cachedMatugenSchemes + currentValue: Theme.getMatugenScheme(SettingsData.matugenScheme).label + enabled: Theme.matugenAvailable + opacity: enabled ? 1 : 0.4 + onValueChanged: value => { + for (var i = 0; i < Theme.availableMatugenSchemes.length; i++) { + var option = Theme.availableMatugenSchemes[i]; + if (option.label === value) { + SettingsData.setMatugenScheme(option.value); + break; + } + } + } + } + + StyledText { + text: { + var scheme = Theme.getMatugenScheme(SettingsData.matugenScheme); + return scheme.description + " (" + scheme.value + ")"; + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + } + + SettingsSliderRow { + tab: "theme" + tags: ["matugen", "contrast", "dynamic"] + settingKey: "matugenContrast" + text: I18n.tr("Matugen Contrast") + description: I18n.tr("Adjusts contrast of generated colors (-100 = minimum, 0 = standard, 100 = maximum)") + value: Math.round(SettingsData.matugenContrast * 100) + minimum: -100 + maximum: 100 + unit: "%" + defaultValue: 0 + enabled: Theme.matugenAvailable + opacity: enabled ? 1 : 0.4 + onSliderDragFinished: finalValue => SettingsData.setMatugenContrast(finalValue / 100) + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: Theme.currentThemeName === "custom" && Theme.currentThemeCategory !== "registry" + + Row { + width: parent.width + spacing: Theme.spacingM + + DankActionButton { + buttonSize: 48 + iconName: "folder_open" + iconSize: Theme.iconSize + backgroundColor: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + iconColor: Theme.primary + onClicked: fileBrowserModal.open() + } + + Column { + width: parent.width - 48 - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: SettingsData.customThemeFile ? SettingsData.customThemeFile.split('/').pop() : I18n.tr("No custom theme file", "no custom theme file status") + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + elide: Text.ElideMiddle + maximumLineCount: 1 + width: parent.width + } + + StyledText { + text: SettingsData.customThemeFile || I18n.tr("Click to select a custom theme JSON file", "custom theme file hint") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideMiddle + maximumLineCount: 1 + width: parent.width + } + } + } + } + + Column { + id: registrySection + width: parent.width + spacing: Theme.spacingM + visible: Theme.currentThemeCategory === "registry" + + Grid { + id: themeGrid + property int cardWidth: registrySection.width < 350 ? 100 : 140 + property int cardHeight: registrySection.width < 350 ? 72 : 100 + columns: Math.max(1, Math.floor((registrySection.width + spacing) / (cardWidth + spacing))) + spacing: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + visible: themeColorsTab.installedRegistryThemes.length > 0 + + Repeater { + model: themeColorsTab.installedRegistryThemes + + Rectangle { + id: themeCard + property bool isActive: Theme.currentThemeCategory === "registry" && Theme.currentThemeName === "custom" && SettingsData.customThemeFile && SettingsData.customThemeFile.endsWith((modelData.sourceDir || modelData.id) + "/theme.json") + property bool hasVariants: modelData.hasVariants || false + property var variants: modelData.variants || null + property string selectedVariant: hasVariants ? SettingsData.getRegistryThemeVariant(modelData.id, variants?.default || "") : "" + property string previewPath: { + const baseDir = Quickshell.env("HOME") + "/.config/DankMaterialShell/themes/" + (modelData.sourceDir || modelData.id); + const mode = Theme.isLightMode ? "light" : "dark"; + if (hasVariants && selectedVariant) + return baseDir + "/preview-" + selectedVariant + "-" + mode + ".svg"; + return baseDir + "/preview-" + mode + ".svg"; + } + width: themeGrid.cardWidth + height: themeGrid.cardHeight + radius: Theme.cornerRadius + color: Theme.surfaceVariant + border.color: isActive ? Theme.primary : Theme.outline + border.width: isActive ? 2 : 1 + scale: isActive ? 1.03 : 1 + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Image { + id: previewImage + anchors.fill: parent + anchors.margins: 2 + source: "file://" + themeCard.previewPath + fillMode: Image.PreserveAspectFit + smooth: true + mipmap: true + } + + DankIcon { + anchors.centerIn: parent + name: "palette" + size: themeGrid.cardWidth < 120 ? 24 : 32 + color: Theme.primary + visible: previewImage.status === Image.Error || previewImage.status === Image.Null + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: themeGrid.cardWidth < 120 ? 18 : 22 + radius: Theme.cornerRadius + color: Qt.rgba(0, 0, 0, 0.6) + + StyledText { + anchors.centerIn: parent + text: modelData.name + font.pixelSize: themeGrid.cardWidth < 120 ? Theme.fontSizeSmall - 2 : Theme.fontSizeSmall + color: "white" + font.weight: Font.Medium + elide: Text.ElideRight + width: parent.width - Theme.spacingXS * 2 + horizontalAlignment: Text.AlignHCenter + } + } + + Rectangle { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: themeGrid.cardWidth < 120 ? 2 : 4 + width: themeGrid.cardWidth < 120 ? 16 : 20 + height: width + radius: width / 2 + color: Theme.primary + visible: themeCard.isActive + + DankIcon { + anchors.centerIn: parent + name: "check" + size: themeGrid.cardWidth < 120 ? 10 : 14 + color: Theme.surface + } + } + + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: themeGrid.cardWidth < 120 ? 2 : 4 + width: themeGrid.cardWidth < 120 ? 16 : 20 + height: width + radius: width / 2 + color: Theme.secondary + visible: themeCard.hasVariants && !deleteButton.visible + + StyledText { + anchors.centerIn: parent + text: { + if (themeCard.variants?.type === "multi") + return themeCard.variants?.accents?.length || 0; + return themeCard.variants?.options?.length || 0; + } + font.pixelSize: themeGrid.cardWidth < 120 ? Theme.fontSizeSmall - 4 : Theme.fontSizeSmall - 2 + color: Theme.surface + font.weight: Font.Bold + } + } + + MouseArea { + id: cardMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + const themesDir = Quickshell.env("HOME") + "/.config/DankMaterialShell/themes"; + const themePath = themesDir + "/" + (modelData.sourceDir || modelData.id) + "/theme.json"; + SettingsData.set("customThemeFile", themePath); + Theme.switchTheme("custom", true, true); + } + } + + Rectangle { + id: deleteButton + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: themeGrid.cardWidth < 120 ? 2 : 4 + width: themeGrid.cardWidth < 120 ? 18 : 24 + height: width + radius: width / 2 + color: deleteMouseArea.containsMouse ? Theme.error : Qt.rgba(0, 0, 0, 0.6) + opacity: cardMouseArea.containsMouse || deleteMouseArea.containsMouse ? 1 : 0 + visible: opacity > 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + } + } + + DankIcon { + anchors.centerIn: parent + name: "close" + size: themeGrid.cardWidth < 120 ? 10 : 14 + color: "white" + } + + MouseArea { + id: deleteMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + ToastService.showInfo(I18n.tr("Uninstalling: %1", "uninstallation progress").arg(modelData.name)); + DMSService.uninstallTheme(modelData.id, response => { + if (response.error) { + ToastService.showError(I18n.tr("Uninstall failed: %1", "uninstallation error").arg(response.error)); + return; + } + ToastService.showInfo(I18n.tr("Uninstalled: %1", "uninstallation success").arg(modelData.name)); + DMSService.listInstalledThemes(); + }); + } + } + } + } + } + } + + StyledText { + text: I18n.tr("No themes installed. Browse themes to install from the registry.", "no registry themes installed hint") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + visible: themeColorsTab.installedRegistryThemes.length === 0 + horizontalAlignment: Text.AlignHCenter + } + + DankButton { + text: I18n.tr("Browse Themes", "browse themes button") + iconName: "store" + anchors.horizontalCenter: parent.horizontalCenter + onClicked: showThemeBrowser() + } + } + + Column { + id: variantSelector + width: parent.width + spacing: Theme.spacingS + visible: activeThemeId !== "" && activeThemeVariants !== null && (isMultiVariant || (activeThemeVariants.options && activeThemeVariants.options.length > 0)) + + property string activeThemeId: { + switch (Theme.currentThemeCategory) { + case "registry": + if (Theme.currentTheme !== "custom") + return ""; + for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) { + var t = themeColorsTab.installedRegistryThemes[i]; + if (SettingsData.customThemeFile && SettingsData.customThemeFile.endsWith((t.sourceDir || t.id) + "/theme.json")) + return t.id; + } + return ""; + case "custom": + return Theme.currentThemeId || ""; + default: + return ""; + } + } + property var activeThemeVariants: { + if (!activeThemeId) + return null; + switch (Theme.currentThemeCategory) { + case "registry": + for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) { + var t = themeColorsTab.installedRegistryThemes[i]; + if (t.id === activeThemeId && t.hasVariants) + return t.variants; + } + return null; + case "custom": + return Theme.currentThemeVariants || null; + default: + return null; + } + } + property bool isMultiVariant: activeThemeVariants?.type === "multi" + property string colorMode: Theme.isLightMode ? "light" : "dark" + property var multiDefaults: { + if (!isMultiVariant || !activeThemeVariants?.defaults) + return {}; + return activeThemeVariants.defaults[colorMode] || activeThemeVariants.defaults.dark || {}; + } + property var storedMulti: activeThemeId ? SettingsData.getRegistryThemeMultiVariant(activeThemeId, multiDefaults, colorMode) : multiDefaults + property string selectedFlavor: { + var sf = storedMulti.flavor || multiDefaults.flavor || ""; + for (var i = 0; i < flavorOptions.length; i++) { + if (flavorOptions[i].id === sf) + return sf; + } + if (flavorOptions.length > 0) + return flavorOptions[0].id; + return sf; + } + property string selectedAccent: storedMulti.accent || multiDefaults.accent || "" + property var flavorOptions: { + if (!isMultiVariant || !activeThemeVariants?.flavors) + return []; + return activeThemeVariants.flavors.filter(f => { + if (f.mode) + return f.mode === colorMode || f.mode === "both"; + return !!f[colorMode]; + }); + } + property var flavorNames: flavorOptions.map(f => f.name) + property int flavorIndex: { + for (var i = 0; i < flavorOptions.length; i++) { + if (flavorOptions[i].id === selectedFlavor) + return i; + } + return 0; + } + property string selectedVariant: activeThemeId ? SettingsData.getRegistryThemeVariant(activeThemeId, activeThemeVariants?.default || "") : "" + property var variantNames: { + if (!activeThemeVariants?.options) + return []; + return activeThemeVariants.options.map(v => v.name); + } + property int selectedIndex: { + if (!activeThemeVariants?.options || !selectedVariant) + return 0; + for (var i = 0; i < activeThemeVariants.options.length; i++) { + if (activeThemeVariants.options[i].id === selectedVariant) + return i; + } + return 0; + } + + Item { + width: parent.width + height: flavorButtonGroup.implicitHeight + clip: true + visible: variantSelector.isMultiVariant && variantSelector.flavorOptions.length > 1 + + DankButtonGroup { + id: flavorButtonGroup + anchors.horizontalCenter: parent.horizontalCenter + property int _count: variantSelector.flavorNames.length + property real _maxPerItem: _count > 1 ? (parent.width - (_count - 1) * spacing) / _count : parent.width + buttonPadding: _maxPerItem < 55 ? Theme.spacingXS : (_maxPerItem < 75 ? Theme.spacingS : Theme.spacingL) + minButtonWidth: Math.min(_maxPerItem < 55 ? 28 : (_maxPerItem < 75 ? 44 : 64), Math.max(28, Math.floor(_maxPerItem))) + textSize: _maxPerItem < 55 ? Theme.fontSizeSmall - 2 : (_maxPerItem < 75 ? Theme.fontSizeSmall : Theme.fontSizeMedium) + checkEnabled: _maxPerItem >= 55 + property int pendingIndex: -1 + model: variantSelector.flavorNames + currentIndex: pendingIndex >= 0 ? pendingIndex : variantSelector.flavorIndex + selectionMode: "single" + onSelectionChanged: (index, selected) => { + if (!selected) + return; + pendingIndex = index; + } + onAnimationCompleted: { + if (pendingIndex < 0 || pendingIndex >= variantSelector.flavorOptions.length) + return; + const flavorId = variantSelector.flavorOptions[pendingIndex]?.id; + const idx = pendingIndex; + pendingIndex = -1; + if (!flavorId || flavorId === variantSelector.selectedFlavor) + return; + Theme.screenTransition(); + SettingsData.setRegistryThemeMultiVariant(variantSelector.activeThemeId, flavorId, variantSelector.selectedAccent, variantSelector.colorMode); + } + } + } + + Item { + width: parent.width + height: accentColorsGrid.implicitHeight + visible: variantSelector.isMultiVariant && variantSelector.activeThemeVariants?.accents?.length > 0 + + Grid { + id: accentColorsGrid + property int accentCount: variantSelector.activeThemeVariants?.accents?.length ?? 0 + property int dotSize: parent.width < 300 ? 28 : 32 + columns: accentCount > 0 ? Math.ceil(accentCount / 2) : 1 + rowSpacing: Theme.spacingS + columnSpacing: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + + Repeater { + model: variantSelector.activeThemeVariants?.accents || [] + + Rectangle { + required property var modelData + required property int index + property string accentId: modelData.id + property bool isSelected: accentId === variantSelector.selectedAccent + width: accentColorsGrid.dotSize + height: accentColorsGrid.dotSize + radius: width / 2 + color: modelData.color || modelData[variantSelector.selectedFlavor]?.primary || Theme.primary + border.color: Theme.outline + border.width: isSelected ? 2 : 1 + scale: isSelected ? 1.1 : 1 + + Rectangle { + width: accentNameText.contentWidth + Theme.spacingS * 2 + height: accentNameText.contentHeight + Theme.spacingXS * 2 + color: Theme.surfaceContainer + radius: Theme.cornerRadius + anchors.bottom: parent.top + anchors.bottomMargin: Theme.spacingXS + anchors.horizontalCenter: parent.horizontalCenter + visible: accentMouseArea.containsMouse + + StyledText { + id: accentNameText + text: modelData.name + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.centerIn: parent + } + } + + MouseArea { + id: accentMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (parent.isSelected) + return; + Theme.screenTransition(); + SettingsData.setRegistryThemeMultiVariant(variantSelector.activeThemeId, variantSelector.selectedFlavor, parent.accentId, variantSelector.colorMode); + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + } + } + } + } + + Item { + width: parent.width + height: variantButtonGroup.implicitHeight + clip: true + visible: !variantSelector.isMultiVariant && variantSelector.variantNames.length > 0 + + DankButtonGroup { + id: variantButtonGroup + anchors.horizontalCenter: parent.horizontalCenter + property int _count: variantSelector.variantNames.length + property real _maxPerItem: _count > 1 ? (parent.width - (_count - 1) * spacing) / _count : parent.width + buttonPadding: _maxPerItem < 55 ? Theme.spacingXS : (_maxPerItem < 75 ? Theme.spacingS : Theme.spacingL) + minButtonWidth: Math.min(_maxPerItem < 55 ? 28 : (_maxPerItem < 75 ? 44 : 64), Math.max(28, Math.floor(_maxPerItem))) + textSize: _maxPerItem < 55 ? Theme.fontSizeSmall - 2 : (_maxPerItem < 75 ? Theme.fontSizeSmall : Theme.fontSizeMedium) + checkEnabled: _maxPerItem >= 55 + property int pendingIndex: -1 + model: variantSelector.variantNames + currentIndex: pendingIndex >= 0 ? pendingIndex : variantSelector.selectedIndex + selectionMode: "single" + onSelectionChanged: (index, selected) => { + if (!selected) + return; + pendingIndex = index; + } + onAnimationCompleted: { + if (pendingIndex < 0 || !variantSelector.activeThemeVariants?.options) + return; + const variantId = variantSelector.activeThemeVariants.options[pendingIndex]?.id; + const idx = pendingIndex; + pendingIndex = -1; + if (!variantId || variantId === variantSelector.selectedVariant) + return; + Theme.screenTransition(); + SettingsData.setRegistryThemeVariant(variantSelector.activeThemeId, variantId); + } + } + } + } + } + } + + SettingsCard { + tab: "theme" + tags: ["automatic", "color", "mode", "schedule", "sunrise", "sunset"] + title: I18n.tr("Automatic Color Mode") + settingKey: "automaticColorMode" + iconName: "schedule" + + Column { + width: parent.width + spacing: Theme.spacingM + + DankToggle { + id: themeModeAutoToggle + width: parent.width + text: I18n.tr("Automatic Control") + checked: SessionData.themeModeAutoEnabled + onToggled: checked => { + SessionData.setThemeModeAutoEnabled(checked); + } + + Connections { + target: SessionData + function onThemeModeAutoEnabledChanged() { + themeModeAutoToggle.checked = SessionData.themeModeAutoEnabled; + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: SessionData.themeModeAutoEnabled + + DankToggle { + width: parent.width + text: I18n.tr("Share Gamma Control Settings") + checked: SessionData.themeModeShareGammaSettings + onToggled: checked => { + SessionData.setThemeModeShareGammaSettings(checked); + } + } + + Item { + width: parent.width + height: 45 + Theme.spacingM + + DankTabBar { + id: themeModeTabBar + width: 200 + height: 45 + anchors.horizontalCenter: parent.horizontalCenter + model: [ + { + "text": I18n.tr("Time", "theme auto mode tab"), + "icon": "access_time" + }, + { + "text": I18n.tr("Location", "theme auto mode tab"), + "icon": "place" + } + ] + + Component.onCompleted: { + currentIndex = SessionData.themeModeAutoMode === "location" ? 1 : 0; + Qt.callLater(updateIndicator); + } + + onTabClicked: index => { + SessionData.setThemeModeAutoMode(index === 1 ? "location" : "time"); + currentIndex = index; + } + + Connections { + target: SessionData + function onThemeModeAutoModeChanged() { + themeModeTabBar.currentIndex = SessionData.themeModeAutoMode === "location" ? 1 : 0; + Qt.callLater(themeModeTabBar.updateIndicator); + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: SessionData.themeModeAutoMode === "time" && !SessionData.themeModeShareGammaSettings + + Column { + spacing: Theme.spacingXS + anchors.horizontalCenter: parent.horizontalCenter + + Row { + spacing: Theme.spacingM + + StyledText { + text: "" + width: 50 + height: 20 + } + + StyledText { + text: I18n.tr("Hour") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: 70 + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + text: I18n.tr("Minute") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: 70 + horizontalAlignment: Text.AlignHCenter + } + } + + Row { + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Start") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: 50 + height: 40 + verticalAlignment: Text.AlignVCenter + } + + DankDropdown { + dropdownWidth: 70 + currentValue: SessionData.themeModeStartHour.toString() + options: { + var hours = []; + for (var i = 0; i < 24; i++) + hours.push(i.toString()); + return hours; + } + onValueChanged: value => { + SessionData.setThemeModeStartHour(parseInt(value)); + } + } + + DankDropdown { + dropdownWidth: 70 + currentValue: SessionData.themeModeStartMinute.toString().padStart(2, '0') + options: { + var minutes = []; + for (var i = 0; i < 60; i += 5) { + minutes.push(i.toString().padStart(2, '0')); + } + return minutes; + } + onValueChanged: value => { + SessionData.setThemeModeStartMinute(parseInt(value)); + } + } + } + + Row { + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("End") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: 50 + height: 40 + verticalAlignment: Text.AlignVCenter + } + + DankDropdown { + dropdownWidth: 70 + currentValue: SessionData.themeModeEndHour.toString() + options: { + var hours = []; + for (var i = 0; i < 24; i++) + hours.push(i.toString()); + return hours; + } + onValueChanged: value => { + SessionData.setThemeModeEndHour(parseInt(value)); + } + } + + DankDropdown { + dropdownWidth: 70 + currentValue: SessionData.themeModeEndMinute.toString().padStart(2, '0') + options: { + var minutes = []; + for (var i = 0; i < 60; i += 5) { + minutes.push(i.toString().padStart(2, '0')); + } + return minutes; + } + onValueChanged: value => { + SessionData.setThemeModeEndMinute(parseInt(value)); + } + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: SessionData.themeModeAutoMode === "location" && !SessionData.themeModeShareGammaSettings + + DankToggle { + id: themeModeIpLocationToggle + width: parent.width + text: I18n.tr("Use IP Location") + checked: SessionData.nightModeUseIPLocation || false + onToggled: checked => { + SessionData.setNightModeUseIPLocation(checked); + } + + Connections { + target: SessionData + function onNightModeUseIPLocationChanged() { + themeModeIpLocationToggle.checked = SessionData.nightModeUseIPLocation; + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: !SessionData.nightModeUseIPLocation + + StyledText { + text: I18n.tr("Manual Coordinates") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + horizontalAlignment: Text.AlignHCenter + width: parent.width + } + + Row { + spacing: Theme.spacingL + anchors.horizontalCenter: parent.horizontalCenter + + Column { + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Latitude") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + DankTextField { + width: 120 + height: 40 + text: SessionData.latitude.toString() + placeholderText: "0.0" + onEditingFinished: { + const lat = parseFloat(text); + if (!isNaN(lat) && lat >= -90 && lat <= 90 && lat !== SessionData.latitude) { + SessionData.setLatitude(lat); + } + } + } + } + + Column { + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Longitude") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + DankTextField { + width: 120 + height: 40 + text: SessionData.longitude.toString() + placeholderText: "0.0" + onEditingFinished: { + const lon = parseFloat(text); + if (!isNaN(lon) && lon >= -180 && lon <= 180 && lon !== SessionData.longitude) { + SessionData.setLongitude(lon); + } + } + } + } + } + + StyledText { + text: I18n.tr("Uses sunrise/sunset times based on your location.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + } + + StyledText { + width: parent.width + text: I18n.tr("Using shared settings from Gamma Control") + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + visible: SessionData.themeModeShareGammaSettings + } + + Rectangle { + width: parent.width + height: statusRow.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Row { + id: statusRow + anchors.centerIn: parent + spacing: Theme.spacingL + width: parent.width - Theme.spacingM * 2 + + Column { + spacing: 2 + width: (parent.width - Theme.spacingL * 2) / 3 + anchors.verticalCenter: parent.verticalCenter + + Row { + spacing: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + + Rectangle { + width: 8 + height: 8 + radius: 4 + color: SessionData.themeModeAutoEnabled ? Theme.success : Theme.error + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Automation") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + } + + StyledText { + text: SessionData.themeModeAutoEnabled ? I18n.tr("Enabled") : I18n.tr("Disabled") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + horizontalAlignment: Text.AlignHCenter + width: parent.width + } + } + + Column { + spacing: 2 + width: (parent.width - Theme.spacingL * 2) / 3 + anchors.verticalCenter: parent.verticalCenter + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingS + + DankIcon { + name: SessionData.isLightMode ? "light_mode" : "dark_mode" + size: Theme.iconSize + color: SessionData.isLightMode ? "#FFA726" : "#7E57C2" + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: SessionData.isLightMode ? I18n.tr("Light Mode") : I18n.tr("Dark Mode") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Bold + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + StyledText { + text: I18n.tr("Active") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + horizontalAlignment: Text.AlignHCenter + width: parent.width + } + } + + Column { + spacing: 2 + width: (parent.width - Theme.spacingL * 2) / 3 + anchors.verticalCenter: parent.verticalCenter + visible: SessionData.themeModeAutoEnabled && SessionData.themeModeNextTransition + + Row { + spacing: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + + DankIcon { + name: "schedule" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Next Transition") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + StyledText { + text: themeColorsTab.formatThemeAutoTime(SessionData.themeModeNextTransition) + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + horizontalAlignment: Text.AlignHCenter + width: parent.width + } + } + } + } + } + } + } + + SettingsCard { + tab: "theme" + tags: ["light", "dark", "mode", "appearance"] + title: I18n.tr("Color Mode") + settingKey: "colorMode" + iconName: "contrast" + + SettingsToggleRow { + tab: "theme" + tags: ["light", "dark", "mode"] + settingKey: "isLightMode" + text: I18n.tr("Light Mode") + description: I18n.tr("Use light theme instead of dark theme") + checked: SessionData.isLightMode + onToggled: checked => { + Theme.screenTransition(); + Theme.setLightMode(checked); + } + } + } + + SettingsCard { + tab: "theme" + tags: ["transparency", "opacity", "widget", "styling"] + title: I18n.tr("Widget Styling") + settingKey: "widgetStyling" + iconName: "opacity" + + SettingsButtonGroupRow { + tab: "theme" + tags: ["widget", "style", "colorful", "default"] + settingKey: "widgetColorMode" + text: I18n.tr("Widget Style") + description: I18n.tr("Change bar appearance") + model: [I18n.tr("Default", "widget style option"), I18n.tr("Colorful", "widget style option")] + currentIndex: SettingsData.widgetColorMode === "colorful" ? 1 : 0 + onSelectionChanged: (index, selected) => { + if (!selected) + return; + SettingsData.set("widgetColorMode", index === 1 ? "colorful" : "default"); + } + } + + SettingsButtonGroupRow { + tab: "theme" + tags: ["widget", "background", "color"] + settingKey: "widgetBackgroundColor" + text: I18n.tr("Widget Background Color") + description: I18n.tr("Choose the background color for widgets") + model: ["sth", "s", "sc", "sch"] + buttonHeight: 20 + minButtonWidth: 32 + buttonPadding: Theme.spacingS + checkIconSize: Theme.iconSizeSmall - 2 + textSize: Theme.fontSizeSmall - 2 + spacing: 1 + currentIndex: { + switch (SettingsData.widgetBackgroundColor) { + case "sth": + return 0; + case "s": + return 1; + case "sc": + return 2; + case "sch": + return 3; + default: + return 0; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + const colorOptions = ["sth", "s", "sc", "sch"]; + SettingsData.set("widgetBackgroundColor", colorOptions[index]); + } + } + + SettingsDropdownRow { + tab: "theme" + tags: ["control", "center", "tile", "button", "color", "active"] + settingKey: "controlCenterTileColorMode" + text: I18n.tr("Control Center Tile Color") + description: I18n.tr("Active tile background and icon color", "control center tile color setting description") + options: [I18n.tr("Primary", "tile color option"), I18n.tr("Primary Container", "tile color option"), I18n.tr("Secondary", "tile color option"), I18n.tr("Surface Variant", "tile color option")] + currentValue: { + switch (SettingsData.controlCenterTileColorMode) { + case "primaryContainer": + return I18n.tr("Primary Container", "tile color option"); + case "secondary": + return I18n.tr("Secondary", "tile color option"); + case "surfaceVariant": + return I18n.tr("Surface Variant", "tile color option"); + default: + return I18n.tr("Primary", "tile color option"); + } + } + onValueChanged: value => { + if (value === I18n.tr("Primary Container", "tile color option")) { + SettingsData.set("controlCenterTileColorMode", "primaryContainer"); + } else if (value === I18n.tr("Secondary", "tile color option")) { + SettingsData.set("controlCenterTileColorMode", "secondary"); + } else if (value === I18n.tr("Surface Variant", "tile color option")) { + SettingsData.set("controlCenterTileColorMode", "surfaceVariant"); + } else { + SettingsData.set("controlCenterTileColorMode", "primary"); + } + } + } + + SettingsDropdownRow { + tab: "theme" + tags: ["button", "color", "primary", "accent"] + settingKey: "buttonColorMode" + text: I18n.tr("Button Color") + description: I18n.tr("Color for primary action buttons") + options: [I18n.tr("Primary", "button color option"), I18n.tr("Primary Container", "button color option"), I18n.tr("Secondary", "button color option"), I18n.tr("Surface Variant", "button color option")] + currentValue: { + switch (SettingsData.buttonColorMode) { + case "primaryContainer": + return I18n.tr("Primary Container", "button color option"); + case "secondary": + return I18n.tr("Secondary", "button color option"); + case "surfaceVariant": + return I18n.tr("Surface Variant", "button color option"); + default: + return I18n.tr("Primary", "button color option"); + } + } + onValueChanged: value => { + if (value === I18n.tr("Primary Container", "button color option")) { + SettingsData.set("buttonColorMode", "primaryContainer"); + } else if (value === I18n.tr("Secondary", "button color option")) { + SettingsData.set("buttonColorMode", "secondary"); + } else if (value === I18n.tr("Surface Variant", "button color option")) { + SettingsData.set("buttonColorMode", "surfaceVariant"); + } else { + SettingsData.set("buttonColorMode", "primary"); + } + } + } + + SettingsSliderRow { + tab: "theme" + tags: ["popup", "transparency", "opacity", "modal"] + settingKey: "popupTransparency" + text: I18n.tr("Popup Transparency") + description: I18n.tr("Controls opacity of all popouts, modals, and their content layers") + value: Math.round(SettingsData.popupTransparency * 100) + minimum: 0 + maximum: 100 + unit: "%" + defaultValue: 100 + onSliderValueChanged: newValue => SettingsData.set("popupTransparency", newValue / 100) + } + + SettingsSliderRow { + tab: "theme" + tags: ["corner", "radius", "rounded", "square"] + settingKey: "cornerRadius" + text: I18n.tr("Corner Radius") + description: I18n.tr("0 = square corners") + value: SettingsData.cornerRadius + minimum: 0 + maximum: 32 + unit: "px" + defaultValue: 12 + onSliderValueChanged: newValue => SettingsData.setCornerRadius(newValue) + } + + SettingsToggleRow { + tab: "theme" + tags: ["elevation", "shadow", "lift", "m3", "material"] + settingKey: "m3ElevationEnabled" + text: I18n.tr("Shadows") + description: I18n.tr("Material inspired shadows and elevation on modals, popouts, and dialogs") + checked: SettingsData.m3ElevationEnabled ?? true + onToggled: checked => SettingsData.set("m3ElevationEnabled", checked) + } + + SettingsSliderRow { + tab: "theme" + tags: ["elevation", "shadow", "intensity", "blur", "m3"] + settingKey: "m3ElevationIntensity" + text: I18n.tr("Shadow Intensity") + description: I18n.tr("Controls the base blur radius and offset of shadows") + value: SettingsData.m3ElevationIntensity ?? 12 + minimum: 0 + maximum: 100 + unit: "px" + defaultValue: 12 + visible: SettingsData.m3ElevationEnabled ?? true + onSliderValueChanged: newValue => SettingsData.set("m3ElevationIntensity", newValue) + } + + SettingsSliderRow { + tab: "theme" + tags: ["elevation", "shadow", "opacity", "transparency", "m3"] + settingKey: "m3ElevationOpacity" + text: I18n.tr("Shadow Opacity") + description: I18n.tr("Controls the transparency of the shadow") + value: SettingsData.m3ElevationOpacity ?? 30 + minimum: 0 + maximum: 100 + unit: "%" + defaultValue: 30 + visible: SettingsData.m3ElevationEnabled ?? true + onSliderValueChanged: newValue => SettingsData.set("m3ElevationOpacity", newValue) + } + + SettingsDropdownRow { + tab: "theme" + tags: ["elevation", "shadow", "color", "m3"] + settingKey: "m3ElevationColorMode" + text: I18n.tr("Shadow Color") + description: I18n.tr("Base color for shadows (opacity is applied automatically)") + options: [I18n.tr("Default (Black)", "shadow color option"), I18n.tr("Text Color", "shadow color option"), I18n.tr("Primary", "shadow color option"), I18n.tr("Surface Variant", "shadow color option"), I18n.tr("Custom", "shadow color option")] + currentValue: { + switch (SettingsData.m3ElevationColorMode) { + case "text": + return I18n.tr("Text Color", "shadow color option"); + case "primary": + return I18n.tr("Primary", "shadow color option"); + case "surfaceVariant": + return I18n.tr("Surface Variant", "shadow color option"); + case "custom": + return I18n.tr("Custom", "shadow color option"); + default: + return I18n.tr("Default (Black)", "shadow color option"); + } + } + visible: SettingsData.m3ElevationEnabled ?? true + onValueChanged: value => { + if (value === I18n.tr("Primary", "shadow color option")) { + SettingsData.set("m3ElevationColorMode", "primary"); + } else if (value === I18n.tr("Surface Variant", "shadow color option")) { + SettingsData.set("m3ElevationColorMode", "surfaceVariant"); + } else if (value === I18n.tr("Custom", "shadow color option")) { + SettingsData.set("m3ElevationColorMode", "custom"); + openM3ShadowColorPicker(); + } else if (value === I18n.tr("Text Color", "shadow color option")) { + SettingsData.set("m3ElevationColorMode", "text"); + } else { + SettingsData.set("m3ElevationColorMode", "default"); + } + } + } + + SettingsDropdownRow { + tab: "theme" + tags: ["elevation", "shadow", "direction", "light", "advanced", "m3"] + settingKey: "m3ElevationLightDirection" + text: I18n.tr("Light Direction") + description: I18n.tr("Controls shadow cast direction for elevation layers") + options: [I18n.tr("Auto (Bar-aware)", "shadow direction option"), I18n.tr("Top (Default)", "shadow direction option"), I18n.tr("Top Left", "shadow direction option"), I18n.tr("Top Right", "shadow direction option"), I18n.tr("Bottom", "shadow direction option")] + currentValue: { + switch (SettingsData.m3ElevationLightDirection) { + case "autoBar": + return I18n.tr("Auto (Bar-aware)", "shadow direction option"); + case "topLeft": + return I18n.tr("Top Left", "shadow direction option"); + case "topRight": + return I18n.tr("Top Right", "shadow direction option"); + case "bottom": + return I18n.tr("Bottom", "shadow direction option"); + default: + return I18n.tr("Top (Default)", "shadow direction option"); + } + } + visible: SettingsData.m3ElevationEnabled ?? true + onValueChanged: value => { + if (value === I18n.tr("Auto (Bar-aware)", "shadow direction option")) { + SettingsData.set("m3ElevationLightDirection", "autoBar"); + } else if (value === I18n.tr("Top Left", "shadow direction option")) { + SettingsData.set("m3ElevationLightDirection", "topLeft"); + } else if (value === I18n.tr("Top Right", "shadow direction option")) { + SettingsData.set("m3ElevationLightDirection", "topRight"); + } else if (value === I18n.tr("Bottom", "shadow direction option")) { + SettingsData.set("m3ElevationLightDirection", "bottom"); + } else { + SettingsData.set("m3ElevationLightDirection", "top"); + } + } + } + + Item { + visible: (SettingsData.m3ElevationEnabled ?? true) && SettingsData.m3ElevationColorMode === "custom" + width: parent.width + implicitHeight: 36 + height: implicitHeight + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Custom Shadow Color") + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + verticalAlignment: Text.AlignVCenter + } + + Rectangle { + width: 26 + height: 26 + radius: 13 + color: SettingsData.m3ElevationCustomColor ?? "#000000" + border.color: Theme.outline + border.width: 1 + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: openM3ShadowColorPicker() + } + } + } + } + + SettingsToggleRow { + tab: "theme" + tags: ["elevation", "shadow", "modal", "dialog", "m3"] + settingKey: "modalElevationEnabled" + text: I18n.tr("Modal Shadows") + description: I18n.tr("Shadow elevation on modals and dialogs") + checked: SettingsData.modalElevationEnabled ?? true + visible: SettingsData.m3ElevationEnabled ?? true + onToggled: checked => SettingsData.set("modalElevationEnabled", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["elevation", "shadow", "popout", "popup", "osd", "dropdown", "m3"] + settingKey: "popoutElevationEnabled" + text: I18n.tr("Popout Shadows") + description: I18n.tr("Shadow elevation on popouts, OSDs, and dropdowns") + checked: SettingsData.popoutElevationEnabled ?? true + visible: SettingsData.m3ElevationEnabled ?? true + onToggled: checked => SettingsData.set("popoutElevationEnabled", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["elevation", "shadow", "bar", "panel", "navigation", "m3"] + settingKey: "barElevationEnabled" + text: I18n.tr("Bar Shadows") + description: I18n.tr("Shadow elevation on bars and panels") + checked: SettingsData.barElevationEnabled ?? true + visible: SettingsData.m3ElevationEnabled ?? true + onToggled: checked => SettingsData.set("barElevationEnabled", checked) + } + } + + SettingsCard { + tab: "theme" + tags: ["blur", "background", "transparency", "glass", "frosted"] + title: I18n.tr("Background Blur") + settingKey: "blurEnabled" + iconName: "blur_on" + + SettingsToggleRow { + tab: "theme" + tags: ["blur", "background", "transparency", "glass", "frosted"] + settingKey: "blurEnabled" + text: I18n.tr("Background Blur") + description: BlurService.available ? I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration.") : I18n.tr("Requires a newer version of Quickshell") + checked: SettingsData.blurEnabled ?? false + enabled: BlurService.available + onToggled: checked => SettingsData.set("blurEnabled", checked) + } + + SettingsDropdownRow { + tab: "theme" + tags: ["blur", "border", "outline", "edge"] + settingKey: "blurBorderColor" + text: I18n.tr("Blur Border Color") + description: I18n.tr("Border color around blurred surfaces") + visible: SettingsData.blurEnabled + options: [I18n.tr("Outline", "blur border color"), I18n.tr("Primary", "blur border color"), I18n.tr("Secondary", "blur border color"), I18n.tr("Text Color", "blur border color"), I18n.tr("Custom", "blur border color")] + currentValue: { + switch (SettingsData.blurBorderColor) { + case "primary": + return I18n.tr("Primary", "blur border color"); + case "secondary": + return I18n.tr("Secondary", "blur border color"); + case "surfaceText": + return I18n.tr("Text Color", "blur border color"); + case "custom": + return I18n.tr("Custom", "blur border color"); + default: + return I18n.tr("Outline", "blur border color"); + } + } + onValueChanged: value => { + if (value === I18n.tr("Primary", "blur border color")) { + SettingsData.set("blurBorderColor", "primary"); + } else if (value === I18n.tr("Secondary", "blur border color")) { + SettingsData.set("blurBorderColor", "secondary"); + } else if (value === I18n.tr("Text Color", "blur border color")) { + SettingsData.set("blurBorderColor", "surfaceText"); + } else if (value === I18n.tr("Custom", "blur border color")) { + SettingsData.set("blurBorderColor", "custom"); + openBlurBorderColorPicker(); + } else { + SettingsData.set("blurBorderColor", "outline"); + } + } + } + + SettingsSliderRow { + tab: "theme" + tags: ["blur", "border", "opacity"] + settingKey: "blurBorderOpacity" + text: I18n.tr("Blur Border Opacity") + visible: SettingsData.blurEnabled + value: Math.round((SettingsData.blurBorderOpacity ?? 1.0) * 100) + minimum: 0 + maximum: 100 + unit: "%" + defaultValue: 100 + onSliderValueChanged: newValue => SettingsData.set("blurBorderOpacity", newValue / 100) + } + } + + SettingsCard { + tab: "theme" + tags: ["niri", "layout", "gaps", "radius", "window", "border"] + title: I18n.tr("Niri Layout Overrides").replace("Niri", "niri") + settingKey: "niriLayout" + iconName: "crop_square" + visible: CompositorService.isNiri + + SettingsToggleRow { + tab: "theme" + tags: ["niri", "gaps", "override"] + settingKey: "niriLayoutGapsOverrideEnabled" + text: I18n.tr("Override Gaps") + description: I18n.tr("Use custom gaps instead of bar spacing") + checked: SettingsData.niriLayoutGapsOverride >= 0 + onToggled: checked => { + if (checked) { + const currentGaps = Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)); + SettingsData.set("niriLayoutGapsOverride", currentGaps); + return; + } + SettingsData.set("niriLayoutGapsOverride", -1); + } + } + + SettingsSliderRow { + tab: "theme" + tags: ["niri", "gaps", "override"] + settingKey: "niriLayoutGapsOverride" + text: I18n.tr("Window Gaps") + description: I18n.tr("Space between windows") + visible: SettingsData.niriLayoutGapsOverride >= 0 + value: Math.max(0, SettingsData.niriLayoutGapsOverride) + minimum: 0 + maximum: 50 + unit: "px" + defaultValue: Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) + onSliderValueChanged: newValue => SettingsData.set("niriLayoutGapsOverride", newValue) + } + + SettingsToggleRow { + tab: "theme" + tags: ["niri", "radius", "override"] + settingKey: "niriLayoutRadiusOverrideEnabled" + text: I18n.tr("Override Corner Radius") + description: I18n.tr("Use custom window radius instead of theme radius") + checked: SettingsData.niriLayoutRadiusOverride >= 0 + onToggled: checked => { + if (checked) { + SettingsData.set("niriLayoutRadiusOverride", SettingsData.cornerRadius); + return; + } + SettingsData.set("niriLayoutRadiusOverride", -1); + } + } + + SettingsSliderRow { + tab: "theme" + tags: ["niri", "radius", "override"] + settingKey: "niriLayoutRadiusOverride" + text: I18n.tr("Window Corner Radius") + description: I18n.tr("Rounded corners for windows") + visible: SettingsData.niriLayoutRadiusOverride >= 0 + value: Math.max(0, SettingsData.niriLayoutRadiusOverride) + minimum: 0 + maximum: 100 + unit: "px" + defaultValue: SettingsData.cornerRadius + onSliderValueChanged: newValue => SettingsData.set("niriLayoutRadiusOverride", newValue) + } + + SettingsToggleRow { + tab: "theme" + tags: ["niri", "border", "override"] + settingKey: "niriLayoutBorderSizeEnabled" + text: I18n.tr("Override Border Size") + description: I18n.tr("Use custom border/focus-ring width") + checked: SettingsData.niriLayoutBorderSize >= 0 + onToggled: checked => { + if (checked) { + SettingsData.set("niriLayoutBorderSize", 2); + return; + } + SettingsData.set("niriLayoutBorderSize", -1); + } + } + + SettingsSliderRow { + tab: "theme" + tags: ["niri", "border", "override"] + settingKey: "niriLayoutBorderSize" + text: I18n.tr("Border Size") + description: I18n.tr("Width of window border and focus ring") + visible: SettingsData.niriLayoutBorderSize >= 0 + value: Math.max(0, SettingsData.niriLayoutBorderSize) + minimum: 0 + maximum: 10 + unit: "px" + defaultValue: 2 + onSliderValueChanged: newValue => SettingsData.set("niriLayoutBorderSize", newValue) + } + } + + SettingsCard { + tab: "theme" + tags: ["hyprland", "layout", "gaps", "radius", "window", "border", "rounding"] + title: I18n.tr("Hyprland Layout Overrides") + settingKey: "hyprlandLayout" + iconName: "crop_square" + visible: CompositorService.isHyprland + + SettingsToggleRow { + tab: "theme" + tags: ["hyprland", "gaps", "override"] + settingKey: "hyprlandLayoutGapsOverrideEnabled" + text: I18n.tr("Override Gaps") + description: I18n.tr("Use custom gaps instead of bar spacing") + checked: SettingsData.hyprlandLayoutGapsOverride >= 0 + onToggled: checked => { + if (checked) { + const currentGaps = Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)); + SettingsData.set("hyprlandLayoutGapsOverride", currentGaps); + return; + } + SettingsData.set("hyprlandLayoutGapsOverride", -1); + } + } + + SettingsSliderRow { + tab: "theme" + tags: ["hyprland", "gaps", "override"] + settingKey: "hyprlandLayoutGapsOverride" + text: I18n.tr("Window Gaps") + description: I18n.tr("Space between windows (gaps_in and gaps_out)") + visible: SettingsData.hyprlandLayoutGapsOverride >= 0 + value: Math.max(0, SettingsData.hyprlandLayoutGapsOverride) + minimum: 0 + maximum: 50 + unit: "px" + defaultValue: Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) + onSliderValueChanged: newValue => SettingsData.set("hyprlandLayoutGapsOverride", newValue) + } + + SettingsToggleRow { + tab: "theme" + tags: ["hyprland", "radius", "override", "rounding"] + settingKey: "hyprlandLayoutRadiusOverrideEnabled" + text: I18n.tr("Override Corner Radius") + description: I18n.tr("Use custom window rounding instead of theme radius") + checked: SettingsData.hyprlandLayoutRadiusOverride >= 0 + onToggled: checked => { + if (checked) { + SettingsData.set("hyprlandLayoutRadiusOverride", SettingsData.cornerRadius); + return; + } + SettingsData.set("hyprlandLayoutRadiusOverride", -1); + } + } + + SettingsSliderRow { + tab: "theme" + tags: ["hyprland", "radius", "override", "rounding"] + settingKey: "hyprlandLayoutRadiusOverride" + text: I18n.tr("Window Rounding") + description: I18n.tr("Rounded corners for windows (decoration.rounding)") + visible: SettingsData.hyprlandLayoutRadiusOverride >= 0 + value: Math.max(0, SettingsData.hyprlandLayoutRadiusOverride) + minimum: 0 + maximum: 100 + unit: "px" + defaultValue: SettingsData.cornerRadius + onSliderValueChanged: newValue => SettingsData.set("hyprlandLayoutRadiusOverride", newValue) + } + + SettingsToggleRow { + tab: "theme" + tags: ["hyprland", "border", "override"] + settingKey: "hyprlandLayoutBorderSizeEnabled" + text: I18n.tr("Override Border Size") + description: I18n.tr("Use custom border size") + checked: SettingsData.hyprlandLayoutBorderSize >= 0 + onToggled: checked => { + if (checked) { + SettingsData.set("hyprlandLayoutBorderSize", 2); + return; + } + SettingsData.set("hyprlandLayoutBorderSize", -1); + } + } + + SettingsSliderRow { + tab: "theme" + tags: ["hyprland", "border", "override"] + settingKey: "hyprlandLayoutBorderSize" + text: I18n.tr("Border Size") + description: I18n.tr("Width of window border (general.border_size)") + visible: SettingsData.hyprlandLayoutBorderSize >= 0 + value: Math.max(0, SettingsData.hyprlandLayoutBorderSize) + minimum: 0 + maximum: 10 + unit: "px" + defaultValue: 2 + onSliderValueChanged: newValue => SettingsData.set("hyprlandLayoutBorderSize", newValue) + } + } + + SettingsCard { + tab: "theme" + tags: ["mangowc", "mango", "dwl", "layout", "gaps", "radius", "window", "border"] + title: I18n.tr("MangoWC Layout Overrides") + settingKey: "mangoLayout" + iconName: "crop_square" + visible: CompositorService.isDwl + + SettingsToggleRow { + tab: "theme" + tags: ["mangowc", "mango", "gaps", "override"] + settingKey: "mangoLayoutGapsOverrideEnabled" + text: I18n.tr("Override Gaps") + description: I18n.tr("Use custom gaps instead of bar spacing") + checked: SettingsData.mangoLayoutGapsOverride >= 0 + onToggled: checked => { + if (checked) { + const currentGaps = Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)); + SettingsData.set("mangoLayoutGapsOverride", currentGaps); + return; + } + SettingsData.set("mangoLayoutGapsOverride", -1); + } + } + + SettingsSliderRow { + tab: "theme" + tags: ["mangowc", "mango", "gaps", "override"] + settingKey: "mangoLayoutGapsOverride" + text: I18n.tr("Window Gaps") + description: I18n.tr("Space between windows (gappih/gappiv/gappoh/gappov)") + visible: SettingsData.mangoLayoutGapsOverride >= 0 + value: Math.max(0, SettingsData.mangoLayoutGapsOverride) + minimum: 0 + maximum: 50 + unit: "px" + defaultValue: Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) + onSliderValueChanged: newValue => SettingsData.set("mangoLayoutGapsOverride", newValue) + } + + SettingsToggleRow { + tab: "theme" + tags: ["mangowc", "mango", "radius", "override"] + settingKey: "mangoLayoutRadiusOverrideEnabled" + text: I18n.tr("Override Corner Radius") + description: I18n.tr("Use custom window radius instead of theme radius") + checked: SettingsData.mangoLayoutRadiusOverride >= 0 + onToggled: checked => { + if (checked) { + SettingsData.set("mangoLayoutRadiusOverride", SettingsData.cornerRadius); + return; + } + SettingsData.set("mangoLayoutRadiusOverride", -1); + } + } + + SettingsSliderRow { + tab: "theme" + tags: ["mangowc", "mango", "radius", "override"] + settingKey: "mangoLayoutRadiusOverride" + text: I18n.tr("Window Corner Radius") + description: I18n.tr("Rounded corners for windows (border_radius)") + visible: SettingsData.mangoLayoutRadiusOverride >= 0 + value: Math.max(0, SettingsData.mangoLayoutRadiusOverride) + minimum: 0 + maximum: 100 + unit: "px" + defaultValue: SettingsData.cornerRadius + onSliderValueChanged: newValue => SettingsData.set("mangoLayoutRadiusOverride", newValue) + } + + SettingsToggleRow { + tab: "theme" + tags: ["mangowc", "mango", "border", "override"] + settingKey: "mangoLayoutBorderSizeEnabled" + text: I18n.tr("Override Border Size") + description: I18n.tr("Use custom border size") + checked: SettingsData.mangoLayoutBorderSize >= 0 + onToggled: checked => { + if (checked) { + SettingsData.set("mangoLayoutBorderSize", 2); + return; + } + SettingsData.set("mangoLayoutBorderSize", -1); + } + } + + SettingsSliderRow { + tab: "theme" + tags: ["mangowc", "mango", "border", "override"] + settingKey: "mangoLayoutBorderSize" + text: I18n.tr("Border Size") + description: I18n.tr("Width of window border (borderpx)") + visible: SettingsData.mangoLayoutBorderSize >= 0 + value: Math.max(0, SettingsData.mangoLayoutBorderSize) + minimum: 0 + maximum: 10 + unit: "px" + defaultValue: 2 + onSliderValueChanged: newValue => SettingsData.set("mangoLayoutBorderSize", newValue) + } + } + + SettingsCard { + tab: "theme" + tags: ["modal", "darken", "background", "overlay"] + title: I18n.tr("Modal Background") + settingKey: "modalBackground" + iconName: "layers" + + SettingsToggleRow { + tab: "theme" + tags: ["modal", "darken", "background", "overlay"] + settingKey: "modalDarkenBackground" + text: I18n.tr("Darken Modal Background") + description: I18n.tr("Show darkened overlay behind modal dialogs") + checked: SettingsData.modalDarkenBackground + onToggled: checked => SettingsData.set("modalDarkenBackground", checked) + } + } + + SettingsCard { + tab: "theme" + tags: ["applications", "portal", "dark", "terminal"] + title: I18n.tr("Applications") + settingKey: "applications" + iconName: "apps" + + SettingsToggleRow { + tab: "theme" + tags: ["portal", "sync", "dark", "mode"] + settingKey: "syncModeWithPortal" + text: I18n.tr("Sync Mode with Portal") + description: I18n.tr("Sync dark mode with settings portals for system-wide theme hints") + checked: SettingsData.syncModeWithPortal + onToggled: checked => SettingsData.set("syncModeWithPortal", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["terminal", "dark", "always"] + settingKey: "terminalsAlwaysDark" + text: I18n.tr("Terminals - Always use Dark Theme") + description: I18n.tr("Force terminal applications to always use dark color schemes") + checked: SettingsData.terminalsAlwaysDark + onToggled: checked => SettingsData.set("terminalsAlwaysDark", checked) + } + } + + SettingsCard { + tab: "theme" + tags: ["cursor", "mouse", "pointer", "theme", "size"] + title: I18n.tr("Cursor Theme") + settingKey: "cursorTheme" + iconName: "mouse" + visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl + + Column { + width: parent.width + spacing: Theme.spacingM + + StyledRect { + id: cursorWarningBox + width: parent.width + height: cursorWarningContent.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + + readonly property bool showError: themeColorsTab.cursorIncludeStatus.exists && !themeColorsTab.cursorIncludeStatus.included + readonly property bool showSetup: !themeColorsTab.cursorIncludeStatus.exists && !themeColorsTab.cursorIncludeStatus.included + + color: (showError || showSetup) ? Theme.withAlpha(Theme.warning, 0.15) : "transparent" + border.color: (showError || showSetup) ? Theme.withAlpha(Theme.warning, 0.3) : "transparent" + border.width: 1 + visible: (showError || showSetup) && !themeColorsTab.checkingCursorInclude + + Row { + id: cursorWarningContent + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + DankIcon { + name: "warning" + size: Theme.iconSize + color: Theme.warning + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - (cursorFixButton.visible ? cursorFixButton.width + Theme.spacingM : 0) - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: cursorWarningBox.showSetup ? I18n.tr("Cursor Config Not Configured") : I18n.tr("Cursor Include Missing") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.warning + } + + StyledText { + text: cursorWarningBox.showSetup ? I18n.tr("Click 'Setup' to create cursor config and add include to your compositor config.") : I18n.tr("dms/cursor config exists but is not included. Cursor settings won't apply.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + } + + DankButton { + id: cursorFixButton + visible: cursorWarningBox.showError || cursorWarningBox.showSetup + text: themeColorsTab.fixingCursorInclude ? I18n.tr("Fixing...") : (cursorWarningBox.showSetup ? I18n.tr("Setup") : I18n.tr("Fix Now")) + backgroundColor: Theme.warning + textColor: Theme.background + enabled: !themeColorsTab.fixingCursorInclude + anchors.verticalCenter: parent.verticalCenter + onClicked: themeColorsTab.fixCursorInclude() + } + } + } + + SettingsDropdownRow { + tab: "theme" + tags: ["cursor", "mouse", "pointer", "theme"] + settingKey: "cursorTheme" + text: I18n.tr("Cursor Theme") + description: I18n.tr("Mouse pointer appearance") + currentValue: SettingsData.cursorSettings.theme + enableFuzzySearch: true + popupWidthOffset: 100 + maxPopupHeight: 236 + options: cachedCursorThemes + onValueChanged: value => { + SettingsData.setCursorTheme(value); + } + } + + SettingsSliderRow { + tab: "theme" + tags: ["cursor", "mouse", "pointer", "size"] + settingKey: "cursorSize" + text: I18n.tr("Cursor Size") + description: I18n.tr("Mouse pointer size in pixels") + value: SettingsData.cursorSettings.size + minimum: 12 + maximum: 128 + unit: "px" + defaultValue: 24 + onSliderValueChanged: newValue => SettingsData.setCursorSize(newValue) + } + + SettingsToggleRow { + tab: "theme" + tags: ["cursor", "hide", "typing"] + settingKey: "cursorHideWhenTyping" + text: I18n.tr("Hide When Typing") + description: I18n.tr("Hide cursor when pressing keyboard keys") + visible: CompositorService.isNiri || CompositorService.isHyprland + checked: { + if (CompositorService.isNiri) + return SettingsData.cursorSettings.niri?.hideWhenTyping || false; + if (CompositorService.isHyprland) + return SettingsData.cursorSettings.hyprland?.hideOnKeyPress || false; + return false; + } + onToggled: checked => { + const updated = JSON.parse(JSON.stringify(SettingsData.cursorSettings)); + if (CompositorService.isNiri) { + if (!updated.niri) + updated.niri = {}; + updated.niri.hideWhenTyping = checked; + } else if (CompositorService.isHyprland) { + if (!updated.hyprland) + updated.hyprland = {}; + updated.hyprland.hideOnKeyPress = checked; + } + SettingsData.set("cursorSettings", updated); + } + } + + SettingsToggleRow { + tab: "theme" + tags: ["cursor", "hide", "touch"] + settingKey: "cursorHideOnTouch" + text: I18n.tr("Hide on Touch") + description: I18n.tr("Hide cursor when using touch input") + visible: CompositorService.isHyprland + checked: SettingsData.cursorSettings.hyprland?.hideOnTouch || false + onToggled: checked => { + const updated = JSON.parse(JSON.stringify(SettingsData.cursorSettings)); + if (!updated.hyprland) + updated.hyprland = {}; + updated.hyprland.hideOnTouch = checked; + SettingsData.set("cursorSettings", updated); + } + } + + SettingsSliderRow { + tab: "theme" + tags: ["cursor", "hide", "timeout", "inactive"] + settingKey: "cursorHideAfterInactive" + text: I18n.tr("Auto-Hide Timeout") + description: I18n.tr("Hide cursor after inactivity (0 = disabled)") + value: { + if (CompositorService.isNiri) + return SettingsData.cursorSettings.niri?.hideAfterInactiveMs || 0; + if (CompositorService.isHyprland) + return SettingsData.cursorSettings.hyprland?.inactiveTimeout || 0; + if (CompositorService.isDwl) + return SettingsData.cursorSettings.dwl?.cursorHideTimeout || 0; + return 0; + } + minimum: 0 + maximum: CompositorService.isNiri ? 5000 : 10 + unit: CompositorService.isNiri ? "ms" : "s" + defaultValue: 0 + onSliderValueChanged: newValue => { + const updated = JSON.parse(JSON.stringify(SettingsData.cursorSettings)); + if (CompositorService.isNiri) { + if (!updated.niri) + updated.niri = {}; + updated.niri.hideAfterInactiveMs = newValue; + } else if (CompositorService.isHyprland) { + if (!updated.hyprland) + updated.hyprland = {}; + updated.hyprland.inactiveTimeout = newValue; + } else if (CompositorService.isDwl) { + if (!updated.dwl) + updated.dwl = {}; + updated.dwl.cursorHideTimeout = newValue; + } + SettingsData.set("cursorSettings", updated); + } + } + } + } + + SettingsCard { + tab: "theme" + tags: ["icon", "theme", "system"] + title: I18n.tr("Icon Theme") + settingKey: "iconTheme" + iconName: "interests" + + SettingsDropdownRow { + tab: "theme" + tags: ["icon", "theme", "system"] + settingKey: "iconTheme" + text: I18n.tr("Icon Theme") + description: I18n.tr("DankShell & System Icons (requires restart)") + currentValue: SettingsData.iconTheme + enableFuzzySearch: true + popupWidthOffset: 100 + maxPopupHeight: 236 + options: cachedIconThemes + onValueChanged: value => { + SettingsData.setIconTheme(value); + if (Quickshell.env("QT_QPA_PLATFORMTHEME") != "gtk3" && Quickshell.env("QT_QPA_PLATFORMTHEME") != "qt6ct" && Quickshell.env("QT_QPA_PLATFORMTHEME_QT6") != "qt6ct") { + ToastService.showError(I18n.tr("Missing Environment Variables", "qt theme env error title"), I18n.tr("You need to set either:\nQT_QPA_PLATFORMTHEME=gtk3 OR\nQT_QPA_PLATFORMTHEME=qt6ct\nas environment variables, and then restart the shell.\n\nqt6ct requires qt6ct-kde to be installed.", "qt theme env error body")); + } + } + } + } + + SettingsCard { + tab: "theme" + tags: ["matugen", "templates", "theming"] + title: I18n.tr("Matugen Templates") + settingKey: "matugenTemplates" + iconName: "auto_awesome" + collapsible: true + expanded: false + visible: Theme.matugenAvailable + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "user", "templates"] + settingKey: "runUserMatugenTemplates" + text: I18n.tr("Run User Templates") + description: "" + checked: SettingsData.runUserMatugenTemplates + onToggled: checked => SettingsData.set("runUserMatugenTemplates", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "dms", "templates"] + settingKey: "runDmsMatugenTemplates" + text: I18n.tr("Run DMS Templates") + description: "" + checked: SettingsData.runDmsMatugenTemplates + onToggled: checked => SettingsData.set("runDmsMatugenTemplates", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "gtk", "template"] + settingKey: "matugenTemplateGtk" + text: "GTK" + description: getTemplateDescription("gtk", "") + descriptionColor: getTemplateDescriptionColor("gtk") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateGtk + onToggled: checked => SettingsData.set("matugenTemplateGtk", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "niri", "template"] + settingKey: "matugenTemplateNiri" + text: "niri" + description: getTemplateDescription("niri", "") + descriptionColor: getTemplateDescriptionColor("niri") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateNiri + onToggled: checked => SettingsData.set("matugenTemplateNiri", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "hyprland", "template"] + settingKey: "matugenTemplateHyprland" + text: "Hyprland" + description: getTemplateDescription("hyprland", "") + descriptionColor: getTemplateDescriptionColor("hyprland") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateHyprland + onToggled: checked => SettingsData.set("matugenTemplateHyprland", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "mangowc", "template"] + settingKey: "matugenTemplateMangowc" + text: "mangowc" + description: getTemplateDescription("mangowc", "") + descriptionColor: getTemplateDescriptionColor("mangowc") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateMangowc + onToggled: checked => SettingsData.set("matugenTemplateMangowc", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "qt5ct", "template"] + settingKey: "matugenTemplateQt5ct" + text: "qt5ct" + description: getTemplateDescription("qt5ct", "") + descriptionColor: getTemplateDescriptionColor("qt5ct") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateQt5ct + onToggled: checked => SettingsData.set("matugenTemplateQt5ct", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "qt6ct", "template"] + settingKey: "matugenTemplateQt6ct" + text: "qt6ct" + description: getTemplateDescription("qt6ct", "") + descriptionColor: getTemplateDescriptionColor("qt6ct") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateQt6ct + onToggled: checked => SettingsData.set("matugenTemplateQt6ct", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "firefox", "template"] + settingKey: "matugenTemplateFirefox" + text: "Firefox" + description: getTemplateDescription("firefox", "") + descriptionColor: getTemplateDescriptionColor("firefox") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateFirefox + onToggled: checked => SettingsData.set("matugenTemplateFirefox", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "pywalfox", "template"] + settingKey: "matugenTemplatePywalfox" + text: "pywalfox" + description: getTemplateDescription("pywalfox", "") + descriptionColor: getTemplateDescriptionColor("pywalfox") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplatePywalfox + onToggled: checked => SettingsData.set("matugenTemplatePywalfox", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "zenbrowser", "template"] + settingKey: "matugenTemplateZenBrowser" + text: "zenbrowser" + description: getTemplateDescription("zenbrowser", "") + descriptionColor: getTemplateDescriptionColor("zenbrowser") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateZenBrowser + onToggled: checked => SettingsData.set("matugenTemplateZenBrowser", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "vesktop", "discord", "template"] + settingKey: "matugenTemplateVesktop" + text: "vesktop" + description: getTemplateDescription("vesktop", "") + descriptionColor: getTemplateDescriptionColor("vesktop") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateVesktop + onToggled: checked => SettingsData.set("matugenTemplateVesktop", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "equibop", "discord", "template"] + settingKey: "matugenTemplateEquibop" + text: "equibop" + description: getTemplateDescription("equibop", "") + descriptionColor: getTemplateDescriptionColor("equibop") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateEquibop + onToggled: checked => SettingsData.set("matugenTemplateEquibop", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "ghostty", "terminal", "template"] + settingKey: "matugenTemplateGhostty" + text: "Ghostty" + description: getTemplateDescription("ghostty", "") + descriptionColor: getTemplateDescriptionColor("ghostty") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateGhostty + onToggled: checked => SettingsData.set("matugenTemplateGhostty", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "kitty", "terminal", "template"] + settingKey: "matugenTemplateKitty" + text: "kitty" + description: getTemplateDescription("kitty", "") + descriptionColor: getTemplateDescriptionColor("kitty") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateKitty + onToggled: checked => SettingsData.set("matugenTemplateKitty", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "foot", "terminal", "template"] + settingKey: "matugenTemplateFoot" + text: "foot" + description: getTemplateDescription("foot", "") + descriptionColor: getTemplateDescriptionColor("foot") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateFoot + onToggled: checked => SettingsData.set("matugenTemplateFoot", checked) + } + + SettingsDivider { + visible: neovimThemeToggle.visible && neovimThemeToggle.checked + } + + SettingsToggleRow { + id: neovimThemeToggle + tab: "theme" + tags: ["matugen", "neovim", "terminal", "template"] + settingKey: "matugenTemplateNeovim" + text: "neovim" + description: getTemplateDescription("nvim", I18n.tr("Required plugin: ") + "https://github.com/AvengeMedia/base46") + descriptionColor: getTemplateDescriptionColor("nvim") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateNeovim + onToggled: checked => SettingsData.set("matugenTemplateNeovim", checked) + } + + SettingsDropdownRow { + text: I18n.tr("Dark mode base") + tab: "theme" + tags: ["matugen", "neovim", "terminal", "template"] + settingKey: "matugenTemplateNeovimSettings" + description: "Base to derive dark theme from" + visible: neovimThemeToggle.visible && neovimThemeToggle.checked + currentValue: SettingsData.matugenTemplateNeovimSettings?.dark?.baseTheme ?? "github_dark" + options: ["aquarium", "ashes", "aylin", "ayu_dark", "bearded-arc", "carbonfox", "catppuccin", "chadracula", "chadracula-evondev", "chadtain", "chocolate", "darcula-dark", "dark_horizon", "decay", "default-dark", "doomchad", "eldritch", "embark", "everblush", "everforest", "falcon", "flexoki", "flouromachine", "gatekeeper", "github_dark", "gruvbox", "gruvchad", "hiberbee", "horizon", "jabuti", "jellybeans", "kanagawa", "kanagawa-dragon", "material-darker", "material-deep-ocean", "melange", "midnight_breeze", "mito-laser", "monekai", "monochrome", "mountain", "neofusion", "nightfox", "nightlamp", "nightowl", "nord", "obsidian-ember", "oceanic-next", "onedark", "onenord", "oxocarbon", "palenight", "pastelDark", "pastelbeans", "penumbra_dark", "poimandres", "radium", "rosepine", "rxyhn", "scaryforest", "seoul256_dark", "solarized_dark", "solarized_osaka", "starlight", "sweetpastel", "tokyodark", "tokyonight", "tomorrow_night", "tundra", "vesper", "vscode_dark", "wombat", "yoru", "zenburn"] + enableFuzzySearch: true + onValueChanged: value => { + const settings = SettingsData.matugenTemplateNeovimSettings; + settings.dark.baseTheme = value; + SettingsData.set("matugenTemplateNeovimSettings", settings); + } + } + + SettingsDropdownRow { + text: I18n.tr("Light mode base") + tab: "theme" + tags: ["matugen", "neovim", "terminal", "template"] + settingKey: "matugenTemplateNeovimSettings" + description: "Base to derive light theme from" + visible: neovimThemeToggle.visible && neovimThemeToggle.checked + currentValue: SettingsData.matugenTemplateNeovimSettings?.light?.baseTheme ?? "github_light" + options: ["ayu_light", "blossom_light", "catppuccin-latte", "default-light", "everforest_light", "flex-light", "flexoki-light", "github_light", "gruvbox_light", "material-lighter", "nano-light", "oceanic-light", "one_light", "onenord_light", "penumbra_light", "rosepine-dawn", "seoul256_light", "solarized_light", "sunrise_breeze", "vscode_light"] + enableFuzzySearch: true + onValueChanged: value => { + const settings = SettingsData.matugenTemplateNeovimSettings; + settings.light.baseTheme = value; + SettingsData.set("matugenTemplateNeovimSettings", settings); + } + } + + SettingsSliderRow { + text: I18n.tr("Dark mode harmony") + tags: ["matugen", "neovim", "terminal", "template"] + settingKey: "matugenTemplateNeovimSettings" + description: "How much should the base dark theme be tinted" + visible: neovimThemeToggle.visible && neovimThemeToggle.checked + minimum: 0 + maximum: 100 + value: (SettingsData.matugenTemplateNeovimSettings?.dark?.harmony ?? 0.5) * 100 + defaultValue: 50 + onSliderValueChanged: value => { + const settings = SettingsData.matugenTemplateNeovimSettings; + settings.dark.harmony = value / 100; + SettingsData.set("matugenTemplateNeovimSettings", settings); + } + } + + SettingsSliderRow { + text: I18n.tr("Light mode harmony") + tags: ["matugen", "neovim", "terminal", "template"] + settingKey: "matugenTemplateNeovimSettings" + description: "How much should the base light theme be tinted" + visible: neovimThemeToggle.visible && neovimThemeToggle.checked + minimum: 0 + maximum: 100 + value: (SettingsData.matugenTemplateNeovimSettings?.light?.harmony ?? 0.5) * 100 + defaultValue: 50 + onSliderValueChanged: value => { + const settings = SettingsData.matugenTemplateNeovimSettings; + settings.light.harmony = value / 100; + SettingsData.set("matugenTemplateNeovimSettings", settings); + } + } + + SettingsToggleRow { + text: I18n.tr("Follow DMS background color") + tags: ["matugen", "neovim", "terminal", "template"] + settingKey: "matugenTemplateNeovimSetBackground" + visible: neovimThemeToggle.visible && neovimThemeToggle.checked + checked: SettingsData.matugenTemplateNeovimSetBackground ?? true + onToggled: checked => SettingsData.set("matugenTemplateNeovimSetBackground", checked) + } + + SettingsDivider { + visible: neovimThemeToggle.visible && neovimThemeToggle.checked + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "alacritty", "terminal", "template"] + settingKey: "matugenTemplateAlacritty" + text: "Alacritty" + description: getTemplateDescription("alacritty", "") + descriptionColor: getTemplateDescriptionColor("alacritty") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateAlacritty + onToggled: checked => SettingsData.set("matugenTemplateAlacritty", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "wezterm", "terminal", "template"] + settingKey: "matugenTemplateWezterm" + text: "WezTerm" + description: getTemplateDescription("wezterm", "") + descriptionColor: getTemplateDescriptionColor("wezterm") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateWezterm + onToggled: checked => SettingsData.set("matugenTemplateWezterm", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "dgop", "template"] + settingKey: "matugenTemplateDgop" + text: "dgop" + description: getTemplateDescription("dgop", "") + descriptionColor: getTemplateDescriptionColor("dgop") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateDgop + onToggled: checked => SettingsData.set("matugenTemplateDgop", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "kcolorscheme", "kde", "template"] + settingKey: "matugenTemplateKcolorscheme" + text: "KColorScheme" + description: getTemplateDescription("kcolorscheme", "") + descriptionColor: getTemplateDescriptionColor("kcolorscheme") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateKcolorscheme + onToggled: checked => SettingsData.set("matugenTemplateKcolorscheme", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "vscode", "code", "template"] + settingKey: "matugenTemplateVscode" + text: "VS Code" + description: getTemplateDescription("vscode", "") + descriptionColor: getTemplateDescriptionColor("vscode") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateVscode + onToggled: checked => SettingsData.set("matugenTemplateVscode", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "emacs", "template"] + settingKey: "matugenTemplateEmacs" + text: "Emacs" + description: getTemplateDescription("emacs", "") + descriptionColor: getTemplateDescriptionColor("emacs") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateEmacs + onToggled: checked => SettingsData.set("matugenTemplateEmacs", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["matugen", "zed", "template"] + settingKey: "matugenTemplateZed" + text: "Zed" + description: getTemplateDescription("zed", "") + descriptionColor: getTemplateDescriptionColor("zed") + visible: SettingsData.runDmsMatugenTemplates + checked: SettingsData.matugenTemplateZed + onToggled: checked => SettingsData.set("matugenTemplateZed", checked) + } + } + + Rectangle { + width: parent.width + height: warningText.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + DankIcon { + name: "info" + size: Theme.iconSizeSmall + color: Theme.warning + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: warningText + font.pixelSize: Theme.fontSizeSmall + text: I18n.tr("The below settings will modify your GTK and Qt settings. If you wish to preserve your current configurations, please back them up (qt5ct.conf|qt6ct.conf and ~/.config/gtk-3.0|gtk-4.0).") + wrapMode: Text.WordWrap + width: parent.width - Theme.iconSizeSmall - Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + } + } + } + + SettingsCard { + tab: "theme" + tags: ["system", "app", "theming", "gtk", "qt"] + title: I18n.tr("System App Theming") + settingKey: "systemAppTheming" + iconName: "brush" + visible: Theme.matugenAvailable + + Row { + width: parent.width + spacing: Theme.spacingM + + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: 48 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "folder" + size: 16 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Apply GTK Colors") + font.pixelSize: Theme.fontSizeMedium + color: Theme.primary + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Theme.applyGtkColors() + } + } + + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: 48 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "settings" + size: 16 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Apply Qt Colors") + font.pixelSize: Theme.fontSizeMedium + color: Theme.primary + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Theme.applyQtColors() + } + } + } + + StyledText { + text: I18n.tr('Generate baseline GTK3/4 or QT5/QT6 (requires qt6ct-kde) configurations to follow DMS colors. Only needed once.<br /><br />It is recommended to configure <a href="https://github.com/AvengeMedia/DankMaterialShell/blob/master/README.md#Theming" style="text-decoration:none; color:%1;">adw-gtk3</a> prior to applying GTK themes.').arg(Theme.primary) + textFormat: Text.RichText + linkColor: Theme.primary + onLinkActivated: url => Qt.openUrlExternally(url) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignHCenter + + MouseArea { + anchors.fill: parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + } + } + } + } + } + + FileBrowserModal { + id: fileBrowserModal + browserTitle: I18n.tr("Select Custom Theme", "custom theme file browser title") + filterExtensions: ["*.json"] + showHiddenFiles: true + + function selectCustomTheme() { + shouldBeVisible = true; + } + + onFileSelected: function (filePath) { + if (filePath.endsWith(".json")) { + SettingsData.set("customThemeFile", filePath); + Theme.switchTheme("custom"); + close(); + } + } + } + + LazyLoader { + id: themeBrowserLoader + active: false + + ThemeBrowser { + id: themeBrowserItem + } + } + + function showThemeBrowser() { + themeBrowserLoader.active = true; + if (themeBrowserLoader.item) + themeBrowserLoader.item.show(); + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/TimeWeatherTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/TimeWeatherTab.qml new file mode 100644 index 0000000..61f4d43 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/TimeWeatherTab.qml @@ -0,0 +1,1148 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + readonly property string _systemDefaultLabel: I18n.tr("System Default") + + function weekStartQt() { + if (SettingsData.firstDayOfWeek < 0 || SettingsData.firstDayOfWeek >= 7) + return Qt.locale().firstDayOfWeek; + return SettingsData.firstDayOfWeek; + } + + function weekStartJs() { + return weekStartQt() % 7; + } + + function _dayNames() { + return Array(7).fill(0).map((_, i) => new Date(Date.UTC(2026, 2, 1 + i, 0, 0, 0)).toLocaleDateString(I18n.locale(), "dddd")).map(d => d[0].toUpperCase() + d.slice(1)); + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + tab: "time" + tags: ["time", "clock", "format", "24hour"] + title: I18n.tr("Time Format") + settingKey: "timeFormat" + iconName: "schedule" + + SettingsToggleRow { + tab: "time" + tags: ["time", "24hour", "format"] + settingKey: "use24HourClock" + text: I18n.tr("24-Hour Format") + description: I18n.tr("Use 24-hour time format instead of 12-hour AM/PM") + checked: SettingsData.use24HourClock + onToggled: checked => SettingsData.set("use24HourClock", checked) + } + + SettingsToggleRow { + tab: "time" + tags: ["time", "seconds", "clock"] + settingKey: "showSeconds" + text: I18n.tr("Show Seconds") + description: I18n.tr("Display seconds in the clock") + checked: SettingsData.showSeconds + onToggled: checked => SettingsData.set("showSeconds", checked) + } + + SettingsToggleRow { + tab: "time" + tags: ["time", "12hour", "format", "padding", "leading", "zero"] + settingKey: "padHours12Hour" + text: I18n.tr("Pad Hours") + description: "02:31 PM vs 2:31 PM" + checked: SettingsData.padHours12Hour + onToggled: checked => SettingsData.set("padHours12Hour", checked) + visible: !SettingsData.use24HourClock + } + } + + SettingsCard { + tab: "time" + tags: ["date", "format", "calendar"] + title: I18n.tr("Date Format") + settingKey: "dateFormat" + iconName: "calendar_today" + + SettingsToggleRow { + tab: "time" + tags: ["show", "week"] + settingKey: "showWeekNumber" + text: I18n.tr("Show Week Number") + description: I18n.tr("Show week number in the calendar") + checked: SettingsData.showWeekNumber + onToggled: checked => SettingsData.set("showWeekNumber", checked) + } + + SettingsDropdownRow { + tab: "time" + tags: ["first", "day", "week"] + settingKey: "firstDayOfWeek" + text: I18n.tr("First Day of Week") + options: [root._systemDefaultLabel].concat(root._dayNames()) + currentValue: { + if (SettingsData.firstDayOfWeek < 0 || SettingsData.firstDayOfWeek >= 7) + return root._systemDefaultLabel; + return root._dayNames()[root.weekStartJs()]; + } + onValueChanged: value => { + if (value === root._systemDefaultLabel) { + SettingsData.set("firstDayOfWeek", -1); + return; + } + SettingsData.set("firstDayOfWeek", root._dayNames().indexOf(value)); + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsDropdownRow { + tab: "time" + tags: ["date", "format", "topbar"] + settingKey: "clockDateFormat" + text: I18n.tr("Top Bar Format") + description: "Preview: " + (SettingsData.clockDateFormat ? new Date().toLocaleDateString(I18n.locale(), SettingsData.clockDateFormat) : new Date().toLocaleDateString(I18n.locale(), "ddd d")) + options: [I18n.tr("System Default", "date format option"), I18n.tr("Day Date", "date format option"), I18n.tr("Day Month Date", "date format option"), I18n.tr("Month Date", "date format option"), I18n.tr("Numeric (M/D)", "date format option"), I18n.tr("Numeric (D/M)", "date format option"), I18n.tr("Full with Year", "date format option"), I18n.tr("ISO Date", "date format option"), I18n.tr("Full Day & Month", "date format option"), I18n.tr("Custom...", "date format option")] + currentValue: { + if (!SettingsData.clockDateFormat || SettingsData.clockDateFormat.length === 0) + return I18n.tr("System Default", "date format option"); + const presets = [ + { + "format": "ddd d", + "label": I18n.tr("Day Date", "date format option") + }, + { + "format": "ddd MMM d", + "label": I18n.tr("Day Month Date", "date format option") + }, + { + "format": "MMM d", + "label": I18n.tr("Month Date", "date format option") + }, + { + "format": "M/d", + "label": I18n.tr("Numeric (M/D)", "date format option") + }, + { + "format": "d/M", + "label": I18n.tr("Numeric (D/M)", "date format option") + }, + { + "format": "ddd d MMM yyyy", + "label": I18n.tr("Full with Year", "date format option") + }, + { + "format": "yyyy-MM-dd", + "label": I18n.tr("ISO Date", "date format option") + }, + { + "format": "dddd, MMMM d", + "label": I18n.tr("Full Day & Month", "date format option") + } + ]; + const match = presets.find(p => p.format === SettingsData.clockDateFormat); + return match ? match.label : I18n.tr("Custom: ") + SettingsData.clockDateFormat; + } + onValueChanged: value => { + const formatMap = {}; + formatMap[I18n.tr("System Default", "date format option")] = ""; + formatMap[I18n.tr("Day Date", "date format option")] = "ddd d"; + formatMap[I18n.tr("Day Month Date", "date format option")] = "ddd MMM d"; + formatMap[I18n.tr("Month Date", "date format option")] = "MMM d"; + formatMap[I18n.tr("Numeric (M/D)", "date format option")] = "M/d"; + formatMap[I18n.tr("Numeric (D/M)", "date format option")] = "d/M"; + formatMap[I18n.tr("Full with Year", "date format option")] = "ddd d MMM yyyy"; + formatMap[I18n.tr("ISO Date", "date format option")] = "yyyy-MM-dd"; + formatMap[I18n.tr("Full Day & Month", "date format option")] = "dddd, MMMM d"; + if (value === I18n.tr("Custom...", "date format option")) { + customFormatInput.visible = true; + } else { + customFormatInput.visible = false; + SettingsData.set("clockDateFormat", formatMap[value]); + } + } + } + + DankTextField { + id: customFormatInput + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + visible: false + placeholderText: I18n.tr("Enter custom top bar format (e.g., ddd MMM d)") + text: SettingsData.clockDateFormat + onTextChanged: { + if (visible && text) + SettingsData.set("clockDateFormat", text); + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsDropdownRow { + tab: "time" + tags: ["date", "format", "lock", "screen"] + settingKey: "lockDateFormat" + text: I18n.tr("Lock Screen Format") + description: "Preview: " + (SettingsData.lockDateFormat ? new Date().toLocaleDateString(I18n.locale(), SettingsData.lockDateFormat) : new Date().toLocaleDateString(I18n.locale(), Locale.LongFormat)) + options: [I18n.tr("System Default", "date format option"), I18n.tr("Day Date", "date format option"), I18n.tr("Day Month Date", "date format option"), I18n.tr("Month Date", "date format option"), I18n.tr("Numeric (M/D)", "date format option"), I18n.tr("Numeric (D/M)", "date format option"), I18n.tr("Full with Year", "date format option"), I18n.tr("ISO Date", "date format option"), I18n.tr("Full Day & Month", "date format option"), I18n.tr("Custom...", "date format option")] + currentValue: { + if (!SettingsData.lockDateFormat || SettingsData.lockDateFormat.length === 0) + return I18n.tr("System Default", "date format option"); + const presets = [ + { + "format": "ddd d", + "label": I18n.tr("Day Date", "date format option") + }, + { + "format": "ddd MMM d", + "label": I18n.tr("Day Month Date", "date format option") + }, + { + "format": "MMM d", + "label": I18n.tr("Month Date", "date format option") + }, + { + "format": "M/d", + "label": I18n.tr("Numeric (M/D)", "date format option") + }, + { + "format": "d/M", + "label": I18n.tr("Numeric (D/M)", "date format option") + }, + { + "format": "ddd d MMM yyyy", + "label": I18n.tr("Full with Year", "date format option") + }, + { + "format": "yyyy-MM-dd", + "label": I18n.tr("ISO Date", "date format option") + }, + { + "format": "dddd, MMMM d", + "label": I18n.tr("Full Day & Month", "date format option") + } + ]; + const match = presets.find(p => p.format === SettingsData.lockDateFormat); + return match ? match.label : I18n.tr("Custom: ") + SettingsData.lockDateFormat; + } + onValueChanged: value => { + const formatMap = {}; + formatMap[I18n.tr("System Default", "date format option")] = ""; + formatMap[I18n.tr("Day Date", "date format option")] = "ddd d"; + formatMap[I18n.tr("Day Month Date", "date format option")] = "ddd MMM d"; + formatMap[I18n.tr("Month Date", "date format option")] = "MMM d"; + formatMap[I18n.tr("Numeric (M/D)", "date format option")] = "M/d"; + formatMap[I18n.tr("Numeric (D/M)", "date format option")] = "d/M"; + formatMap[I18n.tr("Full with Year", "date format option")] = "ddd d MMM yyyy"; + formatMap[I18n.tr("ISO Date", "date format option")] = "yyyy-MM-dd"; + formatMap[I18n.tr("Full Day & Month", "date format option")] = "dddd, MMMM d"; + if (value === I18n.tr("Custom...", "date format option")) { + customLockFormatInput.visible = true; + } else { + customLockFormatInput.visible = false; + SettingsData.set("lockDateFormat", formatMap[value]); + } + } + } + + DankTextField { + id: customLockFormatInput + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + visible: false + placeholderText: I18n.tr("Enter custom lock screen format (e.g., dddd, MMMM d)") + text: SettingsData.lockDateFormat + onTextChanged: { + if (visible && text) + SettingsData.set("lockDateFormat", text); + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + Rectangle { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + height: formatHelp.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + id: formatHelp + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Format Legend") + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + font.weight: Font.Medium + } + + Row { + width: parent.width + spacing: Theme.spacingL + + Column { + width: (parent.width - Theme.spacingL) / 2 + spacing: 2 + + StyledText { + text: I18n.tr("• d - Day (1-31)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + StyledText { + text: I18n.tr("• dd - Day (01-31)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + StyledText { + text: I18n.tr("• ddd - Day name (Mon)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + StyledText { + text: I18n.tr("• dddd - Day name (Monday)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + StyledText { + text: I18n.tr("• M - Month (1-12)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + + Column { + width: (parent.width - Theme.spacingL) / 2 + spacing: 2 + + StyledText { + text: I18n.tr("• MM - Month (01-12)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + StyledText { + text: I18n.tr("• MMM - Month (Jan)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + StyledText { + text: I18n.tr("• MMMM - Month (January)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + StyledText { + text: I18n.tr("• yy - Year (24)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + StyledText { + text: I18n.tr("• yyyy - Year (2024)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + } + } + } + + SettingsCard { + tab: "time" + tags: ["weather", "enable", "forecast"] + title: I18n.tr("Weather") + settingKey: "weather" + iconName: "cloud" + + SettingsToggleRow { + tab: "time" + tags: ["weather", "enable"] + settingKey: "weatherEnabled" + text: I18n.tr("Enable Weather") + description: I18n.tr("Show weather information in top bar and control center") + checked: SettingsData.weatherEnabled + onToggled: checked => SettingsData.set("weatherEnabled", checked) + } + + Column { + width: parent.width + spacing: 0 + visible: SettingsData.weatherEnabled + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsToggleRow { + tab: "time" + tags: ["weather", "imperial", "fahrenheit", "units"] + settingKey: "useFahrenheit" + text: I18n.tr("Use Imperial Units") + description: I18n.tr("Use Imperial units (°F, mph, inHg) instead of Metric (°C, km/h, hPa)") + checked: SettingsData.useFahrenheit + onToggled: checked => SettingsData.set("useFahrenheit", checked) + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + visible: !SettingsData.useFahrenheit + } + + SettingsToggleRow { + tab: "time" + tags: ["weather", "wind", "speed", "units", "metric"] + settingKey: "windSpeedUnit" + text: I18n.tr("Wind Speed in m/s") + description: I18n.tr("Use meters per second instead of km/h for wind speed") + checked: SettingsData.windSpeedUnit === "ms" + onToggled: checked => SettingsData.set("windSpeedUnit", checked ? "ms" : "kmh") + visible: !SettingsData.useFahrenheit + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsToggleRow { + tab: "time" + tags: ["weather", "location", "auto", "gps"] + settingKey: "useAutoLocation" + text: I18n.tr("Auto Location") + description: I18n.tr("Automatically determine your location using your IP address") + checked: SettingsData.useAutoLocation + onToggled: checked => SettingsData.set("useAutoLocation", checked) + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: !SettingsData.useAutoLocation + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + Item { + width: parent.width + height: locationContent.height + + Column { + id: locationContent + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Custom Location") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + Row { + width: parent.width + spacing: Theme.spacingM + + Column { + width: (parent.width - Theme.spacingM) / 2 + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Latitude") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + DankTextField { + id: latitudeInput + width: parent.width + height: 48 + placeholderText: "40.7128" + backgroundColor: Theme.surfaceVariant + normalBorderColor: Theme.primarySelected + focusedBorderColor: Theme.primary + keyNavigationTab: longitudeInput + + Component.onCompleted: { + if (SettingsData.weatherCoordinates) { + const coords = SettingsData.weatherCoordinates.split(','); + if (coords.length > 0) + text = coords[0].trim(); + } + } + + Connections { + target: SettingsData + function onWeatherCoordinatesChanged() { + if (SettingsData.weatherCoordinates) { + const coords = SettingsData.weatherCoordinates.split(','); + if (coords.length > 0) + latitudeInput.text = coords[0].trim(); + } + } + } + + onTextEdited: { + if (text && longitudeInput.text) { + const coords = text + "," + longitudeInput.text; + SessionData.weatherCoordinates = coords; + SessionData.saveSettings(); + } + } + } + } + + Column { + width: (parent.width - Theme.spacingM) / 2 + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Longitude") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + DankTextField { + id: longitudeInput + width: parent.width + height: 48 + placeholderText: "-74.0060" + backgroundColor: Theme.surfaceVariant + normalBorderColor: Theme.primarySelected + focusedBorderColor: Theme.primary + keyNavigationTab: locationSearchInput + keyNavigationBacktab: latitudeInput + + Component.onCompleted: { + if (SettingsData.weatherCoordinates) { + const coords = SettingsData.weatherCoordinates.split(','); + if (coords.length > 1) + text = coords[1].trim(); + } + } + + Connections { + target: SettingsData + function onWeatherCoordinatesChanged() { + if (SettingsData.weatherCoordinates) { + const coords = SettingsData.weatherCoordinates.split(','); + if (coords.length > 1) + longitudeInput.text = coords[1].trim(); + } + } + } + + onTextEdited: { + if (text && latitudeInput.text) { + const coords = latitudeInput.text + "," + text; + SessionData.weatherCoordinates = coords; + SessionData.saveSettings(); + } + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Location Search") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + font.weight: Font.Medium + } + + DankLocationSearch { + id: locationSearchInput + width: parent.width + currentLocation: "" + placeholderText: I18n.tr("New York, NY") + keyNavigationBacktab: longitudeInput + onLocationSelected: (displayName, coordinates) => { + SettingsData.setWeatherLocation(displayName, coordinates); + const coords = coordinates.split(','); + if (coords.length >= 2) { + latitudeInput.text = coords[0].trim(); + longitudeInput.text = coords[1].trim(); + } + } + } + } + } + } + } + } + } + + SettingsCard { + tab: "time" + tags: ["weather", "current", "display"] + title: I18n.tr("Current Weather") + settingKey: "weather" + iconName: "visibility" + visible: SettingsData.weatherEnabled + + Column { + width: parent.width + spacing: Theme.spacingL + visible: !WeatherService.weather.available + + DankIcon { + name: "cloud_off" + size: Theme.iconSize * 2 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("No Weather Data Available") + font.pixelSize: Theme.fontSizeLarge + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: WeatherService.weather.available + + Item { + width: parent.width + height: 70 + + DankIcon { + id: refreshButton + name: "refresh" + size: Theme.iconSize - 4 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4) + anchors.right: parent.right + anchors.top: parent.top + + property bool isRefreshing: false + enabled: !isRefreshing + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor + onClicked: { + refreshButton.isRefreshing = true; + WeatherService.forceRefresh(); + refreshTimer.restart(); + } + enabled: parent.enabled + } + + Timer { + id: refreshTimer + interval: 2000 + onTriggered: refreshButton.isRefreshing = false + } + + NumberAnimation on rotation { + running: refreshButton.isRefreshing + from: 0 + to: 360 + duration: 1000 + loops: Animation.Infinite + } + } + + Item { + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + width: weatherIcon.width + tempColumn.width + sunriseColumn.width + Theme.spacingM * 2 + height: 70 + + DankIcon { + id: weatherIcon + name: WeatherService.getWeatherIcon(WeatherService.weather.wCode) + size: Theme.iconSize * 1.5 + color: Theme.primary + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + + layer.enabled: Theme.elevationEnabled + layer.effect: MultiEffect { + shadowEnabled: Theme.elevationEnabled + shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel1) + shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel1, 1) + shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined ? Theme.elevationLevel1.blurPx : 4) / Theme.elevationBlurMax)) : 0 + blurMax: Theme.elevationBlurMax + shadowColor: Theme.elevationShadowColor(Theme.elevationLevel1) + shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2 + } + } + + Column { + id: tempColumn + spacing: Theme.spacingXS + anchors.left: weatherIcon.right + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + + Item { + width: tempText.width + unitText.width + Theme.spacingXS + height: tempText.height + + StyledText { + id: tempText + text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°" + font.pixelSize: Theme.fontSizeLarge + 4 + color: Theme.surfaceText + font.weight: Font.Light + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: unitText + text: SettingsData.useFahrenheit ? "F" : "C" + font.pixelSize: Theme.fontSizeMedium + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.left: tempText.right + anchors.leftMargin: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (WeatherService.weather.available) + SettingsData.set("useFahrenheit", !SettingsData.useFahrenheit); + } + enabled: WeatherService.weather.available + } + } + } + + StyledText { + property var feelsLike: SettingsData.useFahrenheit ? (WeatherService.weather.feelsLikeF || WeatherService.weather.tempF) : (WeatherService.weather.feelsLike || WeatherService.weather.temp) + text: I18n.tr("Feels Like %1°").arg(feelsLike) + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) + } + + StyledText { + text: WeatherService.weather.city || "" + font.pixelSize: Theme.fontSizeMedium + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + visible: text.length > 0 + } + } + + Column { + id: sunriseColumn + spacing: Theme.spacingXS + anchors.left: tempColumn.right + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + visible: WeatherService.weather.sunrise && WeatherService.weather.sunset + + Item { + width: sunriseIcon.width + sunriseText.width + Theme.spacingXS + height: sunriseIcon.height + + DankIcon { + id: sunriseIcon + name: "wb_twilight" + size: Theme.iconSize - 6 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: sunriseText + text: WeatherService.weather.sunrise || "" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + anchors.left: sunriseIcon.right + anchors.leftMargin: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + } + } + + Item { + width: sunsetIcon.width + sunsetText.width + Theme.spacingXS + height: sunsetIcon.height + + DankIcon { + id: sunsetIcon + name: "bedtime" + size: Theme.iconSize - 6 + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: sunsetText + text: WeatherService.weather.sunset || "" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + anchors.left: sunsetIcon.right + anchors.leftMargin: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) + } + + GridLayout { + width: parent.width + height: 95 + columns: 6 + columnSpacing: Theme.spacingS + rowSpacing: 0 + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + anchors.horizontalCenter: parent.horizontalCenter + DankIcon { + anchors.centerIn: parent + name: "device_thermostat" + size: Theme.iconSize - 4 + color: Theme.primary + } + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 2 + StyledText { + text: I18n.tr("Feels Like") + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + StyledText { + text: (SettingsData.useFahrenheit ? (WeatherService.weather.feelsLikeF || WeatherService.weather.tempF) : (WeatherService.weather.feelsLike || WeatherService.weather.temp)) + "°" + font.pixelSize: Theme.fontSizeSmall + 1 + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + anchors.horizontalCenter: parent.horizontalCenter + DankIcon { + anchors.centerIn: parent + name: "humidity_low" + size: Theme.iconSize - 4 + color: Theme.primary + } + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 2 + StyledText { + text: I18n.tr("Humidity") + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + StyledText { + text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--" + font.pixelSize: Theme.fontSizeSmall + 1 + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + anchors.horizontalCenter: parent.horizontalCenter + DankIcon { + anchors.centerIn: parent + name: "air" + size: Theme.iconSize - 4 + color: Theme.primary + } + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 2 + StyledText { + text: I18n.tr("Wind") + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + StyledText { + id: windText + text: { + SettingsData.windSpeedUnit; + SettingsData.useFahrenheit; + return WeatherService.formatSpeed(WeatherService.weather.wind) || "--"; + } + font.pixelSize: Theme.fontSizeSmall + 1 + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: SettingsData.useFahrenheit ? Qt.ArrowCursor : Qt.PointingHandCursor + enabled: !SettingsData.useFahrenheit + onClicked: SettingsData.set("windSpeedUnit", SettingsData.windSpeedUnit === "kmh" ? "ms" : "kmh") + } + } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + anchors.horizontalCenter: parent.horizontalCenter + DankIcon { + anchors.centerIn: parent + name: "speed" + size: Theme.iconSize - 4 + color: Theme.primary + } + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 2 + StyledText { + text: I18n.tr("Pressure") + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + StyledText { + text: { + if (!WeatherService.weather.pressure) + return "--"; + if (SettingsData.useFahrenheit) + return (WeatherService.weather.pressure * 0.02953).toFixed(2) + " inHg"; + return WeatherService.weather.pressure + " hPa"; + } + font.pixelSize: Theme.fontSizeSmall + 1 + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + anchors.horizontalCenter: parent.horizontalCenter + DankIcon { + anchors.centerIn: parent + name: "rainy" + size: Theme.iconSize - 4 + color: Theme.primary + } + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 2 + StyledText { + text: I18n.tr("Rain Chance") + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + StyledText { + text: WeatherService.weather.precipitationProbability ? WeatherService.weather.precipitationProbability + "%" : "0%" + font.pixelSize: Theme.fontSizeSmall + 1 + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + anchors.horizontalCenter: parent.horizontalCenter + DankIcon { + anchors.centerIn: parent + name: "wb_sunny" + size: Theme.iconSize - 4 + color: Theme.primary + } + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 2 + StyledText { + text: I18n.tr("Visibility") + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + StyledText { + text: I18n.tr("Good") + font.pixelSize: Theme.fontSizeSmall + 1 + color: Theme.surfaceText + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/TypographyMotionTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/TypographyMotionTab.qml new file mode 100644 index 0000000..8648acb --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/TypographyMotionTab.qml @@ -0,0 +1,490 @@ +import QtQuick +import qs.Common +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + property var cachedFontFamilies: [] + property var cachedMonoFamilies: [] + property bool fontsEnumerated: false + + function enumerateFonts() { + var fonts = []; + var availableFonts = Qt.fontFamilies(); + + for (var i = 0; i < availableFonts.length; i++) { + var fontName = availableFonts[i]; + if (fontName.startsWith(".")) + continue; + fonts.push(fontName); + } + fonts.sort(); + fonts.unshift("Default"); + cachedFontFamilies = fonts; + cachedMonoFamilies = fonts; + } + + Timer { + id: fontEnumerationTimer + interval: 50 + running: false + onTriggered: { + if (fontsEnumerated) + return; + enumerateFonts(); + fontsEnumerated = true; + } + } + + Component.onCompleted: { + fontEnumerationTimer.start(); + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + tab: "typography" + tags: ["font", "family", "text", "typography"] + title: I18n.tr("Typography") + settingKey: "typography" + iconName: "text_fields" + + SettingsDropdownRow { + tab: "typography" + tags: ["font", "family", "normal", "text"] + settingKey: "fontFamily" + text: I18n.tr("Normal Font") + description: I18n.tr("Select the font family for UI text") + options: root.fontsEnumerated ? root.cachedFontFamilies : ["Default"] + currentValue: SettingsData.fontFamily === Theme.defaultFontFamily ? "Default" : (SettingsData.fontFamily || "Default") + enableFuzzySearch: true + popupWidthOffset: 100 + maxPopupHeight: 400 + onValueChanged: value => { + if (value === "Default") + SettingsData.set("fontFamily", Theme.defaultFontFamily); + else + SettingsData.set("fontFamily", value); + } + } + + SettingsDropdownRow { + tab: "typography" + tags: ["font", "monospace", "code", "terminal"] + settingKey: "monoFontFamily" + text: I18n.tr("Monospace Font") + description: I18n.tr("Select monospace font for process list and technical displays") + options: root.fontsEnumerated ? root.cachedMonoFamilies : ["Default"] + currentValue: SettingsData.monoFontFamily === SettingsData.defaultMonoFontFamily ? "Default" : (SettingsData.monoFontFamily || "Default") + enableFuzzySearch: true + popupWidthOffset: 100 + maxPopupHeight: 400 + onValueChanged: value => { + if (value === "Default") + SettingsData.set("monoFontFamily", SettingsData.defaultMonoFontFamily); + else + SettingsData.set("monoFontFamily", value); + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsDropdownRow { + tab: "typography" + tags: ["font", "weight", "bold", "light"] + settingKey: "fontWeight" + text: I18n.tr("Font Weight") + description: I18n.tr("Select font weight for UI text") + options: ["Thin", "Extra Light", "Light", "Regular", "Medium", "Demi Bold", "Bold", "Extra Bold", "Black"] + currentValue: { + switch (SettingsData.fontWeight) { + case Font.Thin: + return "Thin"; + case Font.ExtraLight: + return "Extra Light"; + case Font.Light: + return "Light"; + case Font.Normal: + return "Regular"; + case Font.Medium: + return "Medium"; + case Font.DemiBold: + return "Demi Bold"; + case Font.Bold: + return "Bold"; + case Font.ExtraBold: + return "Extra Bold"; + case Font.Black: + return "Black"; + default: + return "Regular"; + } + } + onValueChanged: value => { + var weight; + switch (value) { + case "Thin": + weight = Font.Thin; + break; + case "Extra Light": + weight = Font.ExtraLight; + break; + case "Light": + weight = Font.Light; + break; + case "Regular": + weight = Font.Normal; + break; + case "Medium": + weight = Font.Medium; + break; + case "Demi Bold": + weight = Font.DemiBold; + break; + case "Bold": + weight = Font.Bold; + break; + case "Extra Bold": + weight = Font.ExtraBold; + break; + case "Black": + weight = Font.Black; + break; + default: + weight = Font.Normal; + break; + } + SettingsData.set("fontWeight", weight); + } + } + + SettingsSliderRow { + tab: "typography" + tags: ["font", "scale", "size", "zoom"] + settingKey: "fontScale" + text: I18n.tr("Font Scale") + description: I18n.tr("Scale all font sizes throughout the shell") + minimum: 75 + maximum: 150 + value: Math.round(SettingsData.fontScale * 100) + unit: "%" + defaultValue: 100 + onSliderValueChanged: newValue => SettingsData.set("fontScale", newValue / 100) + } + } + + SettingsCard { + tab: "typography" + tags: ["animation", "speed", "motion", "duration"] + title: I18n.tr("Animation Speed") + settingKey: "animationSpeed" + iconName: "animation" + + Item { + width: parent.width + height: animationSpeedGroup.implicitHeight + clip: true + + DankButtonGroup { + id: animationSpeedGroup + anchors.horizontalCenter: parent.horizontalCenter + buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL + minButtonWidth: parent.width < 480 ? 44 : 64 + textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium + model: [I18n.tr("None"), I18n.tr("Short"), I18n.tr("Medium"), I18n.tr("Long"), I18n.tr("Custom")] + selectionMode: "single" + currentIndex: SettingsData.animationSpeed + onSelectionChanged: (index, selected) => { + if (!selected) + return; + SettingsData.set("animationSpeed", index); + } + + Connections { + target: SettingsData + function onAnimationSpeedChanged() { + animationSpeedGroup.currentIndex = SettingsData.animationSpeed; + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsSliderRow { + id: durationSlider + tab: "typography" + tags: ["animation", "duration", "custom", "speed"] + settingKey: "customAnimationDuration" + text: I18n.tr("Animation Duration") + description: I18n.tr("Globally scale all animation durations") + minimum: 0 + maximum: 1000 + value: Theme.currentAnimationBaseDuration + unit: "ms" + defaultValue: 200 + onSliderValueChanged: newValue => { + SettingsData.set("animationSpeed", SettingsData.AnimationSpeed.Custom); + SettingsData.set("customAnimationDuration", newValue); + } + + Connections { + target: SettingsData + function onAnimationSpeedChanged() { + if (SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) + return; + durationSlider.value = Theme.currentAnimationBaseDuration; + } + } + + Connections { + target: Theme + function onCurrentAnimationBaseDurationChanged() { + if (SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) + return; + durationSlider.value = Theme.currentAnimationBaseDuration; + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsToggleRow { + tab: "typography" + tags: ["animation", "sync", "popout", "modal", "global"] + settingKey: "syncComponentAnimationSpeeds" + text: I18n.tr("Sync Popouts & Modals") + description: I18n.tr("Popouts and Modals follow global Animation Speed (disable to customize independently)") + checked: SettingsData.syncComponentAnimationSpeeds + onToggled: checked => SettingsData.set("syncComponentAnimationSpeeds", checked) + + Connections { + target: SettingsData + function onSyncComponentAnimationSpeedsChanged() { + } + } + } + } + + SettingsCard { + tab: "typography" + tags: ["animation", "speed", "motion", "duration", "popout", "sync"] + title: I18n.tr("%1 Animation Speed").arg(I18n.tr("Popouts")) + settingKey: "popoutAnimationSpeed" + iconName: "open_in_new" + + Item { + width: parent.width + height: popoutSpeedGroup.implicitHeight + clip: true + + DankButtonGroup { + id: popoutSpeedGroup + anchors.horizontalCenter: parent.horizontalCenter + buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL + minButtonWidth: parent.width < 480 ? 44 : 64 + textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium + model: [I18n.tr("None"), I18n.tr("Short"), I18n.tr("Medium"), I18n.tr("Long"), I18n.tr("Custom")] + selectionMode: "single" + currentIndex: SettingsData.popoutAnimationSpeed + onSelectionChanged: (index, selected) => { + if (!selected) + return; + if (SettingsData.syncComponentAnimationSpeeds) + SettingsData.set("syncComponentAnimationSpeeds", false); + SettingsData.set("popoutAnimationSpeed", index); + } + + Connections { + target: SettingsData + function onPopoutAnimationSpeedChanged() { + popoutSpeedGroup.currentIndex = SettingsData.popoutAnimationSpeed; + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsSliderRow { + id: popoutDurationSlider + tab: "typography" + tags: ["animation", "duration", "custom", "speed", "popout"] + settingKey: "popoutCustomAnimationDuration" + text: I18n.tr("Custom Duration") + description: I18n.tr("%1 custom animation duration").arg(I18n.tr("Popouts")) + minimum: 0 + maximum: 1000 + value: Theme.popoutAnimationDuration + unit: "ms" + defaultValue: 150 + onSliderValueChanged: newValue => { + if (SettingsData.syncComponentAnimationSpeeds) + SettingsData.set("syncComponentAnimationSpeeds", false); + SettingsData.set("popoutAnimationSpeed", SettingsData.AnimationSpeed.Custom); + SettingsData.set("popoutCustomAnimationDuration", newValue); + } + + Connections { + target: SettingsData + function onPopoutAnimationSpeedChanged() { + if (SettingsData.popoutAnimationSpeed === SettingsData.AnimationSpeed.Custom) + return; + popoutDurationSlider.value = Theme.popoutAnimationDuration; + } + } + + Connections { + target: Theme + function onPopoutAnimationDurationChanged() { + if (!SettingsData.syncComponentAnimationSpeeds && SettingsData.popoutAnimationSpeed === SettingsData.AnimationSpeed.Custom) + return; + popoutDurationSlider.value = Theme.popoutAnimationDuration; + } + } + } + } + + SettingsCard { + tab: "typography" + tags: ["animation", "speed", "motion", "duration", "modal", "sync"] + title: I18n.tr("%1 Animation Speed").arg(I18n.tr("Modals")) + settingKey: "modalAnimationSpeed" + iconName: "web_asset" + + Item { + width: parent.width + height: modalSpeedGroup.implicitHeight + clip: true + + DankButtonGroup { + id: modalSpeedGroup + anchors.horizontalCenter: parent.horizontalCenter + buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL + minButtonWidth: parent.width < 480 ? 44 : 64 + textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium + model: [I18n.tr("None"), I18n.tr("Short"), I18n.tr("Medium"), I18n.tr("Long"), I18n.tr("Custom")] + selectionMode: "single" + currentIndex: SettingsData.modalAnimationSpeed + onSelectionChanged: (index, selected) => { + if (!selected) + return; + if (SettingsData.syncComponentAnimationSpeeds) + SettingsData.set("syncComponentAnimationSpeeds", false); + SettingsData.set("modalAnimationSpeed", index); + } + + Connections { + target: SettingsData + function onModalAnimationSpeedChanged() { + modalSpeedGroup.currentIndex = SettingsData.modalAnimationSpeed; + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsSliderRow { + id: modalDurationSlider + tab: "typography" + tags: ["animation", "duration", "custom", "speed", "modal"] + settingKey: "modalCustomAnimationDuration" + text: I18n.tr("Custom Duration") + description: I18n.tr("%1 custom animation duration").arg(I18n.tr("Modals")) + minimum: 0 + maximum: 1000 + value: Theme.modalAnimationDuration + unit: "ms" + defaultValue: 150 + onSliderValueChanged: newValue => { + if (SettingsData.syncComponentAnimationSpeeds) + SettingsData.set("syncComponentAnimationSpeeds", false); + SettingsData.set("modalAnimationSpeed", SettingsData.AnimationSpeed.Custom); + SettingsData.set("modalCustomAnimationDuration", newValue); + } + + Connections { + target: SettingsData + function onModalAnimationSpeedChanged() { + if (SettingsData.modalAnimationSpeed === SettingsData.AnimationSpeed.Custom) + return; + modalDurationSlider.value = Theme.modalAnimationDuration; + } + } + + Connections { + target: Theme + function onModalAnimationDurationChanged() { + if (!SettingsData.syncComponentAnimationSpeeds && SettingsData.modalAnimationSpeed === SettingsData.AnimationSpeed.Custom) + return; + modalDurationSlider.value = Theme.modalAnimationDuration; + } + } + } + } + + SettingsCard { + tab: "typography" + tags: ["animation", "ripple", "effect", "material", "feedback"] + title: I18n.tr("Ripple Effects") + settingKey: "enableRippleEffects" + iconName: "radio_button_unchecked" + + SettingsToggleRow { + tab: "typography" + tags: ["animation", "ripple", "effect", "material", "click"] + settingKey: "enableRippleEffects" + text: I18n.tr("Enable Ripple Effects") + description: I18n.tr("Show Material Design ripple animations on interactive elements") + checked: SettingsData.enableRippleEffects ?? true + onToggled: newValue => SettingsData.set("enableRippleEffects", newValue) + + Connections { + target: SettingsData + function onEnableRippleEffectsChanged() { + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WallpaperTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WallpaperTab.qml new file mode 100644 index 0000000..c0f83c1 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WallpaperTab.qml @@ -0,0 +1,1368 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import qs.Common +import qs.Modals.FileBrowser +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property var parentModal: null + property string selectedMonitorName: { + var screens = Quickshell.screens; + return screens.length > 0 ? screens[0].name : ""; + } + property string currentWallpaper: { + if (!SessionData.perMonitorWallpaper) + return SessionData.wallpaperPath; + var map = SessionData.monitorWallpapers; + var screens = Quickshell.screens; + for (var i = 0; i < screens.length; i++) { + if (screens[i].name !== selectedMonitorName) + continue; + var screen = screens[i]; + if (map[screen.name] !== undefined) + return map[screen.name]; + if (screen.model && map[screen.model] !== undefined) + return map[screen.model]; + var displayName = SettingsData.getScreenDisplayName(screen); + if (displayName && map[displayName] !== undefined) + return map[displayName]; + break; + } + return SessionData.wallpaperPath; + } + + Component.onCompleted: { + WallpaperCyclingService.cyclingActive; + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + tab: "wallpaper" + tags: ["background", "image", "picture"] + title: I18n.tr("Wallpaper") + settingKey: "wallpaper" + iconName: "wallpaper" + + Row { + width: parent.width + spacing: Theme.spacingL + + StyledRect { + id: wallpaperPreview + width: 160 + height: 90 + radius: Theme.cornerRadius + color: Theme.surfaceVariant + + Image { + anchors.fill: parent + anchors.margins: 1 + source: { + var wp = root.currentWallpaper; + if (wp === "" || wp.startsWith("#")) + return ""; + if (wp.startsWith("file://")) + wp = wp.substring(7); + return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/'); + } + fillMode: Image.PreserveAspectCrop + visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#") + sourceSize.width: 160 + sourceSize.height: 160 + asynchronous: true + layer.enabled: true + layer.effect: MultiEffect { + maskEnabled: true + maskSource: wallpaperMask + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: Theme.cornerRadius - 1 + color: root.currentWallpaper.startsWith("#") ? root.currentWallpaper : "transparent" + visible: root.currentWallpaper !== "" && root.currentWallpaper.startsWith("#") + } + + Rectangle { + id: wallpaperMask + anchors.fill: parent + anchors.margins: 1 + radius: Theme.cornerRadius - 1 + color: "black" + visible: false + layer.enabled: true + } + + DankIcon { + anchors.centerIn: parent + name: "image" + size: Theme.iconSizeLarge + 8 + color: Theme.surfaceVariantText + visible: root.currentWallpaper === "" + } + + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: Theme.cornerRadius - 1 + color: Qt.rgba(0, 0, 0, 0.7) + visible: wallpaperMouseArea.containsMouse + + Row { + anchors.centerIn: parent + spacing: 4 + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(255, 255, 255, 0.9) + + DankIcon { + anchors.centerIn: parent + name: "folder_open" + size: 18 + color: "black" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.openMainWallpaperBrowser() + } + } + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(255, 255, 255, 0.9) + + DankIcon { + anchors.centerIn: parent + name: "palette" + size: 18 + color: "black" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!PopoutService.colorPickerModal) + return; + PopoutService.colorPickerModal.selectedColor = root.currentWallpaper.startsWith("#") ? root.currentWallpaper : Theme.primary; + PopoutService.colorPickerModal.pickerTitle = I18n.tr("Choose Wallpaper Color", "wallpaper color picker title"); + PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) { + if (SessionData.perMonitorWallpaper) { + SessionData.setMonitorWallpaper(selectedMonitorName, selectedColor); + } else { + SessionData.setWallpaperColor(selectedColor); + } + }; + PopoutService.colorPickerModal.show(); + } + } + } + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: Qt.rgba(255, 255, 255, 0.9) + visible: root.currentWallpaper !== "" + + DankIcon { + anchors.centerIn: parent + name: "clear" + size: 18 + color: "black" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (SessionData.perMonitorWallpaper) { + SessionData.setMonitorWallpaper(selectedMonitorName, ""); + } else { + if (Theme.currentTheme === Theme.dynamic) + Theme.switchTheme("blue"); + SessionData.clearWallpaper(); + } + } + } + } + } + } + + MouseArea { + id: wallpaperMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + propagateComposedEvents: true + acceptedButtons: Qt.NoButton + } + } + + Column { + width: parent.width - 160 - Theme.spacingL + spacing: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: root.currentWallpaper ? root.currentWallpaper.split('/').pop() : I18n.tr("No wallpaper selected") + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + elide: Text.ElideMiddle + maximumLineCount: 1 + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: root.currentWallpaper + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideMiddle + maximumLineCount: 1 + width: parent.width + horizontalAlignment: Text.AlignLeft + visible: root.currentWallpaper !== "" + } + + Row { + anchors.left: parent.left + spacing: Theme.spacingS + layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight + visible: root.currentWallpaper !== "" + + DankActionButton { + buttonSize: 32 + iconName: "skip_previous" + iconSize: Theme.iconSizeSmall + enabled: root.currentWallpaper && !root.currentWallpaper.startsWith("#") && !root.currentWallpaper.startsWith("we") + opacity: enabled ? 1 : 0.5 + backgroundColor: Theme.surfaceContainerHigh + iconColor: Theme.surfaceText + onClicked: { + if (SessionData.perMonitorWallpaper) { + WallpaperCyclingService.cyclePrevForMonitor(selectedMonitorName); + } else { + WallpaperCyclingService.cyclePrevManually(); + } + } + } + + DankActionButton { + buttonSize: 32 + iconName: "skip_next" + iconSize: Theme.iconSizeSmall + enabled: root.currentWallpaper && !root.currentWallpaper.startsWith("#") && !root.currentWallpaper.startsWith("we") + opacity: enabled ? 1 : 0.5 + backgroundColor: Theme.surfaceContainerHigh + iconColor: Theme.surfaceText + onClicked: { + if (SessionData.perMonitorWallpaper) { + WallpaperCyclingService.cycleNextForMonitor(selectedMonitorName); + } else { + WallpaperCyclingService.cycleNextManually(); + } + } + } + } + } + } + + Item { + width: parent.width + height: fillModeGroup.height + visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#") + + DankButtonGroup { + id: fillModeGroup + property var internalModes: ["Stretch", "Fit", "Fill", "Tile", "TileVertically", "TileHorizontally", "Pad"] + anchors.horizontalCenter: parent.horizontalCenter + model: [I18n.tr("Stretch", "wallpaper fill mode"), I18n.tr("Fit", "wallpaper fill mode"), I18n.tr("Fill", "wallpaper fill mode"), I18n.tr("Tile", "wallpaper fill mode"), I18n.tr("Tile V", "wallpaper fill mode"), I18n.tr("Tile H", "wallpaper fill mode"), I18n.tr("Pad", "wallpaper fill mode")] + selectionMode: "single" + buttonHeight: 28 + minButtonWidth: 48 + buttonPadding: Theme.spacingS + checkIconSize: 0 + textSize: Theme.fontSizeSmall + checkEnabled: false + currentIndex: { + var mode = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaperFillMode(selectedMonitorName) : SettingsData.wallpaperFillMode; + return internalModes.indexOf(mode); + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + if (SessionData.perMonitorWallpaper) { + SessionData.setMonitorWallpaperFillMode(selectedMonitorName, internalModes[index]); + } else { + SettingsData.set("wallpaperFillMode", internalModes[index]); + } + } + + Connections { + target: SettingsData + function onWallpaperFillModeChanged() { + if (SessionData.perMonitorWallpaper) + return; + fillModeGroup.currentIndex = fillModeGroup.internalModes.indexOf(SettingsData.wallpaperFillMode); + } + } + + Connections { + target: root + function onSelectedMonitorNameChanged() { + if (!SessionData.perMonitorWallpaper) + return; + fillModeGroup.currentIndex = Qt.binding(() => { + var mode = SessionData.perMonitorWallpaper ? SessionData.getMonitorWallpaperFillMode(selectedMonitorName) : SettingsData.wallpaperFillMode; + return fillModeGroup.internalModes.indexOf(mode); + }); + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + visible: SessionData.wallpaperPath !== "" + } + + SettingsToggleRow { + tab: "wallpaper" + tags: ["per-mode", "light", "dark", "theme"] + settingKey: "perModeWallpaper" + visible: SessionData.wallpaperPath !== "" + text: I18n.tr("Per-Mode Wallpapers") + description: I18n.tr("Set different wallpapers for light and dark mode") + checked: SessionData.perModeWallpaper + onToggled: toggled => SessionData.setPerModeWallpaper(toggled) + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: SessionData.perModeWallpaper + leftPadding: Theme.spacingM + rightPadding: Theme.spacingM + + Row { + width: parent.width - Theme.spacingM * 2 + spacing: Theme.spacingL + + Column { + width: (parent.width - Theme.spacingL) / 2 + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Light Mode") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledRect { + width: parent.width + height: width * 9 / 16 + radius: Theme.cornerRadius + color: Theme.surfaceVariant + + Image { + anchors.fill: parent + anchors.margins: 1 + source: { + var wp = SessionData.wallpaperPathLight; + if (wp === "" || wp.startsWith("#")) + return ""; + if (wp.startsWith("file://")) + wp = wp.substring(7); + return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/'); + } + fillMode: Image.PreserveAspectCrop + visible: { + var lightWallpaper = SessionData.wallpaperPathLight; + return lightWallpaper !== "" && !lightWallpaper.startsWith("#"); + } + sourceSize.width: 160 + sourceSize.height: 160 + asynchronous: true + layer.enabled: true + layer.effect: MultiEffect { + maskEnabled: true + maskSource: lightMask + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: Theme.cornerRadius - 1 + color: { + var lightWallpaper = SessionData.wallpaperPathLight; + return lightWallpaper.startsWith("#") ? lightWallpaper : "transparent"; + } + visible: { + var lightWallpaper = SessionData.wallpaperPathLight; + return lightWallpaper !== "" && lightWallpaper.startsWith("#"); + } + } + + Rectangle { + id: lightMask + anchors.fill: parent + anchors.margins: 1 + radius: Theme.cornerRadius - 1 + color: "black" + visible: false + layer.enabled: true + } + + DankIcon { + anchors.centerIn: parent + name: "light_mode" + size: Theme.iconSizeLarge + color: Theme.surfaceVariantText + visible: SessionData.wallpaperPathLight === "" + } + + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: Theme.cornerRadius - 1 + color: Qt.rgba(0, 0, 0, 0.7) + visible: lightModeMouseArea.containsMouse + + Row { + anchors.centerIn: parent + spacing: 4 + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: Qt.rgba(255, 255, 255, 0.9) + + DankIcon { + anchors.centerIn: parent + name: "folder_open" + size: 16 + color: "black" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.openLightWallpaperBrowser() + } + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: Qt.rgba(255, 255, 255, 0.9) + + DankIcon { + anchors.centerIn: parent + name: "palette" + size: 16 + color: "black" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!PopoutService.colorPickerModal) + return; + var lightWallpaper = SessionData.wallpaperPathLight; + PopoutService.colorPickerModal.selectedColor = lightWallpaper.startsWith("#") ? lightWallpaper : Theme.primary; + PopoutService.colorPickerModal.pickerTitle = I18n.tr("Choose Light Mode Color", "light mode wallpaper color picker title"); + PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) { + SessionData.wallpaperPathLight = selectedColor; + SessionData.syncWallpaperForCurrentMode(); + SessionData.saveSettings(); + }; + PopoutService.colorPickerModal.show(); + } + } + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: Qt.rgba(255, 255, 255, 0.9) + visible: SessionData.wallpaperPathLight !== "" + + DankIcon { + anchors.centerIn: parent + name: "clear" + size: 16 + color: "black" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + SessionData.wallpaperPathLight = ""; + SessionData.syncWallpaperForCurrentMode(); + SessionData.saveSettings(); + } + } + } + } + } + + MouseArea { + id: lightModeMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + propagateComposedEvents: true + acceptedButtons: Qt.NoButton + } + } + + StyledText { + text: { + var lightWallpaper = SessionData.wallpaperPathLight; + return lightWallpaper ? lightWallpaper.split('/').pop() : I18n.tr("Not set", "wallpaper not set label"); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideMiddle + maximumLineCount: 1 + width: parent.width + } + } + + Column { + width: (parent.width - Theme.spacingL) / 2 + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Dark Mode") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledRect { + width: parent.width + height: width * 9 / 16 + radius: Theme.cornerRadius + color: Theme.surfaceVariant + + Image { + anchors.fill: parent + anchors.margins: 1 + source: { + var wp = SessionData.wallpaperPathDark; + if (wp === "" || wp.startsWith("#")) + return ""; + if (wp.startsWith("file://")) + wp = wp.substring(7); + return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/'); + } + fillMode: Image.PreserveAspectCrop + visible: { + var darkWallpaper = SessionData.wallpaperPathDark; + return darkWallpaper !== "" && !darkWallpaper.startsWith("#"); + } + sourceSize.width: 160 + sourceSize.height: 160 + asynchronous: true + layer.enabled: true + layer.effect: MultiEffect { + maskEnabled: true + maskSource: darkMask + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: Theme.cornerRadius - 1 + color: { + var darkWallpaper = SessionData.wallpaperPathDark; + return darkWallpaper.startsWith("#") ? darkWallpaper : "transparent"; + } + visible: { + var darkWallpaper = SessionData.wallpaperPathDark; + return darkWallpaper !== "" && darkWallpaper.startsWith("#"); + } + } + + Rectangle { + id: darkMask + anchors.fill: parent + anchors.margins: 1 + radius: Theme.cornerRadius - 1 + color: "black" + visible: false + layer.enabled: true + } + + DankIcon { + anchors.centerIn: parent + name: "dark_mode" + size: Theme.iconSizeLarge + color: Theme.surfaceVariantText + visible: SessionData.wallpaperPathDark === "" + } + + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: Theme.cornerRadius - 1 + color: Qt.rgba(0, 0, 0, 0.7) + visible: darkModeMouseArea.containsMouse + + Row { + anchors.centerIn: parent + spacing: 4 + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: Qt.rgba(255, 255, 255, 0.9) + + DankIcon { + anchors.centerIn: parent + name: "folder_open" + size: 16 + color: "black" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.openDarkWallpaperBrowser() + } + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: Qt.rgba(255, 255, 255, 0.9) + + DankIcon { + anchors.centerIn: parent + name: "palette" + size: 16 + color: "black" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!PopoutService.colorPickerModal) + return; + var darkWallpaper = SessionData.wallpaperPathDark; + PopoutService.colorPickerModal.selectedColor = darkWallpaper.startsWith("#") ? darkWallpaper : Theme.primary; + PopoutService.colorPickerModal.pickerTitle = I18n.tr("Choose Dark Mode Color", "dark mode wallpaper color picker title"); + PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) { + SessionData.wallpaperPathDark = selectedColor; + SessionData.syncWallpaperForCurrentMode(); + SessionData.saveSettings(); + }; + PopoutService.colorPickerModal.show(); + } + } + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: Qt.rgba(255, 255, 255, 0.9) + visible: SessionData.wallpaperPathDark !== "" + + DankIcon { + anchors.centerIn: parent + name: "clear" + size: 16 + color: "black" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + SessionData.wallpaperPathDark = ""; + SessionData.syncWallpaperForCurrentMode(); + SessionData.saveSettings(); + } + } + } + } + } + + MouseArea { + id: darkModeMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + propagateComposedEvents: true + acceptedButtons: Qt.NoButton + } + } + + StyledText { + text: { + var darkWallpaper = SessionData.wallpaperPathDark; + return darkWallpaper ? darkWallpaper.split('/').pop() : I18n.tr("Not set", "wallpaper not set label"); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideMiddle + maximumLineCount: 1 + width: parent.width + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + visible: CompositorService.isNiri + } + + SettingsToggleRow { + tab: "wallpaper" + tags: ["blur", "overview", "niri"] + settingKey: "blurWallpaperOnOverview" + visible: CompositorService.isNiri + text: I18n.tr("Blur on Overview") + description: I18n.tr("Blur wallpaper when niri overview is open") + checked: SettingsData.blurWallpaperOnOverview + onToggled: checked => SettingsData.set("blurWallpaperOnOverview", checked) + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + visible: SessionData.wallpaperPath !== "" + } + + SettingsToggleRow { + tab: "wallpaper" + tags: ["per-monitor", "multi-monitor", "display"] + settingKey: "perMonitorWallpaper" + visible: SessionData.wallpaperPath !== "" + text: I18n.tr("Per-Monitor Wallpapers") + description: I18n.tr("Set different wallpapers for each connected monitor") + checked: SessionData.perMonitorWallpaper + onToggled: toggled => SessionData.setPerMonitorWallpaper(toggled) + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: SessionData.perMonitorWallpaper + leftPadding: Theme.spacingM + rightPadding: Theme.spacingM + + SettingsDropdownRow { + tab: "wallpaper" + tags: ["monitor", "display", "screen"] + settingKey: "selectedMonitor" + width: parent.width - Theme.spacingM * 2 + text: I18n.tr("Wallpaper Monitor") + description: I18n.tr("Select monitor to configure wallpaper") + currentValue: { + var screens = Quickshell.screens; + for (var i = 0; i < screens.length; i++) { + if (screens[i].name === selectedMonitorName) { + return SettingsData.getScreenDisplayName(screens[i]); + } + } + return I18n.tr("No monitors", "no monitors available label"); + } + options: { + var screenNames = []; + var screens = Quickshell.screens; + for (var i = 0; i < screens.length; i++) { + screenNames.push(SettingsData.getScreenDisplayName(screens[i])); + } + return screenNames; + } + onValueChanged: value => { + var screens = Quickshell.screens; + for (var i = 0; i < screens.length; i++) { + if (SettingsData.getScreenDisplayName(screens[i]) === value) { + selectedMonitorName = screens[i].name; + return; + } + } + } + } + + SettingsDropdownRow { + tab: "wallpaper" + tags: ["matugen", "target", "monitor", "theming"] + settingKey: "matugenTargetMonitor" + width: parent.width - Theme.spacingM * 2 + text: I18n.tr("Matugen Target Monitor") + description: I18n.tr("Monitor whose wallpaper drives dynamic theming colors") + currentValue: { + var screens = Quickshell.screens; + if (!SettingsData.matugenTargetMonitor || SettingsData.matugenTargetMonitor === "") { + return screens.length > 0 ? SettingsData.getScreenDisplayName(screens[0]) + " " + I18n.tr("(Default)", "default monitor label suffix") : I18n.tr("No monitors", "no monitors available label"); + } + for (var i = 0; i < screens.length; i++) { + if (screens[i].name === SettingsData.matugenTargetMonitor) { + return SettingsData.getScreenDisplayName(screens[i]); + } + } + return SettingsData.matugenTargetMonitor; + } + options: { + var screenNames = []; + var screens = Quickshell.screens; + for (var i = 0; i < screens.length; i++) { + var label = SettingsData.getScreenDisplayName(screens[i]); + if (i === 0 && (!SettingsData.matugenTargetMonitor || SettingsData.matugenTargetMonitor === "")) { + label += " " + I18n.tr("(Default)", "default monitor label suffix"); + } + screenNames.push(label); + } + return screenNames; + } + onValueChanged: value => { + var cleanValue = value.replace(" " + I18n.tr("(Default)", "default monitor label suffix"), ""); + var screens = Quickshell.screens; + for (var i = 0; i < screens.length; i++) { + if (SettingsData.getScreenDisplayName(screens[i]) === cleanValue) { + SettingsData.setMatugenTargetMonitor(screens[i].name); + return; + } + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + visible: (SessionData.wallpaperPath !== "" || SessionData.perMonitorWallpaper) && !SessionData.perModeWallpaper + } + + SettingsToggleRow { + id: cyclingToggle + tab: "wallpaper" + tags: ["cycling", "automatic", "rotate", "slideshow"] + settingKey: "wallpaperCyclingEnabled" + visible: (SessionData.wallpaperPath !== "" || SessionData.perMonitorWallpaper) && !SessionData.perModeWallpaper + text: I18n.tr("Automatic Cycling") + description: I18n.tr("Automatically cycle through wallpapers in the same folder") + checked: SessionData.perMonitorWallpaper ? SessionData.getMonitorCyclingSettings(selectedMonitorName).enabled : SessionData.wallpaperCyclingEnabled + onToggled: toggled => { + if (SessionData.perMonitorWallpaper) { + SessionData.setMonitorCyclingEnabled(selectedMonitorName, toggled); + } else { + SessionData.setWallpaperCyclingEnabled(toggled); + } + } + + Connections { + target: root + function onSelectedMonitorNameChanged() { + cyclingToggle.checked = Qt.binding(() => { + return SessionData.perMonitorWallpaper ? SessionData.getMonitorCyclingSettings(selectedMonitorName).enabled : SessionData.wallpaperCyclingEnabled; + }); + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: SessionData.perMonitorWallpaper ? SessionData.getMonitorCyclingSettings(selectedMonitorName).enabled : SessionData.wallpaperCyclingEnabled + leftPadding: Theme.spacingM + rightPadding: Theme.spacingM + + Row { + spacing: Theme.spacingL + width: parent.width - Theme.spacingM * 2 + + StyledText { + text: I18n.tr("Mode:") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: 200 + height: 45 + Theme.spacingM + + DankTabBar { + id: modeTabBar + width: 200 + height: 45 + model: [ + { + "text": I18n.tr("Interval", "wallpaper cycling mode tab"), + "icon": "schedule" + }, + { + "text": I18n.tr("Time", "wallpaper cycling mode tab"), + "icon": "access_time" + } + ] + currentIndex: { + if (SessionData.perMonitorWallpaper) { + return SessionData.getMonitorCyclingSettings(selectedMonitorName).mode === "time" ? 1 : 0; + } + return SessionData.wallpaperCyclingMode === "time" ? 1 : 0; + } + onTabClicked: index => { + if (SessionData.perMonitorWallpaper) { + SessionData.setMonitorCyclingMode(selectedMonitorName, index === 1 ? "time" : "interval"); + } else { + SessionData.setWallpaperCyclingMode(index === 1 ? "time" : "interval"); + } + } + + Connections { + target: root + function onSelectedMonitorNameChanged() { + modeTabBar.currentIndex = Qt.binding(() => { + if (SessionData.perMonitorWallpaper) { + return SessionData.getMonitorCyclingSettings(selectedMonitorName).mode === "time" ? 1 : 0; + } + return SessionData.wallpaperCyclingMode === "time" ? 1 : 0; + }); + Qt.callLater(modeTabBar.updateIndicator); + } + } + } + } + } + + SettingsDropdownRow { + id: intervalDropdown + property var intervalOptions: [I18n.tr("5 seconds", "wallpaper interval"), I18n.tr("10 seconds", "wallpaper interval"), I18n.tr("15 seconds", "wallpaper interval"), I18n.tr("20 seconds", "wallpaper interval"), I18n.tr("25 seconds", "wallpaper interval"), I18n.tr("30 seconds", "wallpaper interval"), I18n.tr("35 seconds", "wallpaper interval"), I18n.tr("40 seconds", "wallpaper interval"), I18n.tr("45 seconds", "wallpaper interval"), I18n.tr("50 seconds", "wallpaper interval"), I18n.tr("55 seconds", "wallpaper interval"), I18n.tr("1 minute", "wallpaper interval"), I18n.tr("5 minutes", "wallpaper interval"), I18n.tr("15 minutes", "wallpaper interval"), I18n.tr("30 minutes", "wallpaper interval"), I18n.tr("1 hour", "wallpaper interval"), I18n.tr("1 hour 30 minutes", "wallpaper interval"), I18n.tr("2 hours", "wallpaper interval"), I18n.tr("3 hours", "wallpaper interval"), I18n.tr("4 hours", "wallpaper interval"), I18n.tr("6 hours", "wallpaper interval"), I18n.tr("8 hours", "wallpaper interval"), I18n.tr("12 hours", "wallpaper interval")] + + property var intervalValues: [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 300, 900, 1800, 3600, 5400, 7200, 10800, 14400, 21600, 28800, 43200] + tab: "wallpaper" + tags: ["interval", "cycling", "time", "frequency"] + settingKey: "wallpaperCyclingInterval" + width: parent.width - Theme.spacingM * 2 + visible: { + if (SessionData.perMonitorWallpaper) { + return SessionData.getMonitorCyclingSettings(selectedMonitorName).mode === "interval"; + } + return SessionData.wallpaperCyclingMode === "interval"; + } + text: I18n.tr("Interval") + description: I18n.tr("How often to change wallpaper") + options: intervalOptions + currentValue: { + var currentSeconds; + if (SessionData.perMonitorWallpaper) { + currentSeconds = SessionData.getMonitorCyclingSettings(selectedMonitorName).interval; + } else { + currentSeconds = SessionData.wallpaperCyclingInterval; + } + const index = intervalValues.indexOf(currentSeconds); + return index >= 0 ? intervalOptions[index] : I18n.tr("5 minutes", "wallpaper interval"); + } + onValueChanged: value => { + const index = intervalOptions.indexOf(value); + if (index < 0) + return; + if (SessionData.perMonitorWallpaper) { + SessionData.setMonitorCyclingInterval(selectedMonitorName, intervalValues[index]); + } else { + SessionData.setWallpaperCyclingInterval(intervalValues[index]); + } + } + + Connections { + target: root + function onSelectedMonitorNameChanged() { + Qt.callLater(() => { + var currentSeconds; + if (SessionData.perMonitorWallpaper) { + currentSeconds = SessionData.getMonitorCyclingSettings(selectedMonitorName).interval; + } else { + currentSeconds = SessionData.wallpaperCyclingInterval; + } + const index = intervalDropdown.intervalValues.indexOf(currentSeconds); + intervalDropdown.currentValue = index >= 0 ? intervalDropdown.intervalOptions[index] : I18n.tr("5 minutes", "wallpaper interval"); + }); + } + } + } + + Row { + spacing: Theme.spacingM + visible: { + if (SessionData.perMonitorWallpaper) { + return SessionData.getMonitorCyclingSettings(selectedMonitorName).mode === "time"; + } + return SessionData.wallpaperCyclingMode === "time"; + } + width: parent.width - Theme.spacingM * 2 + + StyledText { + text: I18n.tr("Daily at:") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + DankTextField { + id: timeTextField + width: 100 + height: 40 + text: { + if (SessionData.perMonitorWallpaper) { + return SessionData.getMonitorCyclingSettings(selectedMonitorName).time; + } + return SessionData.wallpaperCyclingTime; + } + placeholderText: "00:00" + maximumLength: 5 + topPadding: Theme.spacingS + bottomPadding: Theme.spacingS + onAccepted: { + var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(text); + if (isValid) { + if (SessionData.perMonitorWallpaper) { + SessionData.setMonitorCyclingTime(selectedMonitorName, text); + } else { + SessionData.setWallpaperCyclingTime(text); + } + } else { + if (SessionData.perMonitorWallpaper) { + text = SessionData.getMonitorCyclingSettings(selectedMonitorName).time; + } else { + text = SessionData.wallpaperCyclingTime; + } + } + } + onEditingFinished: { + var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(text); + if (isValid) { + if (SessionData.perMonitorWallpaper) { + SessionData.setMonitorCyclingTime(selectedMonitorName, text); + } else { + SessionData.setWallpaperCyclingTime(text); + } + } else { + if (SessionData.perMonitorWallpaper) { + text = SessionData.getMonitorCyclingSettings(selectedMonitorName).time; + } else { + text = SessionData.wallpaperCyclingTime; + } + } + } + anchors.verticalCenter: parent.verticalCenter + + validator: RegularExpressionValidator { + regularExpression: /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/ + } + + Connections { + target: root + function onSelectedMonitorNameChanged() { + Qt.callLater(() => { + if (SessionData.perMonitorWallpaper) { + timeTextField.text = SessionData.getMonitorCyclingSettings(selectedMonitorName).time; + } else { + timeTextField.text = SessionData.wallpaperCyclingTime; + } + }); + } + } + } + + StyledText { + text: I18n.tr("24-hour format") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + } + + SettingsDropdownRow { + tab: "wallpaper" + tags: ["transition", "effect", "animation", "change"] + settingKey: "wallpaperTransition" + text: I18n.tr("Transition Effect") + description: I18n.tr("Visual effect used when wallpaper changes") + + function getTransitionLabel(t) { + switch (t) { + case "random": + return I18n.tr("Random", "wallpaper transition option"); + case "none": + return I18n.tr("None", "wallpaper transition option"); + case "fade": + return I18n.tr("Fade", "wallpaper transition option"); + case "wipe": + return I18n.tr("Wipe", "wallpaper transition option"); + case "disc": + return I18n.tr("Disc", "wallpaper transition option"); + case "stripes": + return I18n.tr("Stripes", "wallpaper transition option"); + case "iris bloom": + return I18n.tr("Iris Bloom", "wallpaper transition option"); + case "pixelate": + return I18n.tr("Pixelate", "wallpaper transition option"); + case "portal": + return I18n.tr("Portal", "wallpaper transition option"); + default: + return t.charAt(0).toUpperCase() + t.slice(1); + } + } + + currentValue: getTransitionLabel(SessionData.wallpaperTransition) + options: [I18n.tr("Random", "wallpaper transition option")].concat(SessionData.availableWallpaperTransitions.map(t => getTransitionLabel(t))) + onValueChanged: value => { + const transitionMap = {}; + transitionMap[I18n.tr("Random", "wallpaper transition option")] = "random"; + SessionData.availableWallpaperTransitions.forEach(t => { + transitionMap[getTransitionLabel(t)] = t; + }); + SessionData.setWallpaperTransition(transitionMap[value] || value.toLowerCase()); + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: SessionData.wallpaperTransition === "random" + leftPadding: Theme.spacingM + rightPadding: Theme.spacingM + + StyledText { + text: I18n.tr("Include Transitions") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: I18n.tr("Select which transitions to include in randomization") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width - Theme.spacingM * 2 + } + + DankButtonGroup { + id: transitionGroup + width: parent.width - Theme.spacingM * 2 + selectionMode: "multi" + model: SessionData.availableWallpaperTransitions.filter(t => t !== "none") + initialSelection: SessionData.includedTransitions + currentSelection: SessionData.includedTransitions + + onSelectionChanged: (index, selected) => { + const transition = model[index]; + let newIncluded = SessionData.includedTransitions.slice(); + + if (selected && !newIncluded.includes(transition)) { + newIncluded.push(transition); + } else if (!selected && newIncluded.includes(transition)) { + newIncluded = newIncluded.filter(t => t !== transition); + } + + SessionData.includedTransitions = newIncluded; + } + } + } + } + + SettingsCard { + tab: "wallpaper" + tags: ["external", "disable", "swww", "hyprpaper", "swaybg"] + title: I18n.tr("External Wallpaper Management", "wallpaper settings external management") + settingKey: "disableWallpaper" + iconName: "wallpaper" + + SettingsToggleRow { + tab: "wallpaper" + tags: ["disable", "external", "management"] + settingKey: "disableWallpapers" + text: I18n.tr("Disable Built-in Wallpapers", "wallpaper settings disable toggle") + description: I18n.tr("Use an external wallpaper manager like swww, hyprpaper, or swaybg.", "wallpaper settings disable description") + checked: { + var prefs = SettingsData.screenPreferences?.wallpaper; + if (!prefs) + return false; + if (Array.isArray(prefs) && prefs.length === 0) + return true; + return false; + } + onToggled: checked => { + var prefs = SettingsData.screenPreferences || {}; + var newPrefs = Object.assign({}, prefs); + newPrefs.wallpaper = checked ? [] : ["all"]; + SettingsData.set("screenPreferences", newPrefs); + } + } + } + + SettingsCard { + tab: "wallpaper" + tags: ["blur", "layer", "niri", "compositor"] + title: I18n.tr("Blur Wallpaper Layer") + settingKey: "blurWallpaper" + visible: CompositorService.isNiri + + SettingsToggleRow { + tab: "wallpaper" + tags: ["blur", "duplicate", "layer", "compositor"] + settingKey: "blurredWallpaperLayer" + text: I18n.tr("Duplicate Wallpaper with Blur") + description: I18n.tr("Enable compositor-targetable blur layer (namespace: dms:blurwallpaper). Requires manual niri configuration.") + checked: SettingsData.blurredWallpaperLayer + onToggled: checked => SettingsData.set("blurredWallpaperLayer", checked) + } + } + } + } + + function openMainWallpaperBrowser() { + mainWallpaperBrowserLoader.active = true; + if (mainWallpaperBrowserLoader.item) + mainWallpaperBrowserLoader.item.open(); + } + + function openLightWallpaperBrowser() { + lightWallpaperBrowserLoader.active = true; + if (lightWallpaperBrowserLoader.item) + lightWallpaperBrowserLoader.item.open(); + } + + function openDarkWallpaperBrowser() { + darkWallpaperBrowserLoader.active = true; + if (darkWallpaperBrowserLoader.item) + darkWallpaperBrowserLoader.item.open(); + } + + LazyLoader { + id: mainWallpaperBrowserLoader + active: false + + FileBrowserModal { + parentModal: root.parentModal + browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title") + browserIcon: "wallpaper" + browserType: "wallpaper" + showHiddenFiles: true + fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"] + onFileSelected: path => { + if (SessionData.perMonitorWallpaper) { + SessionData.setMonitorWallpaper(selectedMonitorName, path); + } else { + SessionData.setWallpaper(path); + } + close(); + } + } + } + + LazyLoader { + id: lightWallpaperBrowserLoader + active: false + + FileBrowserModal { + parentModal: root.parentModal + browserTitle: I18n.tr("Select Wallpaper", "light mode wallpaper file browser title") + browserIcon: "light_mode" + browserType: "wallpaper" + showHiddenFiles: true + fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"] + onFileSelected: path => { + SessionData.wallpaperPathLight = path; + SessionData.syncWallpaperForCurrentMode(); + SessionData.saveSettings(); + close(); + } + } + } + + LazyLoader { + id: darkWallpaperBrowserLoader + active: false + + FileBrowserModal { + parentModal: root.parentModal + browserTitle: I18n.tr("Select Wallpaper", "dark mode wallpaper file browser title") + browserIcon: "dark_mode" + browserType: "wallpaper" + showHiddenFiles: true + fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"] + onFileSelected: path => { + SessionData.wallpaperPathDark = path; + SessionData.syncWallpaperForCurrentMode(); + SessionData.saveSettings(); + close(); + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WidgetSelectionPopup.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WidgetSelectionPopup.qml new file mode 100644 index 0000000..817e980 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WidgetSelectionPopup.qml @@ -0,0 +1,382 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Widgets + +FloatingWindow { + id: root + + property bool disablePopupTransparency: true + property var allWidgets: [] + property string targetSection: "" + property string searchQuery: "" + property var filteredWidgets: [] + property int selectedIndex: -1 + property bool keyboardNavigationActive: false + property var parentModal: null + + signal widgetSelected(string widgetId, string targetSection) + + function updateFilteredWidgets() { + if (!searchQuery || searchQuery.length === 0) { + filteredWidgets = allWidgets.slice(); + return; + } + + var filtered = []; + var query = searchQuery.toLowerCase(); + + for (var i = 0; i < allWidgets.length; i++) { + var widget = allWidgets[i]; + var text = widget.text ? widget.text.toLowerCase() : ""; + var description = widget.description ? widget.description.toLowerCase() : ""; + var id = widget.id ? widget.id.toLowerCase() : ""; + + if (text.indexOf(query) !== -1 || description.indexOf(query) !== -1 || id.indexOf(query) !== -1) + filtered.push(widget); + } + + filteredWidgets = filtered; + selectedIndex = -1; + keyboardNavigationActive = false; + } + + onAllWidgetsChanged: { + updateFilteredWidgets(); + } + + function selectNext() { + if (filteredWidgets.length === 0) + return; + keyboardNavigationActive = true; + selectedIndex = Math.min(selectedIndex + 1, filteredWidgets.length - 1); + } + + function selectPrevious() { + if (filteredWidgets.length === 0) + return; + keyboardNavigationActive = true; + selectedIndex = Math.max(selectedIndex - 1, -1); + if (selectedIndex === -1) + keyboardNavigationActive = false; + } + + function selectWidget() { + if (selectedIndex < 0 || selectedIndex >= filteredWidgets.length) + return; + var widget = filteredWidgets[selectedIndex]; + root.widgetSelected(widget.id, root.targetSection); + root.hide(); + } + + function show() { + if (parentModal) + parentModal.shouldHaveFocus = false; + visible = true; + Qt.callLater(() => { + searchField.forceActiveFocus(); + }); + } + + function hide() { + visible = false; + if (!parentModal) + return; + parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible); + Qt.callLater(() => { + if (parentModal && parentModal.modalFocusScope) + parentModal.modalFocusScope.forceActiveFocus(); + }); + } + + objectName: "widgetSelectionPopup" + title: I18n.tr("Add Widget") + minimumSize: Qt.size(400, 350) + implicitWidth: 500 + implicitHeight: 550 + color: Theme.surfaceContainer + visible: false + + onVisibleChanged: { + if (visible) { + Qt.callLater(() => { + searchField.forceActiveFocus(); + }); + return; + } + allWidgets = []; + targetSection = ""; + searchQuery = ""; + filteredWidgets = []; + selectedIndex = -1; + keyboardNavigationActive = false; + if (!parentModal) + return; + parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible); + Qt.callLater(() => { + if (parentModal && parentModal.modalFocusScope) + parentModal.modalFocusScope.forceActiveFocus(); + }); + } + + FocusScope { + id: widgetKeyHandler + + anchors.fill: parent + focus: true + + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Escape: + root.hide(); + event.accepted = true; + return; + case Qt.Key_Down: + root.selectNext(); + event.accepted = true; + return; + case Qt.Key_Up: + root.selectPrevious(); + event.accepted = true; + return; + case Qt.Key_Return: + case Qt.Key_Enter: + if (root.keyboardNavigationActive) { + root.selectWidget(); + } else if (root.filteredWidgets.length > 0) { + var firstWidget = root.filteredWidgets[0]; + root.widgetSelected(firstWidget.id, root.targetSection); + root.hide(); + } + event.accepted = true; + return; + } + if (event.modifiers & Qt.ControlModifier) { + switch (event.key) { + case Qt.Key_N: + case Qt.Key_J: + root.selectNext(); + event.accepted = true; + return; + case Qt.Key_P: + case Qt.Key_K: + root.selectPrevious(); + event.accepted = true; + return; + } + } + } + + Column { + anchors.fill: parent + spacing: 0 + + Item { + id: titleBar + width: parent.width + height: 48 + + MouseArea { + anchors.fill: parent + onPressed: windowControls.tryStartMove() + onDoubleClicked: windowControls.tryToggleMaximize() + } + + Rectangle { + anchors.fill: parent + color: Theme.surfaceContainer + opacity: 0.5 + } + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingL + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "add_circle" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Add Widget to %1 Section").arg(root.targetSection) + font.pixelSize: Theme.fontSizeXLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + DankActionButton { + visible: windowControls.supported + circular: false + iconName: root.maximized ? "fullscreen_exit" : "fullscreen" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: windowControls.tryToggleMaximize() + } + + DankActionButton { + circular: false + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: root.hide() + } + } + } + + Item { + width: parent.width + height: parent.height - titleBar.height + + Column { + id: contentColumn + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Select a widget to add. You can add multiple instances of the same widget if needed.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + width: parent.width + wrapMode: Text.WordWrap + } + + DankTextField { + id: searchField + width: parent.width + height: 48 + cornerRadius: Theme.cornerRadius + backgroundColor: Theme.surfaceContainerHigh + normalBorderColor: Theme.outlineMedium + focusedBorderColor: Theme.primary + leftIconName: "search" + leftIconSize: Theme.iconSize + leftIconColor: Theme.surfaceVariantText + leftIconFocusedColor: Theme.primary + showClearButton: true + textColor: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + placeholderText: I18n.tr("Search widgets...") + text: root.searchQuery + focus: true + ignoreLeftRightKeys: true + keyForwardTargets: [widgetKeyHandler] + onTextEdited: { + root.searchQuery = text; + updateFilteredWidgets(); + } + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + root.hide(); + event.accepted = true; + return; + } + if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) + event.accepted = false; + } + } + + DankListView { + id: widgetList + + width: parent.width + height: parent.height - y + spacing: Theme.spacingS + model: root.filteredWidgets + clip: true + + delegate: Rectangle { + width: widgetList.width + height: 60 + radius: Theme.cornerRadius + property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex + color: isSelected ? Theme.primarySelected : widgetArea.containsMouse ? Theme.primaryHover : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: isSelected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: isSelected ? 2 : 1 + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + DankIcon { + name: modelData.icon + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + width: parent.width - Theme.iconSize - Theme.spacingM * 3 + + StyledText { + text: modelData.text + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + elide: Text.ElideRight + width: parent.width + } + + StyledText { + text: modelData.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + elide: Text.ElideRight + width: parent.width + wrapMode: Text.WordWrap + } + } + + DankIcon { + name: "add" + size: Theme.iconSize - 4 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: widgetArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.widgetSelected(modelData.id, root.targetSection); + root.hide(); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } + } + } + + FloatingWindowControls { + id: windowControls + targetWindow: root + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/DeviceAliasRow.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/DeviceAliasRow.qml new file mode 100644 index 0000000..bec5ff1 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/DeviceAliasRow.qml @@ -0,0 +1,180 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + required property var deviceNode + property string deviceType: "output" + + property bool showHideButton: false + property bool isHidden: false + + signal editRequested(var deviceNode) + signal resetRequested(var deviceNode) + signal hideRequested(var deviceNode) + + width: parent?.width ?? 0 + height: deviceRowContent.height + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: deviceMouseArea.containsMouse ? Theme.surfaceHover : "transparent" + + readonly property bool hasCustomAlias: AudioService.hasDeviceAlias(deviceNode?.name ?? "") + readonly property string displayedName: AudioService.displayName(deviceNode) + + Row { + id: deviceRowContent + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingM + + DankIcon { + name: root.deviceType === "input" ? "mic" : "speaker" + size: Theme.iconSize + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Theme.iconSize - Theme.spacingM * 3 - buttonsRow.width + spacing: 2 + + StyledText { + text: root.displayedName + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + elide: Text.ElideRight + horizontalAlignment: Text.AlignLeft + } + + Column { + width: parent.width + spacing: 2 + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: root.deviceNode?.name ?? "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width - (customAliasLabel.visible ? customAliasLabel.width + Theme.spacingS : 0) + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignLeft + } + + Rectangle { + id: customAliasLabel + visible: root.hasCustomAlias + height: customAliasText.implicitHeight + 4 + width: customAliasText.implicitWidth + Theme.spacingS * 2 + radius: 3 + color: Theme.withAlpha(Theme.primary, 0.15) + anchors.verticalCenter: parent.verticalCenter + + StyledText { + id: customAliasText + text: I18n.tr("Custom") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.primary + font.weight: Font.Medium + anchors.centerIn: parent + } + } + } + + StyledText { + visible: root.hasCustomAlias + text: I18n.tr("Original: %1").arg(AudioService.originalName(root.deviceNode)) + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + width: parent.width + elide: Text.ElideRight + opacity: 0.6 + horizontalAlignment: Text.AlignLeft + } + } + } + + Row { + id: buttonsRow + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankActionButton { + id: resetButton + visible: root.hasCustomAlias + buttonSize: 36 + iconName: "restart_alt" + iconSize: 20 + backgroundColor: Theme.surfaceContainerHigh + iconColor: Theme.surfaceVariantText + tooltipText: I18n.tr("Reset to default name") + anchors.verticalCenter: parent.verticalCenter + onClicked: { + root.resetRequested(root.deviceNode); + } + } + + DankActionButton { + id: hideButton + visible: root.showHideButton + buttonSize: 36 + iconName: root.isHidden ? "visibility" : "visibility_off" + iconSize: 20 + backgroundColor: Theme.surfaceContainerHigh + iconColor: root.isHidden ? Theme.primary : Theme.surfaceVariantText + tooltipText: root.isHidden ? I18n.tr("Show device") : I18n.tr("Hide device") + anchors.verticalCenter: parent.verticalCenter + onClicked: { + root.hideRequested(root.deviceNode); + } + } + + DankActionButton { + id: editButton + buttonSize: 36 + iconName: "edit" + iconSize: 20 + backgroundColor: Theme.buttonBg + iconColor: Theme.buttonText + tooltipText: I18n.tr("Set custom name") + anchors.verticalCenter: parent.verticalCenter + visible: !root.isHidden + onClicked: { + root.editRequested(root.deviceNode); + } + } + } + } + + MouseArea { + id: deviceMouseArea + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + z: -1 + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsButtonGroupRow.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsButtonGroupRow.qml new file mode 100644 index 0000000..5e49936 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsButtonGroupRow.qml @@ -0,0 +1,117 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property string tab: "" + property var tags: [] + property string settingKey: "" + + property string text: "" + property string description: "" + + readonly property bool isHighlighted: settingKey !== "" && SettingsSearchService.highlightSection === settingKey + + function findParentFlickable() { + let p = root.parent; + while (p) { + if (p.hasOwnProperty("contentY") && p.hasOwnProperty("contentItem")) + return p; + p = p.parent; + } + return null; + } + + Component.onCompleted: { + if (!settingKey) + return; + let flickable = findParentFlickable(); + if (flickable) + SettingsSearchService.registerCard(settingKey, root, flickable); + } + + Component.onDestruction: { + if (settingKey) + SettingsSearchService.unregisterCard(settingKey); + } + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.primary, root.isHighlighted ? 0.2 : 0) + visible: root.isHighlighted + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + property alias model: buttonGroup.model + property alias currentIndex: buttonGroup.currentIndex + property alias selectionMode: buttonGroup.selectionMode + property alias buttonHeight: buttonGroup.buttonHeight + property alias minButtonWidth: buttonGroup.minButtonWidth + property alias buttonPadding: buttonGroup.buttonPadding + property alias checkIconSize: buttonGroup.checkIconSize + property alias textSize: buttonGroup.textSize + property alias spacing: buttonGroup.spacing + property alias checkEnabled: buttonGroup.checkEnabled + + signal selectionChanged(int index, bool selected) + + width: parent?.width ?? 0 + height: 60 + + Row { + id: contentRow + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + Column { + width: parent.width - buttonGroup.width - Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + StyledText { + text: root.text + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + elide: Text.ElideRight + width: parent.width + visible: root.text !== "" + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: root.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + visible: root.description !== "" + horizontalAlignment: Text.AlignLeft + } + } + + DankButtonGroup { + id: buttonGroup + anchors.verticalCenter: parent.verticalCenter + selectionMode: "single" + onSelectionChanged: (index, selected) => root.selectionChanged(index, selected) + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsCard.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsCard.qml new file mode 100644 index 0000000..65642a0 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsCard.qml @@ -0,0 +1,199 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +StyledRect { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property string tab: "" + property var tags: [] + property string settingKey: "" + + property string title: "" + property string iconName: "" + property bool collapsible: false + property bool expanded: true + property real headerLeftPadding: 0 + + default property alias content: contentColumn.children + property alias headerActions: headerActionsRow.children + + readonly property bool isHighlighted: settingKey !== "" && SettingsSearchService.highlightSection === settingKey + + width: parent?.width ?? 0 + height: { + var hasHeader = root.title !== "" || root.iconName !== ""; + if (collapsed) + return headerRow.height + Theme.spacingL * 2; + var h = Theme.spacingL * 2 + contentColumn.height; + if (hasHeader) + h += headerRow.height + Theme.spacingM; + return h; + } + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + readonly property bool collapsed: collapsible && !expanded + readonly property bool hasHeader: root.title !== "" || root.iconName !== "" + property bool userToggledCollapse: false + + function findParentFlickable() { + let p = root.parent; + while (p) { + if (p.hasOwnProperty("contentY") && p.hasOwnProperty("contentItem")) { + return p; + } + p = p.parent; + } + return null; + } + + Component.onCompleted: { + if (settingKey) { + let flickable = findParentFlickable(); + if (flickable) { + SettingsSearchService.registerCard(settingKey, root, flickable); + } + } + } + + Component.onDestruction: { + if (settingKey) { + SettingsSearchService.unregisterCard(settingKey); + } + } + + Behavior on height { + enabled: root.userToggledCollapse + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + onRunningChanged: { + if (!running) + root.userToggledCollapse = false; + } + } + } + + Rectangle { + id: highlightBorder + anchors.fill: parent + anchors.margins: -2 + radius: root.radius + 2 + color: "transparent" + border.width: 2 + border.color: Theme.primary + opacity: root.isHighlighted ? 1 : 0 + visible: opacity > 0 + z: 100 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + Column { + id: mainColumn + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: root.hasHeader ? Theme.spacingM : 0 + clip: true + + Item { + id: headerRow + width: parent.width + height: root.hasHeader ? Math.max(headerIcon.height, headerText.height, headerActionsRow.height) : 0 + visible: root.hasHeader + + Row { + anchors.left: parent.left + anchors.leftMargin: root.headerLeftPadding + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + id: headerIcon + name: root.iconName + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + visible: root.iconName !== "" + } + + StyledText { + id: headerText + text: root.title + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + visible: root.title !== "" + width: implicitWidth + horizontalAlignment: Text.AlignLeft + } + } + + Row { + id: headerActionsRow + anchors.right: root.collapsible ? caretIcon.left : parent.right + anchors.rightMargin: root.collapsible ? Theme.spacingS : 0 + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + } + + DankIcon { + id: caretIcon + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + name: root.expanded ? "expand_less" : "expand_more" + size: Theme.iconSize - 2 + color: Theme.surfaceVariantText + visible: root.collapsible + } + + MouseArea { + anchors.left: parent.left + anchors.right: headerActionsRow.left + anchors.top: parent.top + anchors.bottom: parent.bottom + enabled: root.collapsible + cursorShape: root.collapsible ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + root.userToggledCollapse = true; + root.expanded = !root.expanded; + } + } + + MouseArea { + visible: root.collapsible + anchors.left: caretIcon.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.leftMargin: -Theme.spacingS + enabled: root.collapsible + cursorShape: root.collapsible ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + root.userToggledCollapse = true; + root.expanded = !root.expanded; + } + } + } + + Column { + id: contentColumn + width: parent.width + spacing: Theme.spacingM + visible: !root.collapsed + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsColorPicker.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsColorPicker.qml new file mode 100644 index 0000000..68011fa --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsColorPicker.qml @@ -0,0 +1,119 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property string colorMode: "primary" + property color customColor: "#ffffff" + property string pickerTitle: I18n.tr("Choose Color") + + signal colorModeSelected(string mode) + signal customColorSelected(color selectedColor) + + width: parent?.width ?? 0 + height: colorColumn.height + Theme.spacingM * 2 + + Column { + id: colorColumn + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Color") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + } + + Row { + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: [ + { + id: "primary", + label: I18n.tr("Primary"), + color: Theme.primary + }, + { + id: "secondary", + label: I18n.tr("Secondary"), + color: Theme.secondary + }, + { + id: "custom", + label: I18n.tr("Custom"), + color: root.customColor + } + ] + + Rectangle { + required property var modelData + required property int index + + width: (parent.width - Theme.spacingS * 2) / 3 + height: 60 + radius: Theme.cornerRadius + color: root.colorMode === modelData.id ? Theme.primarySelected : Theme.surfaceHover + border.color: root.colorMode === modelData.id ? Theme.primary : "transparent" + border.width: 2 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Rectangle { + width: 24 + height: 24 + radius: 12 + color: modelData.color + border.color: Theme.outline + border.width: 1 + anchors.horizontalCenter: parent.horizontalCenter + + DankIcon { + visible: modelData.id === "custom" + anchors.centerIn: parent + name: "colorize" + size: 14 + color: Theme.background + } + } + + StyledText { + text: modelData.label + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData.id !== "custom") { + root.colorModeSelected(modelData.id); + return; + } + PopoutService.colorPickerModal.selectedColor = root.customColor; + PopoutService.colorPickerModal.pickerTitle = root.pickerTitle; + PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) { + root.customColorSelected(selectedColor); + root.colorModeSelected("custom"); + }; + PopoutService.colorPickerModal.show(); + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsDisplayPicker.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsDisplayPicker.qml new file mode 100644 index 0000000..ba453fd --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsDisplayPicker.qml @@ -0,0 +1,103 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import qs.Common +import qs.Widgets + +Item { + id: root + + property var displayPreferences: [] + + signal preferencesChanged(var preferences) + + property bool localAllDisplays: true + + onDisplayPreferencesChanged: { + if (!Array.isArray(displayPreferences) || displayPreferences.length === 0) { + localAllDisplays = true; + return; + } + localAllDisplays = displayPreferences.includes("all"); + } + + width: parent?.width ?? 0 + height: displayColumn.height + Theme.spacingM * 2 + + Column { + id: displayColumn + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Displays") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + } + + DankToggle { + width: parent.width + text: I18n.tr("All displays") + checked: root.localAllDisplays + onToggled: isChecked => { + root.localAllDisplays = isChecked; + if (isChecked) { + root.preferencesChanged(["all"]); + return; + } + var screens = []; + for (var i = 0; i < Quickshell.screens.length; i++) { + var s = Quickshell.screens[i]; + screens.push({ + name: s.name, + model: s.model || "" + }); + } + root.preferencesChanged(screens); + } + } + + Column { + width: parent.width + spacing: Theme.spacingXS + visible: !root.localAllDisplays + + Repeater { + model: Quickshell.screens + + DankToggle { + required property var modelData + + property bool localChecked: { + const prefs = root.displayPreferences; + if (!Array.isArray(prefs) || prefs.includes("all")) + return true; + return prefs.some(p => p.name === modelData.name); + } + + width: parent.width + text: SettingsData.getScreenDisplayName(modelData) + description: modelData.width + "×" + modelData.height + checked: localChecked + onToggled: isChecked => { + localChecked = isChecked; + var prefs = JSON.parse(JSON.stringify(root.displayPreferences)); + if (!Array.isArray(prefs) || prefs.includes("all")) + prefs = []; + prefs = prefs.filter(p => p.name !== modelData.name); + if (isChecked) { + prefs.push({ + name: modelData.name, + model: modelData.model || "" + }); + } + root.preferencesChanged(prefs); + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsDivider.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsDivider.qml new file mode 100644 index 0000000..86d9fe7 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsDivider.qml @@ -0,0 +1,11 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common + +Rectangle { + width: parent?.width ?? 0 + height: 1 + color: Theme.outline + opacity: 0.15 +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsDropdownRow.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsDropdownRow.qml new file mode 100644 index 0000000..c5ec3a6 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsDropdownRow.qml @@ -0,0 +1,60 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +DankDropdown { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property string tab: "" + property var tags: [] + property string settingKey: "" + + readonly property bool isHighlighted: settingKey !== "" && SettingsSearchService.highlightSection === settingKey + + width: parent?.width ?? 0 + addHorizontalPadding: true + usePopupTransparency: false + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.primary, root.isHighlighted ? 0.2 : 0) + visible: root.isHighlighted + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + function findParentFlickable() { + let p = root.parent; + while (p) { + if (p.hasOwnProperty("contentY") && p.hasOwnProperty("contentItem")) + return p; + p = p.parent; + } + return null; + } + + Component.onCompleted: { + if (!settingKey) + return; + let flickable = findParentFlickable(); + if (flickable) + SettingsSearchService.registerCard(settingKey, root, flickable); + } + + Component.onDestruction: { + if (settingKey) + SettingsSearchService.unregisterCard(settingKey); + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsSliderCard.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsSliderCard.qml new file mode 100644 index 0000000..5bdd91a --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsSliderCard.qml @@ -0,0 +1,108 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Widgets + +StyledRect { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property string tab: "" + property var tags: [] + + property string title: "" + property string description: "" + property string iconName: "" + property alias value: slider.value + property alias minimum: slider.minimum + property alias maximum: slider.maximum + property alias unit: slider.unit + property int defaultValue: -1 + + signal sliderValueChanged(int newValue) + signal sliderDragFinished(int finalValue) + width: parent?.width ?? 0 + height: Theme.spacingL * 2 + contentColumn.height + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + id: contentColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingL + anchors.rightMargin: Theme.spacingL + spacing: Theme.spacingS + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + id: headerIcon + name: root.iconName + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + visible: root.iconName !== "" + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + width: parent.width - headerIcon.width - (root.defaultValue >= 0 ? resetButton.width + Theme.spacingS : 0) - Theme.spacingM + + StyledText { + text: root.title + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignLeft + visible: root.title !== "" + } + + StyledText { + text: root.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignLeft + visible: root.description !== "" + } + } + + DankActionButton { + id: resetButton + anchors.verticalCenter: parent.verticalCenter + buttonSize: 36 + iconName: "restart_alt" + iconSize: 20 + visible: root.defaultValue >= 0 && slider.value !== root.defaultValue + backgroundColor: Theme.surfaceContainerHigh + iconColor: Theme.surfaceVariantText + onClicked: { + slider.value = root.defaultValue; + root.sliderValueChanged(root.defaultValue); + root.sliderDragFinished(root.defaultValue); + } + } + } + + DankSlider { + id: slider + width: parent.width + height: 32 + showValue: true + wheelEnabled: false + thumbOutlineColor: Theme.surfaceContainerHigh + onSliderValueChanged: newValue => root.sliderValueChanged(newValue) + onSliderDragFinished: finalValue => root.sliderDragFinished(finalValue) + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsSliderRow.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsSliderRow.qml new file mode 100644 index 0000000..b1ada84 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsSliderRow.qml @@ -0,0 +1,148 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property string tab: "" + property var tags: [] + property string settingKey: "" + + property string text: "" + property string description: "" + + readonly property bool isHighlighted: settingKey !== "" && SettingsSearchService.highlightSection === settingKey + + function findParentFlickable() { + let p = root.parent; + while (p) { + if (p.hasOwnProperty("contentY") && p.hasOwnProperty("contentItem")) + return p; + p = p.parent; + } + return null; + } + + Component.onCompleted: { + if (!settingKey) + return; + let flickable = findParentFlickable(); + if (flickable) + SettingsSearchService.registerCard(settingKey, root, flickable); + } + + Component.onDestruction: { + if (settingKey) + SettingsSearchService.unregisterCard(settingKey); + } + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.primary, root.isHighlighted ? 0.2 : 0) + visible: root.isHighlighted + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + property alias value: slider.value + property alias minimum: slider.minimum + property alias maximum: slider.maximum + property alias step: slider.step + property alias unit: slider.unit + property alias wheelEnabled: slider.wheelEnabled + property alias thumbOutlineColor: slider.thumbOutlineColor + property int defaultValue: -1 + + signal sliderValueChanged(int newValue) + signal sliderDragFinished(int finalValue) + width: parent?.width ?? 0 + height: headerRow.height + Theme.spacingXS + slider.height + + Column { + id: contentColumn + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + Row { + id: headerRow + width: parent.width + height: labelColumn.height + spacing: Theme.spacingS + + Column { + id: labelColumn + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + width: parent.width - resetButtonContainer.width - Theme.spacingS + + StyledText { + text: root.text + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + visible: root.text !== "" + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: root.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + visible: root.description !== "" + horizontalAlignment: Text.AlignLeft + } + } + + Item { + id: resetButtonContainer + width: root.defaultValue >= 0 ? 36 : 0 + height: 36 + anchors.verticalCenter: parent.verticalCenter + + DankActionButton { + id: resetButton + anchors.centerIn: parent + buttonSize: 36 + iconName: "restart_alt" + iconSize: 20 + visible: root.defaultValue >= 0 && slider.value !== root.defaultValue + backgroundColor: Theme.surfaceContainerHigh + iconColor: Theme.surfaceVariantText + onClicked: { + slider.value = root.defaultValue; + root.sliderValueChanged(root.defaultValue); + root.sliderDragFinished(root.defaultValue); + } + } + } + } + + DankSlider { + id: slider + width: parent.width + height: 32 + showValue: true + wheelEnabled: false + thumbOutlineColor: Theme.surfaceContainerHigh + onSliderValueChanged: newValue => root.sliderValueChanged(newValue) + onSliderDragFinished: finalValue => root.sliderDragFinished(finalValue) + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsToggleCard.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsToggleCard.qml new file mode 100644 index 0000000..c36fb7c --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsToggleCard.qml @@ -0,0 +1,120 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Widgets + +StyledRect { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property string tab: "" + property var tags: [] + + property string title: "" + property string description: "" + property string iconName: "" + property bool checked: false + property bool enabled: true + + default property alias content: expandedContent.children + readonly property bool hasContent: expandedContent.children.length > 0 + + signal toggled(bool checked) + + width: parent?.width ?? 0 + height: Theme.spacingL * 2 + mainColumn.height + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + id: mainColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingL + anchors.rightMargin: Theme.spacingL + spacing: Theme.spacingM + + Item { + width: parent.width + height: headerColumn.height + + Column { + id: headerColumn + anchors.left: parent.left + anchors.right: toggleSwitch.left + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingXS + + Row { + spacing: Theme.spacingM + width: parent.width + + DankIcon { + id: headerIcon + name: root.iconName + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + visible: root.iconName !== "" + } + + StyledText { + id: headerText + text: root.title + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + visible: root.title !== "" + width: parent.width - (headerIcon.visible ? headerIcon.width + parent.spacing : 0) + horizontalAlignment: Text.AlignLeft + } + } + + StyledText { + id: descriptionText + text: root.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignLeft + visible: root.description !== "" + } + } + + DankToggle { + id: toggleSwitch + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + hideText: true + checked: root.checked + enabled: root.enabled + onToggled: checked => root.toggled(checked) + } + + StateLayer { + anchors.fill: parent + disabled: !root.enabled + stateColor: Theme.primary + cornerRadius: root.radius + onClicked: { + if (!root.enabled) + return; + root.toggled(!root.checked); + } + } + } + + Column { + id: expandedContent + width: parent.width + spacing: Theme.spacingM + visible: root.checked && root.hasContent + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsToggleRow.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsToggleRow.qml new file mode 100644 index 0000000..684b639 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SettingsToggleRow.qml @@ -0,0 +1,58 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +DankToggle { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property string tab: "" + property var tags: [] + property string settingKey: "" + + readonly property bool isHighlighted: settingKey !== "" && SettingsSearchService.highlightSection === settingKey + + width: parent?.width ?? 0 + + function findParentFlickable() { + let p = root.parent; + while (p) { + if (p.hasOwnProperty("contentY") && p.hasOwnProperty("contentItem")) + return p; + p = p.parent; + } + return null; + } + + Component.onCompleted: { + if (!settingKey) + return; + let flickable = findParentFlickable(); + if (flickable) + SettingsSearchService.registerCard(settingKey, root, flickable); + } + + Component.onDestruction: { + if (settingKey) + SettingsSearchService.unregisterCard(settingKey); + } + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.primary, root.isHighlighted ? 0.2 : 0) + visible: root.isHighlighted + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SystemMonitorVariantCard.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SystemMonitorVariantCard.qml new file mode 100644 index 0000000..1ecd451 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/Widgets/SystemMonitorVariantCard.qml @@ -0,0 +1,390 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property var variant: ({}) + property bool expanded: false + + signal expandToggled(bool isExpanded) + signal deleteRequested + signal nameChanged(string newName) + signal configChanged(string key, var value) + + readonly property var cfg: variant.config || {} + + function updateConfig(key, value) { + var newConfig = JSON.parse(JSON.stringify(cfg)); + newConfig[key] = value; + root.configChanged("config", newConfig); + } + + width: parent?.width ?? 0 + height: variantColumn.height + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + clip: true + + Column { + id: variantColumn + width: parent.width + spacing: 0 + + Item { + width: parent.width + height: headerContent.height + Theme.spacingM * 2 + + Row { + id: headerContent + x: Theme.spacingM + y: Theme.spacingM + width: parent.width - Theme.spacingM * 2 + spacing: Theme.spacingM + + DankIcon { + name: root.expanded ? "expand_less" : "expand_more" + size: Theme.iconSize + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - deleteBtn.width - Theme.spacingM * 2 + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + + StyledText { + text: root.variant.name || "Unnamed" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: parent.width + elide: Text.ElideRight + } + + StyledText { + property var features: { + var f = []; + if (root.cfg.showCpu) + f.push("CPU"); + if (root.cfg.showMemory) + f.push("RAM"); + if (root.cfg.showNetwork) + f.push("Net"); + if (root.cfg.showDisk) + f.push("Disk"); + if (root.cfg.showGpuTemp) + f.push("GPU"); + return f; + } + text: features.length > 0 ? features.join(", ") : I18n.tr("No features enabled") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + elide: Text.ElideRight + } + } + + Rectangle { + id: deleteBtn + width: 32 + height: 32 + radius: 16 + color: deleteMouse.containsMouse ? Theme.error : "transparent" + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: "delete" + size: 16 + color: deleteMouse.containsMouse ? Theme.background : Theme.surfaceVariantText + } + + MouseArea { + id: deleteMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.deleteRequested() + } + } + } + + MouseArea { + anchors.fill: parent + anchors.rightMargin: deleteBtn.width + Theme.spacingM + cursorShape: Qt.PointingHandCursor + onClicked: root.expandToggled(!root.expanded) + } + } + + Column { + width: parent.width + spacing: 0 + visible: root.expanded + + SettingsDivider {} + + Item { + width: parent.width + height: nameRow.height + Theme.spacingM * 2 + + Row { + id: nameRow + x: Theme.spacingM + y: Theme.spacingM + width: parent.width - Theme.spacingM * 2 + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Name") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: 80 + anchors.verticalCenter: parent.verticalCenter + } + + DankTextField { + width: parent.width - 80 - Theme.spacingM + text: root.variant.name || "" + onEditingFinished: { + if (text !== root.variant.name) + root.nameChanged(text); + } + } + } + } + + SettingsDivider {} + + DankToggle { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + text: I18n.tr("Show Header") + checked: root.cfg.showHeader ?? true + onToggled: checked => root.updateConfig("showHeader", checked) + } + + SettingsDivider {} + + DankToggle { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + text: I18n.tr("Show CPU") + checked: root.cfg.showCpu ?? true + onToggled: checked => root.updateConfig("showCpu", checked) + } + + DankToggle { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + text: I18n.tr("Show CPU Graph") + visible: root.cfg.showCpu + checked: root.cfg.showCpuGraph ?? true + onToggled: checked => root.updateConfig("showCpuGraph", checked) + } + + DankToggle { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + text: I18n.tr("Show CPU Temp") + visible: root.cfg.showCpu + checked: root.cfg.showCpuTemp ?? true + onToggled: checked => root.updateConfig("showCpuTemp", checked) + } + + SettingsDivider {} + + DankToggle { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + text: I18n.tr("Show Memory") + checked: root.cfg.showMemory ?? true + onToggled: checked => root.updateConfig("showMemory", checked) + } + + DankToggle { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + text: I18n.tr("Show Memory Graph") + visible: root.cfg.showMemory + checked: root.cfg.showMemoryGraph ?? true + onToggled: checked => root.updateConfig("showMemoryGraph", checked) + } + + DankToggle { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + text: I18n.tr("Show Memory in GB") + visible: root.cfg.showMemory + checked: root.cfg.showInGb ?? false + onToggled: checked => root.updateConfig("showInGb", checked) + } + + SettingsDivider {} + + DankToggle { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + text: I18n.tr("Show Network") + checked: root.cfg.showNetwork ?? true + onToggled: checked => root.updateConfig("showNetwork", checked) + } + + DankToggle { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + text: I18n.tr("Show Network Graph") + visible: root.cfg.showNetwork + checked: root.cfg.showNetworkGraph ?? true + onToggled: checked => root.updateConfig("showNetworkGraph", checked) + } + + SettingsDivider {} + + DankToggle { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + text: I18n.tr("Show Disk") + checked: root.cfg.showDisk ?? true + onToggled: checked => root.updateConfig("showDisk", checked) + } + + SettingsDivider {} + + DankToggle { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + text: I18n.tr("Show GPU Temperature") + checked: root.cfg.showGpuTemp ?? false + onToggled: checked => root.updateConfig("showGpuTemp", checked) + } + + Column { + width: parent.width + spacing: Theme.spacingXS + visible: root.cfg.showGpuTemp && DgopService.availableGpus.length > 0 + + Item { + width: 1 + height: Theme.spacingS + } + + Repeater { + model: DgopService.availableGpus + + Rectangle { + required property var modelData + + width: (parent?.width ?? 0) - Theme.spacingM * 2 + x: Theme.spacingM + height: 40 + radius: Theme.cornerRadius + color: root.cfg.gpuPciId === modelData.pciId ? Theme.primarySelected : Theme.surfaceContainer + border.color: root.cfg.gpuPciId === modelData.pciId ? Theme.primary : "transparent" + border.width: 2 + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: "videocam" + size: Theme.iconSizeSmall + color: root.cfg.gpuPciId === modelData.pciId ? Theme.primary : Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData.displayName || "Unknown GPU" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + width: parent.width - Theme.iconSizeSmall - Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.updateConfig("gpuPciId", modelData.pciId) + } + } + } + + Item { + width: 1 + height: Theme.spacingS + } + } + + SettingsDivider {} + + DankToggle { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + text: I18n.tr("Show Top Processes") + checked: root.cfg.showTopProcesses ?? false + onToggled: checked => root.updateConfig("showTopProcesses", checked) + } + + SettingsDivider {} + + Column { + width: parent.width - Theme.spacingM * 2 + x: Theme.spacingM + topPadding: Theme.spacingM + bottomPadding: Theme.spacingM + spacing: Theme.spacingS + + Row { + width: parent.width + + StyledText { + id: transparencyLabel + text: I18n.tr("Transparency") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - transparencyLabel.width - transparencyValue.width + height: 1 + } + + StyledText { + id: transparencyValue + text: transparencySlider.value + "%" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + } + + DankSlider { + id: transparencySlider + width: parent.width + minimum: 0 + maximum: 100 + value: Math.round((root.cfg.transparency ?? 0.8) * 100) + showValue: false + wheelEnabled: false + onSliderDragFinished: finalValue => root.updateConfig("transparency", finalValue / 100) + } + } + + Item { + width: 1 + height: Theme.spacingM + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WidgetsTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WidgetsTab.qml new file mode 100644 index 0000000..86bc391 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WidgetsTab.qml @@ -0,0 +1,1145 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: widgetsTab + + property var parentModal: null + property string selectedBarId: "default" + + property var selectedBarConfig: { + selectedBarId; + SettingsData.barConfigs; + const index = SettingsData.barConfigs.findIndex(cfg => cfg.id === selectedBarId); + return index !== -1 ? SettingsData.barConfigs[index] : SettingsData.barConfigs[0]; + } + + property bool selectedBarIsVertical: { + selectedBarId; + const pos = selectedBarConfig?.position ?? SettingsData.Position.Top; + return pos === SettingsData.Position.Left || pos === SettingsData.Position.Right; + } + + property bool hasMultipleBars: SettingsData.barConfigs.length > 1 + + DankTooltipV2 { + id: sharedTooltip + } + + property var baseWidgetDefinitions: { + var coreWidgets = [ + { + "id": "layout", + "text": I18n.tr("Layout"), + "description": I18n.tr("Display and switch DWL layouts"), + "icon": "view_quilt", + "enabled": CompositorService.isDwl && DwlService.dwlAvailable, + "warning": !CompositorService.isDwl ? I18n.tr("Requires DWL compositor") : (!DwlService.dwlAvailable ? I18n.tr("DWL service not available") : undefined) + }, + { + "id": "launcherButton", + "text": I18n.tr("App Launcher"), + "description": I18n.tr("Quick access to application launcher"), + "icon": "apps", + "enabled": true + }, + { + "id": "workspaceSwitcher", + "text": I18n.tr("Workspace Switcher"), + "description": I18n.tr("Shows current workspace and allows switching"), + "icon": "view_module", + "enabled": true + }, + { + "id": "focusedWindow", + "text": I18n.tr("Focused Window"), + "description": I18n.tr("Display currently focused application title"), + "icon": "window", + "enabled": true + }, + { + "id": "runningApps", + "text": I18n.tr("Running Apps"), + "description": I18n.tr("Shows all running applications with focus indication"), + "icon": "apps", + "enabled": true + }, + { + "id": "appsDock", + "text": I18n.tr("Apps Dock"), + "description": I18n.tr("Pinned and running apps with drag-and-drop"), + "icon": "dock_to_bottom", + "enabled": true + }, + { + "id": "clock", + "text": I18n.tr("Clock"), + "description": I18n.tr("Current time and date display"), + "icon": "schedule", + "enabled": true + }, + { + "id": "weather", + "text": I18n.tr("Weather Widget"), + "description": I18n.tr("Current weather conditions and temperature"), + "icon": "wb_sunny", + "enabled": true + }, + { + "id": "music", + "text": I18n.tr("Media Controls"), + "description": I18n.tr("Control currently playing media"), + "icon": "music_note", + "enabled": true + }, + { + "id": "clipboard", + "text": I18n.tr("Clipboard Manager"), + "description": I18n.tr("Access clipboard history"), + "icon": "content_paste", + "enabled": true + }, + { + "id": "cpuUsage", + "text": I18n.tr("CPU Usage"), + "description": I18n.tr("CPU usage indicator"), + "icon": "memory", + "enabled": DgopService.dgopAvailable, + "warning": !DgopService.dgopAvailable ? I18n.tr("Requires 'dgop' tool") : undefined + }, + { + "id": "memUsage", + "text": I18n.tr("Memory Usage"), + "description": I18n.tr("Memory usage indicator"), + "icon": "developer_board", + "enabled": DgopService.dgopAvailable, + "warning": !DgopService.dgopAvailable ? I18n.tr("Requires 'dgop' tool") : undefined + }, + { + "id": "diskUsage", + "text": I18n.tr("Disk Usage"), + "description": I18n.tr("Percentage"), + "icon": "storage", + "enabled": DgopService.dgopAvailable, + "warning": !DgopService.dgopAvailable ? I18n.tr("Requires 'dgop' tool") : undefined + }, + { + "id": "cpuTemp", + "text": I18n.tr("CPU Temperature"), + "description": I18n.tr("CPU temperature display"), + "icon": "device_thermostat", + "enabled": DgopService.dgopAvailable, + "warning": !DgopService.dgopAvailable ? I18n.tr("Requires 'dgop' tool") : undefined + }, + { + "id": "gpuTemp", + "text": I18n.tr("GPU Temperature"), + "description": I18n.tr("GPU temperature display"), + "icon": "auto_awesome_mosaic", + "warning": !DgopService.dgopAvailable ? I18n.tr("Requires 'dgop' tool") : I18n.tr("This widget prevents GPU power off states, which can significantly impact battery life on laptops. It is not recommended to use this on laptops with hybrid graphics."), + "enabled": DgopService.dgopAvailable + }, + { + "id": "systemTray", + "text": I18n.tr("System Tray"), + "description": I18n.tr("System notification area icons"), + "icon": "notifications", + "enabled": true + }, + { + "id": "privacyIndicator", + "text": I18n.tr("Privacy Indicator"), + "description": I18n.tr("Shows when microphone, camera, or screen sharing is active"), + "icon": "privacy_tip", + "enabled": true + }, + { + "id": "controlCenterButton", + "text": I18n.tr("Control Center"), + "description": I18n.tr("Access to system controls and settings"), + "icon": "settings", + "enabled": true + }, + { + "id": "notificationButton", + "text": I18n.tr("Notification Center"), + "description": I18n.tr("Access to notifications and do not disturb"), + "icon": "notifications", + "enabled": true + }, + { + "id": "battery", + "text": I18n.tr("Battery"), + "description": I18n.tr("Battery level and power management"), + "icon": "battery_std", + "enabled": true + }, + { + "id": "vpn", + "text": I18n.tr("VPN"), + "description": I18n.tr("VPN status and quick connect"), + "icon": "vpn_lock", + "enabled": true + }, + { + "id": "idleInhibitor", + "text": I18n.tr("Idle Inhibitor"), + "description": I18n.tr("Prevent screen timeout"), + "icon": "motion_sensor_active", + "enabled": true + }, + { + "id": "capsLockIndicator", + "text": I18n.tr("Caps Lock Indicator"), + "description": I18n.tr("Shows when caps lock is active"), + "icon": "shift_lock", + "enabled": true + }, + { + "id": "spacer", + "text": I18n.tr("Spacer"), + "description": I18n.tr("Customizable empty space"), + "icon": "more_horiz", + "enabled": true + }, + { + "id": "separator", + "text": I18n.tr("Separator"), + "description": I18n.tr("Visual divider between widgets"), + "icon": "remove", + "enabled": true + }, + { + "id": "network_speed_monitor", + "text": I18n.tr("Network Speed Monitor"), + "description": I18n.tr("Network download and upload speed display"), + "icon": "network_check", + "warning": !DgopService.dgopAvailable ? I18n.tr("Requires 'dgop' tool") : undefined, + "enabled": DgopService.dgopAvailable + }, + { + "id": "keyboard_layout_name", + "text": I18n.tr("Keyboard Layout Name"), + "description": I18n.tr("Displays the active keyboard layout and allows switching"), + "icon": "keyboard" + }, + { + "id": "notepadButton", + "text": I18n.tr("Notepad"), + "description": I18n.tr("Quick access to notepad"), + "icon": "assignment", + "enabled": true + }, + { + "id": "colorPicker", + "text": I18n.tr("Color Picker"), + "description": I18n.tr("Quick access to color picker"), + "icon": "palette", + "enabled": true + }, + { + "id": "systemUpdate", + "text": I18n.tr("System Update"), + "description": I18n.tr("Check for system updates"), + "icon": "update", + "enabled": SystemUpdateService.distributionSupported + }, + { + "id": "powerMenuButton", + "text": I18n.tr("Power"), + "description": I18n.tr("Display the power system menu"), + "icon": "power_settings_new", + "enabled": true + }, + ]; + + var allPluginVariants = PluginService.getAllPluginVariants(); + for (var i = 0; i < allPluginVariants.length; i++) { + var variant = allPluginVariants[i]; + coreWidgets.push({ + "id": variant.fullId, + "text": variant.name, + "description": variant.description, + "icon": variant.icon, + "enabled": variant.loaded, + "warning": !variant.loaded ? I18n.tr("Plugin is disabled - enable in Plugins settings to use") : undefined + }); + } + + return coreWidgets; + } + + property var defaultLeftWidgets: [ + { + "id": "launcherButton", + "enabled": true + }, + { + "id": "workspaceSwitcher", + "enabled": true + }, + { + "id": "focusedWindow", + "enabled": true + } + ] + property var defaultCenterWidgets: [ + { + "id": "music", + "enabled": true + }, + { + "id": "clock", + "enabled": true + }, + { + "id": "weather", + "enabled": true + } + ] + property var defaultRightWidgets: [ + { + "id": "systemTray", + "enabled": true + }, + { + "id": "clipboard", + "enabled": true + }, + { + "id": "notificationButton", + "enabled": true + }, + { + "id": "battery", + "enabled": true + }, + { + "id": "controlCenterButton", + "enabled": true + } + ] + + function getWidgetsForSection(sectionId) { + switch (sectionId) { + case "left": + return selectedBarConfig?.leftWidgets || []; + case "center": + return selectedBarConfig?.centerWidgets || []; + case "right": + return selectedBarConfig?.rightWidgets || []; + default: + return []; + } + } + + function setWidgetsForSection(sectionId, widgets) { + switch (sectionId) { + case "left": + SettingsData.updateBarConfig(selectedBarId, { + leftWidgets: widgets + }); + break; + case "center": + SettingsData.updateBarConfig(selectedBarId, { + centerWidgets: widgets + }); + break; + case "right": + SettingsData.updateBarConfig(selectedBarId, { + rightWidgets: widgets + }); + break; + } + } + + function getWidgetsForPopup() { + return baseWidgetDefinitions.filter(widget => { + if (widget.warning && widget.warning.includes("Plugin is disabled")) + return false; + if (widget.enabled === false) + return false; + return true; + }); + } + + function addWidgetToSection(widgetId, targetSection) { + var widgetObj = { + "id": widgetId, + "enabled": true + }; + if (widgetId === "spacer") + widgetObj.size = 20; + if (widgetId === "gpuTemp") { + widgetObj.selectedGpuIndex = 0; + widgetObj.pciId = ""; + } + if (widgetId === "controlCenterButton") { + widgetObj.showNetworkIcon = SettingsData.controlCenterShowNetworkIcon; + widgetObj.showBluetoothIcon = SettingsData.controlCenterShowBluetoothIcon; + widgetObj.showAudioIcon = SettingsData.controlCenterShowAudioIcon; + widgetObj.showAudioPercent = SettingsData.controlCenterShowAudioPercent; + widgetObj.showVpnIcon = SettingsData.controlCenterShowVpnIcon; + widgetObj.showBrightnessIcon = SettingsData.controlCenterShowBrightnessIcon; + widgetObj.showBrightnessPercent = SettingsData.controlCenterShowBrightnessPercent; + widgetObj.showMicIcon = SettingsData.controlCenterShowMicIcon; + widgetObj.showMicPercent = SettingsData.controlCenterShowMicPercent; + widgetObj.showBatteryIcon = SettingsData.controlCenterShowBatteryIcon; + widgetObj.showPrinterIcon = SettingsData.controlCenterShowPrinterIcon; + widgetObj.showScreenSharingIcon = SettingsData.controlCenterShowScreenSharingIcon; + widgetObj.controlCenterGroupOrder = ["network", "vpn", "bluetooth", "audio", "microphone", "brightness", "battery", "printer", "screenSharing"]; + } + if (widgetId === "runningApps") { + widgetObj.runningAppsCompactMode = SettingsData.runningAppsCompactMode; + widgetObj.runningAppsGroupByApp = SettingsData.runningAppsGroupByApp; + widgetObj.runningAppsCurrentWorkspace = SettingsData.runningAppsCurrentWorkspace; + widgetObj.runningAppsCurrentMonitor = false; + } + if (widgetId === "diskUsage") { + widgetObj.mountPath = "/"; + widgetObj.diskUsageMode = 0; + } + if (widgetId === "cpuUsage" || widgetId === "memUsage" || widgetId === "cpuTemp" || widgetId === "gpuTemp") + widgetObj.minimumWidth = true; + if (widgetId === "memUsage") + widgetObj.showInGb = false; + + var widgets = getWidgetsForSection(targetSection).slice(); + widgets.push(widgetObj); + setWidgetsForSection(targetSection, widgets); + } + + function removeWidgetFromSection(sectionId, widgetIndex) { + var widgets = getWidgetsForSection(sectionId).slice(); + if (widgetIndex >= 0 && widgetIndex < widgets.length) + widgets.splice(widgetIndex, 1); + setWidgetsForSection(sectionId, widgets); + } + + function cloneWidgetData(widget) { + if (typeof widget === "string") + return { + "id": widget, + "enabled": true + }; + var result = { + "id": widget.id, + "enabled": widget.enabled + }; + var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"]; + for (var i = 0; i < keys.length; i++) { + if (widget[keys[i]] !== undefined) + result[keys[i]] = widget[keys[i]]; + } + return result; + } + + function handleItemEnabledChanged(sectionId, itemId, enabled) { + var widgets = getWidgetsForSection(sectionId).slice(); + for (var i = 0; i < widgets.length; i++) { + var widget = widgets[i]; + var widgetId = typeof widget === "string" ? widget : widget.id; + if (widgetId !== itemId) + continue; + var newWidget = cloneWidgetData(widget); + newWidget.enabled = enabled; + widgets[i] = newWidget; + break; + } + setWidgetsForSection(sectionId, widgets); + } + + function handleItemOrderChanged(sectionId, newOrder) { + setWidgetsForSection(sectionId, newOrder); + } + + function handleSpacerSizeChanged(sectionId, widgetIndex, newSize) { + var widgets = getWidgetsForSection(sectionId).slice(); + if (widgetIndex < 0 || widgetIndex >= widgets.length) + return; + var widget = widgets[widgetIndex]; + var widgetId = typeof widget === "string" ? widget : widget.id; + if (widgetId !== "spacer") + return; + var newWidget = cloneWidgetData(widget); + newWidget.size = newSize; + widgets[widgetIndex] = newWidget; + setWidgetsForSection(sectionId, widgets); + } + + function handleGpuSelectionChanged(sectionId, widgetIndex, selectedGpuIndex) { + var widgets = getWidgetsForSection(sectionId).slice(); + if (widgetIndex < 0 || widgetIndex >= widgets.length) + return; + var pciId = DgopService.availableGpus && DgopService.availableGpus.length > selectedGpuIndex ? DgopService.availableGpus[selectedGpuIndex].pciId : ""; + var newWidget = cloneWidgetData(widgets[widgetIndex]); + newWidget.selectedGpuIndex = selectedGpuIndex; + newWidget.pciId = pciId; + widgets[widgetIndex] = newWidget; + setWidgetsForSection(sectionId, widgets); + } + + function handleDiskMountSelectionChanged(sectionId, widgetIndex, mountPath) { + var widgets = getWidgetsForSection(sectionId).slice(); + if (widgetIndex < 0 || widgetIndex >= widgets.length) + return; + var newWidget = cloneWidgetData(widgets[widgetIndex]); + newWidget.mountPath = mountPath; + widgets[widgetIndex] = newWidget; + setWidgetsForSection(sectionId, widgets); + } + + function handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value) { + var widgets = getWidgetsForSection(sectionId).slice(); + if (widgetIndex < 0 || widgetIndex >= widgets.length) + return; + var newWidget = cloneWidgetData(widgets[widgetIndex]); + newWidget[settingName] = value; + + if (!value) { + switch (settingName) { + case "showAudioIcon": + newWidget.showAudioPercent = false; + break; + case "showMicIcon": + newWidget.showMicPercent = false; + break; + case "showBrightnessIcon": + newWidget.showBrightnessPercent = false; + break; + } + } + + widgets[widgetIndex] = newWidget; + setWidgetsForSection(sectionId, widgets); + } + + function handleControlCenterGroupOrderChanged(sectionId, widgetIndex, groupOrder) { + var widgets = getWidgetsForSection(sectionId).slice(); + if (widgetIndex < 0 || widgetIndex >= widgets.length) + return; + var previousWidget = widgets[widgetIndex]; + var newWidget = cloneWidgetData(previousWidget); + newWidget.controlCenterGroupOrder = groupOrder.slice(); + widgets[widgetIndex] = newWidget; + setWidgetsForSection(sectionId, widgets); + } + + function handlePrivacySettingChanged(sectionId, widgetIndex, settingName, value) { + switch (settingName) { + case "showMicIcon": + SettingsData.set("privacyShowMicIcon", value); + break; + case "showCameraIcon": + SettingsData.set("privacyShowCameraIcon", value); + break; + case "showScreenSharingIcon": + SettingsData.set("privacyShowScreenShareIcon", value); + break; + } + } + + function handleMinimumWidthChanged(sectionId, widgetIndex, enabled) { + var widgets = getWidgetsForSection(sectionId).slice(); + if (widgetIndex < 0 || widgetIndex >= widgets.length) { + setWidgetsForSection(sectionId, widgets); + return; + } + var newWidget = cloneWidgetData(widgets[widgetIndex]); + newWidget.minimumWidth = enabled; + widgets[widgetIndex] = newWidget; + setWidgetsForSection(sectionId, widgets); + } + + function handleShowSwapChanged(sectionId, widgetIndex, enabled) { + var widgets = getWidgetsForSection(sectionId).slice(); + if (widgetIndex < 0 || widgetIndex >= widgets.length) { + setWidgetsForSection(sectionId, widgets); + return; + } + var newWidget = cloneWidgetData(widgets[widgetIndex]); + newWidget.showSwap = enabled; + widgets[widgetIndex] = newWidget; + setWidgetsForSection(sectionId, widgets); + } + + function handleShowInGbChanged(sectionId, widgetIndex, enabled) { + var widgets = getWidgetsForSection(sectionId).slice(); + if (widgetIndex < 0 || widgetIndex >= widgets.length) { + setWidgetsForSection(sectionId, widgets); + return; + } + var newWidget = cloneWidgetData(widgets[widgetIndex]); + newWidget.showInGb = enabled; + widgets[widgetIndex] = newWidget; + setWidgetsForSection(sectionId, widgets); + } + + function handleDiskUsageModeChanged(sectionId, widgetIndex, mode) { + var widgets = getWidgetsForSection(sectionId).slice(); + if (widgetIndex < 0 || widgetIndex >= widgets.length) { + setWidgetsForSection(sectionId, widgets); + return; + } + var newWidget = cloneWidgetData(widgets[widgetIndex]); + newWidget.diskUsageMode = mode; + widgets[widgetIndex] = newWidget; + setWidgetsForSection(sectionId, widgets); + } + + function handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value) { + var widgets = getWidgetsForSection(sectionId).slice(); + if (widgetIndex < 0 || widgetIndex >= widgets.length) { + setWidgetsForSection(sectionId, widgets); + return; + } + var newWidget = cloneWidgetData(widgets[widgetIndex]); + newWidget[settingName] = value; + widgets[widgetIndex] = newWidget; + setWidgetsForSection(sectionId, widgets); + } + + function handleCompactModeChanged(sectionId, widgetId, value) { + var widgets = getWidgetsForSection(sectionId).slice(); + for (var i = 0; i < widgets.length; i++) { + var widget = widgets[i]; + var currentId = typeof widget === "string" ? widget : widget.id; + if (currentId !== widgetId) + continue; + + var newWidget = cloneWidgetData(widget); + switch (widgetId) { + case "music": + newWidget.mediaSize = value; + break; + case "clock": + newWidget.clockCompactMode = value; + break; + case "focusedWindow": + newWidget.focusedWindowCompactMode = value; + break; + case "runningApps": + newWidget.runningAppsCompactMode = value; + break; + case "keyboard_layout_name": + newWidget.keyboardLayoutNameCompactMode = value; + break; + } + widgets[i] = newWidget; + break; + } + setWidgetsForSection(sectionId, widgets); + } + + function getItemsForSection(sectionId) { + var widgets = []; + var widgetData = getWidgetsForSection(sectionId); + widgetData.forEach(widget => { + var isString = typeof widget === "string"; + var widgetId = isString ? widget : widget.id; + var widgetDef = baseWidgetDefinitions.find(w => w.id === widgetId); + if (!widgetDef) + return; + + var item = Object.assign({}, widgetDef); + item.enabled = isString ? true : widget.enabled; + if (!isString) { + if (widget.size !== undefined) + item.size = widget.size; + if (widget.selectedGpuIndex !== undefined) + item.selectedGpuIndex = widget.selectedGpuIndex; + if (widget.pciId !== undefined) + item.pciId = widget.pciId; + if (widget.mountPath !== undefined) + item.mountPath = widget.mountPath; + if (widget.diskUsageMode !== undefined) + item.diskUsageMode = widget.diskUsageMode; + if (widget.showNetworkIcon !== undefined) + item.showNetworkIcon = widget.showNetworkIcon; + if (widget.showBluetoothIcon !== undefined) + item.showBluetoothIcon = widget.showBluetoothIcon; + if (widget.showAudioIcon !== undefined) + item.showAudioIcon = widget.showAudioIcon; + if (widget.showAudioPercent !== undefined) + item.showAudioPercent = widget.showAudioPercent; + if (widget.showVpnIcon !== undefined) + item.showVpnIcon = widget.showVpnIcon; + if (widget.showBrightnessIcon !== undefined) + item.showBrightnessIcon = widget.showBrightnessIcon; + if (widget.showBrightnessPercent !== undefined) + item.showBrightnessPercent = widget.showBrightnessPercent; + if (widget.showMicIcon !== undefined) + item.showMicIcon = widget.showMicIcon; + if (widget.showMicPercent !== undefined) + item.showMicPercent = widget.showMicPercent; + if (widget.showBatteryIcon !== undefined) + item.showBatteryIcon = widget.showBatteryIcon; + if (widget.showPrinterIcon !== undefined) + item.showPrinterIcon = widget.showPrinterIcon; + if (widget.showScreenSharingIcon !== undefined) + item.showScreenSharingIcon = widget.showScreenSharingIcon; + if (widget.controlCenterGroupOrder !== undefined) + item.controlCenterGroupOrder = widget.controlCenterGroupOrder; + if (widget.minimumWidth !== undefined) + item.minimumWidth = widget.minimumWidth; + if (widget.showSwap !== undefined) + item.showSwap = widget.showSwap; + if (widget.showInGb !== undefined) + item.showInGb = widget.showInGb; + if (widget.mediaSize !== undefined) + item.mediaSize = widget.mediaSize; + if (widget.clockCompactMode !== undefined) + item.clockCompactMode = widget.clockCompactMode; + if (widget.focusedWindowCompactMode !== undefined) + item.focusedWindowCompactMode = widget.focusedWindowCompactMode; + if (widget.runningAppsCompactMode !== undefined) + item.runningAppsCompactMode = widget.runningAppsCompactMode; + if (widget.runningAppsGroupByApp !== undefined) + item.runningAppsGroupByApp = widget.runningAppsGroupByApp; + if (widget.runningAppsCurrentWorkspace !== undefined) + item.runningAppsCurrentWorkspace = widget.runningAppsCurrentWorkspace; + if (widget.runningAppsCurrentMonitor !== undefined) + item.runningAppsCurrentMonitor = widget.runningAppsCurrentMonitor; + if (widget.keyboardLayoutNameCompactMode !== undefined) + item.keyboardLayoutNameCompactMode = widget.keyboardLayoutNameCompactMode; + if (widget.barMaxVisibleApps !== undefined) + item.barMaxVisibleApps = widget.barMaxVisibleApps; + if (widget.barMaxVisibleRunningApps !== undefined) + item.barMaxVisibleRunningApps = widget.barMaxVisibleRunningApps; + if (widget.barShowOverflowBadge !== undefined) + item.barShowOverflowBadge = widget.barShowOverflowBadge; + if (widget.trayUseInlineExpansion !== undefined) + item.trayUseInlineExpansion = widget.trayUseInlineExpansion; + } + widgets.push(item); + }); + return widgets; + } + + Component.onCompleted: { + const leftWidgets = selectedBarConfig?.leftWidgets; + const centerWidgets = selectedBarConfig?.centerWidgets; + const rightWidgets = selectedBarConfig?.rightWidgets; + + if (!leftWidgets) + setWidgetsForSection("left", defaultLeftWidgets); + if (!centerWidgets) + setWidgetsForSection("center", defaultCenterWidgets); + if (!rightWidgets) + setWidgetsForSection("right", defaultRightWidgets); + + const sections = ["left", "center", "right"]; + sections.forEach(sectionId => { + var widgets = getWidgetsForSection(sectionId).slice(); + var updated = false; + for (var i = 0; i < widgets.length; i++) { + var widget = widgets[i]; + if (typeof widget === "object" && widget.id === "spacer" && !widget.size) { + widgets[i] = Object.assign({}, widget, { + "size": 20 + }); + updated = true; + } + } + if (updated) + setWidgetsForSection(sectionId, widgets); + }); + } + + LazyLoader { + id: widgetSelectionPopupLoader + active: false + + WidgetSelectionPopup { + id: widgetSelectionPopupItem + parentModal: widgetsTab.parentModal + onWidgetSelected: (widgetId, targetSection) => { + widgetsTab.addWidgetToSection(widgetId, targetSection); + } + } + } + + function showWidgetSelectionPopup(sectionId) { + widgetSelectionPopupLoader.active = true; + if (!widgetSelectionPopupLoader.item) + return; + widgetSelectionPopupLoader.item.targetSection = sectionId; + widgetSelectionPopupLoader.item.allWidgets = widgetsTab.getWidgetsForPopup(); + widgetSelectionPopupLoader.item.show(); + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + StyledRect { + width: parent.width + height: barSelectorContent.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.width: 0 + visible: hasMultipleBars + + Column { + id: barSelectorContent + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "toolbar" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Select Bar") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankButtonGroup { + id: barSelectorGroup + width: parent.width + model: SettingsData.barConfigs.map(cfg => cfg.name || ("Bar " + (SettingsData.barConfigs.indexOf(cfg) + 1))) + currentIndex: { + const idx = SettingsData.barConfigs.findIndex(cfg => cfg.id === selectedBarId); + return idx >= 0 ? idx : 0; + } + checkEnabled: false + onSelectionChanged: (index, selected) => { + if (!selected) + return; + if (index >= 0 && index < SettingsData.barConfigs.length) + selectedBarId = SettingsData.barConfigs[index].id; + } + } + } + } + + StyledRect { + width: parent.width + height: widgetManagementHeader.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.width: 0 + + Column { + id: widgetManagementHeader + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + RowLayout { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "widgets" + size: Theme.iconSize + color: Theme.primary + Layout.alignment: Qt.AlignVCenter + } + + StyledText { + text: I18n.tr("Widget Management") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + Layout.alignment: Qt.AlignVCenter + } + + Item { + height: 1 + Layout.fillWidth: true + } + + Rectangle { + width: resetContentRow.implicitWidth + Theme.spacingM * 2 + height: 28 + radius: Theme.cornerRadius + color: resetArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariant + Layout.alignment: Qt.AlignVCenter + border.width: 0 + + Row { + id: resetContentRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "refresh" + size: 14 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Reset") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: resetArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + setWidgetsForSection("left", defaultLeftWidgets); + setWidgetsForSection("center", defaultCenterWidgets); + setWidgetsForSection("right", defaultRightWidgets); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + StyledText { + width: parent.width + text: I18n.tr("Drag widgets to reorder within sections. Use the eye icon to hide/show widgets (maintains spacing), or X to remove them completely.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingL + + StyledRect { + width: parent.width + height: leftSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.width: 0 + + WidgetsTabSection { + id: leftSection + anchors.fill: parent + anchors.margins: Theme.spacingL + title: selectedBarIsVertical ? I18n.tr("Top Section") : I18n.tr("Left Section") + titleIcon: "format_align_left" + sectionId: "left" + allWidgets: widgetsTab.baseWidgetDefinitions + items: widgetsTab.getItemsForSection("left") + onItemEnabledChanged: (sectionId, itemId, enabled) => { + widgetsTab.handleItemEnabledChanged(sectionId, itemId, enabled); + } + onItemOrderChanged: newOrder => { + widgetsTab.handleItemOrderChanged(sectionId, newOrder); + } + onAddWidget: sectionId => { + showWidgetSelectionPopup(sectionId); + } + onRemoveWidget: (sectionId, index) => { + widgetsTab.removeWidgetFromSection(sectionId, index); + } + onSpacerSizeChanged: (sectionId, index, size) => { + widgetsTab.handleSpacerSizeChanged(sectionId, index, size); + } + onGpuSelectionChanged: (sectionId, index, gpuIndex) => { + widgetsTab.handleGpuSelectionChanged(sectionId, index, gpuIndex); + } + onDiskMountSelectionChanged: (sectionId, index, mountPath) => { + widgetsTab.handleDiskMountSelectionChanged(sectionId, index, mountPath); + } + onControlCenterSettingChanged: (sectionId, index, setting, value) => { + widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value); + } + onControlCenterGroupOrderChanged: (sectionId, index, groupOrder) => { + widgetsTab.handleControlCenterGroupOrderChanged(sectionId, index, groupOrder); + } + onPrivacySettingChanged: (sectionId, index, setting, value) => { + widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value); + } + onMinimumWidthChanged: (sectionId, index, enabled) => { + widgetsTab.handleMinimumWidthChanged(sectionId, index, enabled); + } + onShowSwapChanged: (sectionId, index, enabled) => { + widgetsTab.handleShowSwapChanged(sectionId, index, enabled); + } + onShowInGbChanged: (sectionId, index, enabled) => { + widgetsTab.handleShowInGbChanged(sectionId, index, enabled); + } + onDiskUsageModeChanged: (sectionId, widgetIndex, mode) => { + widgetsTab.handleDiskUsageModeChanged(sectionId, widgetIndex, mode); + } + onCompactModeChanged: (widgetId, value) => { + widgetsTab.handleCompactModeChanged(sectionId, widgetId, value); + } + onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => { + widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value); + } + } + } + + StyledRect { + width: parent.width + height: centerSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.width: 0 + + WidgetsTabSection { + id: centerSection + anchors.fill: parent + anchors.margins: Theme.spacingL + title: selectedBarIsVertical ? I18n.tr("Middle Section") : I18n.tr("Center Section") + titleIcon: "format_align_center" + sectionId: "center" + allWidgets: widgetsTab.baseWidgetDefinitions + items: widgetsTab.getItemsForSection("center") + onItemEnabledChanged: (sectionId, itemId, enabled) => { + widgetsTab.handleItemEnabledChanged(sectionId, itemId, enabled); + } + onItemOrderChanged: newOrder => { + widgetsTab.handleItemOrderChanged(sectionId, newOrder); + } + onAddWidget: sectionId => { + showWidgetSelectionPopup(sectionId); + } + onRemoveWidget: (sectionId, index) => { + widgetsTab.removeWidgetFromSection(sectionId, index); + } + onSpacerSizeChanged: (sectionId, index, size) => { + widgetsTab.handleSpacerSizeChanged(sectionId, index, size); + } + onGpuSelectionChanged: (sectionId, index, gpuIndex) => { + widgetsTab.handleGpuSelectionChanged(sectionId, index, gpuIndex); + } + onDiskMountSelectionChanged: (sectionId, index, mountPath) => { + widgetsTab.handleDiskMountSelectionChanged(sectionId, index, mountPath); + } + onControlCenterSettingChanged: (sectionId, index, setting, value) => { + widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value); + } + onControlCenterGroupOrderChanged: (sectionId, index, groupOrder) => { + widgetsTab.handleControlCenterGroupOrderChanged(sectionId, index, groupOrder); + } + onPrivacySettingChanged: (sectionId, index, setting, value) => { + widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value); + } + onMinimumWidthChanged: (sectionId, index, enabled) => { + widgetsTab.handleMinimumWidthChanged(sectionId, index, enabled); + } + onShowSwapChanged: (sectionId, index, enabled) => { + widgetsTab.handleShowSwapChanged(sectionId, index, enabled); + } + onShowInGbChanged: (sectionId, index, enabled) => { + widgetsTab.handleShowInGbChanged(sectionId, index, enabled); + } + onDiskUsageModeChanged: (sectionId, widgetIndex, mode) => { + widgetsTab.handleDiskUsageModeChanged(sectionId, widgetIndex, mode); + } + onCompactModeChanged: (widgetId, value) => { + widgetsTab.handleCompactModeChanged(sectionId, widgetId, value); + } + onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => { + widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value); + } + } + } + + StyledRect { + width: parent.width + height: rightSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.width: 0 + + WidgetsTabSection { + id: rightSection + anchors.fill: parent + anchors.margins: Theme.spacingL + title: selectedBarIsVertical ? I18n.tr("Bottom Section") : I18n.tr("Right Section") + titleIcon: "format_align_right" + sectionId: "right" + allWidgets: widgetsTab.baseWidgetDefinitions + items: widgetsTab.getItemsForSection("right") + onItemEnabledChanged: (sectionId, itemId, enabled) => { + widgetsTab.handleItemEnabledChanged(sectionId, itemId, enabled); + } + onItemOrderChanged: newOrder => { + widgetsTab.handleItemOrderChanged(sectionId, newOrder); + } + onAddWidget: sectionId => { + showWidgetSelectionPopup(sectionId); + } + onRemoveWidget: (sectionId, index) => { + widgetsTab.removeWidgetFromSection(sectionId, index); + } + onSpacerSizeChanged: (sectionId, index, size) => { + widgetsTab.handleSpacerSizeChanged(sectionId, index, size); + } + onGpuSelectionChanged: (sectionId, index, gpuIndex) => { + widgetsTab.handleGpuSelectionChanged(sectionId, index, gpuIndex); + } + onDiskMountSelectionChanged: (sectionId, index, mountPath) => { + widgetsTab.handleDiskMountSelectionChanged(sectionId, index, mountPath); + } + onControlCenterSettingChanged: (sectionId, index, setting, value) => { + widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value); + } + onControlCenterGroupOrderChanged: (sectionId, index, groupOrder) => { + widgetsTab.handleControlCenterGroupOrderChanged(sectionId, index, groupOrder); + } + onPrivacySettingChanged: (sectionId, index, setting, value) => { + widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value); + } + onMinimumWidthChanged: (sectionId, index, enabled) => { + widgetsTab.handleMinimumWidthChanged(sectionId, index, enabled); + } + onShowSwapChanged: (sectionId, index, enabled) => { + widgetsTab.handleShowSwapChanged(sectionId, index, enabled); + } + onShowInGbChanged: (sectionId, index, enabled) => { + widgetsTab.handleShowInGbChanged(sectionId, index, enabled); + } + onDiskUsageModeChanged: (sectionId, widgetIndex, mode) => { + widgetsTab.handleDiskUsageModeChanged(sectionId, widgetIndex, mode); + } + onCompactModeChanged: (widgetId, value) => { + widgetsTab.handleCompactModeChanged(sectionId, widgetId, value); + } + onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => { + widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value); + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WidgetsTabSection.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WidgetsTabSection.qml new file mode 100644 index 0000000..88c1270 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WidgetsTabSection.qml @@ -0,0 +1,2940 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Widgets +import qs.Services + +Column { + id: root + + property var items: [] + property var allWidgets: [] + property string title: "" + property string titleIcon: "widgets" + property string sectionId: "" + + DankTooltipV2 { + id: sharedTooltip + } + + signal itemEnabledChanged(string sectionId, string itemId, bool enabled) + signal itemOrderChanged(var newOrder) + signal addWidget(string sectionId) + signal removeWidget(string sectionId, int widgetIndex) + signal spacerSizeChanged(string sectionId, int widgetIndex, int newSize) + signal compactModeChanged(string widgetId, var value) + signal gpuSelectionChanged(string sectionId, int widgetIndex, int selectedIndex) + signal diskMountSelectionChanged(string sectionId, int widgetIndex, string mountPath) + signal controlCenterSettingChanged(string sectionId, int widgetIndex, string settingName, bool value) + signal controlCenterGroupOrderChanged(string sectionId, int widgetIndex, var groupOrder) + signal privacySettingChanged(string sectionId, int widgetIndex, string settingName, bool value) + signal minimumWidthChanged(string sectionId, int widgetIndex, bool enabled) + signal showSwapChanged(string sectionId, int widgetIndex, bool enabled) + signal showInGbChanged(string sectionId, int widgetIndex, bool enabled) + signal diskUsageModeChanged(string sectionId, int widgetIndex, int mode) + signal overflowSettingChanged(string sectionId, int widgetIndex, string settingName, var value) + + function cloneWidgetData(widget) { + var result = { + "id": widget.id, + "enabled": widget.enabled + }; + var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"]; + for (var i = 0; i < keys.length; i++) { + if (widget[keys[i]] !== undefined) + result[keys[i]] = widget[keys[i]]; + } + return result; + } + + width: parent.width + height: implicitHeight + spacing: Theme.spacingM + + Row { + spacing: Theme.spacingM + + DankIcon { + name: root.titleIcon + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: root.title + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Column { + id: itemsList + + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: root.items + + delegate: Item { + id: delegateItem + + property bool held: dragArea.pressed + property real originalY: y + + width: itemsList.width + height: 70 + z: held ? 2 : 1 + + Rectangle { + id: itemBackground + + anchors.fill: parent + anchors.margins: 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.8) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 0 + + DankIcon { + name: "drag_indicator" + size: Theme.iconSize - 4 + color: Theme.outline + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + 8 + anchors.verticalCenter: parent.verticalCenter + opacity: 0.8 + } + + DankIcon { + name: modelData.icon + size: Theme.iconSize + color: modelData.enabled ? Theme.primary : Theme.outline + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM * 2 + 40 + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM * 3 + 40 + Theme.iconSize + anchors.right: actionButtons.left + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: modelData.text + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: modelData.enabled ? Theme.surfaceText : Theme.outline + elide: Text.ElideRight + width: parent.width + } + + StyledText { + text: { + if (modelData.id === "gpuTemp") { + var selectedIdx = modelData.selectedGpuIndex !== undefined ? modelData.selectedGpuIndex : 0; + if (DgopService.availableGpus && DgopService.availableGpus.length > selectedIdx) { + var gpu = DgopService.availableGpus[selectedIdx]; + return gpu.driver ? gpu.driver.toUpperCase() : ""; + } + return I18n.tr("No GPU detected"); + } + return modelData.description; + } + font.pixelSize: Theme.fontSizeSmall + color: modelData.enabled ? Theme.outline : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) + elide: Text.ElideRight + width: parent.width + wrapMode: Text.WordWrap + } + } + + Row { + id: actionButtons + + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + DankActionButton { + id: gpuMenuButton + visible: modelData.id === "gpuTemp" + buttonSize: 32 + iconName: "more_vert" + iconSize: 18 + iconColor: Theme.outline + onClicked: { + gpuContextMenu.widgetData = modelData; + gpuContextMenu.sectionId = root.sectionId; + gpuContextMenu.widgetIndex = index; + + var buttonPos = gpuMenuButton.mapToItem(root, 0, 0); + var popupWidth = gpuContextMenu.width; + var popupHeight = gpuContextMenu.height; + + var xPos = buttonPos.x - popupWidth - Theme.spacingS; + if (xPos < 0) { + xPos = buttonPos.x + gpuMenuButton.width + Theme.spacingS; + } + + var yPos = buttonPos.y - popupHeight / 2 + gpuMenuButton.height / 2; + if (yPos < 0) { + yPos = Theme.spacingS; + } else if (yPos + popupHeight > root.height) { + yPos = root.height - popupHeight - Theme.spacingS; + } + + gpuContextMenu.x = xPos; + gpuContextMenu.y = yPos; + gpuContextMenu.open(); + } + } + + Item { + width: 120 + height: 32 + visible: modelData.id === "diskUsage" + DankDropdown { + id: diskMountDropdown + anchors.fill: parent + currentValue: { + const mountPath = modelData.mountPath || "/"; + if (mountPath === "/") { + return "root (/)"; + } + return mountPath; + } + options: { + if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) { + return ["root (/)"]; + } + return DgopService.diskMounts.map(mount => { + if (mount.mount === "/") { + return "root (/)"; + } + return mount.mount; + }); + } + onValueChanged: value => { + const newPath = value === "root (/)" ? "/" : value; + root.diskMountSelectionChanged(root.sectionId, index, newPath); + } + } + } + + DankActionButton { + id: diskMenuButton + visible: modelData.id === "diskUsage" + buttonSize: 32 + iconName: "more_vert" + iconSize: 18 + iconColor: Theme.outline + onClicked: { + diskUsageContextMenu.widgetData = modelData; + diskUsageContextMenu.sectionId = root.sectionId; + diskUsageContextMenu.widgetIndex = index; + + var buttonPos = diskMenuButton.mapToItem(root, 0, 0); + var xPos = buttonPos.x - diskUsageContextMenu.width - Theme.spacingS; + if (xPos < 0) + xPos = buttonPos.x + diskMenuButton.width + Theme.spacingS; + var yPos = buttonPos.y - diskUsageContextMenu.height / 2 + diskMenuButton.height / 2; + if (yPos < 0) + yPos = Theme.spacingS; + else if (yPos + diskUsageContextMenu.height > root.height) + yPos = root.height - diskUsageContextMenu.height - Theme.spacingS; + + diskUsageContextMenu.x = xPos; + diskUsageContextMenu.y = yPos; + diskUsageContextMenu.open(); + } + } + + Item { + width: 32 + height: 32 + visible: modelData.warning !== undefined && modelData.warning !== "" + + DankIcon { + name: "warning" + size: 20 + color: Theme.error + anchors.centerIn: parent + opacity: warningArea.containsMouse ? 1.0 : 0.8 + } + + MouseArea { + id: warningArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + } + + Rectangle { + id: warningTooltip + + property string warningText: (modelData.warning !== undefined && modelData.warning !== "") ? modelData.warning : "" + + width: Math.min(250, warningTooltipText.implicitWidth) + Theme.spacingM * 2 + height: warningTooltipText.implicitHeight + Theme.spacingS * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 0 + visible: warningArea.containsMouse && warningText !== "" + opacity: visible ? 1 : 0 + x: -width - Theme.spacingS + y: (parent.height - height) / 2 + z: 100 + + StyledText { + id: warningTooltipText + anchors.centerIn: parent + anchors.margins: Theme.spacingS + text: warningTooltip.warningText + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + width: Math.min(250, implicitWidth) + wrapMode: Text.WordWrap + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + DankActionButton { + id: minimumWidthButton + buttonSize: 28 + visible: modelData.id === "cpuUsage" || modelData.id === "memUsage" || modelData.id === "cpuTemp" || modelData.id === "gpuTemp" + iconName: "straighten" + iconSize: 16 + iconColor: (modelData.minimumWidth !== undefined ? modelData.minimumWidth : true) ? Theme.primary : Theme.outline + onClicked: { + var currentEnabled = modelData.minimumWidth !== undefined ? modelData.minimumWidth : true; + root.minimumWidthChanged(root.sectionId, index, !currentEnabled); + } + onEntered: { + var currentEnabled = modelData.minimumWidth !== undefined ? modelData.minimumWidth : true; + const tooltipText = currentEnabled ? "Force Padding" : "Dynamic Width"; + sharedTooltip.show(tooltipText, minimumWidthButton, 0, 0, "bottom"); + } + onExited: { + sharedTooltip.hide(); + } + } + + DankActionButton { + id: memMenuButton + visible: modelData.id === "memUsage" + buttonSize: 32 + iconName: "more_vert" + iconSize: 18 + iconColor: Theme.outline + onClicked: { + memUsageContextMenu.widgetData = modelData; + memUsageContextMenu.sectionId = root.sectionId; + memUsageContextMenu.widgetIndex = index; + + var buttonPos = memMenuButton.mapToItem(root, 0, 0); + var popupWidth = memUsageContextMenu.width; + var popupHeight = memUsageContextMenu.height; + + var xPos = buttonPos.x - popupWidth - Theme.spacingS; + if (xPos < 0) { + xPos = buttonPos.x + memMenuButton.width + Theme.spacingS; + } + + var yPos = buttonPos.y - popupHeight / 2 + memMenuButton.height / 2; + if (yPos < 0) { + yPos = Theme.spacingS; + } else if (yPos + popupHeight > root.height) { + yPos = root.height - popupHeight - Theme.spacingS; + } + + memUsageContextMenu.x = xPos; + memUsageContextMenu.y = yPos; + memUsageContextMenu.open(); + } + } + + DankActionButton { + id: musicMenuButton + visible: modelData.id === "music" + buttonSize: 32 + iconName: "more_vert" + iconSize: 18 + iconColor: Theme.outline + onClicked: { + musicContextMenu.widgetData = modelData; + musicContextMenu.sectionId = root.sectionId; + musicContextMenu.widgetIndex = index; + + var buttonPos = musicMenuButton.mapToItem(root, 0, 0); + var popupWidth = musicContextMenu.width; + var popupHeight = musicContextMenu.height; + + var xPos = buttonPos.x - popupWidth - Theme.spacingS; + if (xPos < 0) + xPos = buttonPos.x + musicMenuButton.width + Theme.spacingS; + + var yPos = buttonPos.y - popupHeight / 2 + musicMenuButton.height / 2; + if (yPos < 0) { + yPos = Theme.spacingS; + } else if (yPos + popupHeight > root.height) { + yPos = root.height - popupHeight - Theme.spacingS; + } + + musicContextMenu.x = xPos; + musicContextMenu.y = yPos; + musicContextMenu.open(); + } + } + + DankActionButton { + id: runningAppsMenuButton + visible: modelData.id === "runningApps" + buttonSize: 32 + iconName: "more_vert" + iconSize: 18 + iconColor: Theme.outline + onClicked: { + runningAppsContextMenu.widgetData = modelData; + runningAppsContextMenu.sectionId = root.sectionId; + runningAppsContextMenu.widgetIndex = index; + + var buttonPos = runningAppsMenuButton.mapToItem(root, 0, 0); + var popupWidth = runningAppsContextMenu.width; + var popupHeight = runningAppsContextMenu.height; + + var xPos = buttonPos.x - popupWidth - Theme.spacingS; + if (xPos < 0) + xPos = buttonPos.x + runningAppsMenuButton.width + Theme.spacingS; + + var yPos = buttonPos.y - popupHeight / 2 + runningAppsMenuButton.height / 2; + if (yPos < 0) { + yPos = Theme.spacingS; + } else if (yPos + popupHeight > root.height) { + yPos = root.height - popupHeight - Theme.spacingS; + } + + runningAppsContextMenu.x = xPos; + runningAppsContextMenu.y = yPos; + runningAppsContextMenu.open(); + } + } + + Row { + spacing: Theme.spacingXS + visible: modelData.id === "clock" || modelData.id === "focusedWindow" || modelData.id === "keyboard_layout_name" || modelData.id === "appsDock" || modelData.id === "systemTray" + + DankActionButton { + id: compactModeButton + buttonSize: 28 + visible: modelData.id === "clock" || modelData.id === "focusedWindow" || modelData.id === "keyboard_layout_name" + iconName: { + const isCompact = (() => { + switch (modelData.id) { + case "clock": + return modelData.clockCompactMode !== undefined ? modelData.clockCompactMode : SettingsData.clockCompactMode; + case "focusedWindow": + return modelData.focusedWindowCompactMode !== undefined ? modelData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode; + case "keyboard_layout_name": + return modelData.keyboardLayoutNameCompactMode !== undefined ? modelData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode; + default: + return false; + } + })(); + return isCompact ? "zoom_out" : "zoom_in"; + } + iconSize: 16 + iconColor: { + const isCompact = (() => { + switch (modelData.id) { + case "clock": + return modelData.clockCompactMode !== undefined ? modelData.clockCompactMode : SettingsData.clockCompactMode; + case "focusedWindow": + return modelData.focusedWindowCompactMode !== undefined ? modelData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode; + case "keyboard_layout_name": + return modelData.keyboardLayoutNameCompactMode !== undefined ? modelData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode; + default: + return false; + } + })(); + return isCompact ? Theme.primary : Theme.outline; + } + onClicked: { + const currentValue = (() => { + switch (modelData.id) { + case "clock": + return modelData.clockCompactMode !== undefined ? modelData.clockCompactMode : SettingsData.clockCompactMode; + case "focusedWindow": + return modelData.focusedWindowCompactMode !== undefined ? modelData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode; + case "keyboard_layout_name": + return modelData.keyboardLayoutNameCompactMode !== undefined ? modelData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode; + default: + return false; + } + })(); + root.compactModeChanged(modelData.id, !currentValue); + } + onEntered: { + const isCompact = (() => { + switch (modelData.id) { + case "clock": + return modelData.clockCompactMode !== undefined ? modelData.clockCompactMode : SettingsData.clockCompactMode; + case "focusedWindow": + return modelData.focusedWindowCompactMode !== undefined ? modelData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode; + case "keyboard_layout_name": + return modelData.keyboardLayoutNameCompactMode !== undefined ? modelData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode; + default: + return false; + } + })(); + const tooltipText = isCompact ? "Full Size" : "Compact"; + sharedTooltip.show(tooltipText, compactModeButton, 0, 0, "bottom"); + } + onExited: { + sharedTooltip.hide(); + } + } + + DankActionButton { + id: appsDockMenuButton + buttonSize: 32 + visible: modelData.id === "appsDock" + iconName: "more_vert" + iconSize: 18 + iconColor: Theme.outline + onClicked: { + appsDockContextMenu.widgetData = modelData; + appsDockContextMenu.sectionId = root.sectionId; + appsDockContextMenu.widgetIndex = index; + + var buttonPos = appsDockMenuButton.mapToItem(root, 0, 0); + var popupWidth = appsDockContextMenu.width; + var popupHeight = appsDockContextMenu.height; + + var xPos = buttonPos.x - popupWidth - Theme.spacingS; + if (xPos < 0) + xPos = buttonPos.x + appsDockMenuButton.width + Theme.spacingS; + + var yPos = buttonPos.y - popupHeight / 2 + appsDockMenuButton.height / 2; + if (yPos < 0) { + yPos = Theme.spacingS; + } else if (yPos + popupHeight > root.height) { + yPos = root.height - popupHeight - Theme.spacingS; + } + + appsDockContextMenu.x = xPos; + appsDockContextMenu.y = yPos; + appsDockContextMenu.open(); + } + } + + DankActionButton { + id: trayMenuButton + buttonSize: 32 + visible: modelData.id === "systemTray" + iconName: "more_vert" + iconSize: 18 + iconColor: Theme.outline + onClicked: { + trayContextMenu.widgetData = modelData; + trayContextMenu.sectionId = root.sectionId; + trayContextMenu.widgetIndex = index; + + var buttonPos = trayMenuButton.mapToItem(root, 0, 0); + var popupWidth = trayContextMenu.width; + var popupHeight = trayContextMenu.height; + + var xPos = buttonPos.x - popupWidth - Theme.spacingS; + if (xPos < 0) + xPos = buttonPos.x + trayMenuButton.width + Theme.spacingS; + + var yPos = buttonPos.y - popupHeight / 2 + trayMenuButton.height / 2; + if (yPos < 0) { + yPos = Theme.spacingS; + } else if (yPos + popupHeight > root.height) { + yPos = root.height - popupHeight - Theme.spacingS; + } + + trayContextMenu.x = xPos; + trayContextMenu.y = yPos; + trayContextMenu.open(); + } + } + + Rectangle { + id: compactModeTooltip + width: tooltipText.contentWidth + Theme.spacingM * 2 + height: tooltipText.contentHeight + Theme.spacingS * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 0 + visible: false + opacity: visible ? 1 : 0 + x: -width - Theme.spacingS + y: (parent.height - height) / 2 + z: 100 + + StyledText { + id: tooltipText + anchors.centerIn: parent + text: I18n.tr("Compact Mode") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + DankActionButton { + id: ccMenuButton + visible: modelData.id === "controlCenterButton" + buttonSize: 32 + iconName: "more_vert" + iconSize: 18 + iconColor: Theme.outline + onClicked: { + controlCenterContextMenu.widgetData = modelData; + controlCenterContextMenu.sectionId = root.sectionId; + controlCenterContextMenu.widgetIndex = index; + controlCenterContextMenu.controlCenterGroups = controlCenterContextMenu.getOrderedControlCenterGroups(); + + var buttonPos = ccMenuButton.mapToItem(root, 0, 0); + var popupWidth = controlCenterContextMenu.width; + var popupHeight = controlCenterContextMenu.height; + + var xPos = buttonPos.x - popupWidth - Theme.spacingS; + if (xPos < 0) { + xPos = buttonPos.x + ccMenuButton.width + Theme.spacingS; + } + + var yPos = buttonPos.y - popupHeight / 2 + ccMenuButton.height / 2; + if (yPos < 0) { + yPos = Theme.spacingS; + } else if (yPos + popupHeight > root.height) { + yPos = root.height - popupHeight - Theme.spacingS; + } + + controlCenterContextMenu.x = xPos; + controlCenterContextMenu.y = yPos; + controlCenterContextMenu.open(); + } + } + + DankActionButton { + id: privacyMenuButton + visible: modelData.id === "privacyIndicator" + buttonSize: 32 + iconName: "more_vert" + iconSize: 18 + iconColor: Theme.outline + onClicked: { + privacyContextMenu.widgetData = modelData; + privacyContextMenu.sectionId = root.sectionId; + privacyContextMenu.widgetIndex = index; + + var buttonPos = privacyMenuButton.mapToItem(root, 0, 0); + var popupWidth = privacyContextMenu.width; + var popupHeight = privacyContextMenu.height; + + var xPos = buttonPos.x - popupWidth - Theme.spacingS; + if (xPos < 0) { + xPos = buttonPos.x + privacyMenuButton.width + Theme.spacingS; + } + + var yPos = buttonPos.y - popupHeight / 2 + privacyMenuButton.height / 2; + if (yPos < 0) { + yPos = Theme.spacingS; + } else if (yPos + popupHeight > root.height) { + yPos = root.height - popupHeight - Theme.spacingS; + } + + privacyContextMenu.x = xPos; + privacyContextMenu.y = yPos; + privacyContextMenu.open(); + } + } + + DankActionButton { + id: visibilityButton + visible: modelData.id !== "spacer" + buttonSize: 32 + iconName: modelData.enabled ? "visibility" : "visibility_off" + iconSize: 18 + iconColor: modelData.enabled ? Theme.primary : Theme.outline + onClicked: { + root.itemEnabledChanged(root.sectionId, modelData.id, !modelData.enabled); + } + onEntered: { + const tooltipText = modelData.enabled ? "Hide" : "Show"; + sharedTooltip.show(tooltipText, visibilityButton, 0, 0, "bottom"); + } + onExited: { + sharedTooltip.hide(); + } + } + + Row { + visible: modelData.id === "spacer" + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + DankActionButton { + buttonSize: 24 + iconName: "remove" + iconSize: 14 + iconColor: Theme.outline + onClicked: { + var currentSize = modelData.size || 20; + var newSize = Math.max(5, currentSize - 5); + root.spacerSizeChanged(root.sectionId, index, newSize); + } + } + + StyledText { + text: (modelData.size || 20).toString() + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + DankActionButton { + buttonSize: 24 + iconName: "add" + iconSize: 14 + iconColor: Theme.outline + onClicked: { + var currentSize = modelData.size || 20; + var newSize = Math.min(5000, currentSize + 5); + root.spacerSizeChanged(root.sectionId, index, newSize); + } + } + } + + DankActionButton { + buttonSize: 32 + iconName: "close" + iconSize: 18 + iconColor: Theme.error + onClicked: { + root.removeWidget(root.sectionId, index); + } + } + } + + MouseArea { + id: dragArea + + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: 60 + hoverEnabled: true + cursorShape: Qt.SizeVerCursor + drag.target: held ? delegateItem : undefined + drag.axis: Drag.YAxis + drag.minimumY: -delegateItem.height + drag.maximumY: itemsList.height + preventStealing: true + onPressed: { + delegateItem.z = 2; + delegateItem.originalY = delegateItem.y; + } + onReleased: { + delegateItem.z = 1; + if (drag.active) { + var newIndex = Math.round(delegateItem.y / (delegateItem.height + itemsList.spacing)); + newIndex = Math.max(0, Math.min(newIndex, root.items.length - 1)); + if (newIndex !== index) { + var newItems = root.items.slice(); + var draggedItem = newItems.splice(index, 1)[0]; + newItems.splice(newIndex, 0, draggedItem); + root.itemOrderChanged(newItems.map(item => root.cloneWidgetData(item))); + } + } + delegateItem.x = 0; + delegateItem.y = delegateItem.originalY; + } + } + + Behavior on y { + enabled: !dragArea.held && !dragArea.drag.active + + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } + + Rectangle { + width: 200 + height: 40 + radius: Theme.cornerRadius + color: addButtonArea.containsMouse ? Theme.primaryContainer : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 0 + anchors.horizontalCenter: parent.horizontalCenter + + StyledText { + text: I18n.tr("Add Widget") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + anchors.centerIn: parent + } + + MouseArea { + id: addButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.addWidget(root.sectionId); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + Popup { + id: memUsageContextMenu + + property var widgetData: null + property string sectionId: "" + property int widgetIndex: -1 + + width: 200 + height: 80 + padding: 0 + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: Rectangle { + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 0 + } + + contentItem: Item { + Column { + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 2 + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: swapToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "swap_horiz" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Show Swap") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: swapToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: memUsageContextMenu.widgetData?.showSwap ?? false + onToggled: { + root.showSwapChanged(memUsageContextMenu.sectionId, memUsageContextMenu.widgetIndex, toggled); + } + } + + MouseArea { + id: swapToggleArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + swapToggle.checked = !swapToggle.checked; + root.showSwapChanged(memUsageContextMenu.sectionId, memUsageContextMenu.widgetIndex, swapToggle.checked); + } + } + } + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: gbToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "straighten" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Show in GB") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: gbToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: memUsageContextMenu.widgetData?.showInGb ?? false + onToggled: { + root.showInGbChanged(memUsageContextMenu.sectionId, memUsageContextMenu.widgetIndex, toggled); + } + } + + MouseArea { + id: gbToggleArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + gbToggle.checked = !gbToggle.checked; + root.showInGbChanged(memUsageContextMenu.sectionId, memUsageContextMenu.widgetIndex, gbToggle.checked); + } + } + } + } + } + } + + Popup { + id: trayContextMenu + + property var widgetData: null + property string sectionId: "" + property int widgetIndex: -1 + readonly property var currentWidgetData: (widgetIndex >= 0 && widgetIndex < root.items.length) ? root.items[widgetIndex] : widgetData + + width: 220 + height: contentColumn.implicitHeight + Theme.spacingS * 2 + padding: 0 + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: Rectangle { + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 0 + } + + contentItem: Item { + Column { + id: contentColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 2 + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: trayOverflowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "arrow_selector_tool" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Use Inline Expansion") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: trayOverflowToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false + } + + MouseArea { + id: trayOverflowArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + const newValue = !(trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false); + root.overflowSettingChanged(trayContextMenu.sectionId, trayContextMenu.widgetIndex, "trayUseInlineExpansion", newValue); + } + } + } + } + } + } + + Popup { + id: diskUsageContextMenu + + property var widgetData: null + property string sectionId: "" + property int widgetIndex: -1 + readonly property var currentWidgetData: (widgetIndex >= 0 && widgetIndex < root.items.length) ? root.items[widgetIndex] : widgetData + + width: 240 + height: diskMenuColumn.implicitHeight + Theme.spacingS * 2 + padding: 0 + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: Rectangle { + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 0 + } + + contentItem: Item { + Column { + id: diskMenuColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 2 + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: "transparent" + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("Disk Usage Display") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + } + } + + Repeater { + model: [ + { + label: I18n.tr("Percentage"), + mode: 0, + icon: "percent" + }, + { + label: I18n.tr("Total"), + mode: 1, + icon: "storage" + }, + { + label: I18n.tr("Remaining"), + mode: 2, + icon: "hourglass_empty" + }, + { + label: I18n.tr("Remaining / Total"), + mode: 3, + icon: "pie_chart" + } + ] + + delegate: Rectangle { + required property var modelData + required property int index + + width: diskMenuColumn.width + height: 32 + radius: Theme.cornerRadius + color: diskOptionArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + function isSelected() { + return (diskUsageContextMenu.currentWidgetData?.diskUsageMode ?? 0) === modelData.mode; + } + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: modelData.icon + size: 16 + color: isSelected() ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData.label + font.pixelSize: Theme.fontSizeSmall + color: isSelected() ? Theme.primary : Theme.surfaceText + font.weight: isSelected() ? Font.Medium : Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + DankIcon { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + name: "check" + size: 16 + color: Theme.primary + visible: isSelected() + } + + MouseArea { + id: diskOptionArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.diskUsageModeChanged(diskUsageContextMenu.sectionId, diskUsageContextMenu.widgetIndex, modelData.mode); + diskUsageContextMenu.close(); + } + } + } + } + } + } + } + + Popup { + id: controlCenterContextMenu + + property var widgetData: null + property string sectionId: "" + property int widgetIndex: -1 + + readonly property real minimumContentWidth: controlCenterContentMetrics.implicitWidth + Theme.spacingS * 2 + readonly property real controlCenterRowHeight: 32 + readonly property real controlCenterRowSpacing: 1 + readonly property real controlCenterGroupVerticalPadding: Theme.spacingXS * 2 + readonly property real controlCenterMenuSpacing: 2 + width: Math.max(220, minimumContentWidth) + height: getControlCenterPopupHeight(controlCenterGroups) + padding: 0 + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + onClosed: { + cancelControlCenterDrag(); + } + + readonly property var defaultControlCenterGroups: [ + { + id: "network", + rows: [ + { + icon: "lan", + label: I18n.tr("Network"), + setting: "showNetworkIcon" + } + ] + }, + { + id: "vpn", + rows: [ + { + icon: "vpn_lock", + label: I18n.tr("VPN"), + setting: "showVpnIcon" + } + ] + }, + { + id: "bluetooth", + rows: [ + { + icon: "bluetooth", + label: I18n.tr("Bluetooth"), + setting: "showBluetoothIcon" + } + ] + }, + { + id: "audio", + rows: [ + { + icon: "volume_up", + label: I18n.tr("Audio"), + setting: "showAudioIcon" + }, + { + icon: "percent", + label: I18n.tr("Volume"), + setting: "showAudioPercent" + } + ] + }, + { + id: "microphone", + rows: [ + { + icon: "mic", + label: I18n.tr("Microphone"), + setting: "showMicIcon" + }, + { + icon: "percent", + label: I18n.tr("Microphone Volume"), + setting: "showMicPercent" + } + ] + }, + { + id: "brightness", + rows: [ + { + icon: "brightness_high", + label: I18n.tr("Brightness"), + setting: "showBrightnessIcon" + }, + { + icon: "percent", + label: I18n.tr("Brightness Value"), + setting: "showBrightnessPercent" + } + ] + }, + { + id: "battery", + rows: [ + { + icon: "battery_full", + label: I18n.tr("Battery"), + setting: "showBatteryIcon" + } + ] + }, + { + id: "printer", + rows: [ + { + icon: "print", + label: I18n.tr("Printer"), + setting: "showPrinterIcon" + } + ] + }, + { + id: "screenSharing", + rows: [ + { + icon: "screen_record", + label: I18n.tr("Screen Sharing"), + setting: "showScreenSharingIcon" + } + ] + } + ] + property var controlCenterGroups: defaultControlCenterGroups + property int draggedControlCenterGroupIndex: -1 + property int controlCenterGroupDropIndex: -1 + + function updateControlCenterGroupDropIndex(draggedIndex, localY) { + const totalGroups = controlCenterGroups.length; + let dropIndex = totalGroups; + + for (let i = 0; i < totalGroups; i++) { + const delegate = groupRepeater.itemAt(i); + if (!delegate) + continue; + + const midpoint = delegate.y + delegate.height / 2; + if (localY < midpoint) { + dropIndex = i; + break; + } + } + + controlCenterGroupDropIndex = Math.max(0, Math.min(totalGroups, dropIndex)); + draggedControlCenterGroupIndex = draggedIndex; + } + + function finishControlCenterDrag() { + if (draggedControlCenterGroupIndex < 0) { + controlCenterGroupDropIndex = -1; + return; + } + + const fromIndex = draggedControlCenterGroupIndex; + let toIndex = controlCenterGroupDropIndex; + + draggedControlCenterGroupIndex = -1; + controlCenterGroupDropIndex = -1; + + if (toIndex < 0 || toIndex > controlCenterGroups.length || toIndex === fromIndex || toIndex === fromIndex + 1) + return; + + const groups = controlCenterGroups.slice(); + const moved = groups.splice(fromIndex, 1)[0]; + + if (toIndex > fromIndex) + toIndex -= 1; + + groups.splice(toIndex, 0, moved); + controlCenterGroups = groups; + const reorderedGroupIds = groups.map(group => group.id); + root.controlCenterGroupOrderChanged(sectionId, widgetIndex, reorderedGroupIds); + } + + function cancelControlCenterDrag() { + draggedControlCenterGroupIndex = -1; + controlCenterGroupDropIndex = -1; + } + + function getControlCenterGroupHeight(group) { + const rowCount = group?.rows?.length ?? 0; + if (rowCount <= 0) + return controlCenterGroupVerticalPadding; + + return rowCount * controlCenterRowHeight + Math.max(0, rowCount - 1) * controlCenterRowSpacing + controlCenterGroupVerticalPadding; + } + + function getControlCenterPopupHeight(groups) { + const orderedGroups = groups || []; + let totalHeight = Theme.spacingS * 2; + + for (let i = 0; i < orderedGroups.length; i++) { + totalHeight += getControlCenterGroupHeight(orderedGroups[i]); + if (i < orderedGroups.length - 1) + totalHeight += controlCenterMenuSpacing; + } + + return totalHeight; + } + + function getOrderedControlCenterGroups() { + const baseGroups = defaultControlCenterGroups.slice(); + const currentWidget = contentItem.getCurrentWidgetData(); + const savedOrder = currentWidget?.controlCenterGroupOrder; + if (!savedOrder || !savedOrder.length) + return baseGroups; + + const groupMap = {}; + for (let i = 0; i < baseGroups.length; i++) + groupMap[baseGroups[i].id] = baseGroups[i]; + + const orderedGroups = []; + for (let i = 0; i < savedOrder.length; i++) { + const groupId = savedOrder[i]; + const group = groupMap[groupId]; + if (group) { + orderedGroups.push(group); + delete groupMap[groupId]; + } + } + + for (let i = 0; i < baseGroups.length; i++) { + const group = baseGroups[i]; + if (groupMap[group.id]) + orderedGroups.push(group); + } + + return orderedGroups; + } + + background: Rectangle { + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 0 + } + + contentItem: Item { + function getCurrentWidgetData() { + const widgets = root.items || []; + if (controlCenterContextMenu.widgetIndex >= 0 && controlCenterContextMenu.widgetIndex < widgets.length) + return widgets[controlCenterContextMenu.widgetIndex]; + return controlCenterContextMenu.widgetData; + } + + Column { + id: menuColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 2 + + Item { + id: controlCenterContentMetrics + visible: false + implicitWidth: 16 + Theme.spacingS + 16 + Theme.spacingS + longestControlCenterLabelMetrics.advanceWidth + Theme.spacingM + 40 + Theme.spacingS * 2 + Theme.spacingM + } + + TextMetrics { + id: longestControlCenterLabelMetrics + font.pixelSize: Theme.fontSizeSmall + text: { + const labels = [I18n.tr("Network"), I18n.tr("VPN"), I18n.tr("Bluetooth"), I18n.tr("Audio"), I18n.tr("Volume"), I18n.tr("Microphone"), I18n.tr("Microphone Volume"), I18n.tr("Brightness"), I18n.tr("Brightness Value"), I18n.tr("Battery"), I18n.tr("Printer"), I18n.tr("Screen Sharing")]; + let longest = ""; + for (let i = 0; i < labels.length; i++) { + if (labels[i].length > longest.length) + longest = labels[i]; + } + return longest; + } + } + + Repeater { + id: groupRepeater + model: controlCenterContextMenu.controlCenterGroups + + delegate: Item { + id: delegateRoot + + required property var modelData + required property int index + + function getCheckedState(settingName) { + const wd = controlCenterContextMenu.contentItem.getCurrentWidgetData(); + switch (settingName) { + case "showNetworkIcon": + return wd?.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon; + case "showVpnIcon": + return wd?.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon; + case "showBluetoothIcon": + return wd?.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon; + case "showAudioIcon": + return wd?.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon; + case "showAudioPercent": + return wd?.showAudioPercent ?? SettingsData.controlCenterShowAudioPercent; + case "showMicIcon": + return wd?.showMicIcon ?? SettingsData.controlCenterShowMicIcon; + case "showMicPercent": + return wd?.showMicPercent ?? SettingsData.controlCenterShowMicPercent; + case "showBrightnessIcon": + return wd?.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon; + case "showBrightnessPercent": + return wd?.showBrightnessPercent ?? SettingsData.controlCenterShowBrightnessPercent; + case "showBatteryIcon": + return wd?.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon; + case "showPrinterIcon": + return wd?.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon; + case "showScreenSharingIcon": + return wd?.showScreenSharingIcon ?? SettingsData.controlCenterShowScreenSharingIcon; + default: + return false; + } + } + + readonly property string rootSetting: modelData.rows[0]?.setting ?? "" + readonly property bool rootEnabled: rootSetting ? getCheckedState(rootSetting) : true + readonly property bool isDragged: controlCenterContextMenu.draggedControlCenterGroupIndex === index + readonly property bool showDropIndicatorAbove: controlCenterContextMenu.controlCenterGroupDropIndex === index + readonly property bool showDropIndicatorBelow: controlCenterContextMenu.controlCenterGroupDropIndex === controlCenterContextMenu.controlCenterGroups.length && index === controlCenterContextMenu.controlCenterGroups.length - 1 + + width: menuColumn.width + height: groupBackground.height + + Rectangle { + id: groupBackground + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: groupContent.implicitHeight + Theme.spacingXS * 2 + radius: Theme.cornerRadius + color: isDragged ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.18) : (groupHoverArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent") + opacity: isDragged ? 0.75 : 1.0 + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: -1 + height: 2 + radius: 1 + color: Theme.primary + visible: showDropIndicatorAbove + z: 3 + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.bottomMargin: -1 + height: 2 + radius: 1 + color: Theme.primary + visible: showDropIndicatorBelow + z: 3 + } + + Item { + id: groupContent + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: Theme.spacingXS + implicitHeight: groupColumn.implicitHeight + + Column { + id: groupColumn + anchors.left: parent.left + anchors.right: parent.right + spacing: 1 + + Repeater { + id: groupColumnRepeater + model: modelData.rows + + delegate: Rectangle { + required property var modelData + required property int index + + readonly property var rowData: modelData + readonly property bool isFirstRow: index === 0 + readonly property bool rowEnabled: isFirstRow ? true : delegateRoot.rootEnabled + readonly property bool computedCheckedState: rowEnabled ? getCheckedState(rowData.setting) : false + readonly property bool rowHovered: rowEnabled && (toggleArea.containsMouse || (isFirstRow && groupDragHandleArea.containsMouse)) + + width: groupColumn.width + height: 32 + radius: Theme.cornerRadius + opacity: rowEnabled ? 1.0 : 0.5 + color: rowHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: toggle.left + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + Item { + width: 16 + height: 16 + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: "drag_indicator" + size: 16 + color: groupDragHandleArea.pressed || isDragged ? Theme.primary : Theme.outline + visible: isFirstRow + } + + MouseArea { + id: groupDragHandleArea + anchors.fill: parent + hoverEnabled: true + preventStealing: true + enabled: isFirstRow + cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor + + onPressed: mouse => { + mouse.accepted = true; + const point = mapToItem(menuColumn, mouse.x, mouse.y); + controlCenterContextMenu.updateControlCenterGroupDropIndex(delegateRoot.index, point.y); + } + onPositionChanged: mouse => { + if (!pressed) + return; + mouse.accepted = true; + const point = mapToItem(menuColumn, mouse.x, mouse.y); + controlCenterContextMenu.updateControlCenterGroupDropIndex(delegateRoot.index, point.y); + } + onReleased: mouse => { + mouse.accepted = true; + const point = mapToItem(menuColumn, mouse.x, mouse.y); + controlCenterContextMenu.updateControlCenterGroupDropIndex(delegateRoot.index, point.y); + controlCenterContextMenu.finishControlCenterDrag(); + } + onCanceled: { + controlCenterContextMenu.cancelControlCenterDrag(); + } + } + } + + DankIcon { + name: rowData.icon + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: rowData.label + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: toggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + enabled: rowEnabled + checked: computedCheckedState + + onToggled: { + if (!rowEnabled) + return; + root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, rowData.setting, toggled); + } + } + + MouseArea { + id: toggleArea + anchors.fill: parent + anchors.leftMargin: 16 + Theme.spacingS * 2 + hoverEnabled: true + cursorShape: rowEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: rowEnabled && controlCenterContextMenu.draggedControlCenterGroupIndex < 0 + onPressed: { + if (!rowEnabled) + return; + root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, rowData.setting, !computedCheckedState); + } + } + } + } + } + + MouseArea { + id: groupHoverArea + anchors.fill: parent + hoverEnabled: true + enabled: false + } + } + } + } + } + } + } + + Popup { + id: privacyContextMenu + + property var widgetData: null + property string sectionId: "" + property int widgetIndex: -1 + + width: 200 + height: 160 + padding: 0 + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + onOpened: { + console.log("Privacy context menu opened"); + } + + onClosed: { + console.log("Privacy Center context menu closed"); + } + + background: Rectangle { + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 0 + } + + contentItem: Item { + + Column { + id: menuPrivacyColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 2 + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Always on icons") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + } + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: micToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "mic" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Microphone") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: micToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: SettingsData.privacyShowMicIcon + onToggled: toggled => { + root.privacySettingChanged(privacyContextMenu.sectionId, privacyContextMenu.widgetIndex, "showMicIcon", toggled); + } + } + + MouseArea { + id: micToggleArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + micToggle.checked = !micToggle.checked; + root.privacySettingChanged(privacyContextMenu.sectionId, privacyContextMenu.widgetIndex, "showMicIcon", micToggle.checked); + } + } + } + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: cameraToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "camera_video" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Camera") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: cameraToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: SettingsData.privacyShowCameraIcon + onToggled: toggled => { + root.privacySettingChanged(privacyContextMenu.sectionId, privacyContextMenu.widgetIndex, "showCameraIcon", toggled); + } + } + + MouseArea { + id: cameraToggleArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + cameraToggle.checked = !cameraToggle.checked; + root.privacySettingChanged(privacyContextMenu.sectionId, privacyContextMenu.widgetIndex, "showCameraIcon", cameraToggle.checked); + } + } + } + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: screenshareToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "screen_share" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Screen sharing") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: screenshareToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: SettingsData.privacyShowScreenShareIcon + onToggled: toggled => { + root.privacySettingChanged(privacyContextMenu.sectionId, privacyContextMenu.widgetIndex, "showScreenSharingIcon", toggled); + } + } + + MouseArea { + id: screenshareToggleArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + screenshareToggle.checked = !screenshareToggle.checked; + root.privacySettingChanged(privacyContextMenu.sectionId, privacyContextMenu.widgetIndex, "showScreenSharingIcon", screenshareToggle.checked); + } + } + } + } + } + } + + Popup { + id: gpuContextMenu + + property var widgetData: null + property string sectionId: "" + property int widgetIndex: -1 + + width: 250 + height: gpuMenuColumn.implicitHeight + Theme.spacingS * 2 + padding: 0 + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: Rectangle { + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 0 + } + + contentItem: Item { + Column { + id: gpuMenuColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 2 + + Repeater { + model: DgopService.availableGpus || [] + + delegate: Rectangle { + required property var modelData + required property int index + + width: gpuMenuColumn.width + height: 40 + radius: Theme.cornerRadius + color: gpuOptionArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + property bool isSelected: { + var selectedIdx = gpuContextMenu.widgetData ? (gpuContextMenu.widgetData.selectedGpuIndex !== undefined ? gpuContextMenu.widgetData.selectedGpuIndex : 0) : 0; + return index === selectedIdx; + } + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: checkIcon.left + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "memory" + size: 18 + color: isSelected ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: modelData.driver ? modelData.driver.toUpperCase() : "" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: isSelected ? Theme.primary : Theme.surfaceText + } + + StyledText { + text: modelData.displayName || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideRight + width: 180 + } + } + } + + DankIcon { + id: checkIcon + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + name: "check" + size: 18 + color: Theme.primary + visible: isSelected + } + + MouseArea { + id: gpuOptionArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.gpuSelectionChanged(gpuContextMenu.sectionId, gpuContextMenu.widgetIndex, index); + gpuContextMenu.close(); + } + } + } + } + } + } + } + + Popup { + id: musicContextMenu + + property var widgetData: null + property string sectionId: "" + property int widgetIndex: -1 + + width: 180 + height: musicMenuColumn.implicitHeight + Theme.spacingS * 2 + padding: 0 + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: Rectangle { + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 0 + } + + contentItem: Item { + Column { + id: musicMenuColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 2 + + Repeater { + model: [ + { + icon: "photo_size_select_small", + label: I18n.tr("Small"), + sizeValue: 0 + }, + { + icon: "photo_size_select_actual", + label: I18n.tr("Medium"), + sizeValue: 1 + }, + { + icon: "photo_size_select_large", + label: I18n.tr("Large"), + sizeValue: 2 + }, + { + icon: "fit_screen", + label: I18n.tr("Largest"), + sizeValue: 3 + } + ] + + delegate: Rectangle { + required property var modelData + required property int index + + function isSelected() { + var wd = musicContextMenu.widgetData; + var currentSize = wd?.mediaSize ?? SettingsData.mediaSize; + return currentSize === modelData.sizeValue; + } + + width: musicMenuColumn.width + height: Math.max(18, Theme.fontSizeSmall) + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: musicOptionArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: modelData.icon + size: 18 + color: isSelected() ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData.label + font.pixelSize: Theme.fontSizeSmall + font.weight: isSelected() ? Font.Medium : Font.Normal + color: isSelected() ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankIcon { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + name: "check" + size: 16 + color: Theme.primary + visible: isSelected() + } + + MouseArea { + id: musicOptionArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.compactModeChanged("music", modelData.sizeValue); + musicContextMenu.close(); + } + } + } + } + } + } + } + + Popup { + id: runningAppsContextMenu + + property var widgetData: null + property string sectionId: "" + property int widgetIndex: -1 + + readonly property var currentWidgetData: (widgetIndex >= 0 && widgetIndex < root.items.length) ? root.items[widgetIndex] : widgetData + + width: 240 + height: runningAppsMenuColumn.implicitHeight + Theme.spacingS * 2 + padding: 0 + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: Rectangle { + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 0 + } + + contentItem: Item { + Column { + id: runningAppsMenuColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 2 + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: "transparent" + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("Running Apps Settings") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + } + } + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: raCompactArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "zoom_in" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Compact Mode") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: raCompactToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: runningAppsContextMenu.currentWidgetData?.runningAppsCompactMode ?? SettingsData.runningAppsCompactMode + onToggled: { + root.overflowSettingChanged(runningAppsContextMenu.sectionId, runningAppsContextMenu.widgetIndex, "runningAppsCompactMode", toggled); + } + } + + MouseArea { + id: raCompactArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + raCompactToggle.checked = !raCompactToggle.checked; + root.overflowSettingChanged(runningAppsContextMenu.sectionId, runningAppsContextMenu.widgetIndex, "runningAppsCompactMode", raCompactToggle.checked); + } + } + } + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: raGroupArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "apps" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Group by App") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: raGroupToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: runningAppsContextMenu.currentWidgetData?.runningAppsGroupByApp ?? SettingsData.runningAppsGroupByApp + onToggled: { + root.overflowSettingChanged(runningAppsContextMenu.sectionId, runningAppsContextMenu.widgetIndex, "runningAppsGroupByApp", toggled); + } + } + + MouseArea { + id: raGroupArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + raGroupToggle.checked = !raGroupToggle.checked; + root.overflowSettingChanged(runningAppsContextMenu.sectionId, runningAppsContextMenu.widgetIndex, "runningAppsGroupByApp", raGroupToggle.checked); + } + } + } + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: raWorkspaceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "workspaces" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Current Workspace", "Running apps filter: only show apps from the active workspace") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: raWorkspaceToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: runningAppsContextMenu.currentWidgetData?.runningAppsCurrentWorkspace ?? SettingsData.runningAppsCurrentWorkspace + onToggled: { + root.overflowSettingChanged(runningAppsContextMenu.sectionId, runningAppsContextMenu.widgetIndex, "runningAppsCurrentWorkspace", toggled); + } + } + + MouseArea { + id: raWorkspaceArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + raWorkspaceToggle.checked = !raWorkspaceToggle.checked; + root.overflowSettingChanged(runningAppsContextMenu.sectionId, runningAppsContextMenu.widgetIndex, "runningAppsCurrentWorkspace", raWorkspaceToggle.checked); + } + } + } + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: raDisplayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "monitor" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Current Monitor", "Running apps filter: only show apps from the same monitor") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: raDisplayToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: runningAppsContextMenu.currentWidgetData?.runningAppsCurrentMonitor ?? SettingsData.runningAppsCurrentMonitor + onToggled: { + root.overflowSettingChanged(runningAppsContextMenu.sectionId, runningAppsContextMenu.widgetIndex, "runningAppsCurrentMonitor", toggled); + } + } + + MouseArea { + id: raDisplayArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + raDisplayToggle.checked = !raDisplayToggle.checked; + root.overflowSettingChanged(runningAppsContextMenu.sectionId, runningAppsContextMenu.widgetIndex, "runningAppsCurrentMonitor", raDisplayToggle.checked); + } + } + } + } + } + } + + Popup { + id: appsDockContextMenu + + property var widgetData: null + property string sectionId: "" + property int widgetIndex: -1 + + // Dynamically get current widget data from the items list + readonly property var currentWidgetData: (widgetIndex >= 0 && widgetIndex < root.items.length) ? root.items[widgetIndex] : widgetData + + width: 320 + height: appsDockMenuColumn.implicitHeight + Theme.spacingS * 2 + padding: 0 + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: Rectangle { + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 0 + } + + contentItem: Item { + Column { + id: appsDockMenuColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Apps Dock Settings") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + leftPadding: Theme.spacingS + } + + StyledText { + text: I18n.tr("Overflow") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + leftPadding: Theme.spacingS + } + + Column { + width: parent.width + spacing: Theme.spacingXS + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Max Pinned Apps") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + width: 120 + } + + Row { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + DankActionButton { + buttonSize: 24 + iconName: "remove" + iconSize: 14 + iconColor: Theme.outline + onClicked: { + var current = appsDockContextMenu.currentWidgetData?.barMaxVisibleApps ?? SettingsData.barMaxVisibleApps; + var newVal = Math.max(0, current - 1); + root.overflowSettingChanged(appsDockContextMenu.sectionId, appsDockContextMenu.widgetIndex, "barMaxVisibleApps", newVal); + } + } + + StyledText { + text: { + var val = appsDockContextMenu.currentWidgetData?.barMaxVisibleApps ?? SettingsData.barMaxVisibleApps; + return val === 0 ? I18n.tr("All") : val; + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignHCenter + width: 30 + } + + DankActionButton { + buttonSize: 24 + iconName: "add" + iconSize: 14 + iconColor: Theme.outline + onClicked: { + var current = appsDockContextMenu.currentWidgetData?.barMaxVisibleApps ?? SettingsData.barMaxVisibleApps; + var newVal = current + 1; + root.overflowSettingChanged(appsDockContextMenu.sectionId, appsDockContextMenu.widgetIndex, "barMaxVisibleApps", newVal); + } + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Max Running Apps") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + width: 120 + } + + Row { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + DankActionButton { + buttonSize: 24 + iconName: "remove" + iconSize: 14 + iconColor: Theme.outline + onClicked: { + var current = appsDockContextMenu.currentWidgetData?.barMaxVisibleRunningApps ?? SettingsData.barMaxVisibleRunningApps; + var newVal = Math.max(0, current - 1); + root.overflowSettingChanged(appsDockContextMenu.sectionId, appsDockContextMenu.widgetIndex, "barMaxVisibleRunningApps", newVal); + } + } + + StyledText { + text: { + var val = appsDockContextMenu.currentWidgetData?.barMaxVisibleRunningApps ?? SettingsData.barMaxVisibleRunningApps; + return val === 0 ? I18n.tr("All") : val; + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignHCenter + width: 30 + } + + DankActionButton { + buttonSize: 24 + iconName: "add" + iconSize: 14 + iconColor: Theme.outline + onClicked: { + var current = appsDockContextMenu.currentWidgetData?.barMaxVisibleRunningApps ?? SettingsData.barMaxVisibleRunningApps; + var newVal = current + 1; + root.overflowSettingChanged(appsDockContextMenu.sectionId, appsDockContextMenu.widgetIndex, "barMaxVisibleRunningApps", newVal); + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: badgeToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "notifications" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Show Badge") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: badgeToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: appsDockContextMenu.currentWidgetData?.barShowOverflowBadge ?? SettingsData.barShowOverflowBadge + onToggled: { + root.overflowSettingChanged(appsDockContextMenu.sectionId, appsDockContextMenu.widgetIndex, "barShowOverflowBadge", toggled); + } + } + + MouseArea { + id: badgeToggleArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + badgeToggle.checked = !badgeToggle.checked; + root.overflowSettingChanged(appsDockContextMenu.sectionId, appsDockContextMenu.widgetIndex, "barShowOverflowBadge", badgeToggle.checked); + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + StyledText { + text: I18n.tr("Visual Effects") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + leftPadding: Theme.spacingS + topPadding: Theme.spacingXS + } + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: hideIndicatorsArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "visibility_off" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Hide Indicators") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: hideIndicatorsToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: SettingsData.appsDockHideIndicators + onToggled: { + SettingsData.set("appsDockHideIndicators", toggled); + } + } + + MouseArea { + id: hideIndicatorsArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + hideIndicatorsToggle.checked = !hideIndicatorsToggle.checked; + SettingsData.set("appsDockHideIndicators", hideIndicatorsToggle.checked); + } + } + } + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: colorizeActiveArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "palette" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Colorize Active") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: colorizeActiveToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: SettingsData.appsDockColorizeActive + onToggled: { + SettingsData.set("appsDockColorizeActive", toggled); + } + } + + MouseArea { + id: colorizeActiveArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + colorizeActiveToggle.checked = !colorizeActiveToggle.checked; + SettingsData.set("appsDockColorizeActive", colorizeActiveToggle.checked); + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + visible: SettingsData.appsDockColorizeActive + leftPadding: Theme.spacingL + Theme.spacingS + + StyledText { + text: I18n.tr("Active Color") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + width: 90 + } + + DankButtonGroup { + anchors.verticalCenter: parent.verticalCenter + model: ["pri", "sec", "pc", "err", "ok"] + buttonHeight: 22 + minButtonWidth: 32 + buttonPadding: 4 + checkIconSize: 10 + textSize: 9 + spacing: 1 + currentIndex: { + switch (SettingsData.appsDockActiveColorMode) { + case "secondary": + return 1; + case "primaryContainer": + return 2; + case "error": + return 3; + case "success": + return 4; + default: + return 0; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + const modes = ["primary", "secondary", "primaryContainer", "error", "success"]; + SettingsData.set("appsDockActiveColorMode", modes[index]); + } + } + } + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: enlargeOnHoverArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "zoom_in" + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Enlarge on Hover") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: enlargeOnHoverToggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + checked: SettingsData.appsDockEnlargeOnHover + onToggled: { + SettingsData.set("appsDockEnlargeOnHover", toggled); + } + } + + MouseArea { + id: enlargeOnHoverArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + enlargeOnHoverToggle.checked = !enlargeOnHoverToggle.checked; + SettingsData.set("appsDockEnlargeOnHover", enlargeOnHoverToggle.checked); + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + visible: SettingsData.appsDockEnlargeOnHover + + StyledText { + text: I18n.tr("Enlargement %") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + width: 120 + } + + Row { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + DankActionButton { + buttonSize: 24 + iconName: "remove" + iconSize: 14 + iconColor: Theme.outline + onClicked: { + var current = SettingsData.appsDockEnlargePercentage; + var newVal = Math.max(100, current - 5); + SettingsData.set("appsDockEnlargePercentage", newVal); + } + } + + StyledText { + text: SettingsData.appsDockEnlargePercentage + "%" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignHCenter + width: 50 + } + + DankActionButton { + buttonSize: 24 + iconName: "add" + iconSize: 14 + iconColor: Theme.outline + onClicked: { + var current = SettingsData.appsDockEnlargePercentage; + var newVal = Math.min(150, current + 5); + SettingsData.set("appsDockEnlargePercentage", newVal); + } + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Icon Size %") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + width: 120 + } + + Row { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + DankActionButton { + buttonSize: 24 + iconName: "remove" + iconSize: 14 + iconColor: Theme.outline + onClicked: { + var current = SettingsData.appsDockIconSizePercentage; + var newVal = Math.max(50, current - 5); + SettingsData.set("appsDockIconSizePercentage", newVal); + } + } + + StyledText { + text: SettingsData.appsDockIconSizePercentage + "%" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignHCenter + width: 50 + } + + DankActionButton { + buttonSize: 24 + iconName: "add" + iconSize: 14 + iconColor: Theme.outline + onClicked: { + var current = SettingsData.appsDockIconSizePercentage; + var newVal = Math.min(200, current + 5); + SettingsData.set("appsDockIconSizePercentage", newVal); + } + } + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WindowRulesTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WindowRulesTab.qml new file mode 100644 index 0000000..5e1310e --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WindowRulesTab.qml @@ -0,0 +1,685 @@ +pragma ComponentBehavior: Bound + +import QtCore +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property var parentModal: null + property var windowRulesIncludeStatus: ({ + "exists": false, + "included": false + }) + property bool checkingInclude: false + property bool fixingInclude: false + property var windowRules: [] + property var activeWindows: getActiveWindows() + + signal rulesChanged + + function getActiveWindows() { + const toplevels = ToplevelManager.toplevels?.values || []; + return toplevels.map(t => ({ + appId: t.appId || "", + title: t.title || "" + })); + } + + Connections { + target: ToplevelManager.toplevels + function onValuesChanged() { + root.activeWindows = root.getActiveWindows(); + } + } + + function getWindowRulesConfigPaths() { + const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)); + switch (CompositorService.compositor) { + case "niri": + return { + "configFile": configDir + "/niri/config.kdl", + "rulesFile": configDir + "/niri/dms/windowrules.kdl", + "grepPattern": 'include.*"dms/windowrules.kdl"', + "includeLine": 'include "dms/windowrules.kdl"' + }; + case "hyprland": + return { + "configFile": configDir + "/hypr/hyprland.conf", + "rulesFile": configDir + "/hypr/dms/windowrules.conf", + "grepPattern": 'source.*dms/windowrules.conf', + "includeLine": "source = ./dms/windowrules.conf" + }; + default: + return null; + } + } + + function loadWindowRules() { + const compositor = CompositorService.compositor; + if (compositor !== "niri" && compositor !== "hyprland") { + windowRules = []; + return; + } + + Proc.runCommand("load-windowrules", ["dms", "config", "windowrules", "list", compositor], (output, exitCode) => { + if (exitCode !== 0) { + windowRules = []; + return; + } + try { + const result = JSON.parse(output.trim()); + const allRules = result.rules || []; + windowRules = allRules.filter(r => (r.source || "").includes("dms/windowrules")); + if (result.dmsStatus) { + windowRulesIncludeStatus = { + "exists": result.dmsStatus.exists, + "included": result.dmsStatus.included + }; + } + } catch (e) { + windowRules = []; + } + }); + } + + function removeRule(ruleId) { + const compositor = CompositorService.compositor; + if (compositor !== "niri" && compositor !== "hyprland") + return; + + Proc.runCommand("remove-windowrule", ["dms", "config", "windowrules", "remove", compositor, ruleId], (output, exitCode) => { + if (exitCode === 0) { + loadWindowRules(); + rulesChanged(); + } + }); + } + + function reorderRules(fromIndex, toIndex) { + if (fromIndex === toIndex) + return; + + const compositor = CompositorService.compositor; + if (compositor !== "niri" && compositor !== "hyprland") + return; + + let ids = windowRules.map(r => r.id); + const [moved] = ids.splice(fromIndex, 1); + ids.splice(toIndex, 0, moved); + + Proc.runCommand("reorder-windowrules", ["dms", "config", "windowrules", "reorder", compositor, JSON.stringify(ids)], (output, exitCode) => { + if (exitCode === 0) { + loadWindowRules(); + rulesChanged(); + } + }); + } + + function checkWindowRulesIncludeStatus() { + const compositor = CompositorService.compositor; + if (compositor !== "niri" && compositor !== "hyprland") { + windowRulesIncludeStatus = { + "exists": false, + "included": false + }; + return; + } + + const filename = (compositor === "niri") ? "windowrules.kdl" : "windowrules.conf"; + checkingInclude = true; + Proc.runCommand("check-windowrules-include", ["dms", "config", "resolve-include", compositor, filename], (output, exitCode) => { + checkingInclude = false; + if (exitCode !== 0) { + windowRulesIncludeStatus = { + "exists": false, + "included": false + }; + return; + } + try { + windowRulesIncludeStatus = JSON.parse(output.trim()); + } catch (e) { + windowRulesIncludeStatus = { + "exists": false, + "included": false + }; + } + }); + } + + function fixWindowRulesInclude() { + const paths = getWindowRulesConfigPaths(); + if (!paths) + return; + fixingInclude = true; + const rulesDir = paths.rulesFile.substring(0, paths.rulesFile.lastIndexOf("/")); + const unixTime = Math.floor(Date.now() / 1000); + const backupFile = paths.configFile + ".backup" + unixTime; + Proc.runCommand("fix-windowrules-include", ["sh", "-c", `cp "${paths.configFile}" "${backupFile}" 2>/dev/null; ` + `mkdir -p "${rulesDir}" && ` + `touch "${paths.rulesFile}" && ` + `if ! grep -v '^[[:space:]]*\\(//\\|#\\)' "${paths.configFile}" 2>/dev/null | grep -q '${paths.grepPattern}'; then ` + `echo '' >> "${paths.configFile}" && ` + `echo '${paths.includeLine}' >> "${paths.configFile}"; fi`], (output, exitCode) => { + fixingInclude = false; + if (exitCode !== 0) + return; + checkWindowRulesIncludeStatus(); + loadWindowRules(); + }); + } + + function openRuleModal(window) { + if (!PopoutService.windowRuleModalLoader) + return; + PopoutService.windowRuleModalLoader.active = true; + if (PopoutService.windowRuleModalLoader.item) { + PopoutService.windowRuleModalLoader.item.onRuleSubmitted.connect(loadWindowRules); + PopoutService.windowRuleModalLoader.item.show(window || null); + } + } + + function editRule(rule) { + if (!PopoutService.windowRuleModalLoader) + return; + PopoutService.windowRuleModalLoader.active = true; + if (PopoutService.windowRuleModalLoader.item) { + PopoutService.windowRuleModalLoader.item.onRuleSubmitted.connect(loadWindowRules); + PopoutService.windowRuleModalLoader.item.showEdit(rule); + } + } + + Component.onCompleted: { + if (CompositorService.isNiri || CompositorService.isHyprland) { + checkWindowRulesIncludeStatus(); + loadWindowRules(); + } + } + + DankFlickable { + id: flickable + anchors.fill: parent + clip: true + contentWidth: width + contentHeight: contentColumn.implicitHeight + + Column { + id: contentColumn + width: flickable.width + spacing: Theme.spacingL + topPadding: Theme.spacingXL + bottomPadding: Theme.spacingXL + + StyledRect { + width: Math.min(650, parent.width - Theme.spacingL * 2) + height: headerSection.implicitHeight + Theme.spacingL * 2 + anchors.horizontalCenter: parent.horizontalCenter + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + id: headerSection + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + RowLayout { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "select_window" + size: Theme.iconSize + color: Theme.primary + Layout.alignment: Qt.AlignVCenter + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Window Rules") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + Layout.fillWidth: true + } + + StyledText { + text: I18n.tr("Define rules for window behavior. Saves to %1").arg(CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.conf") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + DankActionButton { + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + circular: false + iconName: "add" + iconSize: Theme.iconSize + iconColor: Theme.primary + onClicked: root.openRuleModal() + } + } + + RowLayout { + width: parent.width + spacing: Theme.spacingM + visible: root.activeWindows.length > 0 + + StyledText { + text: I18n.tr("Create rule for:") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + Layout.alignment: Qt.AlignVCenter + } + + DankDropdown { + id: windowSelector + Layout.fillWidth: true + dropdownWidth: 400 + compactMode: true + emptyText: I18n.tr("Select a window...") + options: root.activeWindows.map(w => { + const label = w.appId + (w.title ? " - " + w.title : ""); + return label.length > 60 ? label.substring(0, 57) + "..." : label; + }) + onValueChanged: value => { + if (!value) + return; + const index = options.indexOf(value); + if (index < 0 || index >= root.activeWindows.length) + return; + const window = root.activeWindows[index]; + root.openRuleModal(window); + currentValue = ""; + } + } + } + } + } + + StyledRect { + id: warningBox + width: Math.min(650, parent.width - Theme.spacingL * 2) + height: warningSection.implicitHeight + Theme.spacingL * 2 + anchors.horizontalCenter: parent.horizontalCenter + radius: Theme.cornerRadius + + readonly property bool showError: root.windowRulesIncludeStatus.exists && !root.windowRulesIncludeStatus.included + readonly property bool showSetup: !root.windowRulesIncludeStatus.exists && !root.windowRulesIncludeStatus.included + + color: (showError || showSetup) ? Theme.withAlpha(Theme.warning, 0.15) : "transparent" + border.color: (showError || showSetup) ? Theme.withAlpha(Theme.warning, 0.3) : "transparent" + border.width: 1 + visible: (showError || showSetup) && !root.checkingInclude && (CompositorService.isNiri || CompositorService.isHyprland) + + Row { + id: warningSection + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + DankIcon { + name: "warning" + size: Theme.iconSize + color: Theme.warning + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - (fixButton.visible ? fixButton.width + Theme.spacingM : 0) - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: warningBox.showSetup ? I18n.tr("Window Rules Not Configured") : I18n.tr("Window Rules Include Missing") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.warning + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + readonly property string rulesFile: CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.conf" + text: warningBox.showSetup ? I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.").arg(rulesFile) : I18n.tr("%1 exists but is not included. Window rules won't apply.").arg(rulesFile) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + + DankButton { + id: fixButton + visible: warningBox.showError || warningBox.showSetup + text: root.fixingInclude ? I18n.tr("Fixing...") : (warningBox.showSetup ? I18n.tr("Setup") : I18n.tr("Fix Now")) + backgroundColor: Theme.warning + textColor: Theme.background + enabled: !root.fixingInclude + anchors.verticalCenter: parent.verticalCenter + onClicked: root.fixWindowRulesInclude() + } + } + } + + StyledRect { + width: Math.min(650, parent.width - Theme.spacingL * 2) + height: rulesSection.implicitHeight + Theme.spacingL * 2 + anchors.horizontalCenter: parent.horizontalCenter + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + id: rulesSection + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + RowLayout { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "list" + size: Theme.iconSize + color: Theme.primary + Layout.alignment: Qt.AlignVCenter + } + + StyledText { + text: I18n.tr("Rules (%1)").arg(root.windowRules?.length ?? 0) + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + Layout.fillWidth: true + } + } + + Column { + width: parent.width + spacing: Theme.spacingXS + visible: !root.windowRules || root.windowRules.length === 0 + + Item { + width: 1 + height: Theme.spacingM + } + + DankIcon { + name: "select_window" + size: 40 + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + opacity: 0.5 + } + + StyledText { + text: I18n.tr("No window rules configured") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + + Item { + width: 1 + height: Theme.spacingM + } + } + + Column { + id: rulesListColumn + width: parent.width + spacing: Theme.spacingXS + visible: root.windowRules && root.windowRules.length > 0 + + Repeater { + model: ScriptModel { + objectProp: "id" + values: root.windowRules || [] + } + + delegate: Item { + id: ruleDelegateItem + required property var modelData + required property int index + + property bool held: ruleDragArea.pressed + property real originalY: y + + readonly property string ruleIdRef: modelData.id + readonly property var liveRuleData: { + const rules = root.windowRules || []; + return rules.find(r => r.id === ruleIdRef) ?? modelData; + } + readonly property string displayName: { + const name = liveRuleData.name || ""; + if (name) + return name; + const m = liveRuleData.matchCriteria || {}; + return m.appId || m.title || I18n.tr("Unnamed Rule"); + } + + width: rulesListColumn.width + height: ruleCard.height + z: held ? 2 : 1 + + Rectangle { + id: ruleCard + width: parent.width + height: ruleContent.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: ruleDelegateItem.liveRuleData.enabled !== false ? Theme.surfaceContainer : Theme.withAlpha(Theme.surfaceContainer, 0.4) + + RowLayout { + id: ruleContent + anchors.fill: parent + anchors.margins: Theme.spacingM + anchors.leftMargin: 28 + spacing: Theme.spacingM + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + StyledText { + text: ruleDelegateItem.displayName + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: ruleDelegateItem.liveRuleData.enabled !== false ? Theme.surfaceText : Theme.surfaceVariantText + elide: Text.ElideRight + Layout.fillWidth: true + } + + StyledText { + text: { + const m = ruleDelegateItem.liveRuleData.matchCriteria || {}; + let parts = []; + if (m.appId) + parts.push(m.appId); + if (m.title) + parts.push("title: " + m.title); + return parts.length > 0 ? parts.join(" · ") : I18n.tr("No match criteria"); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideRight + Layout.fillWidth: true + } + + Flow { + Layout.fillWidth: true + Layout.topMargin: 4 + spacing: Theme.spacingXS + visible: { + const a = ruleDelegateItem.liveRuleData.actions || {}; + return Object.keys(a).some(k => a[k] !== undefined && a[k] !== null && a[k] !== ""); + } + + Repeater { + model: { + const a = ruleDelegateItem.liveRuleData.actions || {}; + const labels = { + "opacity": I18n.tr("Opacity"), + "openFloating": I18n.tr("Float"), + "openMaximized": I18n.tr("Maximize"), + "openMaximizedToEdges": I18n.tr("Max Edges"), + "openFullscreen": I18n.tr("Fullscreen"), + "openFocused": I18n.tr("Focus"), + "openOnOutput": I18n.tr("Output"), + "openOnWorkspace": I18n.tr("Workspace"), + "defaultColumnWidth": I18n.tr("Width"), + "defaultWindowHeight": I18n.tr("Height"), + "variableRefreshRate": I18n.tr("VRR"), + "blockOutFrom": I18n.tr("Block Out"), + "defaultColumnDisplay": I18n.tr("Display"), + "scrollFactor": I18n.tr("Scroll"), + "cornerRadius": I18n.tr("Radius"), + "clipToGeometry": I18n.tr("Clip"), + "tiledState": I18n.tr("Tiled"), + "minWidth": I18n.tr("Min W"), + "maxWidth": I18n.tr("Max W"), + "minHeight": I18n.tr("Min H"), + "maxHeight": I18n.tr("Max H"), + "tile": I18n.tr("Tile"), + "nofocus": I18n.tr("No Focus"), + "noborder": I18n.tr("No Border"), + "noshadow": I18n.tr("No Shadow"), + "nodim": I18n.tr("No Dim"), + "noblur": I18n.tr("No Blur"), + "noanim": I18n.tr("No Anim"), + "norounding": I18n.tr("No Round"), + "pin": I18n.tr("Pin"), + "opaque": I18n.tr("Opaque"), + "size": I18n.tr("Size"), + "move": I18n.tr("Move"), + "monitor": I18n.tr("Monitor"), + "workspace": I18n.tr("Workspace") + }; + return Object.keys(a).filter(k => a[k] !== undefined && a[k] !== null && a[k] !== "").map(k => { + const val = a[k]; + if (typeof val === "boolean") + return labels[k] || k; + return (labels[k] || k) + ": " + val; + }); + } + + delegate: Rectangle { + required property string modelData + width: chipText.implicitWidth + Theme.spacingS * 2 + height: 20 + radius: 10 + color: Theme.withAlpha(Theme.primary, 0.15) + + StyledText { + id: chipText + anchors.centerIn: parent + text: modelData + font.pixelSize: Theme.fontSizeSmall - 2 + color: Theme.primary + } + } + } + } + } + + RowLayout { + Layout.alignment: Qt.AlignVCenter + spacing: 2 + + DankActionButton { + buttonSize: 28 + iconName: "edit" + iconSize: 16 + backgroundColor: "transparent" + iconColor: Theme.surfaceVariantText + onClicked: root.editRule(ruleDelegateItem.liveRuleData) + } + + DankActionButton { + id: deleteBtn + buttonSize: 28 + iconName: "delete" + iconSize: 16 + backgroundColor: "transparent" + iconColor: deleteArea.containsMouse ? Theme.error : Theme.surfaceVariantText + + MouseArea { + id: deleteArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.removeRule(ruleDelegateItem.ruleIdRef) + } + } + } + } + } + + MouseArea { + id: ruleDragArea + anchors.left: parent.left + anchors.top: parent.top + width: 40 + height: ruleCard.height + hoverEnabled: true + cursorShape: Qt.SizeVerCursor + drag.target: ruleDelegateItem.held ? ruleDelegateItem : undefined + drag.axis: Drag.YAxis + preventStealing: true + + onPressed: { + ruleDelegateItem.z = 2; + ruleDelegateItem.originalY = ruleDelegateItem.y; + } + onReleased: { + ruleDelegateItem.z = 1; + if (!drag.active) { + ruleDelegateItem.y = ruleDelegateItem.originalY; + return; + } + const spacing = Theme.spacingXS; + const itemH = ruleDelegateItem.height + spacing; + var newIndex = Math.round(ruleDelegateItem.y / itemH); + newIndex = Math.max(0, Math.min(newIndex, (root.windowRules?.length ?? 1) - 1)); + if (newIndex !== ruleDelegateItem.index) + root.reorderRules(ruleDelegateItem.index, newIndex); + ruleDelegateItem.y = ruleDelegateItem.originalY; + } + } + + DankIcon { + x: Theme.spacingM - 2 + y: (ruleCard.height / 2) - (size / 2) + name: "drag_indicator" + size: 18 + color: Theme.outline + opacity: ruleDragArea.containsMouse || ruleDragArea.pressed ? 1 : 0.5 + } + + Behavior on y { + enabled: !ruleDragArea.pressed && !ruleDragArea.drag.active + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WorkspacesTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WorkspacesTab.qml new file mode 100644 index 0000000..99955fd --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/WorkspacesTab.qml @@ -0,0 +1,526 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + SettingsCard { + width: parent.width + iconName: "view_module" + title: I18n.tr("Workspace Settings") + settingKey: "workspaceSettings" + + SettingsToggleRow { + settingKey: "showWorkspaceIndex" + tags: ["workspace", "index", "numbers", "labels"] + text: I18n.tr("Workspace Index Numbers") + description: I18n.tr("Show workspace index numbers in the top bar workspace switcher") + checked: SettingsData.showWorkspaceIndex + onToggled: checked => SettingsData.set("showWorkspaceIndex", checked) + } + + SettingsToggleRow { + settingKey: "showWorkspaceName" + tags: ["workspace", "name", "labels"] + text: I18n.tr("Workspace Names") + description: I18n.tr("Show workspace name on horizontal bars, and first letter on vertical bars") + checked: SettingsData.showWorkspaceName + onToggled: checked => SettingsData.set("showWorkspaceName", checked) + } + + SettingsToggleRow { + settingKey: "showWorkspacePadding" + tags: ["workspace", "padding", "minimum"] + text: I18n.tr("Workspace Padding") + description: I18n.tr("Always show a minimum of 3 workspaces, even if fewer are available") + checked: SettingsData.showWorkspacePadding + onToggled: checked => SettingsData.set("showWorkspacePadding", checked) + } + + SettingsToggleRow { + settingKey: "showWorkspaceApps" + tags: ["workspace", "apps", "icons", "applications"] + text: I18n.tr("Show Workspace Apps") + description: I18n.tr("Display application icons in workspace indicators") + checked: SettingsData.showWorkspaceApps + visible: CompositorService.isNiri || CompositorService.isHyprland + onToggled: checked => SettingsData.set("showWorkspaceApps", checked) + } + + Item { + width: parent.width + height: maxAppsColumn.height + visible: SettingsData.showWorkspaceApps + opacity: visible ? 1 : 0 + + Column { + id: maxAppsColumn + x: Theme.spacingL + width: 120 + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Max apps to show") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + horizontalAlignment: Text.AlignLeft + } + + DankTextField { + width: 100 + height: 28 + placeholderText: "3" + text: SettingsData.maxWorkspaceIcons + maximumLength: 2 + font.pixelSize: Theme.fontSizeSmall + topPadding: Theme.spacingXS + bottomPadding: Theme.spacingXS + onEditingFinished: SettingsData.set("maxWorkspaceIcons", parseInt(text, 10)) + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } + + SettingsSliderRow { + visible: SettingsData.showWorkspaceApps + text: I18n.tr("Icon Size") + value: SettingsData.workspaceAppIconSizeOffset + minimum: 0 + maximum: 10 + unit: "px" + defaultValue: 0 + onSliderValueChanged: newValue => SettingsData.set("workspaceAppIconSizeOffset", newValue) + } + + SettingsToggleRow { + settingKey: "groupWorkspaceApps" + tags: ["workspace", "apps", "icons", "group", "grouped", "collapse"] + text: I18n.tr("Group Workspace Apps") + description: I18n.tr("Group repeated application icons in unfocused workspaces") + checked: SettingsData.groupWorkspaceApps + visible: SettingsData.showWorkspaceApps + onToggled: checked => SettingsData.set("groupWorkspaceApps", checked) + } + + SettingsToggleRow { + settingKey: "workspaceActiveAppHighlightEnabled" + tags: ["workspace", "apps", "icons", "highlight", "active", "focused"] + text: I18n.tr("Highlight Active Workspace App") + description: I18n.tr("Highlight the currently focused app inside workspace indicators") + checked: SettingsData.workspaceActiveAppHighlightEnabled + visible: SettingsData.showWorkspaceApps + onToggled: checked => SettingsData.set("workspaceActiveAppHighlightEnabled", checked) + } + + SettingsToggleRow { + settingKey: "workspaceFollowFocus" + tags: ["workspace", "focus", "follow", "monitor"] + text: I18n.tr("Follow Monitor Focus") + description: I18n.tr("Show workspaces of the currently focused monitor") + checked: SettingsData.workspaceFollowFocus + visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle + onToggled: checked => SettingsData.set("workspaceFollowFocus", checked) + } + + SettingsToggleRow { + settingKey: "showOccupiedWorkspacesOnly" + tags: ["workspace", "occupied", "active", "windows"] + text: I18n.tr("Show Occupied Workspaces Only") + description: I18n.tr("Display only workspaces that contain windows") + checked: SettingsData.showOccupiedWorkspacesOnly + visible: CompositorService.isNiri || CompositorService.isHyprland + onToggled: checked => SettingsData.set("showOccupiedWorkspacesOnly", checked) + } + + SettingsToggleRow { + settingKey: "reverseScrolling" + tags: ["workspace", "scroll", "scrolling", "reverse", "direction"] + text: I18n.tr("Reverse Scrolling Direction") + description: I18n.tr("Reverse workspace switch direction when scrolling over the bar") + checked: SettingsData.reverseScrolling + visible: CompositorService.isNiri || CompositorService.isHyprland + onToggled: checked => SettingsData.set("reverseScrolling", checked) + } + + SettingsToggleRow { + settingKey: "workspaceDragReorder" + tags: ["workspace", "drag", "reorder", "sort", "move"] + text: I18n.tr("Drag to Reorder") + description: I18n.tr("Drag workspace indicators to reorder them") + checked: SettingsData.workspaceDragReorder + visible: CompositorService.isNiri + onToggled: checked => SettingsData.set("workspaceDragReorder", checked) + } + + SettingsToggleRow { + settingKey: "dwlShowAllTags" + tags: ["dwl", "tags", "workspace"] + text: I18n.tr("Show All Tags") + description: I18n.tr("Show all 9 tags instead of only occupied tags (DWL only)") + checked: SettingsData.dwlShowAllTags + visible: CompositorService.isDwl + onToggled: checked => SettingsData.set("dwlShowAllTags", checked) + } + } + + SettingsCard { + width: parent.width + iconName: "palette" + title: I18n.tr("Workspace Appearance") + settingKey: "workspaceAppearance" + + SettingsButtonGroupRow { + text: I18n.tr("Focused Color") + model: ["pri", "s", "sc", "sch", "none"] + buttonHeight: 22 + minButtonWidth: 36 + buttonPadding: Theme.spacingS + checkIconSize: Theme.iconSizeSmall - 2 + textSize: Theme.fontSizeSmall - 1 + spacing: 1 + currentIndex: { + switch (SettingsData.workspaceColorMode) { + case "s": + return 1; + case "sc": + return 2; + case "sch": + return 3; + case "none": + return 4; + default: + return 0; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + const modes = ["default", "s", "sc", "sch", "none"]; + SettingsData.set("workspaceColorMode", modes[index]); + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsButtonGroupRow { + text: I18n.tr("Occupied Color") + model: ["none", "sec", "s", "sc", "sch", "schh"] + visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl + buttonHeight: 22 + minButtonWidth: 36 + buttonPadding: Theme.spacingS + checkIconSize: Theme.iconSizeSmall - 2 + textSize: Theme.fontSizeSmall - 1 + spacing: 1 + currentIndex: { + switch (SettingsData.workspaceOccupiedColorMode) { + case "sec": + return 1; + case "s": + return 2; + case "sc": + return 3; + case "sch": + return 4; + case "schh": + return 5; + default: + return 0; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + const modes = ["none", "sec", "s", "sc", "sch", "schh"]; + SettingsData.set("workspaceOccupiedColorMode", modes[index]); + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl + } + + SettingsButtonGroupRow { + text: I18n.tr("Unfocused Color") + model: ["def", "s", "sc", "sch"] + buttonHeight: 22 + minButtonWidth: 36 + buttonPadding: Theme.spacingS + checkIconSize: Theme.iconSizeSmall - 2 + textSize: Theme.fontSizeSmall - 1 + spacing: 1 + currentIndex: { + switch (SettingsData.workspaceUnfocusedColorMode) { + case "s": + return 1; + case "sc": + return 2; + case "sch": + return 3; + default: + return 0; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + const modes = ["default", "s", "sc", "sch"]; + SettingsData.set("workspaceUnfocusedColorMode", modes[index]); + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle + } + + SettingsButtonGroupRow { + text: I18n.tr("Urgent Color") + visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle + model: ["err", "pri", "sec", "s", "sc"] + buttonHeight: 22 + minButtonWidth: 36 + buttonPadding: Theme.spacingS + checkIconSize: Theme.iconSizeSmall - 2 + textSize: Theme.fontSizeSmall - 1 + spacing: 1 + currentIndex: { + switch (SettingsData.workspaceUrgentColorMode) { + case "primary": + return 1; + case "secondary": + return 2; + case "s": + return 3; + case "sc": + return 4; + default: + return 0; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + const modes = ["default", "primary", "secondary", "s", "sc"]; + SettingsData.set("workspaceUrgentColorMode", modes[index]); + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsToggleRow { + settingKey: "workspaceFocusedBorderEnabled" + tags: ["workspace", "border", "outline", "focused", "ring"] + text: I18n.tr("Focused Border") + description: I18n.tr("Show an outline ring around the focused workspace indicator") + checked: SettingsData.workspaceFocusedBorderEnabled + onToggled: checked => SettingsData.set("workspaceFocusedBorderEnabled", checked) + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: SettingsData.workspaceFocusedBorderEnabled + leftPadding: Theme.spacingM + + SettingsButtonGroupRow { + width: parent.width - parent.leftPadding + text: I18n.tr("Border Color") + model: [I18n.tr("Surface"), I18n.tr("Secondary"), I18n.tr("Primary")] + currentIndex: { + switch (SettingsData.workspaceFocusedBorderColor) { + case "surfaceText": + return 0; + case "secondary": + return 1; + case "primary": + return 2; + default: + return 2; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + let newColor = "primary"; + switch (index) { + case 0: + newColor = "surfaceText"; + break; + case 1: + newColor = "secondary"; + break; + case 2: + newColor = "primary"; + break; + } + SettingsData.set("workspaceFocusedBorderColor", newColor); + } + } + + SettingsSliderRow { + width: parent.width - parent.leftPadding + text: I18n.tr("Thickness") + value: SettingsData.workspaceFocusedBorderThickness + minimum: 1 + maximum: 6 + unit: "px" + defaultValue: 2 + onSliderValueChanged: newValue => SettingsData.set("workspaceFocusedBorderThickness", newValue) + } + } + } + + SettingsCard { + width: parent.width + iconName: "label" + title: I18n.tr("Named Workspace Icons") + settingKey: "workspaceIcons" + visible: SettingsData.hasNamedWorkspaces() + + StyledText { + width: parent.width + text: I18n.tr("Configure icons for named workspaces. Icons take priority over numbers when both are enabled.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + wrapMode: Text.WordWrap + } + + Repeater { + model: SettingsData.getNamedWorkspaces() + + Rectangle { + width: parent.width + height: workspaceIconRow.implicitHeight + Theme.spacingM + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5) + border.width: 0 + + Row { + id: workspaceIconRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingM + + StyledText { + text: "\"" + modelData + "\"" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + width: 150 + elide: Text.ElideRight + } + + DankIconPicker { + id: iconPicker + anchors.verticalCenter: parent.verticalCenter + + Component.onCompleted: { + var iconData = SettingsData.getWorkspaceNameIcon(modelData); + if (iconData) { + setIcon(iconData.value, iconData.type); + } + } + + onIconSelected: (iconName, iconType) => { + SettingsData.setWorkspaceNameIcon(modelData, { + "type": iconType, + "value": iconName + }); + setIcon(iconName, iconType); + } + + Connections { + target: SettingsData + function onWorkspaceIconsUpdated() { + var iconData = SettingsData.getWorkspaceNameIcon(modelData); + if (iconData) { + iconPicker.setIcon(iconData.value, iconData.type); + } else { + iconPicker.setIcon("", "icon"); + } + } + } + } + + Rectangle { + width: 28 + height: 28 + radius: Theme.cornerRadius + color: clearMouseArea.containsMouse ? Theme.errorHover : Theme.surfaceContainer + border.width: 0 + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: "close" + size: 16 + color: clearMouseArea.containsMouse ? Theme.error : Theme.outline + anchors.centerIn: parent + } + + MouseArea { + id: clearMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: SettingsData.removeWorkspaceNameIcon(modelData) + } + } + + Item { + width: parent.width - 150 - 240 - 28 - Theme.spacingM * 4 + height: 1 + } + } + } + } + } + } + } +} |