summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2')
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ActionPanel.qml256
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/Controller.qml1894
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ControllerUtils.js128
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/DankLauncherV2Modal.qml465
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/GridItem.qml105
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ItemTransformers.js237
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/LauncherContent.qml1075
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/LauncherContextMenu.qml510
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/NavigationHelpers.js254
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ResultItem.qml196
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ResultsList.qml498
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/Scorer.js265
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/Section.qml114
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/SectionHeader.qml340
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/TileItem.qml199
15 files changed, 6536 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ActionPanel.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ActionPanel.qml
new file mode 100644
index 0000000..31ada32
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ActionPanel.qml
@@ -0,0 +1,256 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property var selectedItem: null
+ property var controller: null
+ property bool expanded: false
+ property int selectedActionIndex: 0
+
+ function getPluginContextMenuActions() {
+ if (selectedItem?.type !== "plugin" || !selectedItem?.pluginId)
+ return [];
+ var instance = PluginService.pluginInstances[selectedItem.pluginId];
+ if (!instance)
+ return [];
+ if (typeof instance.getContextMenuActions !== "function")
+ return [];
+ var actions = instance.getContextMenuActions(selectedItem.data);
+ if (!Array.isArray(actions))
+ return [];
+ return actions;
+ }
+
+ readonly property var actions: {
+ var result = [];
+ if (selectedItem?.primaryAction) {
+ result.push(selectedItem.primaryAction);
+ }
+
+ switch (selectedItem?.type) {
+ case "plugin":
+ var pluginActions = getPluginContextMenuActions();
+ for (var i = 0; i < pluginActions.length; i++) {
+ var act = pluginActions[i];
+ result.push({
+ name: act.text || act.name || "",
+ icon: act.icon || "play_arrow",
+ action: "plugin_action",
+ pluginAction: act.action
+ });
+ }
+ break;
+ case "plugin_browse":
+ if (selectedItem?.actions) {
+ for (var i = 0; i < selectedItem.actions.length; i++) {
+ result.push(selectedItem.actions[i]);
+ }
+ }
+ break;
+ case "app":
+ if (selectedItem?.isCore)
+ break;
+ if (SessionService.nvidiaCommand) {
+ result.push({
+ name: I18n.tr("Launch on dGPU"),
+ icon: "memory",
+ action: "launch_dgpu"
+ });
+ }
+ if (selectedItem?.actions) {
+ for (var i = 0; i < selectedItem.actions.length; i++) {
+ result.push(selectedItem.actions[i]);
+ }
+ }
+ break;
+ }
+ return result;
+ }
+
+ readonly property bool hasActions: {
+ switch (selectedItem?.type) {
+ case "app":
+ return !selectedItem?.isCore;
+ case "plugin":
+ return getPluginContextMenuActions().length > 0;
+ case "plugin_browse":
+ return selectedItem?.actions?.length > 0;
+ default:
+ return actions.length > 1;
+ }
+ }
+
+ width: parent?.width ?? 200
+ height: expanded && hasActions ? 52 : 0
+ color: Theme.surfaceContainerHigh
+ radius: Theme.cornerRadius
+
+ clip: true
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Rectangle {
+ anchors.top: parent.top
+ width: parent.width
+ height: 1
+ color: Theme.outlineMedium
+ }
+
+ Item {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+
+ Flickable {
+ id: actionsFlickable
+ anchors.left: parent.left
+ anchors.right: tabHint.left
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ height: parent.height
+ contentWidth: actionsRow.width
+ contentHeight: height
+ clip: true
+ boundsBehavior: Flickable.StopAtBounds
+ flickableDirection: Flickable.HorizontalFlick
+
+ Row {
+ id: actionsRow
+ height: parent.height
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: root.actions
+
+ Rectangle {
+ id: actionButton
+
+ required property var modelData
+ required property int index
+
+ width: actionContent.implicitWidth + Theme.spacingM * 2
+ height: actionsRow.height
+ radius: Theme.cornerRadius
+ color: index === root.selectedActionIndex ? Theme.primaryHover : actionArea.containsMouse ? Theme.surfaceHover : "transparent"
+
+ Row {
+ id: actionContent
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: actionButton.modelData?.icon ?? "play_arrow"
+ size: 16
+ color: actionButton.index === root.selectedActionIndex ? Theme.primary : Theme.surfaceText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: actionButton.modelData?.name ?? ""
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: actionButton.index === root.selectedActionIndex ? Theme.primary : Theme.surfaceText
+ }
+ }
+
+ MouseArea {
+ id: actionArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (root.controller && root.selectedItem) {
+ root.controller.executeAction(root.selectedItem, actionButton.modelData);
+ }
+ }
+ onEntered: root.selectedActionIndex = actionButton.index
+ }
+ }
+ }
+ }
+ }
+
+ StyledText {
+ id: tabHint
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ visible: root.hasActions
+ text: "Tab"
+ font.pixelSize: Theme.fontSizeSmall - 2
+ color: Theme.outlineButton
+ }
+ }
+
+ function toggle() {
+ expanded = !expanded;
+ selectedActionIndex = 0;
+ }
+
+ function show() {
+ expanded = true;
+ selectedActionIndex = actions.length > 1 ? 1 : 0;
+ }
+
+ function hide() {
+ expanded = false;
+ selectedActionIndex = 0;
+ }
+
+ function cycleAction(reverse = false) {
+ if (actions.length > 0) {
+ if (! reverse)
+ selectedActionIndex = (selectedActionIndex + 1) % actions.length;
+ else
+ selectedActionIndex = (selectedActionIndex - 1) % actions.length;
+ ensureSelectedVisible();
+ }
+ }
+
+ function ensureSelectedVisible() {
+ if (selectedActionIndex < 0 || !actionsRow.children || selectedActionIndex >= actionsRow.children.length)
+ return;
+ var buttonX = 0;
+ for (var i = 0; i < selectedActionIndex; i++) {
+ var child = actionsRow.children[i];
+ if (child)
+ buttonX += child.width + actionsRow.spacing;
+ }
+
+ var button = actionsRow.children[selectedActionIndex];
+ if (!button)
+ return;
+ var buttonRight = buttonX + button.width;
+ var viewLeft = actionsFlickable.contentX;
+ var viewRight = viewLeft + actionsFlickable.width;
+
+ if (buttonX < viewLeft) {
+ actionsFlickable.contentX = Math.max(0, buttonX - Theme.spacingS);
+ } else if (buttonRight > viewRight) {
+ actionsFlickable.contentX = Math.min(actionsFlickable.contentWidth - actionsFlickable.width, buttonRight - actionsFlickable.width + Theme.spacingS);
+ }
+ }
+
+ function executeSelectedAction() {
+ if (!controller || !selectedItem || selectedActionIndex >= actions.length)
+ return;
+ var action = actions[selectedActionIndex];
+ if (action.action === "plugin_action" && typeof action.pluginAction === "function") {
+ action.pluginAction();
+ controller.performSearch();
+ controller.itemExecuted();
+ } else {
+ controller.executeAction(selectedItem, action);
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/Controller.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/Controller.qml
new file mode 100644
index 0000000..67cb606
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/Controller.qml
@@ -0,0 +1,1894 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import qs.Common
+import qs.Services
+import "Scorer.js" as Scorer
+import "ControllerUtils.js" as Utils
+import "NavigationHelpers.js" as Nav
+import "ItemTransformers.js" as Transform
+
+Item {
+ id: root
+
+ property string searchQuery: ""
+ property string searchMode: "all"
+ property string previousSearchMode: "all"
+ property bool autoSwitchedToFiles: false
+ property bool isFileSearching: false
+ property var sections: []
+ property var flatModel: []
+ property int selectedFlatIndex: 0
+ property var selectedItem: null
+ property bool isSearching: false
+ property string activePluginId: ""
+ property var collapsedSections: ({})
+ property bool keyboardNavigationActive: false
+ property bool active: false
+ property var _modeSectionsCache: ({})
+ property bool _queryDrivenSearch: false
+ property bool _diskCacheConsumed: false
+ property var sectionViewModes: ({})
+ property var pluginViewPreferences: ({})
+ property int gridColumns: SettingsData.appLauncherGridColumns
+ property int viewModeVersion: 0
+ property string viewModeContext: "spotlight"
+
+ signal itemExecuted
+ signal searchCompleted
+ signal modeChanged(string mode)
+ signal queryChanged(string query)
+ signal viewModeChanged(string sectionId, string mode)
+ signal searchQueryRequested(string query)
+
+ onActiveChanged: {
+ if (!active) {
+ SessionData.addLauncherHistory(searchQuery);
+
+ sections = [];
+ flatModel = [];
+ selectedItem = null;
+ _clearModeCache();
+ }
+ }
+
+ onSearchModeChanged: {
+ if (searchMode === "apps") {
+ _loadAppCategories();
+ } else {
+ appCategory = "";
+ appCategories = [];
+ }
+ }
+
+ Connections {
+ target: SettingsData
+ function onSortAppsAlphabeticallyChanged() {
+ AppSearchService.invalidateLauncherCache();
+ _clearModeCache();
+ }
+ }
+
+ Connections {
+ target: AppSearchService
+ function onCacheVersionChanged() {
+ if (!active)
+ return;
+ _clearModeCache();
+ if (searchMode === "apps") {
+ _loadAppCategories();
+ performSearch();
+ } else if (!searchQuery && searchMode === "all") {
+ performSearch();
+ }
+ }
+ }
+
+ Connections {
+ target: PluginService
+ function onRequestLauncherUpdate(pluginId) {
+ if (!active)
+ return;
+ if (activePluginId === pluginId) {
+ if (activePluginCategories.length <= 1)
+ loadPluginCategories(pluginId);
+ performSearch();
+ return;
+ }
+ if (searchQuery)
+ performSearch();
+ }
+ }
+
+ Process {
+ id: wtypeProcess
+ command: ["wtype", "-M", "ctrl", "-P", "v", "-p", "v", "-m", "ctrl"]
+ running: false
+ }
+
+ Process {
+ id: copyProcess
+ running: false
+ onExited: pasteTimer.start()
+ }
+
+ Timer {
+ id: pasteTimer
+ interval: 200
+ repeat: false
+ onTriggered: wtypeProcess.running = true
+ }
+
+ function pasteSelected() {
+ if (!selectedItem)
+ return;
+ if (!SessionService.wtypeAvailable) {
+ ToastService.showError("wtype not available - install wtype for paste support");
+ return;
+ }
+
+ const pluginId = selectedItem.pluginId;
+ if (!pluginId)
+ return;
+ const pasteArgs = AppSearchService.getPluginPasteArgs(pluginId, selectedItem.data);
+ if (!pasteArgs)
+ return;
+ copyProcess.command = pasteArgs;
+ copyProcess.running = true;
+ itemExecuted();
+ }
+
+ readonly property var sectionDefinitions: [
+ {
+ id: "favorites",
+ title: I18n.tr("Pinned"),
+ icon: "push_pin",
+ priority: 1,
+ defaultViewMode: "list"
+ },
+ {
+ id: "apps",
+ title: I18n.tr("Applications"),
+ icon: "apps",
+ priority: 2,
+ defaultViewMode: "list"
+ },
+ {
+ id: "browse_plugins",
+ title: I18n.tr("Browse"),
+ icon: "category",
+ priority: 2.5,
+ defaultViewMode: "grid"
+ },
+ {
+ id: "files",
+ title: I18n.tr("Files"),
+ icon: "folder",
+ priority: 4,
+ defaultViewMode: "list"
+ },
+ {
+ id: "fallback",
+ title: I18n.tr("Commands"),
+ icon: "terminal",
+ priority: 5,
+ defaultViewMode: "list"
+ }
+ ]
+
+ property int historyIndex: -1
+ property string typingBackup: ""
+
+ function navigateHistory(direction) {
+ let history = SessionData.launcherQueryHistory;
+ if (history.length === 0)
+ return;
+
+ if (historyIndex === -1)
+ typingBackup = searchQuery;
+
+ let nextIndex = historyIndex + (direction === "up" ? 1 : -1);
+ if (nextIndex >= history.length)
+ nextIndex = history.length - 1;
+ if (nextIndex < -1)
+ nextIndex = -1;
+
+ if (nextIndex === historyIndex)
+ return;
+ historyIndex = nextIndex;
+
+ let targetText = (historyIndex === -1) ? typingBackup : history[historyIndex];
+
+ setSearchQuery(targetText);
+ searchQueryRequested(targetText);
+ }
+
+ property string fileSearchType: "all"
+ property string fileSearchExt: ""
+ property string fileSearchFolder: ""
+ property string fileSearchSort: "score"
+
+ property string pluginFilter: ""
+ property string activePluginName: ""
+ property var activePluginCategories: []
+ property string activePluginCategory: ""
+ property string appCategory: ""
+ property var appCategories: []
+
+ function getSectionViewMode(sectionId) {
+ if (sectionId === "browse_plugins")
+ return "list";
+ if (pluginViewPreferences[sectionId]?.enforced)
+ return pluginViewPreferences[sectionId].mode;
+ if (sectionViewModes[sectionId])
+ return sectionViewModes[sectionId];
+
+ var savedModes = viewModeContext === "appDrawer" ? (SettingsData.appDrawerSectionViewModes || {}) : (SettingsData.spotlightSectionViewModes || {});
+ if (savedModes[sectionId])
+ return savedModes[sectionId];
+
+ for (var i = 0; i < sectionDefinitions.length; i++) {
+ if (sectionDefinitions[i].id === sectionId)
+ return sectionDefinitions[i].defaultViewMode || "list";
+ }
+
+ if (pluginViewPreferences[sectionId]?.mode)
+ return pluginViewPreferences[sectionId].mode;
+
+ return "list";
+ }
+
+ function setSectionViewMode(sectionId, mode) {
+ if (sectionId === "browse_plugins")
+ return;
+ if (pluginViewPreferences[sectionId]?.enforced)
+ return;
+ sectionViewModes = Object.assign({}, sectionViewModes, {
+ [sectionId]: mode
+ });
+ viewModeVersion++;
+ if (viewModeContext === "appDrawer") {
+ var savedModes = Object.assign({}, SettingsData.appDrawerSectionViewModes || {}, {
+ [sectionId]: mode
+ });
+ SettingsData.appDrawerSectionViewModes = savedModes;
+ } else {
+ var savedModes = Object.assign({}, SettingsData.spotlightSectionViewModes || {}, {
+ [sectionId]: mode
+ });
+ SettingsData.spotlightSectionViewModes = savedModes;
+ }
+ viewModeChanged(sectionId, mode);
+ }
+
+ function canChangeSectionViewMode(sectionId) {
+ if (sectionId === "browse_plugins")
+ return false;
+ return !pluginViewPreferences[sectionId]?.enforced;
+ }
+
+ function canCollapseSection(sectionId) {
+ return searchMode === "all";
+ }
+
+ function setPluginViewPreference(pluginId, mode, enforced) {
+ var prefs = Object.assign({}, pluginViewPreferences);
+ prefs[pluginId] = {
+ mode: mode,
+ enforced: enforced || false
+ };
+ pluginViewPreferences = prefs;
+ }
+
+ function applyActivePluginViewPreference(pluginId, isBuiltIn) {
+ var sectionId = "plugin_" + pluginId;
+ var pref = null;
+ if (isBuiltIn) {
+ var builtIn = AppSearchService.builtInPlugins[pluginId];
+ if (builtIn && builtIn.viewMode) {
+ pref = {
+ mode: builtIn.viewMode,
+ enforced: builtIn.viewModeEnforced === true
+ };
+ }
+ } else {
+ pref = PluginService.getPluginViewPreference(pluginId);
+ }
+
+ if (pref && pref.mode) {
+ setPluginViewPreference(sectionId, pref.mode, pref.enforced);
+ } else {
+ var prefs = Object.assign({}, pluginViewPreferences);
+ delete prefs[sectionId];
+ pluginViewPreferences = prefs;
+ }
+ }
+
+ function clearActivePluginViewPreference() {
+ var prefs = {};
+ for (var key in pluginViewPreferences) {
+ if (!key.startsWith("plugin_")) {
+ prefs[key] = pluginViewPreferences[key];
+ }
+ }
+ pluginViewPreferences = prefs;
+ }
+
+ property int _searchVersion: 0
+ property bool _pluginPhasePending: false
+ property bool _pluginPhaseForceFirst: false
+ property var _phase1Items: []
+
+ Timer {
+ id: searchDebounce
+ interval: 60
+ onTriggered: root.performSearch()
+ }
+
+ Timer {
+ id: pluginPhaseTimer
+ interval: 1
+ onTriggered: root._performPluginPhase()
+ }
+
+ Timer {
+ id: fileSearchDebounce
+ interval: 200
+ onTriggered: root.performFileSearch()
+ }
+
+ function getOrTransformApp(app) {
+ return AppSearchService.getOrTransformApp(app, transformApp);
+ }
+
+ function setSearchQuery(query) {
+ _searchVersion++;
+ _queryDrivenSearch = true;
+ _pluginPhasePending = false;
+ _phase1Items = [];
+ pluginPhaseTimer.stop();
+ searchQuery = query;
+ searchDebounce.restart();
+
+ var filesInAll = searchMode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll);
+ if (searchMode !== "plugins" && (searchMode === "files" || query.startsWith("/") || filesInAll) && query.length > 0) {
+ fileSearchDebounce.restart();
+ }
+ }
+
+ function setMode(mode, isAutoSwitch) {
+ if (searchMode === mode)
+ return;
+ if (isAutoSwitch) {
+ previousSearchMode = searchMode;
+ autoSwitchedToFiles = true;
+ } else {
+ autoSwitchedToFiles = false;
+ }
+ searchMode = mode;
+ modeChanged(mode);
+ performSearch();
+ var filesInAll = mode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll) && searchQuery.length > 0;
+ if (mode === "files" || filesInAll) {
+ fileSearchDebounce.restart();
+ }
+ }
+
+ function restorePreviousMode() {
+ if (!autoSwitchedToFiles)
+ return;
+ autoSwitchedToFiles = false;
+ searchMode = previousSearchMode;
+ modeChanged(previousSearchMode);
+ performSearch();
+ }
+
+ function cycleMode(reverse = false) {
+ var modes = ["all", "apps", "files", "plugins"];
+ var currentIndex = modes.indexOf(searchMode);
+ if (!reverse)
+ var nextIndex = (currentIndex + 1) % modes.length;
+ else
+ var nextIndex = (currentIndex - 1 + modes.length) % modes.length;
+ setMode(modes[nextIndex]);
+ }
+
+ function reset() {
+ searchQuery = "";
+ searchMode = "all";
+ previousSearchMode = "all";
+ autoSwitchedToFiles = false;
+ isFileSearching = false;
+ fileSearchType = "all";
+ fileSearchExt = "";
+ fileSearchFolder = "";
+ fileSearchSort = "score";
+ sections = [];
+ flatModel = [];
+ selectedFlatIndex = 0;
+ selectedItem = null;
+ isSearching = false;
+ activePluginId = "";
+ activePluginName = "";
+ activePluginCategories = [];
+ activePluginCategory = "";
+ appCategory = "";
+ appCategories = [];
+ pluginFilter = "";
+ collapsedSections = {};
+ _clearModeCache();
+ _queryDrivenSearch = false;
+ _pluginPhasePending = false;
+ _pluginPhaseForceFirst = false;
+ _phase1Items = [];
+ pluginPhaseTimer.stop();
+ }
+
+ function loadPluginCategories(pluginId) {
+ if (!pluginId) {
+ if (activePluginCategories.length > 0) {
+ activePluginCategories = [];
+ activePluginCategory = "";
+ }
+ return;
+ }
+
+ const categories = AppSearchService.getPluginLauncherCategories(pluginId);
+ if (categories.length === activePluginCategories.length) {
+ let same = true;
+ for (let i = 0; i < categories.length; i++) {
+ if (categories[i].id !== activePluginCategories[i]?.id) {
+ same = false;
+ break;
+ }
+ }
+ if (same)
+ return;
+ }
+ activePluginCategories = categories;
+ activePluginCategory = "";
+ AppSearchService.setPluginLauncherCategory(pluginId, "");
+ }
+
+ function setActivePluginCategory(categoryId) {
+ if (activePluginCategory === categoryId)
+ return;
+ activePluginCategory = categoryId;
+ AppSearchService.setPluginLauncherCategory(activePluginId, categoryId);
+ performSearch();
+ }
+
+ function setAppCategory(category) {
+ if (appCategory === category)
+ return;
+ appCategory = category;
+ _queryDrivenSearch = true;
+ _clearModeCache();
+ performSearch();
+ }
+
+ function _loadAppCategories() {
+ appCategories = AppSearchService.getAllCategories();
+ }
+
+ function setFileSearchType(type) {
+ if (fileSearchType === type)
+ return;
+ fileSearchType = type;
+ performFileSearch();
+ }
+
+ function setFileSearchExt(ext) {
+ if (fileSearchExt === ext)
+ return;
+ fileSearchExt = ext;
+ performFileSearch();
+ }
+
+ function setFileSearchFolder(folder) {
+ if (fileSearchFolder === folder)
+ return;
+ fileSearchFolder = folder;
+ performFileSearch();
+ }
+
+ function setFileSearchSort(sort) {
+ if (fileSearchSort === sort)
+ return;
+ fileSearchSort = sort;
+ performFileSearch();
+ }
+
+ function clearPluginFilter() {
+ if (pluginFilter) {
+ pluginFilter = "";
+ performSearch();
+ return true;
+ }
+ return false;
+ }
+
+ function preserveSelectionAfterUpdate(forceFirst) {
+ if (forceFirst)
+ return function () {
+ return getFirstItemIndex();
+ };
+ var previousSelectedId = selectedItem?.id || "";
+ return function (newFlatModel) {
+ if (!previousSelectedId)
+ return getFirstItemIndex();
+ for (var i = 0; i < newFlatModel.length; i++) {
+ if (!newFlatModel[i].isHeader && newFlatModel[i].item?.id === previousSelectedId)
+ return i;
+ }
+ return getFirstItemIndex();
+ };
+ }
+
+ function performSearch() {
+ queryChanged(searchQuery);
+
+ var currentVersion = _searchVersion;
+ isSearching = true;
+ var shouldResetSelection = _queryDrivenSearch;
+ _queryDrivenSearch = false;
+ var restoreSelection = preserveSelectionAfterUpdate(shouldResetSelection);
+
+ var cachedSections = AppSearchService.getCachedDefaultSections();
+ if (!cachedSections && !_diskCacheConsumed && !searchQuery && searchMode === "all" && !pluginFilter) {
+ _diskCacheConsumed = true;
+ var diskSections = _loadDiskCache();
+ if (diskSections) {
+ activePluginId = "";
+ activePluginName = "";
+ activePluginCategories = [];
+ activePluginCategory = "";
+ clearActivePluginViewPreference();
+ for (var i = 0; i < diskSections.length; i++) {
+ if (collapsedSections[diskSections[i].id] !== undefined)
+ diskSections[i].collapsed = collapsedSections[diskSections[i].id];
+ }
+ _applyHighlights(diskSections, "");
+ flatModel = Scorer.flattenSections(diskSections);
+ sections = diskSections;
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+ isSearching = false;
+ searchCompleted();
+ return;
+ }
+ }
+
+ if (cachedSections && !searchQuery && searchMode === "all" && !pluginFilter) {
+ activePluginId = "";
+ activePluginName = "";
+ activePluginCategories = [];
+ activePluginCategory = "";
+ clearActivePluginViewPreference();
+ var modeCache = _getCachedModeData("all");
+ if (modeCache) {
+ _applyHighlights(modeCache.sections, "");
+ sections = modeCache.sections;
+ flatModel = modeCache.flatModel;
+ } else {
+ var newSections = cachedSections.map(function (s) {
+ var copy = Object.assign({}, s, {
+ items: s.items ? s.items.slice() : []
+ });
+ if (collapsedSections[s.id] !== undefined)
+ copy.collapsed = collapsedSections[s.id];
+ return copy;
+ });
+ _applyHighlights(newSections, "");
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+ _setCachedModeData("all", sections, flatModel);
+ }
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+ isSearching = false;
+ searchCompleted();
+ return;
+ }
+
+ var allItems = [];
+
+ var triggerMatch = detectTrigger(searchQuery);
+ if (triggerMatch.pluginId) {
+ var pluginChanged = activePluginId !== triggerMatch.pluginId;
+ activePluginId = triggerMatch.pluginId;
+ activePluginName = getPluginName(triggerMatch.pluginId, triggerMatch.isBuiltIn);
+ applyActivePluginViewPreference(triggerMatch.pluginId, triggerMatch.isBuiltIn);
+
+ if (pluginChanged && !triggerMatch.isBuiltIn)
+ loadPluginCategories(triggerMatch.pluginId);
+
+ var pluginItems = getPluginItems(triggerMatch.pluginId, triggerMatch.query);
+ for (var k = 0; k < pluginItems.length; k++)
+ allItems.push(pluginItems[k]);
+
+ if (triggerMatch.isBuiltIn) {
+ var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query);
+ for (var j = 0; j < builtInItems.length; j++) {
+ allItems.push(transformBuiltInLauncherItem(builtInItems[j], triggerMatch.pluginId));
+ }
+ }
+
+ var dynamicDefs = buildDynamicSectionDefs(allItems);
+ var scoredItems = Scorer.scoreItems(allItems, triggerMatch.query, getFrecencyForItem);
+ var sortAlpha = !triggerMatch.query && SettingsData.sortAppsAlphabetically;
+ var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, 500);
+
+ for (var sid in collapsedSections) {
+ for (var i = 0; i < newSections.length; i++) {
+ if (newSections[i].id === sid) {
+ newSections[i].collapsed = collapsedSections[sid];
+ }
+ }
+ }
+
+ _applyHighlights(newSections, triggerMatch.query);
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+
+ isSearching = false;
+ searchCompleted();
+ return;
+ }
+
+ activePluginId = "";
+ activePluginName = "";
+ activePluginCategories = [];
+ activePluginCategory = "";
+ clearActivePluginViewPreference();
+
+ if (searchMode === "files") {
+ var fileQuery = searchQuery.startsWith("/") ? searchQuery.substring(1).trim() : searchQuery.trim();
+ isFileSearching = fileQuery.length >= 2 && DSearchService.dsearchAvailable;
+ sections = [];
+ flatModel = [];
+ selectedFlatIndex = 0;
+ selectedItem = null;
+ isSearching = false;
+ searchCompleted();
+ return;
+ }
+
+ if (searchMode === "apps") {
+ var isCategoryFiltered = appCategory && appCategory !== I18n.tr("All");
+ var cachedSections = AppSearchService.getCachedDefaultSections();
+ if (cachedSections && !searchQuery && !isCategoryFiltered) {
+ var modeCache = _getCachedModeData("apps");
+ if (modeCache) {
+ _applyHighlights(modeCache.sections, "");
+ sections = modeCache.sections;
+ flatModel = modeCache.flatModel;
+ } else {
+ var appSectionIds = ["favorites", "apps"];
+ var newSections = cachedSections.filter(function (s) {
+ return appSectionIds.indexOf(s.id) !== -1;
+ }).map(function (s) {
+ var copy = Object.assign({}, s, {
+ items: s.items ? s.items.slice() : []
+ });
+ if (collapsedSections[s.id] !== undefined)
+ copy.collapsed = collapsedSections[s.id];
+ return copy;
+ });
+ _applyHighlights(newSections, "");
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+ _setCachedModeData("apps", sections, flatModel);
+ }
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+ isSearching = false;
+ searchCompleted();
+ return;
+ }
+
+ if (isCategoryFiltered) {
+ var rawApps = AppSearchService.getAppsInCategory(appCategory);
+ for (var i = 0; i < rawApps.length; i++) {
+ allItems.push(getOrTransformApp(rawApps[i]));
+ }
+ // Also include core apps (DMS Settings etc.) that match this category
+ var allCoreApps = AppSearchService.getCoreApps("");
+ for (var i = 0; i < allCoreApps.length; i++) {
+ var coreAppCats = AppSearchService.getCategoriesForApp(allCoreApps[i]);
+ if (coreAppCats.indexOf(appCategory) !== -1)
+ allItems.push(transformCoreApp(allCoreApps[i]));
+ }
+ } else {
+ var apps = searchApps(searchQuery);
+ for (var i = 0; i < apps.length; i++) {
+ allItems.push(apps[i]);
+ }
+ }
+
+ var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
+ var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically;
+ var newSections = Scorer.groupBySection(scoredItems, sectionDefinitions, sortAlpha, searchQuery ? 50 : 500);
+
+ for (var sid in collapsedSections) {
+ for (var i = 0; i < newSections.length; i++) {
+ if (newSections[i].id === sid) {
+ newSections[i].collapsed = collapsedSections[sid];
+ }
+ }
+ }
+
+ _applyHighlights(newSections, searchQuery);
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+
+ isSearching = false;
+ searchCompleted();
+ return;
+ }
+
+ if (searchMode === "plugins") {
+ if (!searchQuery && !pluginFilter) {
+ var browseItems = getPluginBrowseItems();
+ for (var k = 0; k < browseItems.length; k++)
+ allItems.push(browseItems[k]);
+ } else if (pluginFilter) {
+ var isBuiltInFilter = !!AppSearchService.builtInPlugins[pluginFilter];
+ applyActivePluginViewPreference(pluginFilter, isBuiltInFilter);
+
+ var filterItems = getPluginItems(pluginFilter, searchQuery);
+ for (var k = 0; k < filterItems.length; k++)
+ allItems.push(filterItems[k]);
+
+ var builtInItems = AppSearchService.getBuiltInLauncherItems(pluginFilter, searchQuery);
+ for (var j = 0; j < builtInItems.length; j++) {
+ allItems.push(transformBuiltInLauncherItem(builtInItems[j], pluginFilter));
+ }
+ } else {
+ var emptyTriggerPlugins = getEmptyTriggerPlugins();
+ for (var i = 0; i < emptyTriggerPlugins.length; i++) {
+ var pluginId = emptyTriggerPlugins[i];
+ var pItems = getPluginItems(pluginId, searchQuery);
+ for (var k = 0; k < pItems.length; k++)
+ allItems.push(pItems[k]);
+ }
+
+ var builtInLauncherPlugins = getBuiltInEmptyTriggerLaunchers();
+ for (var i = 0; i < builtInLauncherPlugins.length; i++) {
+ var pluginId = builtInLauncherPlugins[i];
+ var blItems = AppSearchService.getBuiltInLauncherItems(pluginId, searchQuery);
+ for (var j = 0; j < blItems.length; j++) {
+ allItems.push(transformBuiltInLauncherItem(blItems[j], pluginId));
+ }
+ }
+ }
+
+ var dynamicDefs = buildDynamicSectionDefs(allItems);
+ var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
+ var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically;
+ var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, 500);
+
+ for (var sid in collapsedSections) {
+ for (var i = 0; i < newSections.length; i++) {
+ if (newSections[i].id === sid) {
+ newSections[i].collapsed = collapsedSections[sid];
+ }
+ }
+ }
+
+ _applyHighlights(newSections, searchQuery);
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+
+ isSearching = false;
+ searchCompleted();
+ return;
+ }
+
+ var apps = searchApps(searchQuery);
+ for (var i = 0; i < apps.length; i++) {
+ allItems.push(apps[i]);
+ }
+
+ if (searchMode === "all") {
+ if (searchQuery && searchQuery.length >= 2) {
+ _pluginPhasePending = true;
+ _phase1Items = allItems.slice();
+ _pluginPhaseForceFirst = shouldResetSelection;
+ pluginPhaseTimer.restart();
+ isSearching = true;
+ searchCompleted();
+ return;
+ } else if (!searchQuery) {
+ var emptyTriggerOrdered = getEmptyTriggerPluginsOrdered();
+ for (var i = 0; i < emptyTriggerOrdered.length; i++) {
+ var plugin = emptyTriggerOrdered[i];
+ if (plugin.isBuiltIn) {
+ var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
+ for (var j = 0; j < blItems.length; j++)
+ allItems.push(transformBuiltInLauncherItem(blItems[j], plugin.id));
+ } else {
+ var pItems = getPluginItems(plugin.id, searchQuery);
+ for (var j = 0; j < pItems.length; j++)
+ allItems.push(pItems[j]);
+ }
+ }
+
+ var browseItems = getPluginBrowseItems();
+ for (var i = 0; i < browseItems.length; i++)
+ allItems.push(browseItems[i]);
+ }
+ }
+
+ var dynamicDefs = buildDynamicSectionDefs(allItems);
+
+ if (currentVersion !== _searchVersion) {
+ isSearching = false;
+ return;
+ }
+
+ var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
+ var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically;
+ var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, searchQuery ? 50 : 500);
+
+ if (currentVersion !== _searchVersion) {
+ isSearching = false;
+ return;
+ }
+
+ for (var i = 0; i < newSections.length; i++) {
+ var sid = newSections[i].id;
+ if (collapsedSections[sid] !== undefined) {
+ newSections[i].collapsed = collapsedSections[sid];
+ }
+ }
+
+ _applyHighlights(newSections, searchQuery);
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+
+ if (!AppSearchService.isCacheValid() && !searchQuery && searchMode === "all" && !pluginFilter) {
+ AppSearchService.setCachedDefaultSections(sections, flatModel);
+ _saveDiskCache(sections);
+ }
+
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+
+ isSearching = _pluginPhasePending;
+ searchCompleted();
+ }
+
+ function _performPluginPhase() {
+ _pluginPhasePending = false;
+ if (!searchQuery || searchQuery.length < 2 || searchMode !== "all")
+ return;
+
+ var currentVersion = _searchVersion;
+ var restoreSelection = preserveSelectionAfterUpdate(_pluginPhaseForceFirst);
+ var allItems = _phase1Items;
+ _phase1Items = [];
+
+ var allPluginsOrdered = getAllVisiblePluginsOrdered();
+ var maxPerPlugin = 10;
+ for (var i = 0; i < allPluginsOrdered.length; i++) {
+ if (currentVersion !== _searchVersion)
+ return;
+ var plugin = allPluginsOrdered[i];
+ if (plugin.isBuiltIn) {
+ var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
+ var blLimit = Math.min(blItems.length, maxPerPlugin);
+ for (var j = 0; j < blLimit; j++) {
+ var item = transformBuiltInLauncherItem(blItems[j], plugin.id);
+ item._preScored = 900 - j;
+ allItems.push(item);
+ }
+ } else {
+ var pItems = getPluginItems(plugin.id, searchQuery, maxPerPlugin);
+ for (var j = 0; j < pItems.length; j++) {
+ pItems[j]._preScored = 900 - j;
+ allItems.push(pItems[j]);
+ }
+ }
+ }
+
+ if (currentVersion !== _searchVersion)
+ return;
+
+ var dynamicDefs = buildDynamicSectionDefs(allItems);
+ var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
+ var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, false, 50);
+
+ if (currentVersion !== _searchVersion)
+ return;
+
+ for (var i = 0; i < newSections.length; i++) {
+ var sid = newSections[i].id;
+ if (collapsedSections[sid] !== undefined)
+ newSections[i].collapsed = collapsedSections[sid];
+ }
+
+ _applyHighlights(newSections, searchQuery);
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+ isSearching = false;
+ searchCompleted();
+ }
+
+ function performFileSearch() {
+ if (!DSearchService.dsearchAvailable)
+ return;
+ var fileQuery = "";
+ var effectiveType = fileSearchType || "all";
+ var includeFiles = SettingsData.dankLauncherV2IncludeFilesInAll;
+ var includeFolders = SettingsData.dankLauncherV2IncludeFoldersInAll;
+
+ if (searchQuery.startsWith("/")) {
+ fileQuery = searchQuery.substring(1).trim();
+ } else if (searchMode === "files") {
+ fileQuery = searchQuery.trim();
+ } else if (searchMode === "all" && (includeFiles || includeFolders)) {
+ fileQuery = searchQuery.trim();
+ if (includeFiles && !includeFolders)
+ effectiveType = "file";
+ else if (!includeFiles && includeFolders)
+ effectiveType = "dir";
+ else
+ effectiveType = "all";
+ } else {
+ return;
+ }
+
+ if (fileQuery.length < 2) {
+ isFileSearching = false;
+ return;
+ }
+
+ isFileSearching = true;
+
+ var splitBothTypes = searchMode === "all" && includeFiles && includeFolders && DSearchService.supportsTypeFilter;
+ var queryTypes = splitBothTypes ? ["file", "dir"] : [effectiveType];
+ var pending = queryTypes.length;
+ var aggregatedItems = [];
+
+ for (var t = 0; t < queryTypes.length; t++) {
+ var queryType = queryTypes[t];
+ var params = {
+ limit: 20,
+ fuzzy: true,
+ sort: fileSearchSort || "score",
+ desc: true
+ };
+
+ if (DSearchService.supportsTypeFilter) {
+ params.type = (queryType && queryType !== "all") ? queryType : "all";
+ }
+ if (fileSearchExt) {
+ params.ext = fileSearchExt;
+ }
+ if (fileSearchFolder) {
+ params.folder = fileSearchFolder;
+ }
+
+ DSearchService.search(fileQuery, params, function (response) {
+ pending--;
+ if (!response.error) {
+ var hits = response.result?.hits || [];
+ for (var i = 0; i < hits.length; i++) {
+ var hit = hits[i];
+ var docTypes = hit.locations?.doc_type;
+ var isDir = docTypes ? !!docTypes["dir"] : false;
+ aggregatedItems.push(transformFileResult({
+ path: hit.id || "",
+ score: hit.score || 0,
+ is_dir: isDir
+ }));
+ }
+ }
+ if (pending > 0)
+ return;
+
+ isFileSearching = false;
+ _applyFileSearchResults(aggregatedItems, effectiveType);
+ });
+ }
+ }
+
+ function _applyFileSearchResults(fileItems, effectiveType) {
+ var fileSections = [];
+ var showType = effectiveType;
+ var order = SettingsData.launcherPluginOrder || [];
+ var filesOrderIdx = order.indexOf("__files");
+ var foldersOrderIdx = order.indexOf("__folders");
+ var filesPriority = filesOrderIdx !== -1 ? 2.6 + filesOrderIdx * 0.01 : 4;
+ var foldersPriority = foldersOrderIdx !== -1 ? 2.6 + foldersOrderIdx * 0.01 : 4.1;
+
+ if (showType === "all" && DSearchService.supportsTypeFilter) {
+ var onlyFiles = [];
+ var onlyDirs = [];
+ for (var j = 0; j < fileItems.length; j++) {
+ if (fileItems[j].data?.is_dir)
+ onlyDirs.push(fileItems[j]);
+ else
+ onlyFiles.push(fileItems[j]);
+ }
+ if (onlyFiles.length > 0) {
+ fileSections.push({
+ id: "files",
+ title: I18n.tr("Files"),
+ icon: "insert_drive_file",
+ priority: filesPriority,
+ items: onlyFiles,
+ collapsed: collapsedSections["files"] || false,
+ flatStartIndex: 0
+ });
+ }
+ if (onlyDirs.length > 0) {
+ fileSections.push({
+ id: "folders",
+ title: I18n.tr("Folders"),
+ icon: "folder",
+ priority: foldersPriority,
+ items: onlyDirs,
+ collapsed: collapsedSections["folders"] || false,
+ flatStartIndex: 0
+ });
+ }
+ } else {
+ var filesIcon = showType === "dir" ? "folder" : showType === "file" ? "insert_drive_file" : "folder";
+ var filesTitle = showType === "dir" ? I18n.tr("Folders") : I18n.tr("Files");
+ var singlePriority = showType === "dir" ? foldersPriority : filesPriority;
+ if (fileItems.length > 0) {
+ fileSections.push({
+ id: "files",
+ title: filesTitle,
+ icon: filesIcon,
+ priority: singlePriority,
+ items: fileItems,
+ collapsed: collapsedSections["files"] || false,
+ flatStartIndex: 0
+ });
+ }
+ }
+
+ var newSections;
+ if (searchMode === "files") {
+ newSections = fileSections;
+ } else {
+ var existingNonFile = sections.filter(function (s) {
+ return s.id !== "files" && s.id !== "folders";
+ });
+ newSections = existingNonFile.concat(fileSections);
+ }
+ newSections.sort(function (a, b) {
+ return a.priority - b.priority;
+ });
+ _applyHighlights(newSections, searchQuery);
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+ selectedFlatIndex = getFirstItemIndex();
+ updateSelectedItem();
+ }
+
+ function searchApps(query) {
+ var apps = AppSearchService.searchApplications(query);
+ var items = [];
+
+ for (var i = 0; i < apps.length; i++) {
+ items.push(getOrTransformApp(apps[i]));
+ }
+
+ var coreApps = AppSearchService.getCoreApps(query);
+ for (var i = 0; i < coreApps.length; i++) {
+ items.push(transformCoreApp(coreApps[i]));
+ }
+
+ return items;
+ }
+
+ function transformApp(app) {
+ var appId = app.id || app.execString || app.exec || "";
+ var override = SessionData.getAppOverride(appId);
+ return Transform.transformApp(app, override, [], I18n.tr("Launch"));
+ }
+
+ function transformCoreApp(app) {
+ return Transform.transformCoreApp(app, I18n.tr("Open"));
+ }
+
+ function transformBuiltInLauncherItem(item, pluginId) {
+ return Transform.transformBuiltInLauncherItem(item, pluginId, I18n.tr("Open"));
+ }
+
+ function transformFileResult(file) {
+ return Transform.transformFileResult(file, I18n.tr("Open"), I18n.tr("Open folder"), I18n.tr("Copy path"), I18n.tr("Open in terminal"));
+ }
+
+ function detectTrigger(query) {
+ if (!query || query.length === 0)
+ return {
+ pluginId: null,
+ query: query
+ };
+
+ var pluginTriggers = PluginService.getAllPluginTriggers();
+ for (var trigger in pluginTriggers) {
+ if (trigger && query.startsWith(trigger)) {
+ return {
+ pluginId: pluginTriggers[trigger],
+ query: query.substring(trigger.length).trim()
+ };
+ }
+ }
+
+ var builtInTriggers = AppSearchService.getBuiltInLauncherTriggers();
+ for (var trigger in builtInTriggers) {
+ if (trigger && query.startsWith(trigger)) {
+ return {
+ pluginId: builtInTriggers[trigger],
+ query: query.substring(trigger.length).trim(),
+ isBuiltIn: true
+ };
+ }
+ }
+
+ return {
+ pluginId: null,
+ query: query
+ };
+ }
+
+ function getEmptyTriggerPlugins() {
+ var plugins = PluginService.getPluginsWithEmptyTrigger();
+ var visible = plugins.filter(function (pluginId) {
+ return SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ });
+ return sortPluginIdsByOrder(visible);
+ }
+
+ function getAllLauncherPluginIds() {
+ var launchers = PluginService.getLauncherPlugins();
+ return Object.keys(launchers);
+ }
+
+ function getVisibleLauncherPluginIds() {
+ var launchers = PluginService.getLauncherPlugins();
+ var visible = Object.keys(launchers).filter(function (pluginId) {
+ return SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ });
+ return sortPluginIdsByOrder(visible);
+ }
+
+ function getAllBuiltInLauncherIds() {
+ var launchers = AppSearchService.getBuiltInLauncherPlugins();
+ return Object.keys(launchers);
+ }
+
+ function getVisibleBuiltInLauncherIds() {
+ var launchers = AppSearchService.getBuiltInLauncherPlugins();
+ var visible = Object.keys(launchers).filter(function (pluginId) {
+ return SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ });
+ return sortPluginIdsByOrder(visible);
+ }
+
+ function sortPluginIdsByOrder(pluginIds) {
+ return Utils.sortPluginIdsByOrder(pluginIds, SettingsData.launcherPluginOrder || []);
+ }
+
+ function getAllVisiblePluginsOrdered() {
+ var thirdPartyLaunchers = PluginService.getLauncherPlugins() || {};
+ var builtInLaunchers = AppSearchService.getBuiltInLauncherPlugins() || {};
+ var all = [];
+ for (var id in thirdPartyLaunchers) {
+ if (SettingsData.getPluginAllowWithoutTrigger(id))
+ all.push({
+ id: id,
+ isBuiltIn: false
+ });
+ }
+ for (var id in builtInLaunchers) {
+ if (SettingsData.getPluginAllowWithoutTrigger(id))
+ all.push({
+ id: id,
+ isBuiltIn: true
+ });
+ }
+ return Utils.sortPluginsOrdered(all, SettingsData.launcherPluginOrder || []);
+ }
+
+ function getEmptyTriggerPluginsOrdered() {
+ var thirdParty = PluginService.getPluginsWithEmptyTrigger() || [];
+ var builtIn = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger() || [];
+ var all = [];
+ for (var i = 0; i < thirdParty.length; i++) {
+ var id = thirdParty[i];
+ if (SettingsData.getPluginAllowWithoutTrigger(id))
+ all.push({
+ id: id,
+ isBuiltIn: false
+ });
+ }
+ for (var i = 0; i < builtIn.length; i++) {
+ var id = builtIn[i];
+ if (SettingsData.getPluginAllowWithoutTrigger(id))
+ all.push({
+ id: id,
+ isBuiltIn: true
+ });
+ }
+ return Utils.sortPluginsOrdered(all, SettingsData.launcherPluginOrder || []);
+ }
+
+ function getPluginBrowseItems() {
+ var items = [];
+ var browseLabel = I18n.tr("Browse");
+ var triggerLabel = I18n.tr("Trigger: %1");
+ var noTriggerLabel = I18n.tr("No trigger");
+
+ var launchers = PluginService.getLauncherPlugins();
+ for (var pluginId in launchers) {
+ var trigger = PluginService.getPluginTrigger(pluginId);
+ var isAllowed = SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ items.push(Transform.createPluginBrowseItem(pluginId, launchers[pluginId], trigger, false, isAllowed, browseLabel, triggerLabel, noTriggerLabel));
+ }
+
+ var builtInLaunchers = AppSearchService.getBuiltInLauncherPlugins();
+ for (var pluginId in builtInLaunchers) {
+ var trigger = AppSearchService.getBuiltInPluginTrigger(pluginId);
+ var isAllowed = SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ items.push(Transform.createPluginBrowseItem(pluginId, builtInLaunchers[pluginId], trigger, true, isAllowed, browseLabel, triggerLabel, noTriggerLabel));
+ }
+
+ return items;
+ }
+
+ function getBuiltInEmptyTriggerLaunchers() {
+ var plugins = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger();
+ var visible = plugins.filter(function (pluginId) {
+ return SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ });
+ return sortPluginIdsByOrder(visible);
+ }
+
+ function getPluginItems(pluginId, query, limit) {
+ var items = AppSearchService.getPluginItemsForPlugin(pluginId, query);
+ var count = limit > 0 && limit < items.length ? limit : items.length;
+ var transformed = [];
+
+ for (var i = 0; i < count; i++) {
+ transformed.push(transformPluginItem(items[i], pluginId));
+ }
+
+ return transformed;
+ }
+
+ function getPluginName(pluginId, isBuiltIn) {
+ if (isBuiltIn) {
+ var plugin = AppSearchService.builtInPlugins[pluginId];
+ return plugin ? plugin.name : pluginId;
+ }
+ var launchers = PluginService.getLauncherPlugins();
+ if (launchers[pluginId]) {
+ return launchers[pluginId].name || pluginId;
+ }
+ return pluginId;
+ }
+
+ function getPluginMetadata(pluginId) {
+ var builtIn = AppSearchService.builtInPlugins[pluginId];
+ if (builtIn) {
+ return {
+ name: builtIn.name || pluginId,
+ icon: builtIn.cornerIcon || "extension"
+ };
+ }
+ var launchers = PluginService.getLauncherPlugins();
+ if (launchers[pluginId]) {
+ var rawIcon = launchers[pluginId].icon || "extension";
+ return {
+ name: launchers[pluginId].name || pluginId,
+ icon: Utils.stripIconPrefix(rawIcon)
+ };
+ }
+ return {
+ name: pluginId,
+ icon: "extension"
+ };
+ }
+
+ function buildDynamicSectionDefs(items) {
+ var baseDefs = sectionDefinitions.slice();
+ var pluginSections = {};
+ var order = SettingsData.launcherPluginOrder || [];
+ var orderMap = {};
+ for (var k = 0; k < order.length; k++)
+ orderMap[order[k]] = k;
+ var unorderedPriority = 2.6 + order.length * 0.01;
+
+ for (var i = 0; i < items.length; i++) {
+ var section = items[i].section;
+ if (!section || !section.startsWith("plugin_"))
+ continue;
+ if (pluginSections[section])
+ continue;
+ var pluginId = section.substring(7);
+ var meta = getPluginMetadata(pluginId);
+ var viewPref = getPluginViewPref(pluginId);
+ var orderIdx = orderMap[pluginId];
+ var priority;
+ if (orderIdx !== undefined) {
+ priority = 2.6 + orderIdx * 0.01;
+ } else {
+ priority = unorderedPriority;
+ unorderedPriority += 0.01;
+ }
+
+ pluginSections[section] = {
+ id: section,
+ title: meta.name,
+ icon: meta.icon,
+ priority: priority,
+ defaultViewMode: viewPref.mode || "list"
+ };
+
+ if (viewPref.mode)
+ setPluginViewPreference(section, viewPref.mode, viewPref.enforced);
+ }
+
+ for (var sectionId in pluginSections) {
+ baseDefs.push(pluginSections[sectionId]);
+ }
+
+ baseDefs.sort(function (a, b) {
+ return a.priority - b.priority;
+ });
+ return baseDefs;
+ }
+
+ function getPluginViewPref(pluginId) {
+ var builtIn = AppSearchService.builtInPlugins[pluginId];
+ if (builtIn && builtIn.viewMode) {
+ return {
+ mode: builtIn.viewMode,
+ enforced: builtIn.viewModeEnforced === true
+ };
+ }
+
+ var pref = PluginService.getPluginViewPreference(pluginId);
+ if (pref && pref.mode) {
+ return pref;
+ }
+
+ return {
+ mode: "list",
+ enforced: false
+ };
+ }
+
+ function transformPluginItem(item, pluginId) {
+ return Transform.transformPluginItem(item, pluginId, I18n.tr("Select"));
+ }
+
+ function getFrecencyForItem(item) {
+ if (item.type !== "app")
+ return null;
+
+ var appId = item.id;
+ var usageRanking = AppUsageHistoryData.appUsageRanking || {};
+
+ var idVariants = [appId, appId.replace(".desktop", "")];
+ var usageData = null;
+
+ for (var i = 0; i < idVariants.length; i++) {
+ if (usageRanking[idVariants[i]]) {
+ usageData = usageRanking[idVariants[i]];
+ break;
+ }
+ }
+
+ return {
+ usageCount: usageData?.usageCount || 0
+ };
+ }
+
+ function getFirstItemIndex() {
+ return Nav.getFirstItemIndex(flatModel);
+ }
+
+ function _getCachedModeData(mode) {
+ return _modeSectionsCache[mode] || null;
+ }
+
+ function _setCachedModeData(mode, sectionsData, flatModelData) {
+ var cache = Object.assign({}, _modeSectionsCache);
+ cache[mode] = {
+ sections: sectionsData,
+ flatModel: flatModelData
+ };
+ _modeSectionsCache = cache;
+ }
+
+ function _clearModeCache() {
+ _modeSectionsCache = {};
+ }
+
+ function _saveDiskCache(sectionsData) {
+ var serializable = [];
+ for (var i = 0; i < sectionsData.length; i++) {
+ var s = sectionsData[i];
+ var items = [];
+ var srcItems = s.items || [];
+ for (var j = 0; j < srcItems.length; j++) {
+ var it = srcItems[j];
+ items.push({
+ id: it.id,
+ type: it.type,
+ name: it.name || "",
+ subtitle: it.subtitle || "",
+ icon: it.icon || "",
+ iconType: it.iconType || "image",
+ iconFull: it.iconFull || "",
+ section: it.section || "",
+ isCore: it.isCore || false,
+ isBuiltInLauncher: it.isBuiltInLauncher || false,
+ pluginId: it.pluginId || ""
+ });
+ }
+ serializable.push({
+ id: s.id,
+ title: s.title || "",
+ icon: s.icon || "",
+ priority: s.priority || 0,
+ items: items
+ });
+ }
+ CacheData.saveLauncherCache(serializable);
+ }
+
+ function _actionsFromDesktopEntry(appId) {
+ if (!appId)
+ return [];
+ var entry = DesktopEntries.heuristicLookup(appId);
+ if (!entry || !entry.actions || entry.actions.length === 0)
+ return [];
+ var result = [];
+ for (var i = 0; i < entry.actions.length; i++) {
+ result.push({
+ name: entry.actions[i].name,
+ icon: "play_arrow",
+ actionData: entry.actions[i]
+ });
+ }
+ return result;
+ }
+
+ function _loadDiskCache() {
+ var cached = CacheData.loadLauncherCache();
+ if (!cached || !Array.isArray(cached) || cached.length === 0)
+ return null;
+
+ var sectionsData = [];
+ for (var i = 0; i < cached.length; i++) {
+ var s = cached[i];
+ var items = [];
+ var srcItems = s.items || [];
+ for (var j = 0; j < srcItems.length; j++) {
+ var it = srcItems[j];
+ items.push({
+ id: it.id || "",
+ type: it.type || "app",
+ name: it.name || "",
+ subtitle: it.subtitle || "",
+ icon: it.icon || "",
+ iconType: it.iconType || "image",
+ iconFull: it.iconFull || "",
+ section: it.section || "",
+ isCore: it.isCore || false,
+ isBuiltInLauncher: it.isBuiltInLauncher || false,
+ pluginId: it.pluginId || "",
+ data: {
+ id: it.id
+ },
+ actions: _actionsFromDesktopEntry(it.id),
+ primaryAction: it.type === "app" && !it.isCore ? {
+ name: I18n.tr("Launch"),
+ icon: "open_in_new",
+ action: "launch"
+ } : null,
+ _diskCached: true,
+ _hName: "",
+ _hSub: "",
+ _hRich: false,
+ _preScored: undefined
+ });
+ }
+ sectionsData.push({
+ id: s.id || "",
+ title: s.title || "",
+ icon: s.icon || "",
+ priority: s.priority || 0,
+ items: items,
+ collapsed: false,
+ flatStartIndex: 0
+ });
+ }
+ return sectionsData;
+ }
+
+ function updateSelectedItem() {
+ if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
+ var entry = flatModel[selectedFlatIndex];
+ selectedItem = entry.isHeader ? null : entry.item;
+ } else {
+ selectedItem = null;
+ }
+ }
+
+ function _applyHighlights(sectionsData, query) {
+ if (!query || query.length === 0) {
+ for (var i = 0; i < sectionsData.length; i++) {
+ var items = sectionsData[i].items;
+ for (var j = 0; j < items.length; j++) {
+ var item = items[j];
+ item._hName = item.name || "";
+ item._hSub = item.subtitle || "";
+ item._hRich = false;
+ }
+ }
+ return;
+ }
+
+ var highlightColor = Theme.primary;
+ var nameColor = Theme.surfaceText;
+ var subColor = Theme.surfaceVariantText;
+ var lowerQuery = query.toLowerCase();
+
+ for (var i = 0; i < sectionsData.length; i++) {
+ var items = sectionsData[i].items;
+ for (var j = 0; j < items.length; j++) {
+ var item = items[j];
+ item._hName = _highlightField(item.name || "", lowerQuery, query.length, nameColor, highlightColor);
+ item._hSub = _highlightField(item.subtitle || "", lowerQuery, query.length, subColor, highlightColor);
+ item._hRich = true;
+ }
+ }
+ }
+
+ function _highlightField(text, lowerQuery, queryLen, baseColor, highlightColor) {
+ if (!text)
+ return "";
+ var idx = text.toLowerCase().indexOf(lowerQuery);
+ if (idx === -1)
+ return text;
+ var before = text.substring(0, idx);
+ var match = text.substring(idx, idx + queryLen);
+ var after = text.substring(idx + queryLen);
+ return '<span style="color:' + baseColor + '">' + before + '</span><span style="color:' + highlightColor + '; font-weight:600">' + match + '</span><span style="color:' + baseColor + '">' + after + '</span>';
+ }
+
+ function getCurrentSectionViewMode() {
+ if (selectedFlatIndex < 0 || selectedFlatIndex >= flatModel.length)
+ return "list";
+ var entry = flatModel[selectedFlatIndex];
+ if (!entry || entry.isHeader)
+ return "list";
+ return getSectionViewMode(entry.sectionId);
+ }
+
+ function getGridColumns(sectionId) {
+ return Nav.getGridColumns(getSectionViewMode(sectionId), gridColumns);
+ }
+
+ function _cancelPendingSelectionReset() {
+ _queryDrivenSearch = false;
+ _pluginPhaseForceFirst = false;
+ }
+
+ function selectNext() {
+ keyboardNavigationActive = true;
+ _cancelPendingSelectionReset();
+ var newIndex = Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
+ if (newIndex !== selectedFlatIndex) {
+ selectedFlatIndex = newIndex;
+ updateSelectedItem();
+ }
+ }
+
+ function selectPrevious() {
+ keyboardNavigationActive = true;
+ _cancelPendingSelectionReset();
+ var newIndex = Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
+ if (newIndex !== selectedFlatIndex) {
+ selectedFlatIndex = newIndex;
+ updateSelectedItem();
+ }
+ }
+
+ function selectRight() {
+ keyboardNavigationActive = true;
+ _cancelPendingSelectionReset();
+ var newIndex = Nav.calculateRightIndex(flatModel, selectedFlatIndex, getSectionViewMode);
+ if (newIndex !== selectedFlatIndex) {
+ selectedFlatIndex = newIndex;
+ updateSelectedItem();
+ }
+ }
+
+ function selectLeft() {
+ keyboardNavigationActive = true;
+ _cancelPendingSelectionReset();
+ var newIndex = Nav.calculateLeftIndex(flatModel, selectedFlatIndex, getSectionViewMode);
+ if (newIndex !== selectedFlatIndex) {
+ selectedFlatIndex = newIndex;
+ updateSelectedItem();
+ }
+ }
+
+ function selectNextSection() {
+ keyboardNavigationActive = true;
+ _cancelPendingSelectionReset();
+ var newIndex = Nav.calculateNextSectionIndex(flatModel, selectedFlatIndex);
+ if (newIndex !== selectedFlatIndex) {
+ selectedFlatIndex = newIndex;
+ updateSelectedItem();
+ }
+ }
+
+ function selectPreviousSection() {
+ keyboardNavigationActive = true;
+ _cancelPendingSelectionReset();
+ var newIndex = Nav.calculatePrevSectionIndex(flatModel, selectedFlatIndex);
+ if (newIndex !== selectedFlatIndex) {
+ selectedFlatIndex = newIndex;
+ updateSelectedItem();
+ }
+ }
+
+ function selectPageDown(visibleItems) {
+ keyboardNavigationActive = true;
+ _cancelPendingSelectionReset();
+ var newIndex = Nav.calculatePageDownIndex(flatModel, selectedFlatIndex, visibleItems);
+ if (newIndex !== selectedFlatIndex) {
+ selectedFlatIndex = newIndex;
+ updateSelectedItem();
+ }
+ }
+
+ function selectPageUp(visibleItems) {
+ keyboardNavigationActive = true;
+ _cancelPendingSelectionReset();
+ var newIndex = Nav.calculatePageUpIndex(flatModel, selectedFlatIndex, visibleItems);
+ if (newIndex !== selectedFlatIndex) {
+ selectedFlatIndex = newIndex;
+ updateSelectedItem();
+ }
+ }
+
+ function selectIndex(index) {
+ keyboardNavigationActive = false;
+ if (index >= 0 && index < flatModel.length && !flatModel[index].isHeader) {
+ selectedFlatIndex = index;
+ updateSelectedItem();
+ }
+ }
+
+ function toggleSection(sectionId) {
+ _clearModeCache();
+ var newCollapsed = Object.assign({}, collapsedSections);
+ var currentState = newCollapsed[sectionId];
+
+ if (currentState === undefined) {
+ for (var i = 0; i < sections.length; i++) {
+ if (sections[i].id === sectionId) {
+ currentState = sections[i].collapsed || false;
+ break;
+ }
+ }
+ }
+
+ newCollapsed[sectionId] = !currentState;
+ collapsedSections = newCollapsed;
+
+ var newSections = sections.slice();
+ for (var i = 0; i < newSections.length; i++) {
+ if (newSections[i].id === sectionId) {
+ newSections[i] = Object.assign({}, newSections[i], {
+ collapsed: newCollapsed[sectionId]
+ });
+ }
+ }
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+
+ if (selectedFlatIndex >= flatModel.length) {
+ selectedFlatIndex = getFirstItemIndex();
+ }
+ updateSelectedItem();
+ }
+
+ function executeSelected() {
+ if (searchDebounce.running) {
+ searchDebounce.stop();
+ performSearch();
+ }
+ if (!selectedItem)
+ return;
+ executeItem(selectedItem);
+ }
+
+ function executeItem(item) {
+ if (!item)
+ return;
+
+ SessionData.addLauncherHistory(searchQuery);
+
+ if (item.type === "plugin_browse") {
+ var browsePluginId = item.data?.pluginId;
+ if (!browsePluginId)
+ return;
+ var browseTrigger = item.data.isBuiltIn ? AppSearchService.getBuiltInPluginTrigger(browsePluginId) : PluginService.getPluginTrigger(browsePluginId);
+
+ if (browseTrigger && browseTrigger.length > 0) {
+ searchQueryRequested(browseTrigger);
+ } else {
+ setMode("plugins");
+ pluginFilter = browsePluginId;
+ performSearch();
+ }
+ return;
+ }
+
+ switch (item.type) {
+ case "app":
+ if (item.isCore) {
+ AppSearchService.executeCoreApp(item.data);
+ } else if (item.data?.isAction) {
+ launchAppAction(item.data);
+ } else {
+ launchApp(item.data);
+ }
+ break;
+ case "plugin":
+ if (item.isBuiltInLauncher) {
+ AppSearchService.executeBuiltInLauncherItem(item.data);
+ } else {
+ AppSearchService.executePluginItem(item.data, item.pluginId);
+ }
+ break;
+ case "file":
+ openFile(item.data?.path);
+ break;
+ default:
+ return;
+ }
+
+ itemExecuted();
+ }
+
+ function executeAction(item, action) {
+ if (!item || !action)
+ return;
+ switch (action.action) {
+ case "launch":
+ executeItem(item);
+ break;
+ case "open":
+ openFile(item.data.path);
+ break;
+ case "open_folder":
+ openFolder(item.data.path);
+ break;
+ case "copy_path":
+ copyToClipboard(item.data.path);
+ break;
+ case "open_terminal":
+ openTerminal(item.data.path);
+ break;
+ case "copy":
+ copyToClipboard(item.name);
+ break;
+ case "execute":
+ executeItem(item);
+ break;
+ case "launch_dgpu":
+ if (item.type === "app" && item.data) {
+ launchAppWithNvidia(item.data);
+ }
+ break;
+ case "toggle_all_visibility":
+ if (item.type === "plugin_browse" && item.data?.pluginId) {
+ var pluginId = item.data.pluginId;
+ var currentState = SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ SettingsData.setPluginAllowWithoutTrigger(pluginId, !currentState);
+ performSearch();
+ }
+ return;
+ default:
+ if (item.type === "app" && action.actionData) {
+ launchAppAction({
+ parentApp: item.data,
+ actionData: action.actionData
+ });
+ }
+ }
+
+ itemExecuted();
+ }
+
+ function _resolveDesktopEntry(app) {
+ if (!app)
+ return null;
+ if (app.command)
+ return app;
+ var id = app.id || app.execString || app.exec || "";
+ if (!id)
+ return null;
+ return DesktopEntries.heuristicLookup(id);
+ }
+
+ function launchApp(app) {
+ var entry = _resolveDesktopEntry(app);
+ if (!entry)
+ return;
+ SessionService.launchDesktopEntry(entry);
+ AppUsageHistoryData.addAppUsage(entry);
+ }
+
+ function launchAppWithNvidia(app) {
+ var entry = _resolveDesktopEntry(app);
+ if (!entry)
+ return;
+ SessionService.launchDesktopEntry(entry, true);
+ AppUsageHistoryData.addAppUsage(entry);
+ }
+
+ function launchAppAction(actionItem) {
+ if (!actionItem || !actionItem.actionData)
+ return;
+ var entry = _resolveDesktopEntry(actionItem.parentApp);
+ if (!entry)
+ return;
+ SessionService.launchDesktopAction(entry, actionItem.actionData);
+ AppUsageHistoryData.addAppUsage(entry);
+ }
+
+ function openFile(path) {
+ if (!path)
+ return;
+ Qt.openUrlExternally("file://" + path);
+ }
+
+ function openFolder(path) {
+ if (!path)
+ return;
+ var folder = path.substring(0, path.lastIndexOf("/"));
+ Qt.openUrlExternally("file://" + folder);
+ }
+
+ function openTerminal(path) {
+ if (!path)
+ return;
+ var terminal = Quickshell.env("TERMINAL") || "xterm";
+ Quickshell.execDetached({
+ command: [terminal],
+ workingDirectory: path
+ });
+ }
+
+ function copyToClipboard(text) {
+ if (!text)
+ return;
+ Quickshell.execDetached(["dms", "cl", "copy", text]);
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ControllerUtils.js b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ControllerUtils.js
new file mode 100644
index 0000000..a06285f
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ControllerUtils.js
@@ -0,0 +1,128 @@
+.pragma library
+
+function getFileIcon(filename) {
+ var ext = filename.lastIndexOf(".") > 0 ? filename.substring(filename.lastIndexOf(".") + 1).toLowerCase() : "";
+
+ switch (ext) {
+ case "pdf":
+ return "picture_as_pdf";
+ case "doc":
+ case "docx":
+ case "odt":
+ return "description";
+ case "xls":
+ case "xlsx":
+ case "ods":
+ return "table_chart";
+ case "ppt":
+ case "pptx":
+ case "odp":
+ return "slideshow";
+ case "txt":
+ case "md":
+ case "rst":
+ return "article";
+ case "jpg":
+ case "jpeg":
+ case "png":
+ case "gif":
+ case "svg":
+ case "webp":
+ return "image";
+ case "mp3":
+ case "wav":
+ case "flac":
+ case "ogg":
+ return "audio_file";
+ case "mp4":
+ case "mkv":
+ case "avi":
+ case "webm":
+ return "video_file";
+ case "zip":
+ case "tar":
+ case "gz":
+ case "7z":
+ case "rar":
+ return "folder_zip";
+ case "js":
+ case "ts":
+ case "py":
+ case "rs":
+ case "go":
+ case "java":
+ case "c":
+ case "cpp":
+ case "h":
+ return "code";
+ case "html":
+ case "css":
+ case "htm":
+ return "web";
+ case "json":
+ case "xml":
+ case "yaml":
+ case "yml":
+ return "data_object";
+ case "sh":
+ case "bash":
+ case "zsh":
+ return "terminal";
+ default:
+ return "insert_drive_file";
+ }
+}
+
+function stripIconPrefix(iconName) {
+ if (!iconName)
+ return "extension";
+ if (iconName.startsWith("unicode:"))
+ return iconName.substring(8);
+ if (iconName.startsWith("material:"))
+ return iconName.substring(9);
+ if (iconName.startsWith("image:"))
+ return iconName.substring(6);
+ return iconName;
+}
+
+function detectIconType(iconName) {
+ if (!iconName)
+ return "material";
+ if (iconName.startsWith("unicode:"))
+ return "unicode";
+ if (iconName.startsWith("material:"))
+ return "material";
+ if (iconName.startsWith("image:"))
+ return "image";
+ if (iconName.indexOf("/") >= 0 || iconName.indexOf(".") >= 0)
+ return "image";
+ if (/^[a-z]+-[a-z]/.test(iconName.toLowerCase()))
+ return "image";
+ return "material";
+}
+
+function sortPluginIdsByOrder(pluginIds, order) {
+ if (!order || order.length === 0)
+ return pluginIds;
+ var orderMap = {};
+ for (var i = 0; i < order.length; i++)
+ orderMap[order[i]] = i;
+ return pluginIds.slice().sort(function (a, b) {
+ var aOrder = orderMap[a] !== undefined ? orderMap[a] : 9999;
+ var bOrder = orderMap[b] !== undefined ? orderMap[b] : 9999;
+ return aOrder - bOrder;
+ });
+}
+
+function sortPluginsOrdered(plugins, order) {
+ if (!order || order.length === 0)
+ return plugins;
+ var orderMap = {};
+ for (var i = 0; i < order.length; i++)
+ orderMap[order[i]] = i;
+ return plugins.sort(function (a, b) {
+ var aOrder = orderMap[a.id] !== undefined ? orderMap[a.id] : 9999;
+ var bOrder = orderMap[b.id] !== undefined ? orderMap[b.id] : 9999;
+ return aOrder - bOrder;
+ });
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/DankLauncherV2Modal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/DankLauncherV2Modal.qml
new file mode 100644
index 0000000..859022c
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/DankLauncherV2Modal.qml
@@ -0,0 +1,465 @@
+import QtQuick
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Hyprland
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ visible: false
+
+ property bool spotlightOpen: false
+ property bool keyboardActive: false
+ property bool contentVisible: false
+ property var spotlightContent: launcherContentLoader.item
+ property bool openedFromOverview: false
+ property bool isClosing: false
+ property bool _pendingInitialize: false
+ property string _pendingQuery: ""
+ property string _pendingMode: ""
+ readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
+
+ readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
+ readonly property var effectiveScreen: launcherWindow.screen
+ readonly property real screenWidth: effectiveScreen?.width ?? 1920
+ readonly property real screenHeight: effectiveScreen?.height ?? 1080
+ readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
+
+ readonly property int baseWidth: {
+ switch (SettingsData.dankLauncherV2Size) {
+ case "micro":
+ return 500;
+ case "medium":
+ return 720;
+ case "large":
+ return 860;
+ default:
+ return 620;
+ }
+ }
+ readonly property int baseHeight: {
+ switch (SettingsData.dankLauncherV2Size) {
+ case "micro":
+ return 480;
+ case "medium":
+ return 720;
+ case "large":
+ return 860;
+ default:
+ return 600;
+ }
+ }
+ readonly property int modalWidth: Math.min(baseWidth, screenWidth - 100)
+ readonly property int modalHeight: Math.min(baseHeight, screenHeight - 100)
+ readonly property real modalX: (screenWidth - modalWidth) / 2
+ readonly property real modalY: (screenHeight - modalHeight) / 2
+
+ readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ readonly property real cornerRadius: Theme.cornerRadius
+ readonly property color borderColor: {
+ if (!SettingsData.dankLauncherV2BorderEnabled)
+ return Theme.outlineMedium;
+ switch (SettingsData.dankLauncherV2BorderColor) {
+ case "primary":
+ return Theme.primary;
+ case "secondary":
+ return Theme.secondary;
+ case "outline":
+ return Theme.outline;
+ case "surfaceText":
+ return Theme.surfaceText;
+ default:
+ return Theme.primary;
+ }
+ }
+ readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
+
+ signal dialogClosed
+
+ function _ensureContentLoadedAndInitialize(query, mode) {
+ _pendingQuery = query || "";
+ _pendingMode = mode || "";
+ _pendingInitialize = true;
+ contentVisible = true;
+ launcherContentLoader.active = true;
+
+ if (spotlightContent) {
+ _initializeAndShow(_pendingQuery, _pendingMode);
+ _pendingInitialize = false;
+ }
+ }
+
+ function _initializeAndShow(query, mode) {
+ if (!spotlightContent)
+ return;
+ contentVisible = true;
+ spotlightContent.searchField.forceActiveFocus();
+
+ var targetQuery = "";
+
+ if (query) {
+ targetQuery = query;
+ } else if (SettingsData.rememberLastQuery) {
+ targetQuery = SessionData.launcherLastQuery || "";
+ }
+
+ if (spotlightContent.searchField) {
+ spotlightContent.searchField.text = targetQuery;
+ }
+ if (spotlightContent.controller) {
+ var targetMode = mode || SessionData.launcherLastMode || "all";
+ spotlightContent.controller.searchMode = targetMode;
+ spotlightContent.controller.activePluginId = "";
+ spotlightContent.controller.activePluginName = "";
+ spotlightContent.controller.pluginFilter = "";
+ spotlightContent.controller.fileSearchType = "all";
+ spotlightContent.controller.fileSearchExt = "";
+ spotlightContent.controller.fileSearchFolder = "";
+ spotlightContent.controller.fileSearchSort = "score";
+ spotlightContent.controller.collapsedSections = {};
+ spotlightContent.controller.selectedFlatIndex = 0;
+ spotlightContent.controller.selectedItem = null;
+ spotlightContent.controller.historyIndex = -1;
+ spotlightContent.controller.searchQuery = targetQuery;
+
+ spotlightContent.controller.performSearch();
+ }
+ if (spotlightContent.resetScroll) {
+ spotlightContent.resetScroll();
+ }
+ if (spotlightContent.actionPanel) {
+ spotlightContent.actionPanel.hide();
+ }
+ }
+
+ function _finishShow(query, mode) {
+ spotlightOpen = true;
+ isClosing = false;
+ openedFromOverview = false;
+
+ keyboardActive = true;
+ ModalManager.openModal(root);
+ if (useHyprlandFocusGrab)
+ focusGrab.active = true;
+
+ _ensureContentLoadedAndInitialize(query || "", mode || "");
+ }
+
+ function show() {
+ closeCleanupTimer.stop();
+
+ var focusedScreen = CompositorService.getFocusedScreen();
+ if (focusedScreen && launcherWindow.screen !== focusedScreen) {
+ spotlightOpen = false;
+ isClosing = false;
+ launcherWindow.screen = focusedScreen;
+ Qt.callLater(() => root._finishShow("", ""));
+ return;
+ }
+
+ _finishShow("", "");
+ }
+
+ function showWithQuery(query) {
+ closeCleanupTimer.stop();
+
+ var focusedScreen = CompositorService.getFocusedScreen();
+ if (focusedScreen && launcherWindow.screen !== focusedScreen) {
+ spotlightOpen = false;
+ isClosing = false;
+ launcherWindow.screen = focusedScreen;
+ Qt.callLater(() => root._finishShow(query, ""));
+ return;
+ }
+
+ _finishShow(query, "");
+ }
+
+ function hide() {
+ if (!spotlightOpen)
+ return;
+ openedFromOverview = false;
+ isClosing = true;
+ contentVisible = false;
+
+ keyboardActive = false;
+ spotlightOpen = false;
+ focusGrab.active = false;
+ ModalManager.closeModal(root);
+
+ closeCleanupTimer.start();
+ }
+
+ function toggle() {
+ spotlightOpen ? hide() : show();
+ }
+
+ function showWithMode(mode) {
+ closeCleanupTimer.stop();
+
+ var focusedScreen = CompositorService.getFocusedScreen();
+ if (focusedScreen && launcherWindow.screen !== focusedScreen) {
+ spotlightOpen = false;
+ isClosing = false;
+ launcherWindow.screen = focusedScreen;
+ Qt.callLater(() => root._finishShow("", mode));
+ return;
+ }
+
+ spotlightOpen = true;
+ isClosing = false;
+ openedFromOverview = false;
+
+ keyboardActive = true;
+ ModalManager.openModal(root);
+ if (useHyprlandFocusGrab)
+ focusGrab.active = true;
+
+ _ensureContentLoadedAndInitialize("", mode);
+ }
+
+ function toggleWithMode(mode) {
+ if (spotlightOpen) {
+ hide();
+ } else {
+ showWithMode(mode);
+ }
+ }
+
+ function toggleWithQuery(query) {
+ if (spotlightOpen) {
+ hide();
+ } else {
+ showWithQuery(query);
+ }
+ }
+
+ Timer {
+ id: closeCleanupTimer
+ interval: Theme.modalAnimationDuration + 50
+ repeat: false
+ onTriggered: {
+ isClosing = false;
+ if (root.unloadContentOnClose)
+ launcherContentLoader.active = false;
+ dialogClosed();
+ }
+ }
+
+ Connections {
+ target: spotlightContent?.controller ?? null
+
+ function onModeChanged(mode) {
+ if (spotlightContent.controller.autoSwitchedToFiles)
+ return;
+ SessionData.setLauncherLastMode(mode);
+ }
+ }
+
+ HyprlandFocusGrab {
+ id: focusGrab
+ windows: [launcherWindow]
+ active: false
+
+ onCleared: {
+ if (spotlightOpen) {
+ hide();
+ }
+ }
+ }
+
+ Connections {
+ target: ModalManager
+ function onCloseAllModalsExcept(excludedModal) {
+ if (excludedModal !== root && spotlightOpen) {
+ hide();
+ }
+ }
+ }
+
+ Connections {
+ target: Quickshell
+ function onScreensChanged() {
+ if (Quickshell.screens.length === 0)
+ return;
+
+ const screenName = launcherWindow.screen?.name;
+ if (screenName) {
+ for (let i = 0; i < Quickshell.screens.length; i++) {
+ if (Quickshell.screens[i].name === screenName)
+ return;
+ }
+ }
+
+ if (spotlightOpen)
+ hide();
+
+ const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
+ if (newScreen)
+ launcherWindow.screen = newScreen;
+ }
+ }
+
+ PanelWindow {
+ id: launcherWindow
+ visible: spotlightOpen || isClosing
+ color: "transparent"
+ exclusionMode: ExclusionMode.Ignore
+
+ WindowBlur {
+ targetWindow: launcherWindow
+ readonly property real s: Math.min(1, modalContainer.scale)
+ blurX: root.modalX + root.modalWidth * (1 - s) * 0.5
+ blurY: root.modalY + root.modalHeight * (1 - s) * 0.5
+ blurWidth: (contentVisible && modalContainer.opacity > 0) ? root.modalWidth * s : 0
+ blurHeight: (contentVisible && modalContainer.opacity > 0) ? root.modalHeight * s : 0
+ blurRadius: root.cornerRadius
+ }
+
+ WlrLayershell.namespace: "dms:spotlight"
+ WlrLayershell.layer: {
+ switch (Quickshell.env("DMS_MODAL_LAYER")) {
+ case "bottom":
+ console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
+ return WlrLayershell.Top;
+ case "background":
+ console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
+ return WlrLayershell.Top;
+ case "overlay":
+ return WlrLayershell.Overlay;
+ default:
+ return WlrLayershell.Top;
+ }
+ }
+ WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
+
+ anchors {
+ top: true
+ bottom: true
+ left: true
+ right: true
+ }
+
+ mask: Region {
+ item: spotlightOpen ? fullScreenMask : null
+ }
+
+ Item {
+ id: fullScreenMask
+ anchors.fill: parent
+ }
+
+ Rectangle {
+ id: backgroundDarken
+ anchors.fill: parent
+ color: "black"
+ opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
+ visible: contentVisible || opacity > 0
+
+ Behavior on opacity {
+ DankAnim {
+ duration: Theme.modalAnimationDuration
+ easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: spotlightOpen
+ onClicked: mouse => {
+ var contentX = modalContainer.x;
+ var contentY = modalContainer.y;
+ var contentW = modalContainer.width;
+ var contentH = modalContainer.height;
+
+ if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
+ root.hide();
+ }
+ }
+ }
+
+ Item {
+ id: modalContainer
+ x: root.modalX
+ y: root.modalY
+ width: root.modalWidth
+ height: root.modalHeight
+ visible: contentVisible || opacity > 0
+
+ opacity: contentVisible ? 1 : 0
+ scale: contentVisible ? 1 : 0.96
+ transformOrigin: Item.Center
+
+ Behavior on opacity {
+ DankAnim {
+ duration: Theme.modalAnimationDuration
+ easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
+ }
+ }
+
+ Behavior on scale {
+ DankAnim {
+ duration: Theme.modalAnimationDuration
+ easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
+ }
+ }
+
+ ElevationShadow {
+ id: launcherShadowLayer
+ anchors.fill: parent
+ level: Theme.elevationLevel3
+ fallbackOffset: 6
+ targetColor: root.backgroundColor
+ borderColor: root.borderColor
+ borderWidth: root.borderWidth
+ targetRadius: root.cornerRadius
+ shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onPressed: mouse => mouse.accepted = true
+ }
+
+ FocusScope {
+ anchors.fill: parent
+ focus: keyboardActive
+
+ Loader {
+ id: launcherContentLoader
+ anchors.fill: parent
+ active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
+ asynchronous: false
+ sourceComponent: LauncherContent {
+ focus: true
+ parentModal: root
+ }
+
+ onLoaded: {
+ if (root._pendingInitialize) {
+ root._initializeAndShow(root._pendingQuery, root._pendingMode);
+ root._pendingInitialize = false;
+ }
+ }
+ }
+
+ Keys.onEscapePressed: event => {
+ root.hide();
+ event.accepted = true;
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ radius: root.cornerRadius
+ color: "transparent"
+ border.color: BlurService.borderColor
+ border.width: BlurService.borderWidth
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/GridItem.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/GridItem.qml
new file mode 100644
index 0000000..a7f39c3
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/GridItem.qml
@@ -0,0 +1,105 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property var item: null
+ property bool isSelected: false
+ property bool isHovered: itemArea.containsMouse
+ property var controller: null
+ property int flatIndex: -1
+
+ signal clicked
+ signal rightClicked(real mouseX, real mouseY)
+
+ readonly property string iconValue: {
+ if (!item)
+ return "";
+ switch (item.iconType) {
+ case "material":
+ case "nerd":
+ return "material:" + (item.icon || "apps");
+ case "unicode":
+ return "unicode:" + (item.icon || "");
+ case "composite":
+ return item.iconFull || "";
+ case "image":
+ default:
+ return item.icon || "";
+ }
+ }
+
+ readonly property int computedIconSize: Math.min(48, Math.max(32, width * 0.45))
+
+ radius: Theme.cornerRadius
+ color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent"
+
+ DankRipple {
+ id: rippleLayer
+ rippleColor: Theme.surfaceText
+ cornerRadius: root.radius
+ }
+
+ Column {
+ anchors.centerIn: parent
+ anchors.margins: Theme.spacingS
+ spacing: Theme.spacingS
+ width: parent.width - Theme.spacingM
+
+ AppIconRenderer {
+ width: root.computedIconSize
+ height: root.computedIconSize
+ anchors.horizontalCenter: parent.horizontalCenter
+ iconValue: root.iconValue
+ iconSize: root.computedIconSize
+ fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
+ iconColor: root.isSelected ? Theme.primary : Theme.surfaceText
+ materialIconSizeAdjustment: root.computedIconSize * 0.3
+ }
+
+ Text {
+ width: parent.width
+ text: root.item?._hName ?? root.item?.name ?? ""
+ textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ font.family: Theme.fontFamily
+ color: root.isSelected ? Theme.primary : Theme.surfaceText
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignHCenter
+ maximumLineCount: 2
+ wrapMode: Text.Wrap
+ }
+ }
+
+ MouseArea {
+ id: itemArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+
+ onPressed: mouse => {
+ if (mouse.button === Qt.LeftButton)
+ rippleLayer.trigger(mouse.x, mouse.y);
+ }
+ onClicked: mouse => {
+ if (mouse.button === Qt.RightButton) {
+ var scenePos = mapToItem(null, mouse.x, mouse.y);
+ root.rightClicked(scenePos.x, scenePos.y);
+ } else {
+ root.clicked();
+ }
+ }
+
+ onPositionChanged: {
+ if (root.controller) {
+ root.controller.keyboardNavigationActive = false;
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ItemTransformers.js b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ItemTransformers.js
new file mode 100644
index 0000000..61c108c
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ItemTransformers.js
@@ -0,0 +1,237 @@
+.pragma library
+
+ .import "ControllerUtils.js" as Utils
+
+function transformApp(app, override, defaultActions, primaryActionLabel) {
+ var appId = app.id || app.execString || app.exec || "";
+
+ var actions = [];
+ if (app.actions && app.actions.length > 0) {
+ for (var i = 0; i < app.actions.length; i++) {
+ actions.push({
+ name: app.actions[i].name,
+ icon: "play_arrow",
+ actionData: app.actions[i]
+ });
+ }
+ }
+
+ return {
+ id: appId,
+ type: "app",
+ name: override?.name || app.name || "",
+ subtitle: override?.comment || app.comment || "",
+ icon: override?.icon || app.icon || "application-x-executable",
+ iconType: "image",
+ section: "apps",
+ data: app,
+ keywords: app.keywords || [],
+ actions: actions,
+ primaryAction: {
+ name: primaryActionLabel,
+ icon: "open_in_new",
+ action: "launch"
+ },
+ _hName: "",
+ _hSub: "",
+ _hRich: false,
+ _preScored: undefined
+ };
+}
+
+function transformCoreApp(app, openLabel) {
+ var iconName = "apps";
+ var iconType = "material";
+
+ if (app.icon) {
+ if (app.icon.startsWith("svg+corner:")) {
+ iconType = "composite";
+ } else if (app.icon.startsWith("material:")) {
+ iconName = app.icon.substring(9);
+ } else {
+ iconName = app.icon;
+ iconType = "image";
+ }
+ }
+
+ return {
+ id: app.builtInPluginId || app.action || "",
+ type: "app",
+ name: app.name || "",
+ subtitle: app.comment || "",
+ icon: iconName,
+ iconType: iconType,
+ iconFull: app.icon,
+ section: "apps",
+ data: app,
+ isCore: true,
+ actions: [],
+ primaryAction: {
+ name: openLabel,
+ icon: "open_in_new",
+ action: "launch"
+ },
+ _hName: "",
+ _hSub: "",
+ _hRich: false,
+ _preScored: undefined
+ };
+}
+
+function transformBuiltInLauncherItem(item, pluginId, openLabel) {
+ var rawIcon = item.icon || "extension";
+ var icon = Utils.stripIconPrefix(rawIcon);
+ var iconType = item.iconType;
+ if (!iconType) {
+ if (rawIcon.startsWith("material:"))
+ iconType = "material";
+ else if (rawIcon.startsWith("unicode:"))
+ iconType = "unicode";
+ else
+ iconType = "image";
+ }
+
+ return {
+ id: item.action || "",
+ type: "plugin",
+ name: item.name || "",
+ subtitle: item.comment || "",
+ icon: icon,
+ iconType: iconType,
+ section: "plugin_" + pluginId,
+ data: item,
+ pluginId: pluginId,
+ isBuiltInLauncher: true,
+ keywords: item.keywords || [],
+ actions: [],
+ primaryAction: {
+ name: openLabel,
+ icon: "open_in_new",
+ action: "execute"
+ },
+ _hName: "",
+ _hSub: "",
+ _hRich: false,
+ _preScored: item._preScored
+ };
+}
+
+function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel, openTerminalLabel) {
+ var filename = file.path ? file.path.split("/").pop() : "";
+ var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : "";
+ var isDir = file.is_dir || false;
+
+ var actions = [];
+ if (isDir) {
+ if (openTerminalLabel) {
+ actions.push({
+ name: openTerminalLabel,
+ icon: "terminal",
+ action: "open_terminal"
+ });
+ }
+ } else {
+ actions.push({
+ name: openFolderLabel,
+ icon: "folder_open",
+ action: "open_folder"
+ });
+ }
+ actions.push({
+ name: copyPathLabel,
+ icon: "content_copy",
+ action: "copy_path"
+ });
+
+ return {
+ id: file.path || "",
+ type: "file",
+ name: filename,
+ subtitle: dirname,
+ icon: isDir ? "folder" : Utils.getFileIcon(filename),
+ iconType: "material",
+ section: "files",
+ data: file,
+ actions: actions,
+ primaryAction: {
+ name: openLabel,
+ icon: "open_in_new",
+ action: "open"
+ },
+ _hName: "",
+ _hSub: "",
+ _hRich: false,
+ _preScored: undefined
+ };
+}
+
+function transformPluginItem(item, pluginId, selectLabel) {
+ var rawIcon = item.icon || "extension";
+ var icon = Utils.stripIconPrefix(rawIcon);
+ var iconType = item.iconType;
+ if (!iconType) {
+ if (rawIcon.startsWith("material:"))
+ iconType = "material";
+ else if (rawIcon.startsWith("unicode:"))
+ iconType = "unicode";
+ else
+ iconType = "image";
+ }
+
+ return {
+ id: item.id || item.name || "",
+ type: "plugin",
+ name: item.name || "",
+ subtitle: item.comment || item.description || "",
+ icon: icon,
+ iconType: iconType,
+ section: "plugin_" + pluginId,
+ data: item,
+ pluginId: pluginId,
+ keywords: item.keywords || [],
+ actions: item.actions || [],
+ primaryAction: item.primaryAction || {
+ name: selectLabel,
+ icon: "check",
+ action: "execute"
+ },
+ _hName: "",
+ _hSub: "",
+ _hRich: false,
+ _preScored: item._preScored
+ };
+}
+
+function createPluginBrowseItem(pluginId, plugin, trigger, isBuiltIn, isAllowed, browseLabel, triggerLabel, noTriggerLabel) {
+ var rawIcon = isBuiltIn ? (plugin.cornerIcon || "extension") : (plugin.icon || "extension");
+ return {
+ id: "browse_" + pluginId,
+ type: "plugin_browse",
+ name: plugin.name || pluginId,
+ subtitle: trigger ? triggerLabel.replace("%1", trigger) : noTriggerLabel,
+ icon: isBuiltIn ? rawIcon : Utils.stripIconPrefix(rawIcon),
+ iconType: isBuiltIn ? "material" : Utils.detectIconType(rawIcon),
+ section: "browse_plugins",
+ data: {
+ pluginId: pluginId,
+ plugin: plugin,
+ isBuiltIn: isBuiltIn
+ },
+ actions: [
+ {
+ name: "All",
+ icon: isAllowed ? "visibility" : "visibility_off",
+ action: "toggle_all_visibility"
+ }
+ ],
+ primaryAction: {
+ name: browseLabel,
+ icon: "arrow_forward",
+ action: "browse_plugin"
+ },
+ _hName: "",
+ _hSub: "",
+ _hRich: false,
+ _preScored: undefined
+ };
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/LauncherContent.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/LauncherContent.qml
new file mode 100644
index 0000000..4372515
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/LauncherContent.qml
@@ -0,0 +1,1075 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+FocusScope {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property var parentModal: null
+ property string viewModeContext: "spotlight"
+ property alias searchField: searchField
+ property alias controller: controller
+ property alias resultsList: resultsList
+ property alias actionPanel: actionPanel
+
+ property bool editMode: false
+ property var editingApp: null
+ property string editAppId: ""
+
+ function resetScroll() {
+ resultsList.resetScroll();
+ }
+
+ function focusSearchField() {
+ searchField.forceActiveFocus();
+ }
+
+ function openEditMode(app) {
+ if (!app)
+ return;
+ editingApp = app;
+ editAppId = app.id || app.execString || app.exec || "";
+ var existing = SessionData.getAppOverride(editAppId);
+ editNameField.text = existing?.name || "";
+ editIconField.text = existing?.icon || "";
+ editCommentField.text = existing?.comment || "";
+ editEnvVarsField.text = existing?.envVars || "";
+ editExtraFlagsField.text = existing?.extraFlags || "";
+ editDgpuToggle.checked = existing?.launchOnDgpu || false;
+ editMode = true;
+ Qt.callLater(() => editNameField.forceActiveFocus());
+ }
+
+ function closeEditMode() {
+ editMode = false;
+ editingApp = null;
+ editAppId = "";
+ Qt.callLater(() => searchField.forceActiveFocus());
+ }
+
+ function saveAppOverride() {
+ var override = {};
+ if (editNameField.text.trim())
+ override.name = editNameField.text.trim();
+ if (editIconField.text.trim())
+ override.icon = editIconField.text.trim();
+ if (editCommentField.text.trim())
+ override.comment = editCommentField.text.trim();
+ if (editEnvVarsField.text.trim())
+ override.envVars = editEnvVarsField.text.trim();
+ if (editExtraFlagsField.text.trim())
+ override.extraFlags = editExtraFlagsField.text.trim();
+ if (editDgpuToggle.checked)
+ override.launchOnDgpu = true;
+ SessionData.setAppOverride(editAppId, override);
+ closeEditMode();
+ }
+
+ function resetAppOverride() {
+ SessionData.clearAppOverride(editAppId);
+ closeEditMode();
+ }
+
+ function showContextMenu(item, x, y, fromKeyboard) {
+ if (!item)
+ return;
+ if (!contextMenu.hasContextMenuActions(item))
+ return;
+ contextMenu.show(x, y, item, fromKeyboard);
+ }
+
+ anchors.fill: parent
+ focus: true
+
+ Controller {
+ id: controller
+ active: root.parentModal?.spotlightOpen ?? true
+ viewModeContext: root.viewModeContext
+
+ onItemExecuted: {
+ if (root.parentModal) {
+ root.parentModal.hide();
+ }
+ if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview) {
+ NiriService.toggleOverview();
+ }
+ }
+ }
+
+ LauncherContextMenu {
+ id: contextMenu
+ parent: root
+ controller: root.controller
+ searchField: root.searchField
+ parentHandler: root
+
+ onEditAppRequested: app => {
+ root.openEditMode(app);
+ }
+ }
+
+ Keys.onPressed: event => {
+ if (editMode) {
+ if (event.key === Qt.Key_Escape) {
+ closeEditMode();
+ event.accepted = true;
+ }
+ return;
+ }
+
+ var hasCtrl = event.modifiers & Qt.ControlModifier;
+ event.accepted = true;
+
+ switch (event.key) {
+ case Qt.Key_Escape:
+ if (actionPanel.expanded) {
+ actionPanel.hide();
+ return;
+ }
+ if (controller.clearPluginFilter())
+ return;
+ if (root.parentModal)
+ root.parentModal.hide();
+ return;
+ case Qt.Key_Backspace:
+ if (searchField.text.length === 0) {
+ if (controller.clearPluginFilter())
+ return;
+ if (controller.autoSwitchedToFiles) {
+ controller.restorePreviousMode();
+ return;
+ }
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_Down:
+ if (hasCtrl) {
+ controller.navigateHistory("down");
+ } else {
+ controller.selectNext();
+ }
+ return;
+ case Qt.Key_Up:
+ if (hasCtrl) {
+ controller.navigateHistory("up");
+ } else {
+ controller.selectPrevious();
+ }
+ return;
+ case Qt.Key_PageDown:
+ controller.selectPageDown(8);
+ return;
+ case Qt.Key_PageUp:
+ controller.selectPageUp(8);
+ return;
+ case Qt.Key_Right:
+ if (hasCtrl) {
+ controller.cycleMode();
+ return;
+ }
+ if (controller.getCurrentSectionViewMode() !== "list") {
+ controller.selectRight();
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_Left:
+ if (hasCtrl) {
+ const reverse = true;
+ controller.cycleMode(reverse);
+ return;
+ }
+ if (controller.getCurrentSectionViewMode() !== "list") {
+ controller.selectLeft();
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_H:
+ if (hasCtrl) {
+ const reverse = true;
+ controller.cycleMode(reverse);
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_J:
+ if (hasCtrl) {
+ controller.selectNext();
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_K:
+ if (hasCtrl) {
+ controller.selectPrevious();
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_L:
+ if (hasCtrl) {
+ controller.cycleMode();
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_N:
+ if (hasCtrl) {
+ controller.selectNextSection();
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_P:
+ if (hasCtrl) {
+ controller.selectPreviousSection();
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_Tab:
+ if (hasCtrl && actionPanel.hasActions) {
+ actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
+ return;
+ }
+ controller.selectNext();
+ return;
+ case Qt.Key_Backtab:
+ if (hasCtrl && actionPanel.expanded) {
+ const reverse = true;
+ actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show();
+ return;
+ }
+ controller.selectPrevious();
+ return;
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ if (event.modifiers & Qt.ShiftModifier) {
+ controller.pasteSelected();
+ return;
+ }
+ if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) {
+ actionPanel.executeSelectedAction();
+ } else {
+ controller.executeSelected();
+ }
+ return;
+ case Qt.Key_Menu:
+ case Qt.Key_F10:
+ if (contextMenu.hasContextMenuActions(controller.selectedItem)) {
+ var scenePos = resultsList.getSelectedItemPosition();
+ var localPos = root.mapFromItem(null, scenePos.x, scenePos.y);
+ showContextMenu(controller.selectedItem, localPos.x, localPos.y, true);
+ }
+ return;
+ case Qt.Key_1:
+ if (hasCtrl) {
+ controller.setMode("all");
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_2:
+ if (hasCtrl) {
+ controller.setMode("apps");
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_3:
+ if (hasCtrl) {
+ controller.setMode("files");
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_4:
+ if (hasCtrl) {
+ controller.setMode("plugins");
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_Slash:
+ if (event.modifiers === Qt.NoModifier && searchField.text.length === 0) {
+ controller.setMode("files", true);
+ return;
+ }
+ event.accepted = false;
+ return;
+ default:
+ event.accepted = false;
+ }
+ }
+
+ Item {
+ anchors.fill: parent
+ visible: !editMode && !(root.parentModal?.isClosing ?? false)
+
+ Item {
+ id: footerBar
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.leftMargin: root.parentModal?.borderWidth ?? 1
+ anchors.rightMargin: root.parentModal?.borderWidth ?? 1
+ anchors.bottomMargin: root.parentModal?.borderWidth ?? 1
+ readonly property bool showFooter: SettingsData.dankLauncherV2Size !== "micro" && SettingsData.dankLauncherV2ShowFooter
+ height: showFooter ? 36 : 0
+ visible: showFooter
+ clip: true
+
+ Rectangle {
+ anchors.fill: parent
+ anchors.topMargin: -Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ radius: Theme.cornerRadius
+ }
+
+ Row {
+ id: modeButtonsRow
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+ layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight
+ spacing: 2
+
+ Repeater {
+ model: [
+ {
+ id: "all",
+ label: I18n.tr("All"),
+ icon: "search"
+ },
+ {
+ id: "apps",
+ label: I18n.tr("Apps"),
+ icon: "apps"
+ },
+ {
+ id: "files",
+ label: I18n.tr("Files"),
+ icon: "folder"
+ },
+ {
+ id: "plugins",
+ label: I18n.tr("Plugins"),
+ icon: "extension"
+ }
+ ]
+
+ Rectangle {
+ required property var modelData
+ required property int index
+
+ width: buttonContent.width + Theme.spacingM * 2
+ height: 28
+ radius: Theme.cornerRadius
+ color: controller.searchMode === modelData.id || modeArea.containsMouse ? Theme.primaryContainer : "transparent"
+
+ Row {
+ id: buttonContent
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: modelData.icon
+ size: 14
+ color: controller.searchMode === modelData.id ? Theme.primary : Theme.surfaceVariantText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: modelData.label
+ font.pixelSize: Theme.fontSizeSmall
+ color: controller.searchMode === modelData.id ? Theme.primary : Theme.surfaceText
+ }
+ }
+
+ MouseArea {
+ id: modeArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: controller.setMode(modelData.id)
+ }
+ }
+ }
+ }
+
+ Row {
+ id: hintsRow
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight
+ spacing: Theme.spacingM
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: "↑↓ " + I18n.tr("nav")
+ font.pixelSize: Theme.fontSizeSmall - 1
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: "↵ " + I18n.tr("open")
+ font.pixelSize: Theme.fontSizeSmall - 1
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: "Ctrl-Tab " + I18n.tr("actions")
+ font.pixelSize: Theme.fontSizeSmall - 1
+ color: Theme.surfaceVariantText
+ visible: actionPanel.hasActions
+ }
+ }
+ }
+
+ Column {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.bottom: footerBar.top
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ anchors.topMargin: Theme.spacingM
+ spacing: Theme.spacingXS
+ clip: false
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Rectangle {
+ id: pluginBadge
+ visible: controller.activePluginName.length > 0
+ width: visible ? pluginBadgeContent.implicitWidth + Theme.spacingM : 0
+ height: searchField.height
+ radius: 16
+ color: Theme.primary
+
+ Row {
+ id: pluginBadgeContent
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: "extension"
+ size: 14
+ color: Theme.primaryText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: controller.activePluginName
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.primaryText
+ }
+ }
+
+ Behavior on width {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ DankTextField {
+ id: searchField
+ width: parent.width - (pluginBadge.visible ? pluginBadge.width + Theme.spacingS : 0)
+ cornerRadius: Theme.cornerRadius
+ backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ normalBorderColor: Theme.outlineMedium
+ focusedBorderColor: Theme.primary
+ leftIconName: controller.activePluginId ? "extension" : controller.searchQuery.startsWith("/") ? "folder" : "search"
+ leftIconSize: Theme.iconSize
+ leftIconColor: Theme.surfaceVariantText
+ leftIconFocusedColor: Theme.primary
+ showClearButton: true
+ textColor: Theme.surfaceText
+ font.pixelSize: Theme.fontSizeLarge
+ enabled: root.parentModal ? root.parentModal.spotlightOpen : true
+ placeholderText: ""
+ ignoreUpDownKeys: true
+ ignoreTabKeys: true
+ keyForwardTargets: [root]
+
+ onTextChanged: {
+ controller.setSearchQuery(text);
+ if (actionPanel.expanded) {
+ actionPanel.hide();
+ }
+ }
+
+ Keys.onPressed: event => {
+ if (event.key === Qt.Key_Escape) {
+ if (root.parentModal) {
+ root.parentModal.hide();
+ }
+ event.accepted = true;
+ } else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter)) {
+ if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) {
+ actionPanel.executeSelectedAction();
+ } else {
+ controller.executeSelected();
+ }
+ event.accepted = true;
+ }
+ }
+ }
+ }
+
+ Row {
+ id: categoryRow
+ width: parent.width
+ readonly property bool showPluginCategories: controller.activePluginCategories.length > 0
+ height: showPluginCategories ? 36 : 0
+ visible: showPluginCategories
+ spacing: Theme.spacingS
+
+ clip: true
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ DankDropdown {
+ id: categoryDropdown
+ visible: categoryRow.showPluginCategories
+ width: Math.min(200, parent.width)
+ compactMode: true
+ dropdownWidth: 200
+ popupWidth: 240
+ maxPopupHeight: 300
+ enableFuzzySearch: controller.activePluginCategories.length > 8
+ currentValue: {
+ const cats = controller.activePluginCategories;
+ const current = controller.activePluginCategory;
+ if (!current)
+ return cats.length > 0 ? cats[0].name : "";
+ for (let i = 0; i < cats.length; i++) {
+ if (cats[i].id === current)
+ return cats[i].name;
+ }
+ return cats.length > 0 ? cats[0].name : "";
+ }
+ options: {
+ const cats = controller.activePluginCategories;
+ const names = [];
+ for (let i = 0; i < cats.length; i++)
+ names.push(cats[i].name);
+ return names;
+ }
+
+ onValueChanged: value => {
+ const cats = controller.activePluginCategories;
+ for (let i = 0; i < cats.length; i++) {
+ if (cats[i].name === value) {
+ controller.setActivePluginCategory(cats[i].id);
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ Item {
+ id: fileFilterRow
+ width: parent.width
+ height: showFileFilters ? fileFilterContent.height : 0
+ visible: showFileFilters
+
+ readonly property bool showFileFilters: controller.searchMode === "files"
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Row {
+ id: fileFilterContent
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Row {
+ id: typeChips
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+ visible: DSearchService.supportsTypeFilter
+
+ Repeater {
+ model: [
+ {
+ id: "all",
+ label: I18n.tr("All"),
+ icon: "search"
+ },
+ {
+ id: "file",
+ label: I18n.tr("Files"),
+ icon: "insert_drive_file"
+ },
+ {
+ id: "dir",
+ label: I18n.tr("Folders"),
+ icon: "folder"
+ }
+ ]
+
+ Rectangle {
+ required property var modelData
+ required property int index
+
+ width: chipContent.width + Theme.spacingM * 2
+ height: sortDropdown.height
+ radius: Theme.cornerRadius
+ color: controller.fileSearchType === modelData.id || chipArea.containsMouse ? Theme.primaryContainer : "transparent"
+
+ Row {
+ id: chipContent
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: modelData.icon
+ size: 14
+ color: controller.fileSearchType === modelData.id ? Theme.primary : Theme.surfaceVariantText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: modelData.label
+ font.pixelSize: Theme.fontSizeSmall
+ color: controller.fileSearchType === modelData.id ? Theme.primary : Theme.surfaceText
+ }
+ }
+
+ MouseArea {
+ id: chipArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: controller.setFileSearchType(modelData.id)
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: 1
+ height: 20
+ anchors.verticalCenter: parent.verticalCenter
+ color: Theme.outlineMedium
+ visible: typeChips.visible
+ }
+
+ DankDropdown {
+ id: sortDropdown
+ anchors.verticalCenter: parent.verticalCenter
+ width: Math.min(130, parent.width / 3)
+ compactMode: true
+ dropdownWidth: 130
+ popupWidth: 150
+ maxPopupHeight: 200
+ currentValue: {
+ switch (controller.fileSearchSort) {
+ case "score":
+ return I18n.tr("Score");
+ case "name":
+ return I18n.tr("Name");
+ case "modified":
+ return I18n.tr("Modified");
+ case "size":
+ return I18n.tr("Size");
+ default:
+ return I18n.tr("Score");
+ }
+ }
+ options: [I18n.tr("Score"), I18n.tr("Name"), I18n.tr("Modified"), I18n.tr("Size")]
+
+ onValueChanged: value => {
+ var sortMap = {};
+ sortMap[I18n.tr("Score")] = "score";
+ sortMap[I18n.tr("Name")] = "name";
+ sortMap[I18n.tr("Modified")] = "modified";
+ sortMap[I18n.tr("Size")] = "size";
+ controller.setFileSearchSort(sortMap[value] || "score");
+ }
+ }
+
+ DankTextField {
+ id: extFilterField
+ anchors.verticalCenter: parent.verticalCenter
+ width: Math.min(100, parent.width / 4)
+ height: sortDropdown.height
+ placeholderText: I18n.tr("ext")
+ font.pixelSize: Theme.fontSizeSmall
+ showClearButton: text.length > 0
+
+ onTextChanged: {
+ controller.setFileSearchExt(text.trim());
+ }
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
+ ResultsList {
+ id: resultsList
+ anchors.fill: parent
+ controller: root.controller
+
+ onItemRightClicked: (index, item, sceneX, sceneY) => {
+ if (item && contextMenu.hasContextMenuActions(item)) {
+ var localPos = root.mapFromItem(null, sceneX, sceneY);
+ root.showContextMenu(item, localPos.x, localPos.y, false);
+ }
+ }
+ }
+ }
+
+ ActionPanel {
+ id: actionPanel
+ width: parent.width
+ selectedItem: controller.selectedItem
+ controller: controller
+ }
+ }
+ }
+
+ Connections {
+ target: controller
+ function onSelectedItemChanged() {
+ if (actionPanel.expanded && !actionPanel.hasActions) {
+ actionPanel.hide();
+ }
+ }
+ function onSearchQueryRequested(query) {
+ searchField.text = query;
+ searchField.cursorPosition = query.length;
+ }
+ function onModeChanged() {
+ extFilterField.text = "";
+ }
+ }
+
+ FocusScope {
+ id: editView
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ visible: editMode
+ focus: editMode
+
+ Keys.onPressed: event => {
+ if (event.key === Qt.Key_Escape) {
+ closeEditMode();
+ event.accepted = true;
+ } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ if (event.modifiers & Qt.ControlModifier) {
+ saveAppOverride();
+ event.accepted = true;
+ }
+ } else if (event.key === Qt.Key_S && event.modifiers & Qt.ControlModifier) {
+ saveAppOverride();
+ event.accepted = true;
+ }
+ }
+
+ Column {
+ anchors.fill: parent
+ spacing: Theme.spacingM
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingM
+
+ Rectangle {
+ width: 40
+ height: 40
+ radius: Theme.cornerRadius
+ color: backButtonArea.containsMouse ? Theme.surfaceHover : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "arrow_back"
+ size: 20
+ color: Theme.surfaceText
+ }
+
+ MouseArea {
+ id: backButtonArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: closeEditMode()
+ }
+ }
+
+ Image {
+ width: 40
+ height: 40
+ source: Paths.resolveIconUrl(editingApp?.icon || "application-x-executable")
+ sourceSize.width: 40
+ sourceSize.height: 40
+ fillMode: Image.PreserveAspectFit
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+
+ StyledText {
+ text: I18n.tr("Edit App")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ StyledText {
+ text: editingApp?.name || ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 1
+ color: Theme.outlineMedium
+ }
+
+ Flickable {
+ width: parent.width
+ height: parent.height - y - buttonsRow.height - Theme.spacingM
+ contentHeight: editFieldsColumn.height
+ clip: true
+ boundsBehavior: Flickable.StopAtBounds
+
+ Column {
+ id: editFieldsColumn
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Column {
+ width: parent.width
+ spacing: 4
+
+ StyledText {
+ text: I18n.tr("Name")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ DankTextField {
+ id: editNameField
+ width: parent.width
+ placeholderText: editingApp?.name || ""
+ keyNavigationTab: editIconField
+ keyNavigationBacktab: editExtraFlagsField
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: 4
+
+ StyledText {
+ text: I18n.tr("Icon")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ DankTextField {
+ id: editIconField
+ width: parent.width
+ placeholderText: editingApp?.icon || ""
+ keyNavigationTab: editCommentField
+ keyNavigationBacktab: editNameField
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: 4
+
+ StyledText {
+ text: I18n.tr("Description")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ DankTextField {
+ id: editCommentField
+ width: parent.width
+ placeholderText: editingApp?.comment || ""
+ keyNavigationTab: editEnvVarsField
+ keyNavigationBacktab: editIconField
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: 4
+
+ StyledText {
+ text: I18n.tr("Environment Variables")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ StyledText {
+ text: "KEY=value KEY2=value2"
+ font.pixelSize: Theme.fontSizeSmall - 1
+ color: Theme.surfaceVariantText
+ }
+
+ DankTextField {
+ id: editEnvVarsField
+ width: parent.width
+ placeholderText: "VAR=value"
+ keyNavigationTab: editExtraFlagsField
+ keyNavigationBacktab: editCommentField
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: 4
+
+ StyledText {
+ text: I18n.tr("Extra Arguments")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ DankTextField {
+ id: editExtraFlagsField
+ width: parent.width
+ placeholderText: "--flag --option=value"
+ keyNavigationTab: editNameField
+ keyNavigationBacktab: editEnvVarsField
+ }
+ }
+
+ DankToggle {
+ id: editDgpuToggle
+ width: parent.width
+ text: I18n.tr("Launch on dGPU by default")
+ visible: SessionService.nvidiaCommand.length > 0
+ checked: false
+ onToggled: checked => editDgpuToggle.checked = checked
+ }
+ }
+ }
+
+ Row {
+ id: buttonsRow
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: Theme.spacingM
+
+ Rectangle {
+ id: resetButton
+ width: 90
+ height: 40
+ radius: Theme.cornerRadius
+ color: resetButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
+ visible: SessionData.getAppOverride(editAppId) !== null
+
+ StyledText {
+ text: I18n.tr("Reset")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.error
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: resetButtonArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: resetAppOverride()
+ }
+ }
+
+ Rectangle {
+ id: cancelButton
+ width: 90
+ height: 40
+ radius: Theme.cornerRadius
+ color: cancelButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
+
+ StyledText {
+ text: I18n.tr("Cancel")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: cancelButtonArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: closeEditMode()
+ }
+ }
+
+ Rectangle {
+ id: saveButton
+ width: 90
+ height: 40
+ radius: Theme.cornerRadius
+ color: saveButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.9) : Theme.primary
+
+ StyledText {
+ text: I18n.tr("Save")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.primaryText
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: saveButtonArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: saveAppOverride()
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/LauncherContextMenu.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/LauncherContextMenu.qml
new file mode 100644
index 0000000..3c55590
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/LauncherContextMenu.qml
@@ -0,0 +1,510 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Popup {
+ id: root
+
+ property var item: null
+ property var controller: null
+ property var searchField: null
+ property var parentHandler: null
+
+ signal hideRequested
+ signal editAppRequested(var app)
+
+ function hasContextMenuActions(spotlightItem) {
+ if (!spotlightItem)
+ return false;
+ if (spotlightItem.type === "app")
+ return true;
+ if (spotlightItem.type === "plugin" && spotlightItem.pluginId) {
+ var instance = PluginService.pluginInstances[spotlightItem.pluginId];
+ if (!instance)
+ return false;
+ if (typeof instance.getContextMenuActions !== "function")
+ return false;
+ var actions = instance.getContextMenuActions(spotlightItem.data);
+ return Array.isArray(actions) && actions.length > 0;
+ }
+ return false;
+ }
+
+ readonly property bool isCoreApp: item?.type === "app" && !!item?.isCore
+ readonly property var coreAppData: isCoreApp ? item?.data ?? null : null
+ readonly property var desktopEntry: !isCoreApp ? (item?.data ?? null) : null
+ readonly property string appId: {
+ if (isCoreApp) {
+ return item?.id || coreAppData?.builtInPluginId || "";
+ }
+ return desktopEntry?.id || desktopEntry?.execString || "";
+ }
+ readonly property bool isPinned: appId ? SessionData.isPinnedApp(appId) : false
+ readonly property bool isRegularApp: item?.type === "app" && !item.isCore && desktopEntry
+ readonly property bool isPluginItem: item?.type === "plugin"
+
+ function getPluginContextMenuActions() {
+ if (!isPluginItem || !item?.pluginId)
+ return [];
+
+ var instance = PluginService.pluginInstances[item.pluginId];
+ if (!instance)
+ return [];
+ if (typeof instance.getContextMenuActions !== "function")
+ return [];
+
+ var actions = instance.getContextMenuActions(item.data);
+ if (!Array.isArray(actions))
+ return [];
+
+ return actions;
+ }
+
+ function executePluginAction(actionOrObj) {
+ var actionFunc = typeof actionOrObj === "function" ? actionOrObj : actionOrObj?.action;
+ var closeLauncher = typeof actionOrObj === "object" && actionOrObj?.closeLauncher;
+
+ if (typeof actionFunc === "function")
+ actionFunc();
+
+ if (closeLauncher) {
+ controller?.itemExecuted();
+ } else {
+ controller?.performSearch();
+ }
+ hide();
+ }
+
+ readonly property var menuItems: {
+ var items = [];
+
+ if (isPluginItem) {
+ var pluginActions = getPluginContextMenuActions();
+ for (var i = 0; i < pluginActions.length; i++) {
+ var act = pluginActions[i];
+ items.push({
+ type: "item",
+ icon: act.icon || "play_arrow",
+ text: act.text || act.name || "",
+ pluginAction: act
+ });
+ }
+ return items;
+ }
+
+ if (item?.type === "app") {
+ items.push({
+ type: "item",
+ icon: isPinned ? "keep_off" : "push_pin",
+ text: isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock"),
+ action: togglePin
+ });
+ }
+
+ if (isRegularApp) {
+ items.push({
+ type: "item",
+ icon: "visibility_off",
+ text: I18n.tr("Hide App"),
+ action: hideCurrentApp
+ });
+ items.push({
+ type: "item",
+ icon: "edit",
+ text: I18n.tr("Edit App"),
+ action: editCurrentApp
+ });
+ }
+
+ if (item?.actions && item.actions.length > 0) {
+ items.push({
+ type: "separator"
+ });
+ for (var i = 0; i < item.actions.length; i++) {
+ var act = item.actions[i];
+ items.push({
+ type: "item",
+ icon: act.icon || "play_arrow",
+ text: act.name || "",
+ actionData: act
+ });
+ }
+ }
+
+ items.push({
+ type: "separator"
+ });
+
+ if (isRegularApp && SessionService.nvidiaCommand) {
+ items.push({
+ type: "item",
+ icon: "memory",
+ text: I18n.tr("Launch on dGPU"),
+ action: launchWithNvidia
+ });
+ }
+
+ items.push({
+ type: "item",
+ icon: "launch",
+ text: I18n.tr("Launch"),
+ action: launchApp
+ });
+
+ return items;
+ }
+
+ function show(x, y, spotlightItem, fromKeyboard) {
+ if (!spotlightItem?.data)
+ return;
+ item = spotlightItem;
+ selectedMenuIndex = fromKeyboard ? 0 : -1;
+ keyboardNavigation = fromKeyboard;
+
+ if (parentHandler)
+ parentHandler.enabled = false;
+
+ Qt.callLater(() => {
+ var parentW = parent?.width ?? 500;
+ var parentH = parent?.height ?? 600;
+ var menuW = width > 0 ? width : 200;
+ var menuH = height > 0 ? height : 200;
+ var margin = 8;
+
+ var posX = x + 4;
+ var posY = y + 4;
+
+ if (posX + menuW > parentW - margin) {
+ posX = Math.max(margin, parentW - menuW - margin);
+ }
+ if (posY + menuH > parentH - margin) {
+ posY = Math.max(margin, parentH - menuH - margin);
+ }
+
+ root.x = posX;
+ root.y = posY;
+ open();
+ });
+ }
+
+ function hide() {
+ if (parentHandler)
+ parentHandler.enabled = true;
+ close();
+ }
+
+ function togglePin() {
+ if (!appId)
+ return;
+ if (isPinned)
+ SessionData.removePinnedApp(appId);
+ else
+ SessionData.addPinnedApp(appId);
+ hide();
+ }
+
+ function hideCurrentApp() {
+ if (!appId)
+ return;
+ SessionData.hideApp(appId);
+ controller?.performSearch();
+ hide();
+ }
+
+ function editCurrentApp() {
+ if (!desktopEntry)
+ return;
+ editAppRequested(desktopEntry);
+ hide();
+ }
+
+ function launchApp() {
+ if (isCoreApp) {
+ if (!coreAppData)
+ return;
+ AppSearchService.executeCoreApp(coreAppData);
+ controller?.itemExecuted();
+ hide();
+ return;
+ }
+ if (!desktopEntry)
+ return;
+ SessionService.launchDesktopEntry(desktopEntry);
+ AppUsageHistoryData.addAppUsage(desktopEntry);
+ controller?.itemExecuted();
+ hide();
+ }
+
+ function launchWithNvidia() {
+ if (!desktopEntry)
+ return;
+ SessionService.launchDesktopEntry(desktopEntry, true);
+ AppUsageHistoryData.addAppUsage(desktopEntry);
+ controller?.itemExecuted();
+ hide();
+ }
+
+ function executeDesktopAction(actionData) {
+ if (!desktopEntry || !actionData)
+ return;
+ SessionService.launchDesktopAction(desktopEntry, actionData.actionData || actionData);
+ AppUsageHistoryData.addAppUsage(desktopEntry);
+ controller?.itemExecuted();
+ hide();
+ }
+
+ property int selectedMenuIndex: 0
+ property bool keyboardNavigation: false
+
+ readonly property int visibleItemCount: {
+ var count = 0;
+ for (var i = 0; i < menuItems.length; i++) {
+ if (menuItems[i].type === "item")
+ count++;
+ }
+ return count;
+ }
+
+ function selectNext() {
+ if (visibleItemCount > 0)
+ selectedMenuIndex = (selectedMenuIndex + 1) % visibleItemCount;
+ }
+
+ function selectPrevious() {
+ if (visibleItemCount > 0)
+ selectedMenuIndex = (selectedMenuIndex - 1 + visibleItemCount) % visibleItemCount;
+ }
+
+ function activateSelected() {
+ var itemIndex = 0;
+ for (var i = 0; i < menuItems.length; i++) {
+ if (menuItems[i].type !== "item")
+ continue;
+ if (itemIndex === selectedMenuIndex) {
+ var menuItem = menuItems[i];
+ if (menuItem.action)
+ menuItem.action();
+ else if (menuItem.pluginAction)
+ executePluginAction(menuItem.pluginAction);
+ else if (menuItem.actionData)
+ executeDesktopAction(menuItem.actionData);
+ return;
+ }
+ itemIndex++;
+ }
+ }
+
+ width: menuContainer.implicitWidth
+ height: menuContainer.implicitHeight
+ padding: 0
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+ modal: true
+ dim: false
+ background: Item {}
+
+ onOpened: {
+ Qt.callLater(() => keyboardHandler.forceActiveFocus());
+ }
+
+ onClosed: {
+ if (parentHandler)
+ parentHandler.enabled = true;
+ if (searchField?.visible) {
+ Qt.callLater(() => searchField.forceActiveFocus());
+ }
+ }
+
+ enter: Transition {
+ NumberAnimation {
+ property: "opacity"
+ from: 0
+ to: 1
+ duration: Theme.shortDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ exit: Transition {
+ NumberAnimation {
+ property: "opacity"
+ from: 1
+ to: 0
+ duration: Theme.shortDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ contentItem: Item {
+ id: keyboardHandler
+ focus: true
+ implicitWidth: menuContainer.implicitWidth
+ implicitHeight: menuContainer.implicitHeight
+
+ Keys.onPressed: event => {
+ switch (event.key) {
+ case Qt.Key_Down:
+ root.selectNext();
+ event.accepted = true;
+ return;
+ case Qt.Key_Up:
+ root.selectPrevious();
+ event.accepted = true;
+ return;
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ root.activateSelected();
+ event.accepted = true;
+ return;
+ case Qt.Key_Escape:
+ case Qt.Key_Left:
+ root.hide();
+ event.accepted = true;
+ return;
+ }
+ }
+
+ Rectangle {
+ id: menuContainer
+ anchors.fill: parent
+ implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
+ implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ radius: Theme.cornerRadius
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
+ border.width: 1
+
+ Rectangle {
+ anchors.fill: parent
+ anchors.topMargin: 4
+ anchors.leftMargin: 2
+ anchors.rightMargin: -2
+ anchors.bottomMargin: -4
+ radius: parent.radius
+ color: Qt.rgba(0, 0, 0, 0.15)
+ z: -1
+ }
+
+ Column {
+ id: menuColumn
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+ spacing: 1
+
+ Repeater {
+ model: root.menuItems
+
+ Item {
+ id: menuItemDelegate
+ required property var modelData
+ required property int index
+
+ width: menuColumn.width
+ height: modelData.type === "separator" ? 5 : 32
+
+ readonly property int itemIndex: {
+ var count = 0;
+ for (var i = 0; i < index; i++) {
+ if (root.menuItems[i].type === "item")
+ count++;
+ }
+ return count;
+ }
+
+ Rectangle {
+ visible: menuItemDelegate.modelData.type === "separator"
+ width: parent.width - Theme.spacingS * 2
+ height: parent.height
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: "transparent"
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: parent.width
+ height: 1
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
+ }
+ }
+
+ Rectangle {
+ visible: menuItemDelegate.modelData.type === "item"
+ width: parent.width
+ height: parent.height
+ radius: Theme.cornerRadius
+ color: {
+ if (root.keyboardNavigation && root.selectedMenuIndex === menuItemDelegate.itemIndex) {
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
+ }
+ return itemMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent";
+ }
+
+ Row {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingS
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ Item {
+ width: Theme.iconSize - 2
+ height: Theme.iconSize - 2
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ visible: (menuItemDelegate.modelData?.icon ?? "").length > 0
+ name: menuItemDelegate.modelData?.icon ?? ""
+ size: Theme.iconSize - 2
+ color: Theme.surfaceText
+ opacity: 0.7
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ StyledText {
+ text: menuItemDelegate.modelData.text || ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Normal
+ anchors.verticalCenter: parent.verticalCenter
+ elide: Text.ElideRight
+ width: parent.width - (Theme.iconSize - 2) - Theme.spacingS
+ }
+ }
+
+ DankRipple {
+ id: menuItemRipple
+ rippleColor: Theme.surfaceText
+ cornerRadius: Theme.cornerRadius
+ }
+
+ MouseArea {
+ id: itemMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onEntered: {
+ root.keyboardNavigation = false;
+ root.selectedMenuIndex = menuItemDelegate.itemIndex;
+ }
+ onPressed: mouse => menuItemRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ var menuItem = menuItemDelegate.modelData;
+ if (menuItem.action)
+ menuItem.action();
+ else if (menuItem.pluginAction)
+ root.executePluginAction(menuItem.pluginAction);
+ else if (menuItem.actionData)
+ root.executeDesktopAction(menuItem.actionData);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/NavigationHelpers.js b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/NavigationHelpers.js
new file mode 100644
index 0000000..1e78b11
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/NavigationHelpers.js
@@ -0,0 +1,254 @@
+.pragma library
+
+function getFirstItemIndex(flatModel) {
+ for (var i = 0; i < flatModel.length; i++) {
+ if (!flatModel[i].isHeader)
+ return i;
+ }
+ return 0;
+}
+
+function findNextNonHeaderIndex(flatModel, startIndex) {
+ for (var i = startIndex; i < flatModel.length; i++) {
+ if (!flatModel[i].isHeader)
+ return i;
+ }
+ return -1;
+}
+
+function findPrevNonHeaderIndex(flatModel, startIndex) {
+ for (var i = startIndex; i >= 0; i--) {
+ if (!flatModel[i].isHeader)
+ return i;
+ }
+ return -1;
+}
+
+function getSectionBounds(flatModel, sectionId) {
+ if (flatModel._sectionBounds && flatModel._sectionBounds[sectionId])
+ return flatModel._sectionBounds[sectionId];
+
+ var start = -1, end = -1;
+ for (var i = 0; i < flatModel.length; i++) {
+ if (flatModel[i].isHeader && flatModel[i].section?.id === sectionId) {
+ start = i + 1;
+ } else if (start >= 0 && !flatModel[i].isHeader && flatModel[i].sectionId === sectionId) {
+ end = i;
+ } else if (start >= 0 && end >= 0 && flatModel[i].sectionId !== sectionId) {
+ break;
+ }
+ }
+ return {
+ start: start,
+ end: end,
+ count: end >= start ? end - start + 1 : 0
+ };
+}
+
+function getGridColumns(viewMode, gridColumns) {
+ switch (viewMode) {
+ case "tile":
+ return 3;
+ case "grid":
+ return gridColumns;
+ default:
+ return 1;
+ }
+}
+
+function calculateNextIndex(flatModel, selectedFlatIndex, sectionId, viewMode, gridColumns, getSectionViewModeFn) {
+ if (flatModel.length === 0)
+ return selectedFlatIndex;
+
+ var entry = flatModel[selectedFlatIndex];
+ if (!entry || entry.isHeader) {
+ var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
+ return next !== -1 ? next : selectedFlatIndex;
+ }
+
+ var actualViewMode = viewMode || getSectionViewModeFn(entry.sectionId);
+ if (actualViewMode === "list") {
+ var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
+ return next !== -1 ? next : selectedFlatIndex;
+ }
+
+ var bounds = getSectionBounds(flatModel, entry.sectionId);
+ var cols = getGridColumns(actualViewMode, gridColumns);
+ var posInSection = selectedFlatIndex - bounds.start;
+ var newPosInSection = posInSection + cols;
+
+ if (newPosInSection < bounds.count) {
+ return bounds.start + newPosInSection;
+ }
+
+ var currentRow = Math.floor(posInSection / cols);
+ var lastRow = Math.floor((bounds.count - 1) / cols);
+ if (currentRow < lastRow) {
+ return bounds.start + bounds.count - 1;
+ }
+
+ var nextSection = findNextNonHeaderIndex(flatModel, bounds.end + 1);
+ return nextSection !== -1 ? nextSection : selectedFlatIndex;
+}
+
+function calculatePrevIndex(flatModel, selectedFlatIndex, sectionId, viewMode, gridColumns, getSectionViewModeFn) {
+ if (flatModel.length === 0)
+ return selectedFlatIndex;
+
+ var entry = flatModel[selectedFlatIndex];
+ if (!entry || entry.isHeader) {
+ var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
+ return prev !== -1 ? prev : selectedFlatIndex;
+ }
+
+ var actualViewMode = viewMode || getSectionViewModeFn(entry.sectionId);
+ if (actualViewMode === "list") {
+ var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
+ return prev !== -1 ? prev : selectedFlatIndex;
+ }
+
+ var bounds = getSectionBounds(flatModel, entry.sectionId);
+ var cols = getGridColumns(actualViewMode, gridColumns);
+ var posInSection = selectedFlatIndex - bounds.start;
+ var newPosInSection = posInSection - cols;
+
+ if (newPosInSection >= 0) {
+ return bounds.start + newPosInSection;
+ }
+
+ var prevItem = findPrevNonHeaderIndex(flatModel, bounds.start - 1);
+ return prevItem !== -1 ? prevItem : selectedFlatIndex;
+}
+
+function calculateRightIndex(flatModel, selectedFlatIndex, getSectionViewModeFn) {
+ if (flatModel.length === 0)
+ return selectedFlatIndex;
+
+ var entry = flatModel[selectedFlatIndex];
+ if (!entry || entry.isHeader) {
+ var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
+ return next !== -1 ? next : selectedFlatIndex;
+ }
+
+ var viewMode = getSectionViewModeFn(entry.sectionId);
+ if (viewMode === "list") {
+ var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
+ return next !== -1 ? next : selectedFlatIndex;
+ }
+
+ var bounds = getSectionBounds(flatModel, entry.sectionId);
+ var posInSection = selectedFlatIndex - bounds.start;
+ if (posInSection + 1 < bounds.count) {
+ return bounds.start + posInSection + 1;
+ }
+ return selectedFlatIndex;
+}
+
+function calculateLeftIndex(flatModel, selectedFlatIndex, getSectionViewModeFn) {
+ if (flatModel.length === 0)
+ return selectedFlatIndex;
+
+ var entry = flatModel[selectedFlatIndex];
+ if (!entry || entry.isHeader) {
+ var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
+ return prev !== -1 ? prev : selectedFlatIndex;
+ }
+
+ var viewMode = getSectionViewModeFn(entry.sectionId);
+ if (viewMode === "list") {
+ var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
+ return prev !== -1 ? prev : selectedFlatIndex;
+ }
+
+ var bounds = getSectionBounds(flatModel, entry.sectionId);
+ var posInSection = selectedFlatIndex - bounds.start;
+ if (posInSection > 0) {
+ return bounds.start + posInSection - 1;
+ }
+ return selectedFlatIndex;
+}
+
+function calculateNextSectionIndex(flatModel, selectedFlatIndex) {
+ var currentSection = null;
+ if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
+ currentSection = flatModel[selectedFlatIndex].sectionId;
+ }
+
+ var foundCurrent = false;
+ for (var i = 0; i < flatModel.length; i++) {
+ if (flatModel[i].isHeader) {
+ if (foundCurrent) {
+ for (var j = i + 1; j < flatModel.length; j++) {
+ if (!flatModel[j].isHeader)
+ return j;
+ }
+ }
+ if (flatModel[i].section.id === currentSection) {
+ foundCurrent = true;
+ }
+ }
+ }
+ return selectedFlatIndex;
+}
+
+function calculatePrevSectionIndex(flatModel, selectedFlatIndex) {
+ var currentSection = null;
+ if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
+ currentSection = flatModel[selectedFlatIndex].sectionId;
+ }
+
+ var lastSectionStart = -1;
+ var prevSectionStart = -1;
+
+ for (var i = 0; i < flatModel.length; i++) {
+ if (flatModel[i].isHeader) {
+ if (flatModel[i].section.id === currentSection) {
+ break;
+ }
+ prevSectionStart = lastSectionStart;
+ lastSectionStart = i;
+ }
+ }
+
+ if (prevSectionStart >= 0) {
+ for (var j = prevSectionStart + 1; j < flatModel.length; j++) {
+ if (!flatModel[j].isHeader)
+ return j;
+ }
+ }
+ return selectedFlatIndex;
+}
+
+function calculatePageDownIndex(flatModel, selectedFlatIndex, visibleItems) {
+ if (flatModel.length === 0)
+ return selectedFlatIndex;
+
+ var itemsToSkip = visibleItems || 8;
+ var newIndex = selectedFlatIndex;
+
+ for (var i = 0; i < itemsToSkip; i++) {
+ var next = findNextNonHeaderIndex(flatModel, newIndex + 1);
+ if (next === -1)
+ break;
+ newIndex = next;
+ }
+
+ return newIndex;
+}
+
+function calculatePageUpIndex(flatModel, selectedFlatIndex, visibleItems) {
+ if (flatModel.length === 0)
+ return selectedFlatIndex;
+
+ var itemsToSkip = visibleItems || 8;
+ var newIndex = selectedFlatIndex;
+
+ for (var i = 0; i < itemsToSkip; i++) {
+ var prev = findPrevNonHeaderIndex(flatModel, newIndex - 1);
+ if (prev === -1)
+ break;
+ newIndex = prev;
+ }
+
+ return newIndex;
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ResultItem.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ResultItem.qml
new file mode 100644
index 0000000..0a50cd3
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ResultItem.qml
@@ -0,0 +1,196 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property var item: null
+ property bool isSelected: false
+ property bool isHovered: itemArea.containsMouse || allModeToggleArea.containsMouse
+ property var controller: null
+ property int flatIndex: -1
+
+ signal clicked
+ signal rightClicked(real mouseX, real mouseY)
+
+ readonly property string iconValue: {
+ if (!item)
+ return "";
+ switch (item.iconType) {
+ case "material":
+ case "nerd":
+ return "material:" + (item.icon || "apps");
+ case "unicode":
+ return "unicode:" + (item.icon || "");
+ case "composite":
+ return item.iconFull || "";
+ case "image":
+ default:
+ return item.icon || "";
+ }
+ }
+
+ width: parent?.width ?? 200
+ height: 52
+ color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent"
+ radius: Theme.cornerRadius
+
+ DankRipple {
+ id: rippleLayer
+ rippleColor: Theme.surfaceText
+ cornerRadius: root.radius
+ }
+
+ MouseArea {
+ id: itemArea
+ z: 1
+ anchors.fill: parent
+ anchors.rightMargin: root.item?.type === "plugin_browse" ? 40 : 0
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+
+ onPressed: mouse => {
+ if (mouse.button === Qt.LeftButton)
+ rippleLayer.trigger(mouse.x, mouse.y);
+ }
+ onClicked: mouse => {
+ if (mouse.button === Qt.RightButton) {
+ var scenePos = mapToItem(null, mouse.x, mouse.y);
+ root.rightClicked(scenePos.x, scenePos.y);
+ } else {
+ root.clicked();
+ }
+ }
+
+ onPositionChanged: {
+ if (root.controller)
+ root.controller.keyboardNavigationActive = false;
+ }
+ }
+
+ Row {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ spacing: Theme.spacingM
+
+ AppIconRenderer {
+ width: 36
+ height: 36
+ anchors.verticalCenter: parent.verticalCenter
+ iconValue: root.iconValue
+ iconSize: 36
+ fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
+ materialIconSizeAdjustment: 12
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.width - 36 - Theme.spacingM * 3 - rightContent.width
+ spacing: 2
+
+ Text {
+ width: parent.width
+ text: root.item?._hName ?? root.item?.name ?? ""
+ textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ font.family: Theme.fontFamily
+ color: Theme.surfaceText
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ Text {
+ width: parent.width
+ text: root.item?._hSub ?? root.item?.subtitle ?? ""
+ textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: Theme.fontFamily
+ color: Theme.surfaceVariantText
+ elide: Text.ElideRight
+ clip: true
+ visible: (root.item?.subtitle ?? "").length > 0
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+
+ Row {
+ id: rightContent
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ Rectangle {
+ id: allModeToggle
+ visible: root.item?.type === "plugin_browse"
+ width: 28
+ height: 28
+ radius: 14
+ anchors.verticalCenter: parent.verticalCenter
+ color: allModeToggleArea.containsMouse ? Theme.surfaceHover : "transparent"
+
+ property bool isAllowed: {
+ if (root.item?.type !== "plugin_browse")
+ return false;
+ var pluginId = root.item?.data?.pluginId;
+ if (!pluginId)
+ return false;
+ SettingsData.launcherPluginVisibility;
+ return SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ }
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: allModeToggle.isAllowed ? "visibility" : "visibility_off"
+ size: 18
+ color: allModeToggle.isAllowed ? Theme.primary : Theme.surfaceVariantText
+ }
+
+ MouseArea {
+ id: allModeToggleArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ var pluginId = root.item?.data?.pluginId;
+ if (!pluginId)
+ return;
+ SettingsData.setPluginAllowWithoutTrigger(pluginId, !allModeToggle.isAllowed);
+ }
+ }
+ }
+
+ Rectangle {
+ visible: !!root.item?.type && root.item.type !== "app" && root.item.type !== "plugin_browse"
+ width: typeBadge.implicitWidth + Theme.spacingS * 2
+ height: 20
+ radius: 10
+ color: Theme.surfaceVariantAlpha
+ anchors.verticalCenter: parent.verticalCenter
+
+ StyledText {
+ id: typeBadge
+ anchors.centerIn: parent
+ text: {
+ if (!root.item)
+ return "";
+ switch (root.item.type) {
+ case "plugin":
+ return I18n.tr("Plugin");
+ case "file":
+ return root.item.data?.is_dir ? I18n.tr("Folder") : I18n.tr("File");
+ default:
+ return "";
+ }
+ }
+ font.pixelSize: Theme.fontSizeSmall - 2
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ResultsList.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ResultsList.qml
new file mode 100644
index 0000000..53a2d45
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ResultsList.qml
@@ -0,0 +1,498 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ property var controller: null
+ property int gridColumns: controller?.gridColumns ?? 4
+ property var _visualRows: []
+ property var _flatIndexToRowMap: ({})
+ property var _cumulativeHeights: []
+
+ signal itemRightClicked(int index, var item, real mouseX, real mouseY)
+
+ function _rebuildVisualModel() {
+ var sections = root.controller?.sections ?? [];
+ var rows = [];
+ var indexMap = {};
+ var cumHeights = [];
+ var cumY = 0;
+
+ for (var s = 0; s < sections.length; s++) {
+ var section = sections[s];
+ var sectionId = section.id;
+
+ cumHeights.push(cumY);
+ rows.push({
+ _rowId: "h_" + sectionId,
+ type: "header",
+ section: section,
+ sectionId: sectionId,
+ height: 32
+ });
+ cumY += 32;
+
+ if (section.collapsed)
+ continue;
+
+ var versionTrigger = root.controller?.viewModeVersion ?? 0;
+ void (versionTrigger);
+ var mode = root.controller?.getSectionViewMode(sectionId) ?? "list";
+ var items = section.items ?? [];
+ var flatStartIndex = section.flatStartIndex ?? 0;
+
+ if (mode === "list") {
+ for (var i = 0; i < items.length; i++) {
+ var flatIdx = flatStartIndex + i;
+ indexMap[flatIdx] = rows.length;
+ cumHeights.push(cumY);
+ rows.push({
+ _rowId: items[i].id,
+ type: "list_item",
+ item: items[i],
+ flatIndex: flatIdx,
+ sectionId: sectionId,
+ height: 52
+ });
+ cumY += 52;
+ }
+ } else {
+ var cols = root.controller?.getGridColumns(sectionId) ?? root.gridColumns;
+ var cellWidth = mode === "tile" ? Math.floor(root.width / 3) : Math.floor(root.width / root.gridColumns);
+ var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
+ var numRows = Math.ceil(items.length / cols);
+
+ for (var r = 0; r < numRows; r++) {
+ var rowItems = [];
+ for (var c = 0; c < cols; c++) {
+ var idx = r * cols + c;
+ if (idx >= items.length)
+ break;
+ var fi = flatStartIndex + idx;
+ indexMap[fi] = rows.length;
+ rowItems.push({
+ item: items[idx],
+ flatIndex: fi
+ });
+ }
+ cumHeights.push(cumY);
+ rows.push({
+ _rowId: "gr_" + sectionId + "_" + r,
+ type: "grid_row",
+ items: rowItems,
+ sectionId: sectionId,
+ viewMode: mode,
+ cols: cols,
+ height: cellHeight
+ });
+ cumY += cellHeight;
+ }
+ }
+ }
+
+ root._flatIndexToRowMap = indexMap;
+ root._cumulativeHeights = cumHeights;
+ root._visualRows = rows;
+ }
+
+ onGridColumnsChanged: Qt.callLater(_rebuildVisualModel)
+ onWidthChanged: Qt.callLater(_rebuildVisualModel)
+
+ Connections {
+ target: root.controller
+ function onSectionsChanged() {
+ Qt.callLater(root._rebuildVisualModel);
+ }
+ function onViewModeVersionChanged() {
+ Qt.callLater(root._rebuildVisualModel);
+ }
+ function onSearchModeChanged() {
+ root._visualRows = [];
+ root._cumulativeHeights = [];
+ root._flatIndexToRowMap = {};
+ }
+ }
+
+ function resetScroll() {
+ mainListView.contentY = mainListView.originY;
+ }
+
+ function ensureVisible(index) {
+ if (index < 0 || !controller?.flatModel || index >= controller.flatModel.length)
+ return;
+ var entry = controller.flatModel[index];
+ if (!entry || entry.isHeader)
+ return;
+ var rowIndex = _flatIndexToRowMap[index];
+ if (rowIndex === undefined)
+ return;
+
+ mainListView.positionViewAtIndex(rowIndex, ListView.Contain);
+
+ if (stickyHeader.visible && rowIndex < _cumulativeHeights.length) {
+ var rowY = _cumulativeHeights[rowIndex];
+ var scrollY = mainListView.contentY - mainListView.originY;
+ if (rowY < scrollY + stickyHeader.height) {
+ mainListView.contentY = Math.max(mainListView.originY, rowY - stickyHeader.height + mainListView.originY);
+ }
+ }
+ }
+
+ function getSelectedItemPosition() {
+ var fallback = mapToItem(null, width / 2, height / 2);
+ if (!controller?.flatModel || controller.selectedFlatIndex < 0)
+ return fallback;
+
+ var entry = controller.flatModel[controller.selectedFlatIndex];
+ if (!entry || entry.isHeader)
+ return fallback;
+
+ var rowIndex = _flatIndexToRowMap[controller.selectedFlatIndex];
+ if (rowIndex === undefined)
+ return fallback;
+
+ var rowY = (rowIndex < _cumulativeHeights.length) ? _cumulativeHeights[rowIndex] : 0;
+ var row = _visualRows[rowIndex];
+ if (!row)
+ return fallback;
+
+ var itemX = width / 2;
+ var itemH = row.height;
+
+ if (row.type === "grid_row") {
+ var rowItems = row.items;
+ for (var i = 0; i < rowItems.length; i++) {
+ if (rowItems[i].flatIndex === controller.selectedFlatIndex) {
+ var cellWidth = row.viewMode === "tile" ? Math.floor(width / 3) : Math.floor(width / row.cols);
+ itemX = i * cellWidth + cellWidth / 2;
+ break;
+ }
+ }
+ }
+
+ var visualY = rowY - mainListView.contentY + mainListView.originY + itemH / 2;
+ var clampedY = Math.max(40, Math.min(height - 40, visualY));
+ return mapToItem(null, itemX, clampedY);
+ }
+
+ Connections {
+ target: root.controller
+ function onSelectedFlatIndexChanged() {
+ if (root.controller?.keyboardNavigationActive) {
+ Qt.callLater(() => root.ensureVisible(root.controller.selectedFlatIndex));
+ }
+ }
+ }
+
+ DankListView {
+ id: mainListView
+ anchors.fill: parent
+ clip: true
+ scrollBarTopMargin: (root.controller?.sections?.length > 0) ? 32 : 0
+
+ model: ScriptModel {
+ values: root._visualRows
+ objectProp: "_rowId"
+ }
+
+ add: null
+ remove: null
+ displaced: null
+ move: null
+
+ delegate: Item {
+ id: delegateRoot
+ required property var modelData
+ required property int index
+
+ width: mainListView.width
+ height: modelData?.height ?? 52
+
+ SectionHeader {
+ anchors.fill: parent
+ visible: delegateRoot.modelData?.type === "header"
+ section: delegateRoot.modelData?.section ?? null
+ controller: root.controller
+ viewMode: {
+ var vt = root.controller?.viewModeVersion ?? 0;
+ void (vt);
+ return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list";
+ }
+ canChangeViewMode: {
+ var vt = root.controller?.viewModeVersion ?? 0;
+ void (vt);
+ return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
+ }
+ canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
+ }
+
+ ResultItem {
+ anchors.fill: parent
+ visible: delegateRoot.modelData?.type === "list_item"
+ item: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.item ?? null) : null
+ isSelected: delegateRoot.modelData?.type === "list_item" && (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
+ controller: root.controller
+ flatIndex: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.flatIndex ?? -1) : -1
+
+ onClicked: {
+ if (root.controller && delegateRoot.modelData?.item) {
+ root.controller.executeItem(delegateRoot.modelData.item);
+ }
+ }
+
+ onRightClicked: (mouseX, mouseY) => {
+ root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
+ }
+ }
+
+ Row {
+ id: gridRowContent
+ anchors.fill: parent
+ visible: delegateRoot.modelData?.type === "grid_row"
+
+ Repeater {
+ model: delegateRoot.modelData?.type === "grid_row" ? (delegateRoot.modelData?.items ?? []) : []
+
+ Item {
+ id: gridCellDelegate
+ required property var modelData
+ required property int index
+
+ readonly property real cellWidth: delegateRoot.modelData?.viewMode === "tile" ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns))
+
+ width: cellWidth
+ height: delegateRoot.height
+
+ GridItem {
+ width: parent.width - 4
+ height: parent.height - 4
+ anchors.centerIn: parent
+ visible: delegateRoot.modelData?.viewMode === "grid"
+ item: gridCellDelegate.modelData?.item ?? null
+ isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
+ controller: root.controller
+ flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
+
+ onClicked: {
+ if (root.controller && gridCellDelegate.modelData?.item) {
+ root.controller.executeItem(gridCellDelegate.modelData.item);
+ }
+ }
+
+ onRightClicked: (mouseX, mouseY) => {
+ root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
+ }
+ }
+
+ TileItem {
+ width: parent.width - 4
+ height: parent.height - 4
+ anchors.centerIn: parent
+ visible: delegateRoot.modelData?.viewMode === "tile"
+ item: gridCellDelegate.modelData?.item ?? null
+ isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
+ controller: root.controller
+ flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
+
+ onClicked: {
+ if (root.controller && gridCellDelegate.modelData?.item) {
+ root.controller.executeItem(gridCellDelegate.modelData.item);
+ }
+ }
+
+ onRightClicked: (mouseX, mouseY) => {
+ root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: bottomShadow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: 24
+ z: 100
+ visible: {
+ if (BlurService.enabled)
+ return false;
+ if (mainListView.contentHeight <= mainListView.height)
+ return false;
+ var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5;
+ if (atBottom)
+ return false;
+
+ var flatModel = root.controller?.flatModel;
+ if (!flatModel || flatModel.length === 0)
+ return false;
+ var lastItemIdx = -1;
+ for (var i = flatModel.length - 1; i >= 0; i--) {
+ if (!flatModel[i].isHeader) {
+ lastItemIdx = i;
+ break;
+ }
+ }
+ if (lastItemIdx >= 0 && root.controller?.selectedFlatIndex === lastItemIdx)
+ return false;
+ return true;
+ }
+ gradient: Gradient {
+ GradientStop {
+ position: 0.0
+ color: "transparent"
+ }
+ GradientStop {
+ position: 1.0
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ }
+ }
+ }
+
+ Rectangle {
+ id: stickyHeader
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ height: 32
+ z: 101
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ visible: stickyHeaderSection !== null
+
+ readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
+
+ readonly property var stickyHeaderSection: {
+ var scrollY = mainListView.contentY - mainListView.originY;
+ if (scrollY <= 0)
+ return null;
+
+ var rows = root._visualRows;
+ var heights = root._cumulativeHeights;
+ if (rows.length === 0 || heights.length === 0)
+ return null;
+
+ var lo = 0;
+ var hi = rows.length - 1;
+ while (lo < hi) {
+ var mid = (lo + hi + 1) >> 1;
+ if (mid < heights.length && heights[mid] <= scrollY)
+ lo = mid;
+ else
+ hi = mid - 1;
+ }
+
+ for (var i = lo; i >= 0; i--) {
+ if (rows[i].type === "header")
+ return rows[i].section;
+ }
+ return null;
+ }
+
+ SectionHeader {
+ width: parent.width
+ section: stickyHeader.stickyHeaderSection
+ controller: root.controller
+ viewMode: {
+ void (stickyHeader.versionTrigger);
+ return root.controller?.getSectionViewMode(stickyHeader.stickyHeaderSection?.id) ?? "list";
+ }
+ canChangeViewMode: {
+ void (stickyHeader.versionTrigger);
+ return root.controller?.canChangeSectionViewMode(stickyHeader.stickyHeaderSection?.id) ?? false;
+ }
+ canCollapse: {
+ void (stickyHeader.versionTrigger);
+ return root.controller?.canCollapseSection(stickyHeader.stickyHeaderSection?.id) ?? false;
+ }
+ isSticky: true
+ }
+ }
+
+ Item {
+ anchors.centerIn: parent
+ visible: (!root.controller?.sections || root.controller.sections.length === 0) && !root.controller?.isFileSearching
+ width: emptyColumn.implicitWidth
+ height: emptyColumn.implicitHeight
+
+ Column {
+ id: emptyColumn
+ spacing: Theme.spacingM
+
+ DankIcon {
+ anchors.horizontalCenter: parent.horizontalCenter
+ name: getEmptyIcon()
+ size: 48
+ color: Theme.outlineButton
+
+ function getEmptyIcon() {
+ var mode = root.controller?.searchMode ?? "all";
+ switch (mode) {
+ case "files":
+ var fileType = root.controller?.fileSearchType ?? "all";
+ switch (fileType) {
+ case "dir":
+ return "folder_open";
+ case "file":
+ return "insert_drive_file";
+ default:
+ return "folder_open";
+ }
+ case "plugins":
+ return "extension";
+ case "apps":
+ return "apps";
+ default:
+ return "search_off";
+ }
+ }
+ }
+
+ StyledText {
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: getEmptyText()
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ horizontalAlignment: Text.AlignHCenter
+
+ function getEmptyText() {
+ var mode = root.controller?.searchMode ?? "all";
+ var hasQuery = root.controller?.searchQuery?.length > 0;
+
+ switch (mode) {
+ case "files":
+ if (!DSearchService.dsearchAvailable)
+ return I18n.tr("File search requires dsearch\nInstall from github.com/AvengeMedia/danksearch");
+ if (!hasQuery)
+ return I18n.tr("Type to search files");
+ if (root.controller.searchQuery.length < 2)
+ return I18n.tr("Type at least 2 characters");
+ var fileType = root.controller?.fileSearchType ?? "all";
+ switch (fileType) {
+ case "dir":
+ return I18n.tr("No folders found");
+ case "file":
+ return I18n.tr("No files found");
+ default:
+ return I18n.tr("No results found");
+ }
+ case "plugins":
+ return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
+ case "apps":
+ return I18n.tr("No apps found");
+ default:
+ return I18n.tr("No results found");
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/Scorer.js b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/Scorer.js
new file mode 100644
index 0000000..7d54df7
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/Scorer.js
@@ -0,0 +1,265 @@
+.pragma library
+
+const Weights = {
+ exactMatch: 10000,
+ prefixMatch: 5000,
+ wordBoundary: 3000,
+ substring: 500,
+ fuzzy: 100,
+ frecency: 2000,
+ typeBonus: {
+ app: 1000,
+ plugin: 900,
+ file: 800,
+ action: 600
+ }
+}
+
+function tokenize(text) {
+ return text.toLowerCase().trim().split(/[\s\-_]+/).filter(function (w) { return w.length > 0 })
+}
+
+function hasWordBoundaryMatch(text, query) {
+ var textWords = tokenize(text)
+ var queryWords = tokenize(query)
+
+ if (queryWords.length === 0) return false
+ if (queryWords.length > textWords.length) return false
+
+ for (var i = 0; i <= textWords.length - queryWords.length; i++) {
+ var allMatch = true
+ for (var j = 0; j < queryWords.length; j++) {
+ if (!textWords[i + j].startsWith(queryWords[j])) {
+ allMatch = false
+ break
+ }
+ }
+ if (allMatch) return true
+ }
+ return false
+}
+
+function levenshteinDistance(s1, s2) {
+ var len1 = s1.length
+ var len2 = s2.length
+ var prev = new Array(len2 + 1)
+ var curr = new Array(len2 + 1)
+
+ for (var j = 0; j <= len2; j++)
+ prev[j] = j
+
+ for (var i = 1; i <= len1; i++) {
+ curr[0] = i
+ for (var j = 1; j <= len2; j++) {
+ var cost = s1[i - 1] === s2[j - 1] ? 0 : 1
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost)
+ }
+ var tmp = prev
+ prev = curr
+ curr = tmp
+ }
+ return prev[len2]
+}
+
+function fuzzyScore(text, query) {
+ var maxDistance = query.length === 3 ? 1 : query.length <= 6 ? 2 : 3
+ var bestScore = 0
+
+ if (Math.abs(text.length - query.length) <= maxDistance) {
+ var distance = levenshteinDistance(text, query)
+ if (distance <= maxDistance) {
+ var maxLen = Math.max(text.length, query.length)
+ bestScore = 1 - (distance / maxLen)
+ }
+ }
+
+ var words = tokenize(text)
+ for (var i = 0; i < words.length && bestScore < 0.8; i++) {
+ if (Math.abs(words[i].length - query.length) > maxDistance) continue
+ var wordDistance = levenshteinDistance(words[i], query)
+ if (wordDistance <= maxDistance) {
+ var wordMaxLen = Math.max(words[i].length, query.length)
+ var score = 1 - (wordDistance / wordMaxLen)
+ bestScore = Math.max(bestScore, score)
+ }
+ }
+
+ return bestScore
+}
+
+function getTimeBucketWeight(daysSinceUsed) {
+ for (var i = 0; i < TimeBuckets.length; i++) {
+ if (daysSinceUsed <= TimeBuckets[i].maxDays) {
+ return TimeBuckets[i].weight
+ }
+ }
+ return 10
+}
+
+function calculateTextScore(name, query) {
+ if (name === query) return Weights.exactMatch
+ if (name.startsWith(query)) return Weights.prefixMatch
+ if (hasWordBoundaryMatch(name, query)) return Weights.wordBoundary
+ if (name.includes(query)) return Weights.substring
+
+ if (query.length >= 3) {
+ var fs = fuzzyScore(name, query)
+ if (fs > 0) return fs * Weights.fuzzy
+ }
+
+ return 0
+}
+
+function score(item, query, frecencyData) {
+ var typeBonus = Weights.typeBonus[item.type] || 0
+
+ if (!query || query.length === 0) {
+ var usageCount = frecencyData ? frecencyData.usageCount : 0
+ return typeBonus + (usageCount * 100)
+ }
+
+ var name = (item.name || "").toLowerCase()
+ var q = query.toLowerCase()
+
+ var textScore = calculateTextScore(name, q)
+
+ if (textScore === 0 && item.subtitle) {
+ var subtitleScore = calculateTextScore(item.subtitle.toLowerCase(), q)
+ textScore = subtitleScore * 0.5
+ }
+
+ if (textScore === 0 && item.keywords) {
+ for (var i = 0; i < item.keywords.length; i++) {
+ var keywordScore = calculateTextScore(item.keywords[i].toLowerCase(), q)
+ if (keywordScore > 0) {
+ textScore = keywordScore * 0.3
+ break
+ }
+ }
+ }
+
+ if (textScore === 0) return 0
+
+ var usageBonus = frecencyData ? Math.min(frecencyData.usageCount * 50, Weights.frecency) : 0
+
+ return textScore + usageBonus + typeBonus
+}
+
+function scoreItems(items, query, getFrecencyFn) {
+ var scored = []
+
+ for (var i = 0; i < items.length; i++) {
+ var item = items[i]
+ var itemScore
+
+ if (item._preScored !== undefined && (query || item._preScored > 900)) {
+ itemScore = item._preScored
+ } else {
+ var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null
+ itemScore = score(item, query, frecencyData)
+ }
+
+ if (itemScore > 0 || !query || query.length === 0) {
+ scored.push({
+ item: item,
+ score: itemScore
+ })
+ }
+ }
+
+ scored.sort(function (a, b) {
+ return b.score - a.score
+ })
+
+ return scored
+}
+
+function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSection) {
+ var sections = {}
+ var result = []
+ var limit = maxPerSection || 50
+
+ for (var i = 0; i < sectionOrder.length; i++) {
+ var sectionId = sectionOrder[i].id
+ sections[sectionId] = {
+ id: sectionId,
+ title: sectionOrder[i].title,
+ icon: sectionOrder[i].icon,
+ priority: sectionOrder[i].priority,
+ items: [],
+ collapsed: false,
+ flatStartIndex: 0
+ }
+ }
+
+ for (var i = 0; i < scoredItems.length; i++) {
+ var scoredItem = scoredItems[i]
+ var item = scoredItem.item
+ var sectionId = item.section || "apps"
+
+ if (sections[sectionId] && sections[sectionId].items.length < limit) {
+ sections[sectionId].items.push(item)
+ } else if (sections["apps"] && sections["apps"].items.length < limit) {
+ sections["apps"].items.push(item)
+ }
+ }
+
+ for (var i = 0; i < sectionOrder.length; i++) {
+ var section = sections[sectionOrder[i].id]
+ if (section && section.items.length > 0) {
+ if (sortAlphabetically && section.id === "apps") {
+ section.items.sort(function (a, b) {
+ return (a.name || "").localeCompare(b.name || "")
+ })
+ }
+ result.push(section)
+ }
+ }
+
+ return result
+}
+
+function flattenSections(sections) {
+ var flat = []
+ flat._sectionBounds = null
+ var bounds = {}
+
+ for (var i = 0; i < sections.length; i++) {
+ var section = sections[i]
+
+ flat.push({
+ isHeader: true,
+ section: section,
+ sectionId: section.id,
+ sectionIndex: i
+ })
+
+ var itemStart = flat.length
+ section.flatStartIndex = itemStart
+
+ if (!section.collapsed) {
+ for (var j = 0; j < section.items.length; j++) {
+ flat.push({
+ isHeader: false,
+ item: section.items[j],
+ sectionId: section.id,
+ sectionIndex: i,
+ indexInSection: j
+ })
+ }
+ }
+
+ var itemEnd = flat.length - 1
+ var itemCount = flat.length - itemStart
+ if (itemCount > 0) {
+ bounds[section.id] = {
+ start: itemStart,
+ end: itemEnd,
+ count: itemCount
+ }
+ }
+ }
+
+ flat._sectionBounds = bounds
+ return flat
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/Section.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/Section.qml
new file mode 100644
index 0000000..aba4c2f
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/Section.qml
@@ -0,0 +1,114 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import qs.Common
+
+Item {
+ id: root
+
+ property var section: null
+ property var controller: null
+ property string viewMode: "list"
+ property int gridColumns: 4
+ property int startIndex: 0
+
+ signal itemClicked(int flatIndex)
+ signal itemRightClicked(int flatIndex, var item, real mouseX, real mouseY)
+
+ height: headerItem.height + (section?.collapsed ? 0 : contentLoader.height + Theme.spacingXS)
+ width: parent?.width ?? 200
+
+ SectionHeader {
+ id: headerItem
+ width: parent.width
+ section: root.section
+ controller: root.controller
+ viewMode: root.viewMode
+ canChangeViewMode: root.controller?.canChangeSectionViewMode(root.section?.id) ?? true
+
+ onViewModeToggled: {
+ if (root.controller && root.section) {
+ var newMode = root.viewMode === "list" ? "grid" : "list";
+ root.controller.setSectionViewMode(root.section.id, newMode);
+ }
+ }
+ }
+
+ Loader {
+ id: contentLoader
+ anchors.top: headerItem.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: Theme.spacingXS
+ active: !root.section?.collapsed
+ visible: active
+
+ sourceComponent: root.viewMode === "grid" ? gridComponent : listComponent
+
+ Component {
+ id: listComponent
+
+ Column {
+ spacing: 2
+ width: contentLoader.width
+
+ Repeater {
+ model: ScriptModel {
+ values: root.section?.items ?? []
+ objectProp: "id"
+ }
+
+ ResultItem {
+ required property var modelData
+ required property int index
+
+ width: parent?.width ?? 200
+ item: modelData
+ isSelected: (root.startIndex + index) === root.controller?.selectedFlatIndex
+ controller: root.controller
+ flatIndex: root.startIndex + index
+
+ onClicked: root.itemClicked(root.startIndex + index)
+ onRightClicked: (mouseX, mouseY) => {
+ root.itemRightClicked(root.startIndex + index, modelData, mouseX, mouseY);
+ }
+ }
+ }
+ }
+ }
+
+ Component {
+ id: gridComponent
+
+ Flow {
+ width: contentLoader.width
+ spacing: 4
+
+ Repeater {
+ model: ScriptModel {
+ values: root.section?.items ?? []
+ objectProp: "id"
+ }
+
+ GridItem {
+ required property var modelData
+ required property int index
+
+ width: Math.floor(contentLoader.width / root.gridColumns)
+ height: width + 24
+ item: modelData
+ isSelected: (root.startIndex + index) === root.controller?.selectedFlatIndex
+ controller: root.controller
+ flatIndex: root.startIndex + index
+
+ onClicked: root.itemClicked(root.startIndex + index)
+ onRightClicked: (mouseX, mouseY) => {
+ root.itemRightClicked(root.startIndex + index, modelData, mouseX, mouseY);
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/SectionHeader.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/SectionHeader.qml
new file mode 100644
index 0000000..ee366c7
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/SectionHeader.qml
@@ -0,0 +1,340 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property var section: null
+ property var controller: null
+ property string viewMode: "list"
+ property bool canChangeViewMode: true
+ property bool canCollapse: true
+ property bool isSticky: false
+
+ signal viewModeToggled
+
+ width: parent?.width ?? 200
+ height: 32
+ color: isSticky ? "transparent" : (hoverArea.containsMouse ? Theme.surfaceHover : "transparent")
+ radius: Theme.cornerRadius
+
+ MouseArea {
+ id: hoverArea
+ anchors.fill: parent
+ hoverEnabled: true
+ acceptedButtons: Qt.NoButton
+ }
+
+ Row {
+ id: leftContent
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ // Whether the apps category picker should replace the plain title
+ readonly property bool hasAppCategories: root.section?.id === "apps" && (root.controller?.appCategories?.length ?? 0) > 0
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ // Hide section icon when the category chip already shows one
+ visible: !leftContent.hasAppCategories
+ name: root.section?.icon ?? "folder"
+ size: 16
+ color: Theme.surfaceVariantText
+ }
+
+ // Plain title — hidden when the category chip is shown
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ visible: !leftContent.hasAppCategories
+ text: root.section?.title ?? ""
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ }
+
+ // Compact inline category chip — only visible on the apps section
+ Item {
+ id: categoryChip
+ visible: leftContent.hasAppCategories
+ anchors.verticalCenter: parent.verticalCenter
+ // Size to content with a fixed-min width so it doesn't jump around
+ width: chipRow.implicitWidth + Theme.spacingM * 2
+ height: 24
+
+ readonly property string currentCategory: root.controller?.appCategory || (root.controller?.appCategories?.length > 0 ? root.controller.appCategories[0] : "")
+ readonly property var iconMap: {
+ const cats = root.controller?.appCategories ?? [];
+ const m = {};
+ cats.forEach(c => { m[c] = AppSearchService.getCategoryIcon(c); });
+ return m;
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ radius: Theme.cornerRadius
+ color: chipArea.containsMouse || categoryPopup.visible ? Theme.surfaceContainerHigh : "transparent"
+ border.color: categoryPopup.visible ? Theme.primary : Theme.outlineMedium
+ border.width: categoryPopup.visible ? 2 : 1
+ }
+
+ Row {
+ id: chipRow
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: categoryChip.iconMap[categoryChip.currentCategory] ?? "apps"
+ size: 14
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: categoryChip.currentCategory
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ }
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: categoryPopup.visible ? "expand_less" : "expand_more"
+ size: 14
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ MouseArea {
+ id: chipArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (categoryPopup.visible) {
+ categoryPopup.close();
+ } else {
+ const pos = categoryChip.mapToItem(Overlay.overlay, 0, 0);
+ categoryPopup.x = pos.x;
+ categoryPopup.y = pos.y + categoryChip.height + 4;
+ categoryPopup.open();
+ }
+ }
+ }
+
+ Popup {
+ id: categoryPopup
+ parent: Overlay.overlay
+ width: Math.max(categoryChip.width, 180)
+ padding: 0
+ modal: true
+ dim: false
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+ background: Rectangle { color: "transparent" }
+
+ contentItem: Rectangle {
+ radius: Theme.cornerRadius
+ color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1)
+ border.color: Theme.primary
+ border.width: 2
+
+ ElevationShadow {
+ anchors.fill: parent
+ z: -1
+ level: Theme.elevationLevel2
+ fallbackOffset: 4
+ targetRadius: parent.radius
+ targetColor: parent.color
+ borderColor: parent.border.color
+ borderWidth: parent.border.width
+ shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
+ }
+
+ ListView {
+ id: categoryList
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+ model: root.controller?.appCategories ?? []
+ spacing: 2
+ clip: true
+ interactive: contentHeight > height
+ implicitHeight: contentHeight
+
+ delegate: Rectangle {
+ id: catDelegate
+ required property string modelData
+ required property int index
+ width: categoryList.width
+ height: 32
+ radius: Theme.cornerRadius
+ readonly property bool isCurrent: categoryChip.currentCategory === modelData
+ color: isCurrent ? Theme.primaryHover : catArea.containsMouse ? Theme.primaryHoverLight : "transparent"
+
+ Row {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ spacing: Theme.spacingS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: categoryChip.iconMap[catDelegate.modelData] ?? "apps"
+ size: 16
+ color: catDelegate.isCurrent ? Theme.primary : Theme.surfaceText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: catDelegate.modelData
+ font.pixelSize: Theme.fontSizeMedium
+ color: catDelegate.isCurrent ? Theme.primary : Theme.surfaceText
+ font.weight: catDelegate.isCurrent ? Font.Medium : Font.Normal
+ }
+ }
+
+ MouseArea {
+ id: catArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (root.controller)
+ root.controller.setAppCategory(catDelegate.modelData);
+ categoryPopup.close();
+ }
+ }
+ }
+ }
+ }
+
+ // Size to list content, cap at 10 visible items
+ height: Math.min((root.controller?.appCategories?.length ?? 0) * 34, 10 * 34) + Theme.spacingS * 2 + 4
+ }
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: root.section?.items?.length ?? 0
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.outlineButton
+ }
+ }
+
+ Row {
+ id: rightContent
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ Row {
+ id: viewModeRow
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+ visible: root.canChangeViewMode && !root.section?.collapsed
+
+ Repeater {
+ model: [
+ {
+ mode: "list",
+ icon: "view_list"
+ },
+ {
+ mode: "grid",
+ icon: "grid_view"
+ },
+ {
+ mode: "tile",
+ icon: "view_module"
+ }
+ ]
+
+ Rectangle {
+ required property var modelData
+ required property int index
+
+ width: 20
+ height: 20
+ radius: 4
+ color: root.viewMode === modelData.mode ? Theme.primaryHover : modeArea.containsMouse ? Theme.surfaceHover : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: parent.modelData.icon
+ size: 14
+ color: root.viewMode === parent.modelData.mode ? Theme.primary : Theme.surfaceVariantText
+ }
+
+ MouseArea {
+ id: modeArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (root.viewMode !== parent.modelData.mode && root.controller && root.section) {
+ root.controller.setSectionViewMode(root.section.id, parent.modelData.mode);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Item {
+ id: collapseButton
+ width: root.canCollapse ? 24 : 0
+ height: 24
+ visible: root.canCollapse
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: root.section?.collapsed ? "expand_more" : "expand_less"
+ size: 16
+ color: collapseArea.containsMouse ? Theme.primary : Theme.surfaceVariantText
+ }
+
+ MouseArea {
+ id: collapseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (root.controller && root.section) {
+ root.controller.toggleSection(root.section.id);
+ }
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ anchors.rightMargin: rightContent.width + Theme.spacingS
+ cursorShape: root.canCollapse ? Qt.PointingHandCursor : Qt.ArrowCursor
+ enabled: root.canCollapse
+ onClicked: {
+ if (root.canCollapse && root.controller && root.section) {
+ root.controller.toggleSection(root.section.id);
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: 1
+ color: Theme.outlineMedium
+ visible: root.isSticky
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/TileItem.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/TileItem.qml
new file mode 100644
index 0000000..0b48802
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/TileItem.qml
@@ -0,0 +1,199 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell.Wayland
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property var item: null
+ property bool isSelected: false
+ property bool isHovered: itemArea.containsMouse
+ property var controller: null
+ property int flatIndex: -1
+
+ signal clicked
+ signal rightClicked(real mouseX, real mouseY)
+
+ radius: Theme.cornerRadius
+ color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryPressed : "transparent"
+ border.width: isSelected ? 2 : 0
+ border.color: Theme.primary
+
+ readonly property string toplevelId: item?.data?.toplevelId ?? ""
+ readonly property var waylandToplevel: {
+ if (!toplevelId || !item?.pluginId)
+ return null;
+ const pluginInstance = PluginService.pluginInstances[item.pluginId];
+ if (!pluginInstance?.getToplevelById)
+ return null;
+ return pluginInstance.getToplevelById(toplevelId);
+ }
+ readonly property bool hasScreencopy: waylandToplevel !== null
+
+ readonly property string iconValue: {
+ if (!item)
+ return "";
+ if (hasScreencopy)
+ return "";
+ var data = item.data;
+ if (data?.imageUrl)
+ return "image:" + data.imageUrl;
+ if (data?.imagePath)
+ return "image:" + data.imagePath;
+ if (data?.path && isImageFile(data.path))
+ return "image:" + data.path;
+ switch (item.iconType) {
+ case "material":
+ case "nerd":
+ return "material:" + (item.icon || "image");
+ case "unicode":
+ return "unicode:" + (item.icon || "");
+ case "composite":
+ return item.iconFull || "";
+ case "image":
+ default:
+ return item.icon || "";
+ }
+ }
+
+ function isImageFile(path) {
+ if (!path)
+ return false;
+ var ext = path.split('.').pop().toLowerCase();
+ return ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "jxl", "avif", "heif", "exr"].indexOf(ext) >= 0;
+ }
+
+ DankRipple {
+ id: rippleLayer
+ rippleColor: Theme.surfaceText
+ cornerRadius: root.radius
+ }
+
+ Item {
+ anchors.fill: parent
+ anchors.margins: 4
+
+ Rectangle {
+ id: imageContainer
+ anchors.fill: parent
+ radius: Theme.cornerRadius - 2
+ color: Theme.surfaceContainerHigh
+ clip: true
+
+ ScreencopyView {
+ id: screencopyView
+ anchors.fill: parent
+ captureSource: root.waylandToplevel
+ live: root.hasScreencopy
+ visible: root.hasScreencopy
+
+ Rectangle {
+ anchors.fill: parent
+ color: root.isHovered ? Theme.withAlpha(Theme.surfaceVariant, 0.2) : "transparent"
+ }
+ }
+
+ AppIconRenderer {
+ anchors.fill: parent
+ iconValue: root.iconValue
+ iconSize: Math.min(parent.width, parent.height)
+ fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
+ materialIconSizeAdjustment: iconSize * 0.3
+ visible: !root.hasScreencopy
+ }
+
+ Rectangle {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: labelText.implicitHeight + Theme.spacingS * 2
+ color: Theme.withAlpha(Theme.surfaceContainer, 0.85)
+ visible: root.item?.name?.length > 0
+
+ Text {
+ id: labelText
+ anchors.fill: parent
+ anchors.margins: Theme.spacingXS
+ text: root.item?._hName ?? root.item?.name ?? ""
+ textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: Theme.fontFamily
+ color: Theme.surfaceText
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignLeft
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+
+ Rectangle {
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingXS
+ width: 20
+ height: 20
+ radius: 10
+ color: Theme.primary
+ visible: root.isSelected
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "check"
+ size: 14
+ color: Theme.primaryText
+ }
+ }
+
+ Rectangle {
+ id: attributionBadge
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.margins: Theme.spacingXS
+ width: root.hasScreencopy ? 28 : 40
+ height: root.hasScreencopy ? 28 : 16
+ radius: root.hasScreencopy ? 14 : 4
+ color: root.hasScreencopy ? Theme.surfaceContainer : "transparent"
+ visible: attributionImage.status === Image.Ready
+ opacity: 0.95
+
+ Image {
+ id: attributionImage
+ anchors.fill: parent
+ anchors.margins: root.hasScreencopy ? 4 : 0
+ fillMode: Image.PreserveAspectFit
+ source: root.item?.data?.attribution || ""
+ mipmap: true
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ id: itemArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+
+ onPressed: mouse => {
+ if (mouse.button === Qt.LeftButton)
+ rippleLayer.trigger(mouse.x, mouse.y);
+ }
+ onClicked: mouse => {
+ if (mouse.button === Qt.RightButton) {
+ var scenePos = mapToItem(null, mouse.x, mouse.y);
+ root.rightClicked(scenePos.x, scenePos.y);
+ return;
+ }
+ root.clicked();
+ }
+
+ onPositionChanged: {
+ if (root.controller)
+ root.controller.keyboardNavigationActive = false;
+ }
+ }
+}