import QtQuick import qs.Common import qs.Modals.FileBrowser import qs.Services import qs.Widgets Item { id: recentAppsTab FileBrowserModal { id: logoFileBrowser browserTitle: I18n.tr("Select Launcher Logo") browserIcon: "image" browserType: "generic" filterExtensions: ["*.svg", "*.png", "*.jpg", "*.jpeg", "*.webp"] onFileSelected: path => { SettingsData.set("launcherLogoCustomPath", path.replace("file://", "")); } } DankFlickable { anchors.fill: parent clip: true contentHeight: mainColumn.height + Theme.spacingXL contentWidth: width Column { id: mainColumn width: Math.min(550, parent.width - Theme.spacingL * 2) anchors.horizontalCenter: parent.horizontalCenter spacing: Theme.spacingXL StyledRect { width: parent.width height: launcherLogoSection.implicitHeight + Theme.spacingL * 2 radius: Theme.cornerRadius color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.width: 0 Column { id: launcherLogoSection anchors.fill: parent anchors.margins: Theme.spacingL spacing: Theme.spacingM Row { width: parent.width spacing: Theme.spacingM DankIcon { name: "apps" size: Theme.iconSize color: Theme.primary anchors.verticalCenter: parent.verticalCenter } StyledText { text: I18n.tr("Launcher Button Logo") font.pixelSize: Theme.fontSizeLarge font.weight: Font.Medium color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } } StyledText { width: parent.width text: I18n.tr("Choose the logo displayed on the launcher button in DankBar") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText wrapMode: Text.WordWrap } Item { width: parent.width height: logoModeGroup.height DankButtonGroup { id: logoModeGroup anchors.horizontalCenter: parent.horizontalCenter model: { const modes = [I18n.tr("Apps Icon"), I18n.tr("OS Logo"), I18n.tr("Dank")]; if (CompositorService.isNiri) { modes.push("niri"); } else if (CompositorService.isHyprland) { modes.push("Hyprland"); } else if (CompositorService.isDwl) { modes.push("mango"); } else if (CompositorService.isSway) { modes.push("Sway"); } else { modes.push(I18n.tr("Compositor")); } modes.push(I18n.tr("Custom")); return modes; } currentIndex: { if (SettingsData.launcherLogoMode === "apps") return 0; if (SettingsData.launcherLogoMode === "os") return 1; if (SettingsData.launcherLogoMode === "dank") return 2; if (SettingsData.launcherLogoMode === "compositor") return 3; if (SettingsData.launcherLogoMode === "custom") return 4; return 0; } onSelectionChanged: (index, selected) => { if (!selected) return; if (index === 0) { SettingsData.set("launcherLogoMode", "apps"); } else if (index === 1) { SettingsData.set("launcherLogoMode", "os"); } else if (index === 2) { SettingsData.set("launcherLogoMode", "dank"); } else if (index === 3) { SettingsData.set("launcherLogoMode", "compositor"); } else if (index === 4) { SettingsData.set("launcherLogoMode", "custom"); } } } } Row { width: parent.width visible: SettingsData.launcherLogoMode === "custom" opacity: visible ? 1 : 0 spacing: Theme.spacingM Behavior on opacity { NumberAnimation { duration: Theme.mediumDuration easing.type: Theme.emphasizedEasing } } StyledRect { width: parent.width - selectButton.width - Theme.spacingM height: 36 radius: Theme.cornerRadius color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9) border.color: Theme.outlineStrong border.width: 1 StyledText { anchors.left: parent.left anchors.leftMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter text: SettingsData.launcherLogoCustomPath || I18n.tr("Select an image file...") font.pixelSize: Theme.fontSizeMedium color: SettingsData.launcherLogoCustomPath ? Theme.surfaceText : Theme.outlineButton width: parent.width - Theme.spacingM * 2 elide: Text.ElideMiddle } } DankActionButton { id: selectButton iconName: "folder_open" width: 36 height: 36 onClicked: logoFileBrowser.open() } } Column { width: parent.width spacing: Theme.spacingL visible: SettingsData.launcherLogoMode !== "apps" opacity: visible ? 1 : 0 Behavior on opacity { NumberAnimation { duration: Theme.mediumDuration easing.type: Theme.emphasizedEasing } } Column { width: parent.width spacing: Theme.spacingM StyledText { text: I18n.tr("Color Override") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceText font.weight: Font.Medium anchors.horizontalCenter: parent.horizontalCenter } Row { anchors.horizontalCenter: parent.horizontalCenter spacing: Theme.spacingM DankButtonGroup { id: colorModeGroup model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")] currentIndex: { const override = SettingsData.launcherLogoColorOverride; if (override === "") return 0; if (override === "primary") return 1; if (override === "surface") return 2; return 3; } onSelectionChanged: (index, selected) => { if (!selected) return; if (index === 0) { SettingsData.set("launcherLogoColorOverride", ""); } else if (index === 1) { SettingsData.set("launcherLogoColorOverride", "primary"); } else if (index === 2) { SettingsData.set("launcherLogoColorOverride", "surface"); } else if (index === 3) { const currentOverride = SettingsData.launcherLogoColorOverride; const isPreset = currentOverride === "" || currentOverride === "primary" || currentOverride === "surface"; if (isPreset) { SettingsData.set("launcherLogoColorOverride", "#ffffff"); } } } } Rectangle { visible: { const override = SettingsData.launcherLogoColorOverride; return override !== "" && override !== "primary" && override !== "surface"; } width: 36 height: 36 radius: 18 color: { const override = SettingsData.launcherLogoColorOverride; if (override !== "" && override !== "primary" && override !== "surface") { return override; } return "#ffffff"; } border.color: Theme.outline border.width: 1 anchors.verticalCenter: parent.verticalCenter MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { if (PopoutService.colorPickerModal) { PopoutService.colorPickerModal.selectedColor = SettingsData.launcherLogoColorOverride; PopoutService.colorPickerModal.pickerTitle = I18n.tr("Choose Launcher Logo Color"); PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) { SettingsData.set("launcherLogoColorOverride", selectedColor); }; PopoutService.colorPickerModal.show(); } } } } } } Column { width: parent.width spacing: Theme.spacingS Column { width: 120 spacing: Theme.spacingS anchors.horizontalCenter: parent.horizontalCenter StyledText { text: I18n.tr("Size Offset") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceText font.weight: Font.Medium anchors.horizontalCenter: parent.horizontalCenter } DankSlider { width: 100 height: 20 minimum: -12 maximum: 12 value: SettingsData.launcherLogoSizeOffset unit: "" showValue: true wheelEnabled: false thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) anchors.horizontalCenter: parent.horizontalCenter onSliderValueChanged: newValue => { SettingsData.set("launcherLogoSizeOffset", newValue); } } } } Item { width: parent.width height: customControlsFlow.height visible: { const override = SettingsData.launcherLogoColorOverride; return override !== "" && override !== "primary" && override !== "surface"; } opacity: visible ? 1 : 0 Behavior on opacity { NumberAnimation { duration: Theme.mediumDuration easing.type: Theme.emphasizedEasing } } Flow { id: customControlsFlow anchors.horizontalCenter: parent.horizontalCenter spacing: Theme.spacingS Column { width: 120 spacing: Theme.spacingS StyledText { text: I18n.tr("Brightness") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceText font.weight: Font.Medium anchors.horizontalCenter: parent.horizontalCenter } DankSlider { width: 100 height: 20 minimum: 0 maximum: 100 value: Math.round(SettingsData.launcherLogoBrightness * 100) unit: "%" showValue: true wheelEnabled: false thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) anchors.horizontalCenter: parent.horizontalCenter onSliderValueChanged: newValue => { SettingsData.set("launcherLogoBrightness", newValue / 100); } } } Column { width: 120 spacing: Theme.spacingS StyledText { text: I18n.tr("Contrast") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceText font.weight: Font.Medium anchors.horizontalCenter: parent.horizontalCenter } DankSlider { width: 100 height: 20 minimum: 0 maximum: 200 value: Math.round(SettingsData.launcherLogoContrast * 100) unit: "%" showValue: true wheelEnabled: false thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) anchors.horizontalCenter: parent.horizontalCenter onSliderValueChanged: newValue => { SettingsData.set("launcherLogoContrast", newValue / 100); } } } Column { width: 120 spacing: Theme.spacingS StyledText { text: I18n.tr("Invert on mode change") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceText font.weight: Font.Medium anchors.horizontalCenter: parent.horizontalCenter } DankToggle { width: 32 height: 18 checked: SettingsData.launcherLogoColorInvertOnMode anchors.horizontalCenter: parent.horizontalCenter onToggled: checked => { SettingsData.set("launcherLogoColorInvertOnMode", checked); } } } } } } } } StyledRect { width: parent.width height: launchPrefixSection.implicitHeight + Theme.spacingL * 2 radius: Theme.cornerRadius color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.width: 0 Column { id: launchPrefixSection anchors.fill: parent anchors.margins: Theme.spacingL spacing: Theme.spacingM Row { width: parent.width spacing: Theme.spacingM DankIcon { name: "terminal" size: Theme.iconSize color: Theme.primary anchors.verticalCenter: parent.verticalCenter } StyledText { text: I18n.tr("Launch Prefix") font.pixelSize: Theme.fontSizeLarge font.weight: Font.Medium color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } } StyledText { width: parent.width text: I18n.tr("Add a custom prefix to all application launches. This can be used for things like 'uwsm-app', 'systemd-run', or other command wrappers.") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText wrapMode: Text.WordWrap } DankTextField { width: parent.width text: SettingsData.launchPrefix placeholderText: I18n.tr("Enter launch prefix (e.g., 'uwsm-app')") onTextEdited: { SettingsData.set("launchPrefix", text); } } } } StyledRect { width: parent.width height: sortingSection.implicitHeight + Theme.spacingL * 2 radius: Theme.cornerRadius color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.width: 0 Column { id: sortingSection anchors.fill: parent anchors.margins: Theme.spacingL spacing: Theme.spacingM Row { width: parent.width spacing: Theme.spacingM DankIcon { name: "sort_by_alpha" size: Theme.iconSize color: Theme.primary anchors.verticalCenter: parent.verticalCenter } StyledText { text: I18n.tr("Sort Alphabetically") font.pixelSize: Theme.fontSizeLarge font.weight: Font.Medium color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } Item { width: parent.width - parent.children[0].width - parent.children[1].width - sortToggle.width - Theme.spacingM * 3 height: 1 } DankToggle { id: sortToggle width: 32 height: 18 checked: SettingsData.sortAppsAlphabetically anchors.verticalCenter: parent.verticalCenter onToggled: checked => { SettingsData.set("sortAppsAlphabetically", checked); } } } StyledText { width: parent.width text: I18n.tr("When enabled, apps are sorted alphabetically. When disabled, apps are sorted by usage frequency.") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText wrapMode: Text.WordWrap } } } StyledRect { width: parent.width height: gridColumnsSection.implicitHeight + Theme.spacingL * 2 radius: Theme.cornerRadius color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.width: 0 Column { id: gridColumnsSection anchors.fill: parent anchors.margins: Theme.spacingL spacing: Theme.spacingM Row { width: parent.width spacing: Theme.spacingM DankIcon { name: "grid_view" size: Theme.iconSize color: Theme.primary anchors.verticalCenter: parent.verticalCenter } StyledText { text: I18n.tr("Grid Columns") font.pixelSize: Theme.fontSizeLarge font.weight: Font.Medium color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } } StyledText { width: parent.width text: I18n.tr("Adjust the number of columns in grid view mode.") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText wrapMode: Text.WordWrap } Column { width: 120 spacing: Theme.spacingS anchors.horizontalCenter: parent.horizontalCenter DankSlider { width: 100 height: 20 minimum: 2 maximum: 8 value: SettingsData.appLauncherGridColumns unit: "" showValue: true wheelEnabled: false thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) anchors.horizontalCenter: parent.horizontalCenter onSliderValueChanged: newValue => { SettingsData.set("appLauncherGridColumns", newValue); } } } } } StyledRect { width: parent.width height: niriOverviewSection.implicitHeight + Theme.spacingL * 2 radius: Theme.cornerRadius color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.width: 0 visible: CompositorService.isNiri Column { id: niriOverviewSection anchors.fill: parent anchors.margins: Theme.spacingL spacing: Theme.spacingM Row { width: parent.width spacing: Theme.spacingM DankIcon { name: "open_in_new" size: Theme.iconSize color: Theme.primary anchors.verticalCenter: parent.verticalCenter } StyledText { text: I18n.tr("Close Overview on Launch") font.pixelSize: Theme.fontSizeLarge font.weight: Font.Medium color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } Item { width: parent.width - parent.children[0].width - parent.children[1].width - niriOverviewToggle.width - Theme.spacingM * 3 height: 1 } DankToggle { id: niriOverviewToggle width: 32 height: 18 checked: SettingsData.spotlightCloseNiriOverview anchors.verticalCenter: parent.verticalCenter onToggled: checked => { SettingsData.set("spotlightCloseNiriOverview", checked); } } } StyledText { width: parent.width text: I18n.tr("When enabled, launching an app from the launcher will automatically close the Niri overview if it's open.") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText wrapMode: Text.WordWrap } } } StyledRect { width: parent.width height: niriOverlayEnabledSection.implicitHeight + Theme.spacingL * 2 radius: Theme.cornerRadius color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.width: 0 visible: CompositorService.isNiri Column { id: niriOverlayEnabledSection anchors.fill: parent anchors.margins: Theme.spacingL spacing: Theme.spacingM Row { width: parent.width spacing: Theme.spacingM DankIcon { name: "layers" size: Theme.iconSize color: Theme.primary anchors.verticalCenter: parent.verticalCenter } StyledText { text: I18n.tr("Enable Overview Overlay") font.pixelSize: Theme.fontSizeLarge font.weight: Font.Medium color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } Item { width: parent.width - parent.children[0].width - parent.children[1].width - niriOverlayToggle.width - Theme.spacingM * 3 height: 1 } DankToggle { id: niriOverlayToggle width: 32 height: 18 checked: SettingsData.niriOverviewOverlayEnabled anchors.verticalCenter: parent.verticalCenter onToggled: checked => { SettingsData.set("niriOverviewOverlayEnabled", checked); } } } StyledText { width: parent.width text: I18n.tr("When enabled, shows the launcher overlay when typing in Niri overview mode. Disable this if you prefer to not have the launcher when typing on Niri overview or want to use other launcher in the overview.") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText wrapMode: Text.WordWrap } } } StyledRect { width: parent.width height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2 radius: Theme.cornerRadius color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.width: 0 Column { id: recentlyUsedSection property var rankedAppsModel: { var apps = []; for (var appId in (AppUsageHistoryData.appUsageRanking || {})) { var appData = (AppUsageHistoryData.appUsageRanking || {})[appId]; apps.push({ "id": appId, "name": appData.name, "exec": appData.exec, "icon": appData.icon, "comment": appData.comment, "usageCount": appData.usageCount, "lastUsed": appData.lastUsed }); } apps.sort(function (a, b) { if (a.usageCount !== b.usageCount) return b.usageCount - a.usageCount; return a.name.localeCompare(b.name); }); return apps.slice(0, 20); } anchors.fill: parent anchors.margins: Theme.spacingL spacing: Theme.spacingM Row { width: parent.width spacing: Theme.spacingM DankIcon { name: "history" size: Theme.iconSize color: Theme.primary anchors.verticalCenter: parent.verticalCenter } StyledText { text: I18n.tr("Recently Used Apps") font.pixelSize: Theme.fontSizeLarge font.weight: Font.Medium color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } Item { width: parent.width - parent.children[0].width - parent.children[1].width - clearAllButton.width - Theme.spacingM * 3 height: 1 } DankActionButton { id: clearAllButton iconName: "delete_sweep" iconSize: Theme.iconSize - 2 iconColor: Theme.error anchors.verticalCenter: parent.verticalCenter onClicked: { AppUsageHistoryData.appUsageRanking = {}; AppUsageHistoryData.saveSettings(); } } } StyledText { width: parent.width text: I18n.tr("Apps are ordered by usage frequency, then last used, then alphabetically.") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText wrapMode: Text.WordWrap } Column { id: rankedAppsList width: parent.width spacing: Theme.spacingS Repeater { model: recentlyUsedSection.rankedAppsModel delegate: Rectangle { width: rankedAppsList.width height: 48 radius: Theme.cornerRadius color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) border.width: 0 Row { anchors.left: parent.left anchors.leftMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter spacing: Theme.spacingM StyledText { text: (index + 1).toString() font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium color: Theme.primary width: 20 anchors.verticalCenter: parent.verticalCenter } Image { width: 24 height: 24 source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable" sourceSize.width: 24 sourceSize.height: 24 fillMode: Image.PreserveAspectFit anchors.verticalCenter: parent.verticalCenter onStatusChanged: { if (status === Image.Error) source = "image://icon/application-x-executable"; } } Column { anchors.verticalCenter: parent.verticalCenter spacing: 2 StyledText { text: modelData.name || "Unknown App" font.pixelSize: Theme.fontSizeMedium font.weight: Font.Medium color: Theme.surfaceText } StyledText { text: { if (!modelData.lastUsed) return "Never used"; var date = new Date(modelData.lastUsed); var now = new Date(); var diffMs = now - date; var diffMins = Math.floor(diffMs / (1000 * 60)); var diffHours = Math.floor(diffMs / (1000 * 60 * 60)); var diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffMins < 1) return I18n.tr("Last launched just now"); if (diffMins < 60) return I18n.tr("Last launched %1 minute%2 ago").arg(diffMins).arg(diffMins === 1 ? "" : "s"); if (diffHours < 24) return I18n.tr("Last launched %1 hour%2 ago").arg(diffHours).arg(diffHours === 1 ? "" : "s"); if (diffDays < 7) return I18n.tr("Last launched %1 day%2 ago").arg(diffDays).arg(diffDays === 1 ? "" : "s"); return I18n.tr("Last launched %1").arg(date.toLocaleDateString()); } font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText } } } DankActionButton { anchors.right: parent.right anchors.rightMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter circular: true iconName: "close" iconSize: 16 iconColor: Theme.error onClicked: { var currentRanking = Object.assign({}, AppUsageHistoryData.appUsageRanking || {}); delete currentRanking[modelData.id]; AppUsageHistoryData.appUsageRanking = currentRanking; AppUsageHistoryData.saveSettings(); } } } } StyledText { width: parent.width text: recentlyUsedSection.rankedAppsModel.length === 0 ? "No apps have been launched yet." : "" font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceVariantText horizontalAlignment: Text.AlignHCenter visible: recentlyUsedSection.rankedAppsModel.length === 0 } } } } } } }