1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

welcome: add a first launch welcome page with doctor integration

fixes #760
This commit is contained in:
bbedward
2026-01-04 19:07:34 -05:00
parent 7ac5191e8d
commit d23fc9f2df
20 changed files with 1890 additions and 13 deletions

View File

@@ -676,7 +676,7 @@ func checkOptionalDependencies() []checkResult {
}{ }{
{"matugen", "matugen", "", "Dynamic theming", true}, {"matugen", "matugen", "", "Dynamic theming", true},
{"dgop", "dgop", "", "System monitoring", true}, {"dgop", "dgop", "", "System monitoring", true},
{"cava", "cava", "", "Audio waveform", false}, {"cava", "cava", "", "Audio visualizer", true},
{"khal", "khal", "", "Calendar events", false}, {"khal", "khal", "", "Calendar events", false},
{"Network", "nmcli", "iwctl", "Network management", false}, {"Network", "nmcli", "iwctl", "Network management", false},
{"danksearch", "dsearch", "", "File search", false}, {"danksearch", "dsearch", "", "File search", false},

View File

@@ -336,7 +336,7 @@ Singleton {
if (typeof SettingsData !== "undefined" && SettingsData.theme) { if (typeof SettingsData !== "undefined" && SettingsData.theme) {
Theme.switchTheme(SettingsData.theme); Theme.switchTheme(SettingsData.theme);
} else { } else {
Theme.switchTheme("blue"); Theme.switchTheme("purple");
} }
} }
} }

View File

@@ -67,7 +67,7 @@ Singleton {
property alias dankBarCenterWidgetsModel: centerWidgetsModel property alias dankBarCenterWidgetsModel: centerWidgetsModel
property alias dankBarRightWidgetsModel: rightWidgetsModel property alias dankBarRightWidgetsModel: rightWidgetsModel
property string currentThemeName: "blue" property string currentThemeName: "purple"
property string currentThemeCategory: "generic" property string currentThemeCategory: "generic"
property string customThemeFile: "" property string customThemeFile: ""
property var registryThemeVariants: ({}) property var registryThemeVariants: ({})

View File

@@ -30,7 +30,7 @@ Singleton {
return useAuto ? Math.max(4, spacing) : manualValue; return useAuto ? Math.max(4, spacing) : manualValue;
} }
property string currentTheme: "blue" property string currentTheme: "purple"
property string currentThemeCategory: "generic" property string currentThemeCategory: "generic"
property bool isLightMode: typeof SessionData !== "undefined" ? SessionData.isLightMode : false property bool isLightMode: typeof SessionData !== "undefined" ? SessionData.isLightMode : false
property bool colorsFileLoadFailed: false property bool colorsFileLoadFailed: false
@@ -196,7 +196,7 @@ Singleton {
readonly property var currentThemeData: { readonly property var currentThemeData: {
if (currentTheme === "custom") { if (currentTheme === "custom") {
return customThemeData || StockThemes.getThemeByName("blue", isLightMode); return customThemeData || StockThemes.getThemeByName("purple", isLightMode);
} else if (currentTheme === dynamic) { } else if (currentTheme === dynamic) {
return { return {
"primary": getMatugenColor("primary", "#42a5f5"), "primary": getMatugenColor("primary", "#42a5f5"),

View File

@@ -6,7 +6,7 @@ function percentToUnit(v) {
} }
var SPEC = { var SPEC = {
currentThemeName: { def: "blue", onChange: "applyStoredTheme" }, currentThemeName: { def: "purple", onChange: "applyStoredTheme" },
currentThemeCategory: { def: "generic" }, currentThemeCategory: { def: "generic" },
customThemeFile: { def: "" }, customThemeFile: { def: "" },
registryThemeVariants: { def: {} }, registryThemeVariants: { def: {} },

View File

@@ -3,6 +3,7 @@ import Quickshell
import qs.Common import qs.Common
import qs.Modals import qs.Modals
import qs.Modals.Clipboard import qs.Modals.Clipboard
import qs.Modals.Greeter
import qs.Modals.Settings import qs.Modals.Settings
import qs.Modals.Spotlight import qs.Modals.Spotlight
import qs.Modules import qs.Modules
@@ -816,4 +817,20 @@ Item {
id: niriOverviewOverlay id: niriOverviewOverlay
} }
} }
Loader {
id: greeterLoader
active: false
sourceComponent: GreeterModal {
onGreeterCompleted: greeterLoader.active = false
Component.onCompleted: show()
}
Connections {
target: FirstLaunchService
function onGreeterRequested() {
greeterLoader.active = true;
}
}
}
} }

View File

@@ -41,14 +41,14 @@ FloatingWindow {
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
if (parentModal) { if (parentModal && "shouldHaveFocus" in parentModal) {
parentModal.shouldHaveFocus = false; parentModal.shouldHaveFocus = false;
parentModal.allowFocusOverride = true; parentModal.allowFocusOverride = true;
} }
content.reset(); content.reset();
Qt.callLater(() => content.forceActiveFocus()); Qt.callLater(() => content.forceActiveFocus());
} else { } else {
if (parentModal) { if (parentModal && "allowFocusOverride" in parentModal) {
parentModal.allowFocusOverride = false; parentModal.allowFocusOverride = false;
parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible); parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible);
} }

View File

@@ -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")
}
}
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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 {}
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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
}
}
}

