diff --git a/core/cmd/dms/commands_doctor.go b/core/cmd/dms/commands_doctor.go index aa63185a..e6e01a99 100644 --- a/core/cmd/dms/commands_doctor.go +++ b/core/cmd/dms/commands_doctor.go @@ -676,7 +676,7 @@ func checkOptionalDependencies() []checkResult { }{ {"matugen", "matugen", "", "Dynamic theming", true}, {"dgop", "dgop", "", "System monitoring", true}, - {"cava", "cava", "", "Audio waveform", false}, + {"cava", "cava", "", "Audio visualizer", true}, {"khal", "khal", "", "Calendar events", false}, {"Network", "nmcli", "iwctl", "Network management", false}, {"danksearch", "dsearch", "", "File search", false}, diff --git a/quickshell/Common/SessionData.qml b/quickshell/Common/SessionData.qml index 7fe54ad0..e59432e4 100644 --- a/quickshell/Common/SessionData.qml +++ b/quickshell/Common/SessionData.qml @@ -336,7 +336,7 @@ Singleton { if (typeof SettingsData !== "undefined" && SettingsData.theme) { Theme.switchTheme(SettingsData.theme); } else { - Theme.switchTheme("blue"); + Theme.switchTheme("purple"); } } } diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 46340931..10effe51 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -67,7 +67,7 @@ Singleton { property alias dankBarCenterWidgetsModel: centerWidgetsModel property alias dankBarRightWidgetsModel: rightWidgetsModel - property string currentThemeName: "blue" + property string currentThemeName: "purple" property string currentThemeCategory: "generic" property string customThemeFile: "" property var registryThemeVariants: ({}) diff --git a/quickshell/Common/Theme.qml b/quickshell/Common/Theme.qml index 545290e1..1fd0b0e8 100644 --- a/quickshell/Common/Theme.qml +++ b/quickshell/Common/Theme.qml @@ -30,7 +30,7 @@ Singleton { return useAuto ? Math.max(4, spacing) : manualValue; } - property string currentTheme: "blue" + property string currentTheme: "purple" property string currentThemeCategory: "generic" property bool isLightMode: typeof SessionData !== "undefined" ? SessionData.isLightMode : false property bool colorsFileLoadFailed: false @@ -196,7 +196,7 @@ Singleton { readonly property var currentThemeData: { if (currentTheme === "custom") { - return customThemeData || StockThemes.getThemeByName("blue", isLightMode); + return customThemeData || StockThemes.getThemeByName("purple", isLightMode); } else if (currentTheme === dynamic) { return { "primary": getMatugenColor("primary", "#42a5f5"), diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index cc949b65..fe696ee0 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -6,7 +6,7 @@ function percentToUnit(v) { } var SPEC = { - currentThemeName: { def: "blue", onChange: "applyStoredTheme" }, + currentThemeName: { def: "purple", onChange: "applyStoredTheme" }, currentThemeCategory: { def: "generic" }, customThemeFile: { def: "" }, registryThemeVariants: { def: {} }, diff --git a/quickshell/DMSShell.qml b/quickshell/DMSShell.qml index c5ea0296..fca98531 100644 --- a/quickshell/DMSShell.qml +++ b/quickshell/DMSShell.qml @@ -3,6 +3,7 @@ import Quickshell import qs.Common import qs.Modals import qs.Modals.Clipboard +import qs.Modals.Greeter import qs.Modals.Settings import qs.Modals.Spotlight import qs.Modules @@ -816,4 +817,20 @@ Item { id: niriOverviewOverlay } } + + Loader { + id: greeterLoader + active: false + sourceComponent: GreeterModal { + onGreeterCompleted: greeterLoader.active = false + Component.onCompleted: show() + } + + Connections { + target: FirstLaunchService + function onGreeterRequested() { + greeterLoader.active = true; + } + } + } } diff --git a/quickshell/Modals/FileBrowser/FileBrowserModal.qml b/quickshell/Modals/FileBrowser/FileBrowserModal.qml index 441cdcaa..6d80e5b6 100644 --- a/quickshell/Modals/FileBrowser/FileBrowserModal.qml +++ b/quickshell/Modals/FileBrowser/FileBrowserModal.qml @@ -41,14 +41,14 @@ FloatingWindow { onVisibleChanged: { if (visible) { - if (parentModal) { + if (parentModal && "shouldHaveFocus" in parentModal) { parentModal.shouldHaveFocus = false; parentModal.allowFocusOverride = true; } content.reset(); Qt.callLater(() => content.forceActiveFocus()); } else { - if (parentModal) { + if (parentModal && "allowFocusOverride" in parentModal) { parentModal.allowFocusOverride = false; parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible); } diff --git a/quickshell/Modals/Greeter/GreeterCompletePage.qml b/quickshell/Modals/Greeter/GreeterCompletePage.qml new file mode 100644 index 00000000..8a5ca005 --- /dev/null +++ b/quickshell/Modals/Greeter/GreeterCompletePage.qml @@ -0,0 +1,492 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property var greeterRoot: parent ? parent.greeterRoot : null + + readonly property real headerIconContainerSize: Math.round(Theme.iconSize * 2) + readonly property real sectionIconSize: Theme.iconSizeSmall + 2 + readonly property real keybindRowHeight: Math.round(Theme.fontSizeMedium * 2) + readonly property real keyBadgeHeight: Math.round(Theme.fontSizeSmall * 1.83) + + readonly property var featureNames: ({ + "spotlight": "App Launcher", + "clipboard": "Clipboard", + "processlist": "Task Manager", + "settings": "Settings", + "notifications": "Notifications", + "notepad": "Notepad", + "hotkeys": "Keybinds", + "lock": "Lock Screen", + "dankdash": "Dashboard" + }) + + function getFeatureDesc(action) { + const match = action.match(/dms\s+ipc\s+call\s+(\w+)/); + if (match && featureNames[match[1]]) + return featureNames[match[1]]; + return null; + } + + readonly property var dmsKeybinds: { + if (!greeterRoot || !greeterRoot.cheatsheetLoaded || !greeterRoot.cheatsheetData || !greeterRoot.cheatsheetData.binds) + return []; + const seen = new Set(); + const binds = []; + const allBinds = greeterRoot.cheatsheetData.binds; + for (const category in allBinds) { + const categoryBinds = allBinds[category]; + for (let i = 0; i < categoryBinds.length; i++) { + const bind = categoryBinds[i]; + if (!bind.key || !bind.action) + continue; + if (!bind.action.includes("dms")) + continue; + if (!(bind.action.includes("spawn") || bind.action.includes("exec"))) + continue; + const feature = getFeatureDesc(bind.action); + if (!feature) + continue; + if (seen.has(feature)) + continue; + seen.add(feature); + binds.push({ + key: bind.key, + desc: feature + }); + } + } + return binds; + } + + readonly property bool hasKeybinds: dmsKeybinds.length > 0 + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingL * 2 + contentWidth: width + + Column { + id: mainColumn + anchors.horizontalCenter: parent.horizontalCenter + width: Math.min(640, parent.width - Theme.spacingXL * 2) + topPadding: Theme.spacingL + spacing: Theme.spacingL + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingM + + Rectangle { + width: root.headerIconContainerSize + height: root.headerIconContainerSize + radius: Math.round(root.headerIconContainerSize * 0.29) + color: Theme.withAlpha(Theme.success, 0.15) + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: "check_circle" + size: Theme.iconSize + 4 + color: Theme.success + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: I18n.tr("You're All Set!", "greeter completion page title") + font.pixelSize: Theme.fontSizeXLarge + font.weight: Font.Bold + color: Theme.surfaceText + } + + StyledText { + text: I18n.tr("DankMaterialShell is ready to use", "greeter completion page subtitle") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: root.hasKeybinds + + Row { + width: parent.width + spacing: Theme.spacingS + + DankIcon { + name: "keyboard" + size: root.sectionIconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("DMS Shortcuts", "greeter keybinds section header") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Rectangle { + id: keybindsRect + width: parent.width + height: keybindsGrid.height + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + readonly property bool useTwoColumns: width > 500 + readonly property int columnCount: useTwoColumns ? 2 : 1 + readonly property real itemWidth: useTwoColumns ? (width - Theme.spacingM * 3) / 2 : width - Theme.spacingM * 2 + property real maxKeyWidth: 0 + + Grid { + id: keybindsGrid + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingM + columns: keybindsRect.columnCount + rowSpacing: Theme.spacingS + columnSpacing: Theme.spacingM + + Repeater { + model: root.dmsKeybinds + + Row { + width: keybindsRect.itemWidth + height: root.keybindRowHeight + spacing: Theme.spacingS + + Item { + width: keybindsRect.maxKeyWidth + height: parent.height + + Row { + id: keysRow + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + property real naturalWidth: { + let w = 0; + for (let i = 0; i < children.length; i++) { + if (children[i].visible) + w += children[i].width + (i > 0 ? Theme.spacingXS : 0); + } + return w; + } + + Component.onCompleted: { + Qt.callLater(() => { + if (naturalWidth > keybindsRect.maxKeyWidth) + keybindsRect.maxKeyWidth = naturalWidth; + }); + } + + Repeater { + model: (modelData.key || "").split("+") + + Rectangle { + width: singleKeyText.implicitWidth + Theme.spacingM + height: root.keyBadgeHeight + radius: Theme.spacingXS + color: Theme.surfaceContainerHighest + border.width: 1 + border.color: Theme.outline + + StyledText { + id: singleKeyText + anchors.centerIn: parent + color: Theme.secondary + text: modelData + font.pixelSize: Theme.fontSizeSmall - 1 + font.weight: Font.Medium + isMonospace: true + } + } + } + } + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - keybindsRect.maxKeyWidth - Theme.spacingS + text: modelData.desc || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + elide: Text.ElideRight + } + } + } + } + } + } + + Rectangle { + width: parent.width + height: noKeybindsColumn.height + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + visible: !root.hasKeybinds + + Column { + id: noKeybindsColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + Row { + spacing: Theme.spacingS + + DankIcon { + name: "keyboard" + size: root.sectionIconSize + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("No DMS shortcuts configured", "greeter no keybinds message") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + + Rectangle { + width: parent.width + height: Math.round(Theme.fontSizeMedium * 2.85) + radius: Theme.cornerRadius + color: Theme.surfaceContainerHighest + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: Theme.primary + opacity: noKeybindsLinkMouse.containsMouse ? 0.12 : 0 + } + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "menu_book" + size: root.sectionIconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Configure Keybinds", "greeter configure keybinds link") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + DankIcon { + name: "open_in_new" + size: Theme.iconSizeSmall - 2 + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: noKeybindsLinkMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + let url = "https://danklinux.com/docs/dankmaterialshell/keybinds-ipc"; + if (CompositorService.isNiri) + url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings"; + else if (CompositorService.isHyprland) + url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-1"; + else if (CompositorService.isDwl) + url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-2"; + Qt.openUrlExternally(url); + } + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outlineMedium + opacity: 0.3 + visible: root.hasKeybinds + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Row { + width: parent.width + spacing: Theme.spacingS + + DankIcon { + name: "settings" + size: root.sectionIconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Configure", "greeter settings section header") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Grid { + width: parent.width + columns: 2 + rowSpacing: Theme.spacingS + columnSpacing: Theme.spacingS + + GreeterSettingsCard { + width: (parent.width - Theme.spacingS) / 2 + iconName: "display_settings" + title: I18n.tr("Displays", "greeter settings link") + description: I18n.tr("Resolution, position, scale", "greeter displays description") + onClicked: PopoutService.openSettingsWithTab("display_config") + } + + GreeterSettingsCard { + width: (parent.width - Theme.spacingS) / 2 + iconName: "wallpaper" + title: I18n.tr("Wallpaper", "greeter settings link") + description: I18n.tr("Background image", "greeter wallpaper description") + onClicked: PopoutService.openSettingsWithTab("wallpaper") + } + + GreeterSettingsCard { + width: (parent.width - Theme.spacingS) / 2 + iconName: "format_paint" + title: I18n.tr("Theme & Colors", "greeter settings link") + description: I18n.tr("Dynamic colors, presets", "greeter theme description") + onClicked: PopoutService.openSettingsWithTab("theme") + } + + GreeterSettingsCard { + width: (parent.width - Theme.spacingS) / 2 + iconName: "notifications" + title: I18n.tr("Notifications", "greeter settings link") + description: I18n.tr("Popup behavior, position", "greeter notifications description") + onClicked: PopoutService.openSettingsWithTab("notifications") + } + + GreeterSettingsCard { + width: (parent.width - Theme.spacingS) / 2 + iconName: "toolbar" + title: I18n.tr("DankBar", "greeter settings link") + description: I18n.tr("Widgets, layout, style", "greeter dankbar description") + onClicked: PopoutService.openSettingsWithTab("dankbar_settings") + } + + GreeterSettingsCard { + width: (parent.width - Theme.spacingS) / 2 + iconName: "keyboard" + title: I18n.tr("Keybinds", "greeter settings link") + description: I18n.tr("niri shortcuts config", "greeter keybinds niri description") + visible: KeybindsService.available + onClicked: PopoutService.openSettingsWithTab("keybinds") + } + + GreeterSettingsCard { + width: (parent.width - Theme.spacingS) / 2 + iconName: "dock_to_bottom" + title: I18n.tr("Dock", "greeter settings link") + description: I18n.tr("Position, pinned apps", "greeter dock description") + visible: !KeybindsService.available + onClicked: PopoutService.openSettingsWithTab("dock") + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outlineMedium + opacity: 0.3 + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Row { + width: parent.width + spacing: Theme.spacingS + + DankIcon { + name: "explore" + size: root.sectionIconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Explore", "greeter explore section header") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + GreeterQuickLink { + width: (parent.width - Theme.spacingS * 2) / 3 + iconName: "menu_book" + title: I18n.tr("Docs", "greeter documentation link") + isExternal: true + onClicked: Qt.openUrlExternally("https://danklinux.com/docs") + } + + GreeterQuickLink { + width: (parent.width - Theme.spacingS * 2) / 3 + iconName: "extension" + title: I18n.tr("Plugins", "greeter plugins link") + isExternal: true + onClicked: Qt.openUrlExternally("https://danklinux.com/plugins") + } + + GreeterQuickLink { + width: (parent.width - Theme.spacingS * 2) / 3 + iconName: "palette" + title: I18n.tr("Themes", "greeter themes link") + isExternal: true + onClicked: Qt.openUrlExternally("https://danklinux.com/plugins?tab=themes") + } + } + } + } + } +} diff --git a/quickshell/Modals/Greeter/GreeterDoctorPage.qml b/quickshell/Modals/Greeter/GreeterDoctorPage.qml new file mode 100644 index 00000000..ef503e04 --- /dev/null +++ b/quickshell/Modals/Greeter/GreeterDoctorPage.qml @@ -0,0 +1,421 @@ +import QtQuick +import Quickshell.Io +import qs.Common +import qs.Widgets + +Item { + id: root + + property bool isRunning: false + property bool hasRun: false + property var doctorResults: null + property int errorCount: 0 + property int warningCount: 0 + property int okCount: 0 + property int infoCount: 0 + property string selectedFilter: "error" + + readonly property real loadingContainerSize: Math.round(Theme.iconSize * 5) + readonly property real pulseRingSize: Math.round(Theme.iconSize * 3.3) + readonly property real centerIconContainerSize: Math.round(Theme.iconSize * 2.67) + readonly property real headerIconContainerSize: Math.round(Theme.iconSize * 2) + + readonly property var filteredResults: { + if (!doctorResults?.results) + return []; + return doctorResults.results.filter(r => r.status === selectedFilter); + } + + function runDoctor() { + hasRun = false; + isRunning = true; + doctorProcess.running = true; + } + + Component.onCompleted: runDoctor() + + Item { + id: loadingView + anchors.fill: parent + visible: root.isRunning + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXL + + Item { + width: root.loadingContainerSize + height: root.loadingContainerSize + anchors.horizontalCenter: parent.horizontalCenter + + Rectangle { + id: pulseRing1 + anchors.centerIn: parent + width: root.pulseRingSize + height: root.pulseRingSize + radius: root.pulseRingSize / 2 + color: "transparent" + border.width: Math.round(Theme.spacingXS * 0.75) + border.color: Theme.primary + opacity: 0 + + SequentialAnimation on opacity { + running: root.isRunning + loops: Animation.Infinite + NumberAnimation { + from: 0.8 + to: 0 + duration: 1500 + easing.type: Easing.OutQuad + } + } + + SequentialAnimation on scale { + running: root.isRunning + loops: Animation.Infinite + NumberAnimation { + from: 0.5 + to: 1.5 + duration: 1500 + easing.type: Easing.OutQuad + } + } + } + + Rectangle { + id: pulseRing2 + anchors.centerIn: parent + width: root.pulseRingSize + height: root.pulseRingSize + radius: root.pulseRingSize / 2 + color: "transparent" + border.width: Math.round(Theme.spacingXS * 0.75) + border.color: Theme.secondary + opacity: 0 + + SequentialAnimation on opacity { + running: root.isRunning + loops: Animation.Infinite + NumberAnimation { + from: 0.8 + to: 0 + duration: 1500 + easing.type: Easing.OutQuad + } + } + + SequentialAnimation on scale { + running: root.isRunning + loops: Animation.Infinite + NumberAnimation { + from: 0.3 + to: 1.3 + duration: 1500 + easing.type: Easing.OutQuad + } + } + } + + Rectangle { + anchors.centerIn: parent + width: root.centerIconContainerSize + height: root.centerIconContainerSize + radius: root.centerIconContainerSize / 2 + color: Theme.primaryContainer + + DankIcon { + anchors.centerIn: parent + name: "vital_signs" + size: Theme.iconSizeLarge + color: Theme.primary + } + + SequentialAnimation on scale { + running: root.isRunning + loops: Animation.Infinite + NumberAnimation { + from: 1 + to: 1.1 + duration: 750 + easing.type: Easing.InOutQuad + } + NumberAnimation { + from: 1.1 + to: 1 + duration: 750 + easing.type: Easing.InOutQuad + } + } + } + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("System Check", "greeter doctor page title") + font.pixelSize: Theme.fontSizeXLarge + font.weight: Font.Bold + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("Analyzing configuration...", "greeter doctor page loading text") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + + Item { + id: resultsView + anchors.fill: parent + visible: root.hasRun && !root.isRunning + opacity: (root.hasRun && !root.isRunning) ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Column { + id: headerSection + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: Theme.spacingL + anchors.leftMargin: Theme.spacingXL + anchors.rightMargin: Theme.spacingXL + spacing: Theme.spacingL + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingM + + Rectangle { + width: root.headerIconContainerSize + height: root.headerIconContainerSize + radius: Math.round(root.headerIconContainerSize * 0.29) + color: root.errorCount > 0 ? Theme.errorContainer : Theme.primaryContainer + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: root.errorCount > 0 ? "warning" : "check_circle" + size: Theme.iconSize + 4 + color: root.errorCount > 0 ? Theme.error : Theme.primary + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: I18n.tr("System Check", "greeter doctor page title") + font.pixelSize: Theme.fontSizeXLarge + font.weight: Font.Bold + color: Theme.surfaceText + } + + StyledText { + text: root.errorCount > 0 ? I18n.tr("%1 issue(s) found", "greeter doctor page error count").arg(root.errorCount) : I18n.tr("All checks passed", "greeter doctor page success") + font.pixelSize: Theme.fontSizeMedium + color: root.errorCount > 0 ? Theme.error : Theme.surfaceVariantText + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + GreeterStatusCard { + width: (parent.width - Theme.spacingS * 3) / 4 + count: root.errorCount + label: I18n.tr("Errors", "greeter doctor page status card") + iconName: "error" + iconColor: Theme.error + bgColor: Theme.errorContainer || Theme.withAlpha(Theme.error, 0.15) + selected: root.selectedFilter === "error" + onClicked: root.selectedFilter = "error" + } + + GreeterStatusCard { + width: (parent.width - Theme.spacingS * 3) / 4 + count: root.warningCount + label: I18n.tr("Warnings", "greeter doctor page status card") + iconName: "warning" + iconColor: Theme.warning + bgColor: Theme.withAlpha(Theme.warning, 0.15) + selected: root.selectedFilter === "warn" + onClicked: root.selectedFilter = "warn" + } + + GreeterStatusCard { + width: (parent.width - Theme.spacingS * 3) / 4 + count: root.infoCount + label: I18n.tr("Info", "greeter doctor page status card") + iconName: "info" + iconColor: Theme.secondary + bgColor: Theme.withAlpha(Theme.secondary, 0.15) + selected: root.selectedFilter === "info" + onClicked: root.selectedFilter = "info" + } + + GreeterStatusCard { + width: (parent.width - Theme.spacingS * 3) / 4 + count: root.okCount + label: I18n.tr("OK", "greeter doctor page status card") + iconName: "check_circle" + iconColor: Theme.success + bgColor: Theme.withAlpha(Theme.success, 0.15) + selected: root.selectedFilter === "ok" + onClicked: root.selectedFilter = "ok" + } + } + } + + Rectangle { + id: resultsContainer + anchors.top: headerSection.bottom + anchors.bottom: footerSection.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: Theme.spacingL + anchors.bottomMargin: Theme.spacingM + anchors.leftMargin: Theme.spacingXL + anchors.rightMargin: Theme.spacingXL + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + clip: true + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + visible: root.filteredResults.length === 0 + + DankIcon { + name: { + switch (root.selectedFilter) { + case "error": + return "check_circle"; + case "warn": + return "thumb_up"; + case "info": + return "info"; + default: + return "verified"; + } + } + size: Math.round(Theme.iconSize * 1.67) + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: { + switch (root.selectedFilter) { + case "error": + return I18n.tr("No errors", "greeter doctor page empty state"); + case "warn": + return I18n.tr("No warnings", "greeter doctor page empty state"); + case "info": + return I18n.tr("No info items", "greeter doctor page empty state"); + default: + return I18n.tr("No checks passed", "greeter doctor page empty state"); + } + } + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + + DankFlickable { + anchors.fill: parent + anchors.margins: Theme.spacingM + clip: true + contentHeight: resultsColumn.height + contentWidth: width + visible: root.filteredResults.length > 0 + + Column { + id: resultsColumn + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: root.filteredResults + + GreeterDoctorResultItem { + width: resultsColumn.width + resultData: modelData + } + } + } + } + } + + Row { + id: footerSection + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: Theme.spacingL + spacing: Theme.spacingM + + DankButton { + text: I18n.tr("Run Again", "greeter doctor page button") + iconName: "refresh" + backgroundColor: Theme.surfaceContainerHighest + textColor: Theme.surfaceText + onClicked: root.runDoctor() + } + } + } + + Process { + id: doctorProcess + command: ["dms", "doctor", "--json"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + root.isRunning = false; + root.hasRun = true; + try { + root.doctorResults = JSON.parse(text); + if (root.doctorResults?.summary) { + root.errorCount = root.doctorResults.summary.errors || 0; + root.warningCount = root.doctorResults.summary.warnings || 0; + root.okCount = root.doctorResults.summary.ok || 0; + root.infoCount = root.doctorResults.summary.info || 0; + } + if (root.errorCount > 0) + root.selectedFilter = "error"; + else if (root.warningCount > 0) + root.selectedFilter = "warn"; + else if (root.infoCount > 0) + root.selectedFilter = "info"; + else + root.selectedFilter = "ok"; + } catch (e) { + console.error("GreeterDoctorPage: Failed to parse doctor output:", e); + } + } + } + + onExited: exitCode => { + if (exitCode !== 0) { + root.isRunning = false; + root.hasRun = true; + } + } + } +} diff --git a/quickshell/Modals/Greeter/GreeterDoctorResultItem.qml b/quickshell/Modals/Greeter/GreeterDoctorResultItem.qml new file mode 100644 index 00000000..31462e44 --- /dev/null +++ b/quickshell/Modals/Greeter/GreeterDoctorResultItem.qml @@ -0,0 +1,96 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property var resultData: null + + readonly property string status: resultData?.status || "ok" + readonly property string statusIcon: { + switch (status) { + case "error": + return "error"; + case "warn": + return "warning"; + case "info": + return "info"; + default: + return "check_circle"; + } + } + readonly property color statusColor: { + switch (status) { + case "error": + return Theme.error; + case "warn": + return Theme.warning; + case "info": + return Theme.secondary; + default: + return Theme.success; + } + } + + height: Math.round(Theme.fontSizeMedium * 3.4) + radius: Theme.cornerRadius + color: Theme.withAlpha(statusColor, 0.08) + + DankIcon { + id: statusIcon + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + name: root.statusIcon + size: Theme.iconSize - 4 + color: root.statusColor + } + + Column { + anchors.left: statusIcon.right + anchors.leftMargin: Theme.spacingS + anchors.right: categoryChip.visible ? categoryChip.left : parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: 1 + + StyledText { + width: parent.width + text: root.resultData?.name || "" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + elide: Text.ElideRight + } + + StyledText { + width: parent.width + text: root.resultData?.message || "" + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + elide: Text.ElideRight + visible: text.length > 0 + } + } + + Rectangle { + id: categoryChip + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + height: Math.round(Theme.fontSizeSmall * 1.67) + width: categoryText.implicitWidth + Theme.spacingS + radius: Theme.spacingXS + color: Theme.surfaceContainerHighest + visible: !!(root.resultData?.category) + + StyledText { + id: categoryText + anchors.centerIn: parent + text: root.resultData?.category || "" + font.pixelSize: Theme.fontSizeSmall - 2 + color: Theme.surfaceVariantText + } + } +} diff --git a/quickshell/Modals/Greeter/GreeterFeatureCard.qml b/quickshell/Modals/Greeter/GreeterFeatureCard.qml new file mode 100644 index 00000000..22b26831 --- /dev/null +++ b/quickshell/Modals/Greeter/GreeterFeatureCard.qml @@ -0,0 +1,57 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property string iconName: "" + property string title: "" + property string description: "" + + readonly property real iconContainerSize: Math.round(Theme.iconSize * 1.5) + + height: Math.round(Theme.fontSizeMedium * 6.4) + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + Rectangle { + width: root.iconContainerSize + height: root.iconContainerSize + radius: Math.round(root.iconContainerSize * 0.28) + color: Theme.primaryContainer + anchors.horizontalCenter: parent.horizontalCenter + + DankIcon { + anchors.centerIn: parent + name: root.iconName + size: Theme.iconSize - 4 + color: Theme.primary + } + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 2 + + StyledText { + text: root.title + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: root.description + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } +} diff --git a/quickshell/Modals/Greeter/GreeterModal.qml b/quickshell/Modals/Greeter/GreeterModal.qml new file mode 100644 index 00000000..edd52b0e --- /dev/null +++ b/quickshell/Modals/Greeter/GreeterModal.qml @@ -0,0 +1,327 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Common +import qs.Services +import qs.Widgets + +FloatingWindow { + id: root + + property int currentPage: 0 + readonly property int totalPages: 3 + readonly property var pageComponents: [welcomePage, doctorPage, completePage] + + property var cheatsheetData: ({}) + property bool cheatsheetLoaded: false + + readonly property int modalWidth: 720 + readonly property int modalHeight: screen ? Math.min(760, screen.height - 80) : 760 + + signal greeterCompleted + + Component.onCompleted: Qt.callLater(loadCheatsheet) + + function loadCheatsheet() { + const provider = KeybindsService.cheatsheetProvider; + if (KeybindsService.cheatsheetAvailable && provider && !cheatsheetLoaded) { + cheatsheetProcess.command = ["dms", "keybinds", "show", provider]; + cheatsheetProcess.running = true; + } + } + + Connections { + target: KeybindsService + function onCheatsheetAvailableChanged() { + if (KeybindsService.cheatsheetAvailable && !root.cheatsheetLoaded) + loadCheatsheet(); + } + } + + function getKeybind(actionPattern) { + if (!cheatsheetLoaded || !cheatsheetData.binds) + return ""; + for (const category in cheatsheetData.binds) { + const binds = cheatsheetData.binds[category]; + for (let i = 0; i < binds.length; i++) { + const bind = binds[i]; + if (bind.action && bind.action.includes(actionPattern)) + return bind.key || ""; + } + } + return ""; + } + + function show() { + currentPage = 0; + visible = true; + } + + function nextPage() { + if (currentPage < totalPages - 1) + currentPage++; + } + + function prevPage() { + if (currentPage > 0) + currentPage--; + } + + function finish() { + FirstLaunchService.markFirstLaunchComplete(); + greeterCompleted(); + visible = false; + } + + function skip() { + FirstLaunchService.markFirstLaunchComplete(); + greeterCompleted(); + visible = false; + } + + objectName: "greeterModal" + title: I18n.tr("Welcome", "greeter modal window title") + minimumSize: Qt.size(modalWidth, modalHeight) + maximumSize: Qt.size(modalWidth, modalHeight) + color: Theme.surfaceContainer + visible: false + + Process { + id: cheatsheetProcess + running: false + + stdout: StdioCollector { + onStreamFinished: { + const trimmed = text.trim(); + if (trimmed.length === 0) + return; + try { + root.cheatsheetData = JSON.parse(trimmed); + root.cheatsheetLoaded = true; + } catch (e) { + console.warn("Greeter: Failed to parse cheatsheet:", e); + } + } + } + } + + FocusScope { + id: contentFocusScope + anchors.fill: parent + focus: true + + Keys.onEscapePressed: event => { + root.skip(); + event.accepted = true; + } + + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Return: + case Qt.Key_Enter: + if (root.currentPage < root.totalPages - 1) + root.nextPage(); + else + root.finish(); + event.accepted = true; + break; + case Qt.Key_Left: + if (root.currentPage > 0) + root.prevPage(); + event.accepted = true; + break; + case Qt.Key_Right: + if (root.currentPage < root.totalPages - 1) + root.nextPage(); + event.accepted = true; + break; + } + } + + MouseArea { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: headerRow.height + Theme.spacingM + onPressed: windowControls.tryStartMove() + onDoubleClicked: windowControls.tryToggleMaximize() + } + + Item { + id: headerRow + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingM + height: Math.round(Theme.fontSizeMedium * 2.85) + + Rectangle { + id: pageIndicatorContainer + readonly property real indicatorHeight: Math.round(Theme.fontSizeMedium * 2) + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: pageIndicatorRow.width + Theme.spacingM * 2 + height: indicatorHeight + radius: indicatorHeight / 2 + color: Theme.surfaceContainerHigh + + Row { + id: pageIndicatorRow + anchors.centerIn: parent + spacing: Theme.spacingS + + Repeater { + model: root.totalPages + + Rectangle { + required property int index + property bool isActive: index === root.currentPage + readonly property real dotSize: Math.round(Theme.spacingS * 1.3) + + width: isActive ? dotSize * 3 : dotSize + height: dotSize + radius: dotSize / 2 + color: isActive ? Theme.primary : Theme.surfaceTextAlpha + anchors.verticalCenter: parent.verticalCenter + + Behavior on width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + } + } + } + } + } + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + DankActionButton { + visible: windowControls.supported + iconName: root.maximized ? "fullscreen_exit" : "fullscreen" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: windowControls.tryToggleMaximize() + } + + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: root.skip() + + DankTooltip { + text: I18n.tr("Skip setup", "greeter skip button tooltip") + } + } + } + } + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: headerRow.bottom + anchors.bottom: footerRow.top + anchors.topMargin: Theme.spacingS + + Loader { + id: pageLoader + anchors.fill: parent + sourceComponent: root.pageComponents[root.currentPage] + + property var greeterRoot: root + } + } + + Rectangle { + id: footerRow + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: Math.round(Theme.fontSizeMedium * 4.5) + color: Theme.surfaceContainerHigh + + Rectangle { + anchors.top: parent.top + width: parent.width + height: 1 + color: Theme.outlineMedium + opacity: 0.5 + } + + Row { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingL + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankButton { + visible: root.currentPage < root.totalPages - 1 + text: I18n.tr("Skip", "greeter skip button") + backgroundColor: "transparent" + textColor: Theme.surfaceVariantText + onClicked: root.skip() + } + + DankButton { + visible: root.currentPage > 0 + text: I18n.tr("Back", "greeter back button") + iconName: "arrow_back" + backgroundColor: Theme.surfaceContainerHighest + textColor: Theme.surfaceText + onClicked: root.prevPage() + } + + DankButton { + visible: root.currentPage < root.totalPages - 1 + enabled: !(root.currentPage === 1 && pageLoader.item && pageLoader.item.isRunning) + text: root.currentPage === 0 ? I18n.tr("Get Started", "greeter first page button") : I18n.tr("Next", "greeter next button") + iconName: "arrow_forward" + backgroundColor: Theme.primary + textColor: Theme.primaryText + onClicked: root.nextPage() + } + + DankButton { + visible: root.currentPage === root.totalPages - 1 + text: I18n.tr("Finish", "greeter finish button") + iconName: "check" + backgroundColor: Theme.primary + textColor: Theme.primaryText + onClicked: root.finish() + } + } + } + } + + FloatingWindowControls { + id: windowControls + targetWindow: root + } + + Component { + id: welcomePage + GreeterWelcomePage {} + } + + Component { + id: doctorPage + GreeterDoctorPage {} + } + + Component { + id: completePage + GreeterCompletePage {} + } +} diff --git a/quickshell/Modals/Greeter/GreeterQuickLink.qml b/quickshell/Modals/Greeter/GreeterQuickLink.qml new file mode 100644 index 00000000..d064275f --- /dev/null +++ b/quickshell/Modals/Greeter/GreeterQuickLink.qml @@ -0,0 +1,60 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property string iconName: "" + property string title: "" + property bool isExternal: false + + signal clicked + + height: Math.round(Theme.fontSizeMedium * 3.1) + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: Theme.primary + opacity: mouseArea.containsMouse ? 0.12 : 0 + } + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: root.iconName + size: Theme.iconSizeSmall + 2 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: root.title + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + DankIcon { + visible: root.isExternal + name: "open_in_new" + size: Theme.iconSizeSmall - 2 + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.clicked() + } +} diff --git a/quickshell/Modals/Greeter/GreeterSettingsCard.qml b/quickshell/Modals/Greeter/GreeterSettingsCard.qml new file mode 100644 index 00000000..85e6b1cf --- /dev/null +++ b/quickshell/Modals/Greeter/GreeterSettingsCard.qml @@ -0,0 +1,78 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property string iconName: "" + property string title: "" + property string description: "" + + signal clicked + + readonly property real iconContainerSize: Math.round(Theme.iconSize * 1.5) + + height: Math.round(Theme.fontSizeMedium * 4.5) + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: Theme.primary + opacity: mouseArea.containsMouse ? 0.12 : 0 + } + + Row { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + Rectangle { + width: root.iconContainerSize + height: root.iconContainerSize + radius: Math.round(root.iconContainerSize * 0.28) + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: root.iconName + size: Theme.iconSize - 4 + color: Theme.primaryText + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + width: parent.width - root.iconContainerSize - Theme.spacingM + + StyledText { + text: root.title + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: root.description + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + width: parent.width + elide: Text.ElideRight + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.clicked() + } +} diff --git a/quickshell/Modals/Greeter/GreeterStatusCard.qml b/quickshell/Modals/Greeter/GreeterStatusCard.qml new file mode 100644 index 00000000..af0d5d67 --- /dev/null +++ b/quickshell/Modals/Greeter/GreeterStatusCard.qml @@ -0,0 +1,75 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property int count: 0 + property string label: "" + property string iconName: "" + property color iconColor: Theme.surfaceText + property color bgColor: Theme.surfaceContainerHigh + property bool selected: false + + signal clicked + + height: Math.round(Theme.fontSizeMedium * 5) + radius: Theme.cornerRadius + color: bgColor + border.width: selected ? 2 : 0 + border.color: selected ? iconColor : "transparent" + scale: mouseArea.pressed ? 0.97 : 1 + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.width { + NumberAnimation { + duration: Theme.shortDuration + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.clicked() + } + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingS + + DankIcon { + name: root.iconName + size: Theme.iconSize - 4 + color: root.iconColor + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: root.count.toString() + font.pixelSize: Theme.fontSizeXLarge + font.weight: Font.Bold + color: root.iconColor + anchors.verticalCenter: parent.verticalCenter + } + } + + StyledText { + text: root.label + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + } +} diff --git a/quickshell/Modals/Greeter/GreeterWelcomePage.qml b/quickshell/Modals/Greeter/GreeterWelcomePage.qml new file mode 100644 index 00000000..3e4635f4 --- /dev/null +++ b/quickshell/Modals/Greeter/GreeterWelcomePage.qml @@ -0,0 +1,150 @@ +import QtQuick +import QtQuick.Effects +import qs.Common +import qs.Widgets + +Item { + id: root + + readonly property real logoSize: Math.round(Theme.iconSize * 5.3) + + Column { + id: mainColumn + anchors.centerIn: parent + width: Math.min(Math.round(Theme.fontSizeMedium * 43), parent.width - Theme.spacingXL * 2) + spacing: Theme.spacingXL + + Column { + width: parent.width + spacing: Theme.spacingM + + Image { + width: root.logoSize + height: width * (569.94629 / 506.50931) + anchors.horizontalCenter: parent.horizontalCenter + 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 + } + } + + Column { + width: parent.width + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Welcome to DankMaterialShell", "greeter welcome page title") + font.pixelSize: Theme.fontSizeXLarge + 4 + font.weight: Font.Bold + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("A modern desktop shell for Wayland compositors", "greeter welcome page tagline") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outlineMedium + opacity: 0.3 + } + + Column { + width: parent.width + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Features", "greeter welcome page section header") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + Grid { + width: parent.width + columns: 3 + rowSpacing: Theme.spacingS + columnSpacing: Theme.spacingS + + GreeterFeatureCard { + width: (parent.width - Theme.spacingS * 2) / 3 + iconName: "auto_awesome" + title: I18n.tr("Dynamic Theming", "greeter feature card title") + description: I18n.tr("Colors from wallpaper", "greeter feature card description") + } + + GreeterFeatureCard { + width: (parent.width - Theme.spacingS * 2) / 3 + iconName: "format_paint" + title: I18n.tr("App Theming", "greeter feature card title") + description: I18n.tr("GTK, Qt, IDEs, more", "greeter feature card description") + } + + GreeterFeatureCard { + width: (parent.width - Theme.spacingS * 2) / 3 + iconName: "download" + title: I18n.tr("Theme Registry", "greeter feature card title") + description: I18n.tr("Community themes", "greeter feature card description") + } + + GreeterFeatureCard { + width: (parent.width - Theme.spacingS * 2) / 3 + iconName: "view_carousel" + title: I18n.tr("DankBar", "greeter feature card title") + description: I18n.tr("Modular widget bar", "greeter feature card description") + } + + GreeterFeatureCard { + width: (parent.width - Theme.spacingS * 2) / 3 + iconName: "extension" + title: I18n.tr("Plugins", "greeter feature card title") + description: I18n.tr("Extensible architecture", "greeter feature card description") + } + + GreeterFeatureCard { + width: (parent.width - Theme.spacingS * 2) / 3 + iconName: "layers" + title: I18n.tr("Multi-Monitor", "greeter feature card title") + description: I18n.tr("Per-screen config", "greeter feature card description") + } + + GreeterFeatureCard { + width: (parent.width - Theme.spacingS * 2) / 3 + iconName: "nightlight" + title: I18n.tr("Display Control", "greeter feature card title") + description: I18n.tr("Night mode & gamma", "greeter feature card description") + } + + GreeterFeatureCard { + width: (parent.width - Theme.spacingS * 2) / 3 + iconName: "tune" + title: I18n.tr("Control Center", "greeter feature card title") + description: I18n.tr("Quick system toggles", "greeter feature card description") + } + + GreeterFeatureCard { + width: (parent.width - Theme.spacingS * 2) / 3 + iconName: "density_small" + title: I18n.tr("System Tray", "greeter feature card title") + description: I18n.tr("Background app icons", "greeter feature card description") + } + } + } + } +} diff --git a/quickshell/Modules/Greetd/GreetdSettings.qml b/quickshell/Modules/Greetd/GreetdSettings.qml index 8faeaf65..f8696fcf 100644 --- a/quickshell/Modules/Greetd/GreetdSettings.qml +++ b/quickshell/Modules/Greetd/GreetdSettings.qml @@ -1,7 +1,6 @@ pragma Singleton pragma ComponentBehavior: Bound -import QtCore import QtQuick import Quickshell import Quickshell.Io @@ -15,7 +14,7 @@ Singleton { return greetCfgDir + "/settings.json"; } - property string currentThemeName: "blue" + property string currentThemeName: "purple" property bool settingsLoaded: false property string customThemeFile: "" property string matugenScheme: "scheme-tonal-spot" @@ -48,7 +47,7 @@ Singleton { try { if (content && content.trim()) { const settings = JSON.parse(content); - currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "blue"; + currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple"; customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : ""; matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot"; use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true; diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index 251a7890..37b141e0 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -24,6 +24,7 @@ PanelWindow { readonly property string clearText: I18n.tr("Dismiss") signal entered + signal exitStarted signal exitFinished function startExit() { @@ -31,6 +32,7 @@ PanelWindow { return; } exiting = true; + exitStarted(); exitAnim.restart(); exitWatchdog.restart(); if (NotificationService.removeFromVisibleNotifications) @@ -61,7 +63,7 @@ PanelWindow { win.exitFinished(); } - visible: hasValidData + visible: !_finalized WlrLayershell.layer: { const envLayer = Quickshell.env("DMS_NOTIFICATION_LAYER"); if (envLayer) { @@ -211,7 +213,7 @@ PanelWindow { y: Theme.snap((win.height - alignedHeight) / 2, dpr) width: alignedWidth height: alignedHeight - visible: win.hasValidData + visible: !win._finalized property real swipeOffset: 0 readonly property real dismissThreshold: isTopCenter ? height * 0.4 : width * 0.35 diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml index 85874c43..d6f3d186 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml @@ -20,6 +20,7 @@ QtObject { popupComponent: Component { NotificationPopup { onEntered: manager._onPopupEntered(this) + onExitStarted: manager._onPopupExitStarted(this) onExitFinished: manager._onPopupExitFinished(this) } } @@ -276,6 +277,14 @@ QtObject { function _onPopupEntered(p) { } + function _onPopupExitStarted(p) { + if (!p) + return; + const survivors = _active().sort((a, b) => a.screenY - b.screenY); + for (let k = 0; k < survivors.length; ++k) + survivors[k].screenY = topMargin + k * baseNotificationHeight; + } + function _onPopupExitFinished(p) { if (!p) { return; diff --git a/quickshell/Services/FirstLaunchService.qml b/quickshell/Services/FirstLaunchService.qml new file mode 100644 index 00000000..eae07b76 --- /dev/null +++ b/quickshell/Services/FirstLaunchService.qml @@ -0,0 +1,94 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtCore +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Common + +Singleton { + id: root + + readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/DankMaterialShell" + readonly property string settingsPath: configDir + "/settings.json" + readonly property string firstLaunchMarkerPath: configDir + "/.firstlaunch" + + property bool isFirstLaunch: false + property bool checkComplete: false + property bool greeterDismissed: false + + readonly property bool shouldShowGreeter: checkComplete && isFirstLaunch && !greeterDismissed + + signal greeterRequested + signal greeterCompleted + + Component.onCompleted: { + checkFirstLaunch(); + } + + function checkFirstLaunch() { + firstLaunchCheckProcess.running = true; + } + + function markFirstLaunchComplete() { + greeterDismissed = true; + touchMarkerProcess.running = true; + greeterCompleted(); + } + + function dismissGreeter() { + greeterDismissed = true; + } + + Process { + id: firstLaunchCheckProcess + + command: ["sh", "-c", ` + SETTINGS='` + settingsPath + `' + MARKER='` + firstLaunchMarkerPath + `' + if [ -f "$MARKER" ]; then + echo 'skip' + elif [ -f "$SETTINGS" ]; then + echo 'existing_user' + else + echo 'first' + fi + `] + running: false + + stdout: SplitParser { + onRead: data => { + const result = data.trim(); + root.checkComplete = true; + + if (result === "first") { + root.isFirstLaunch = true; + console.info("FirstLaunchService: First launch detected, greeter will be shown"); + root.greeterRequested(); + } else if (result === "existing_user") { + root.isFirstLaunch = false; + console.info("FirstLaunchService: Existing user detected, silently creating marker"); + touchMarkerProcess.running = true; + } else { + root.isFirstLaunch = false; + } + } + } + } + + Process { + id: touchMarkerProcess + + command: ["sh", "-c", "mkdir -p '" + configDir + "' && touch '" + firstLaunchMarkerPath + "'"] + running: false + + onExited: exitCode => { + if (exitCode === 0) { + console.info("FirstLaunchService: First launch marker created"); + } else { + console.warn("FirstLaunchService: Failed to create first launch marker"); + } + } + } +}