View File

@@ -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")
}
}
}
}
}

View File

@@ -1,7 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtCore
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
@@ -15,7 +14,7 @@ Singleton {
return greetCfgDir + "/settings.json"; return greetCfgDir + "/settings.json";
} }
property string currentThemeName: "blue" property string currentThemeName: "purple"
property bool settingsLoaded: false property bool settingsLoaded: false
property string customThemeFile: "" property string customThemeFile: ""
property string matugenScheme: "scheme-tonal-spot" property string matugenScheme: "scheme-tonal-spot"
@@ -48,7 +47,7 @@ Singleton {
try { try {
if (content && content.trim()) { if (content && content.trim()) {
const settings = JSON.parse(content); 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 : ""; customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot"; matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true; use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;

View File

@@ -24,6 +24,7 @@ PanelWindow {
readonly property string clearText: I18n.tr("Dismiss") readonly property string clearText: I18n.tr("Dismiss")
signal entered signal entered
signal exitStarted
signal exitFinished signal exitFinished
function startExit() { function startExit() {
@@ -31,6 +32,7 @@ PanelWindow {
return; return;
} }
exiting = true; exiting = true;
exitStarted();
exitAnim.restart(); exitAnim.restart();
exitWatchdog.restart(); exitWatchdog.restart();
if (NotificationService.removeFromVisibleNotifications) if (NotificationService.removeFromVisibleNotifications)
@@ -61,7 +63,7 @@ PanelWindow {
win.exitFinished(); win.exitFinished();
} }
visible: hasValidData visible: !_finalized
WlrLayershell.layer: { WlrLayershell.layer: {
const envLayer = Quickshell.env("DMS_NOTIFICATION_LAYER"); const envLayer = Quickshell.env("DMS_NOTIFICATION_LAYER");
if (envLayer) { if (envLayer) {
@@ -211,7 +213,7 @@ PanelWindow {
y: Theme.snap((win.height - alignedHeight) / 2, dpr) y: Theme.snap((win.height - alignedHeight) / 2, dpr)
width: alignedWidth width: alignedWidth
height: alignedHeight height: alignedHeight
visible: win.hasValidData visible: !win._finalized
property real swipeOffset: 0 property real swipeOffset: 0
readonly property real dismissThreshold: isTopCenter ? height * 0.4 : width * 0.35 readonly property real dismissThreshold: isTopCenter ? height * 0.4 : width * 0.35

View File

@@ -20,6 +20,7 @@ QtObject {
popupComponent: Component { popupComponent: Component {
NotificationPopup { NotificationPopup {
onEntered: manager._onPopupEntered(this) onEntered: manager._onPopupEntered(this)
onExitStarted: manager._onPopupExitStarted(this)
onExitFinished: manager._onPopupExitFinished(this) onExitFinished: manager._onPopupExitFinished(this)
} }
} }
@@ -276,6 +277,14 @@ QtObject {
function _onPopupEntered(p) { 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) { function _onPopupExitFinished(p) {
if (!p) { if (!p) {
return; return;

View File

@@ -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");
}
}
}
}