diff --git a/quickshell/Modals/DankLauncherV2/ActionPanel.qml b/quickshell/Modals/DankLauncherV2/ActionPanel.qml index 957bece1..d26aa90c 100644 --- a/quickshell/Modals/DankLauncherV2/ActionPanel.qml +++ b/quickshell/Modals/DankLauncherV2/ActionPanel.qml @@ -33,7 +33,8 @@ Rectangle { result.push(selectedItem.primaryAction); } - if (selectedItem?.type === "plugin") { + switch (selectedItem?.type) { + case "plugin": var pluginActions = getPluginContextMenuActions(); for (var i = 0; i < pluginActions.length; i++) { var act = pluginActions[i]; @@ -44,7 +45,17 @@ Rectangle { pluginAction: act.action }); } - } else if (selectedItem?.type === "app" && !selectedItem?.isCore) { + 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 (selectedItem?.actions) { for (var i = 0; i < selectedItem.actions.length; i++) { result.push(selectedItem.actions[i]); @@ -57,18 +68,22 @@ Rectangle { action: "launch_dgpu" }); } + break; } return result; } readonly property bool hasActions: { - if (selectedItem?.type === "app" && !selectedItem?.isCore) - return true; - if (selectedItem?.type === "plugin") { - var pluginActions = getPluginContextMenuActions(); - return pluginActions.length > 0; + 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; } - return actions.length > 1; } width: parent?.width ?? 200 diff --git a/quickshell/Modals/DankLauncherV2/Controller.qml b/quickshell/Modals/DankLauncherV2/Controller.qml index 4fe5e9d8..54d664bf 100644 --- a/quickshell/Modals/DankLauncherV2/Controller.qml +++ b/quickshell/Modals/DankLauncherV2/Controller.qml @@ -6,6 +6,9 @@ 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 @@ -692,241 +695,26 @@ Item { function transformApp(app) { var appId = app.id || app.execString || app.exec || ""; var override = SessionData.getAppOverride(appId); - - 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: I18n.tr("Launch"), - icon: "open_in_new", - action: "launch" - } - }; + return Transform.transformApp(app, override, [], I18n.tr("Launch")); } function transformCoreApp(app) { - 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: I18n.tr("Open"), - icon: "open_in_new", - action: "launch" - } - }; + return Transform.transformCoreApp(app, I18n.tr("Open")); } function transformBuiltInLauncherItem(item, pluginId) { - var rawIcon = item.icon || "extension"; - var icon = 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: I18n.tr("Open"), - icon: "open_in_new", - action: "execute" - } - }; + return Transform.transformBuiltInLauncherItem(item, pluginId, I18n.tr("Open")); } function transformFileResult(file) { - var filename = file.path ? file.path.split("/").pop() : ""; - var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : ""; - - return { - id: file.path || "", - type: "file", - name: filename, - subtitle: dirname, - icon: getFileIcon(filename), - iconType: "material", - section: "files", - data: file, - actions: [ - { - name: I18n.tr("Open folder"), - icon: "folder_open", - action: "open_folder" - }, - { - name: I18n.tr("Copy path"), - icon: "content_copy", - action: "copy_path" - } - ], - primaryAction: { - name: I18n.tr("Open"), - icon: "open_in_new", - action: "open" - } - }; - } - - function getFileIcon(filename) { - var ext = filename.lastIndexOf(".") > 0 ? filename.substring(filename.lastIndexOf(".") + 1).toLowerCase() : ""; - - var iconMap = { - "pdf": "picture_as_pdf", - "doc": "description", - "docx": "description", - "odt": "description", - "xls": "table_chart", - "xlsx": "table_chart", - "ods": "table_chart", - "ppt": "slideshow", - "pptx": "slideshow", - "odp": "slideshow", - "txt": "article", - "md": "article", - "rst": "article", - "jpg": "image", - "jpeg": "image", - "png": "image", - "gif": "image", - "svg": "image", - "webp": "image", - "mp3": "audio_file", - "wav": "audio_file", - "flac": "audio_file", - "ogg": "audio_file", - "mp4": "video_file", - "mkv": "video_file", - "avi": "video_file", - "webm": "video_file", - "zip": "folder_zip", - "tar": "folder_zip", - "gz": "folder_zip", - "7z": "folder_zip", - "rar": "folder_zip", - "js": "code", - "ts": "code", - "py": "code", - "rs": "code", - "go": "code", - "java": "code", - "c": "code", - "cpp": "code", - "h": "code", - "html": "web", - "css": "web", - "htm": "web", - "json": "data_object", - "xml": "data_object", - "yaml": "data_object", - "yml": "data_object", - "sh": "terminal", - "bash": "terminal", - "zsh": "terminal" - }; - - return iconMap[ext] || "insert_drive_file"; + return Transform.transformFileResult(file, I18n.tr("Open"), I18n.tr("Open folder"), I18n.tr("Copy path")); } function evaluateCalculator(query) { - if (!query || query.length === 0) + var calc = Utils.evaluateCalculator(query); + if (!calc) return null; - - var mathExpr = query.replace(/[^0-9+\-*/().%\s^]/g, ""); - if (mathExpr.length < 2) - return null; - - var hasMath = /[+\-*/^%]/.test(query) && /\d/.test(query); - if (!hasMath) - return null; - - try { - var sanitized = mathExpr.replace(/\^/g, "**"); - var result = Function('"use strict"; return (' + sanitized + ')')(); - - if (typeof result === "number" && isFinite(result)) { - var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, ""); - - return { - id: "calculator_result", - type: "calculator", - name: displayResult, - subtitle: query + " =", - icon: "calculate", - iconType: "material", - section: "calculator", - data: { - expression: query, - result: result - }, - actions: [], - primaryAction: { - name: I18n.tr("Copy"), - icon: "content_copy", - action: "copy" - } - }; - } - } catch (e) {} - - return null; + return Transform.createCalculatorItem(calc, query, I18n.tr("Copy")); } function detectTrigger(query) { @@ -998,17 +786,7 @@ Item { } function sortPluginIdsByOrder(pluginIds) { - var order = SettingsData.launcherPluginOrder || []; - if (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; - }); + return Utils.sortPluginIdsByOrder(pluginIds, SettingsData.launcherPluginOrder || []); } function getAllVisiblePluginsOrdered() { @@ -1029,17 +807,7 @@ Item { isBuiltIn: true }); } - var order = SettingsData.launcherPluginOrder || []; - if (order.length === 0) - return all; - var orderMap = {}; - for (var i = 0; i < order.length; i++) - orderMap[order[i]] = i; - return all.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; - }); + return Utils.sortPluginsOrdered(all, SettingsData.launcherPluginOrder || []); } function getEmptyTriggerPluginsOrdered() { @@ -1062,72 +830,27 @@ Item { isBuiltIn: true }); } - var order = SettingsData.launcherPluginOrder || []; - if (order.length === 0) - return all; - var orderMap = {}; - for (var i = 0; i < order.length; i++) - orderMap[order[i]] = i; - return all.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; - }); + 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 plugin = launchers[pluginId]; var trigger = PluginService.getPluginTrigger(pluginId); - var rawIcon = plugin.icon || "extension"; - items.push({ - id: "browse_" + pluginId, - type: "plugin_browse", - name: plugin.name || pluginId, - subtitle: trigger ? I18n.tr("Trigger: %1").arg(trigger) : I18n.tr("No trigger"), - icon: stripIconPrefix(rawIcon), - iconType: detectIconType(rawIcon), - section: "browse_plugins", - data: { - pluginId: pluginId, - plugin: plugin - }, - actions: [], - primaryAction: { - name: I18n.tr("Browse"), - icon: "arrow_forward", - action: "browse_plugin" - } - }); + 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 plugin = builtInLaunchers[pluginId]; var trigger = AppSearchService.getBuiltInPluginTrigger(pluginId); - items.push({ - id: "browse_" + pluginId, - type: "plugin_browse", - name: plugin.name || pluginId, - subtitle: trigger ? I18n.tr("Trigger: %1").arg(trigger) : I18n.tr("No trigger"), - icon: plugin.cornerIcon || "extension", - iconType: "material", - section: "browse_plugins", - data: { - pluginId: pluginId, - plugin: plugin, - isBuiltIn: true - }, - actions: [], - primaryAction: { - name: I18n.tr("Browse"), - icon: "arrow_forward", - action: "browse_plugin" - } - }); + var isAllowed = SettingsData.getPluginAllowWithoutTrigger(pluginId); + items.push(Transform.createPluginBrowseItem(pluginId, builtInLaunchers[pluginId], trigger, true, isAllowed, browseLabel, triggerLabel, noTriggerLabel)); } return items; @@ -1152,34 +875,6 @@ Item { return transformed; } - 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 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 getPluginName(pluginId, isBuiltIn) { if (isBuiltIn) { var plugin = AppSearchService.builtInPlugins[pluginId]; @@ -1205,7 +900,7 @@ Item { var rawIcon = launchers[pluginId].icon || "extension"; return { name: launchers[pluginId].name || pluginId, - icon: stripIconPrefix(rawIcon) + icon: Utils.stripIconPrefix(rawIcon) }; } return { @@ -1274,36 +969,7 @@ Item { } function transformPluginItem(item, pluginId) { - var rawIcon = item.icon || "extension"; - var icon = 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: I18n.tr("Select"), - icon: "check", - action: "execute" - } - }; + return Transform.transformPluginItem(item, pluginId, I18n.tr("Select")); } function getFrecencyForItem(item) { @@ -1329,11 +995,7 @@ Item { } function getFirstItemIndex() { - for (var i = 0; i < flatModel.length; i++) { - if (!flatModel[i].isHeader) - return i; - } - return 0; + return Nav.getFirstItemIndex(flatModel); } function updateSelectedItem() { @@ -1354,266 +1016,67 @@ Item { return getSectionViewMode(entry.sectionId); } - function findNextNonHeaderIndex(startIndex) { - for (var i = startIndex; i < flatModel.length; i++) { - if (!flatModel[i].isHeader) - return i; - } - return -1; - } - - function findPrevNonHeaderIndex(startIndex) { - for (var i = startIndex; i >= 0; i--) { - if (!flatModel[i].isHeader) - return i; - } - return -1; - } - - function getSectionBounds(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(sectionId) { - var mode = getSectionViewMode(sectionId); - if (mode === "tile") - return 3; - if (mode === "grid") - return gridColumns; - return 1; + return Nav.getGridColumns(getSectionViewMode(sectionId), gridColumns); } function selectNext() { keyboardNavigationActive = true; - if (flatModel.length === 0) - return; - var entry = flatModel[selectedFlatIndex]; - if (!entry || entry.isHeader) { - var next = findNextNonHeaderIndex(selectedFlatIndex + 1); - if (next !== -1) { - selectedFlatIndex = next; - updateSelectedItem(); - } - return; - } - - var viewMode = getSectionViewMode(entry.sectionId); - if (viewMode === "list") { - var next = findNextNonHeaderIndex(selectedFlatIndex + 1); - if (next !== -1) { - selectedFlatIndex = next; - updateSelectedItem(); - } - return; - } - - var bounds = getSectionBounds(entry.sectionId); - var cols = getGridColumns(entry.sectionId); - var posInSection = selectedFlatIndex - bounds.start; - var newPosInSection = posInSection + cols; - - if (newPosInSection < bounds.count) { - selectedFlatIndex = bounds.start + newPosInSection; + var newIndex = Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode); + if (newIndex !== selectedFlatIndex) { + selectedFlatIndex = newIndex; updateSelectedItem(); - } else { - var nextSection = findNextNonHeaderIndex(bounds.end + 1); - if (nextSection !== -1) { - selectedFlatIndex = nextSection; - updateSelectedItem(); - } } } function selectPrevious() { keyboardNavigationActive = true; - if (flatModel.length === 0) - return; - var entry = flatModel[selectedFlatIndex]; - if (!entry || entry.isHeader) { - var prev = findPrevNonHeaderIndex(selectedFlatIndex - 1); - if (prev !== -1) { - selectedFlatIndex = prev; - updateSelectedItem(); - } - return; - } - - var viewMode = getSectionViewMode(entry.sectionId); - if (viewMode === "list") { - var prev = findPrevNonHeaderIndex(selectedFlatIndex - 1); - if (prev !== -1) { - selectedFlatIndex = prev; - updateSelectedItem(); - } - return; - } - - var bounds = getSectionBounds(entry.sectionId); - var cols = getGridColumns(entry.sectionId); - var posInSection = selectedFlatIndex - bounds.start; - var newPosInSection = posInSection - cols; - - if (newPosInSection >= 0) { - selectedFlatIndex = bounds.start + newPosInSection; + var newIndex = Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode); + if (newIndex !== selectedFlatIndex) { + selectedFlatIndex = newIndex; updateSelectedItem(); - } else { - var prevItem = findPrevNonHeaderIndex(bounds.start - 1); - if (prevItem !== -1) { - selectedFlatIndex = prevItem; - updateSelectedItem(); - } } } function selectRight() { keyboardNavigationActive = true; - if (flatModel.length === 0) - return; - var entry = flatModel[selectedFlatIndex]; - if (!entry || entry.isHeader) { - var next = findNextNonHeaderIndex(selectedFlatIndex + 1); - if (next !== -1) { - selectedFlatIndex = next; - updateSelectedItem(); - } - return; - } - - var viewMode = getSectionViewMode(entry.sectionId); - if (viewMode === "list") { - var next = findNextNonHeaderIndex(selectedFlatIndex + 1); - if (next !== -1) { - selectedFlatIndex = next; - updateSelectedItem(); - } - return; - } - - var bounds = getSectionBounds(entry.sectionId); - var posInSection = selectedFlatIndex - bounds.start; - if (posInSection + 1 < bounds.count) { - selectedFlatIndex = bounds.start + posInSection + 1; + var newIndex = Nav.calculateRightIndex(flatModel, selectedFlatIndex, getSectionViewMode); + if (newIndex !== selectedFlatIndex) { + selectedFlatIndex = newIndex; updateSelectedItem(); } } function selectLeft() { keyboardNavigationActive = true; - if (flatModel.length === 0) - return; - var entry = flatModel[selectedFlatIndex]; - if (!entry || entry.isHeader) { - var prev = findPrevNonHeaderIndex(selectedFlatIndex - 1); - if (prev !== -1) { - selectedFlatIndex = prev; - updateSelectedItem(); - } - return; - } - - var viewMode = getSectionViewMode(entry.sectionId); - if (viewMode === "list") { - var prev = findPrevNonHeaderIndex(selectedFlatIndex - 1); - if (prev !== -1) { - selectedFlatIndex = prev; - updateSelectedItem(); - } - return; - } - - var bounds = getSectionBounds(entry.sectionId); - var posInSection = selectedFlatIndex - bounds.start; - if (posInSection > 0) { - selectedFlatIndex = bounds.start + posInSection - 1; + var newIndex = Nav.calculateLeftIndex(flatModel, selectedFlatIndex, getSectionViewMode); + if (newIndex !== selectedFlatIndex) { + selectedFlatIndex = newIndex; updateSelectedItem(); } } function selectNextSection() { keyboardNavigationActive = true; - 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) { - selectedFlatIndex = j; - updateSelectedItem(); - return; - } - } - } - if (flatModel[i].section.id === currentSection) { - foundCurrent = true; - } - } + var newIndex = Nav.calculateNextSectionIndex(flatModel, selectedFlatIndex); + if (newIndex !== selectedFlatIndex) { + selectedFlatIndex = newIndex; + updateSelectedItem(); } } function selectPreviousSection() { keyboardNavigationActive = true; - 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) { - selectedFlatIndex = j; - updateSelectedItem(); - return; - } - } + var newIndex = Nav.calculatePrevSectionIndex(flatModel, selectedFlatIndex); + if (newIndex !== selectedFlatIndex) { + selectedFlatIndex = newIndex; + updateSelectedItem(); } } function selectPageDown(visibleItems) { keyboardNavigationActive = true; - if (flatModel.length === 0) - return; - var itemsToSkip = visibleItems || 8; - var newIndex = selectedFlatIndex; - - for (var i = 0; i < itemsToSkip; i++) { - var next = findNextNonHeaderIndex(newIndex + 1); - if (next === -1) - break; - newIndex = next; - } - + var newIndex = Nav.calculatePageDownIndex(flatModel, selectedFlatIndex, visibleItems); if (newIndex !== selectedFlatIndex) { selectedFlatIndex = newIndex; updateSelectedItem(); @@ -1622,18 +1085,7 @@ Item { function selectPageUp(visibleItems) { keyboardNavigationActive = true; - if (flatModel.length === 0) - return; - var itemsToSkip = visibleItems || 8; - var newIndex = selectedFlatIndex; - - for (var i = 0; i < itemsToSkip; i++) { - var prev = findPrevNonHeaderIndex(newIndex - 1); - if (prev === -1) - break; - newIndex = prev; - } - + var newIndex = Nav.calculatePageUpIndex(flatModel, selectedFlatIndex, visibleItems); if (newIndex !== selectedFlatIndex) { selectedFlatIndex = newIndex; updateSelectedItem(); @@ -1764,6 +1216,14 @@ Item { 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({ diff --git a/quickshell/Modals/DankLauncherV2/ControllerUtils.js b/quickshell/Modals/DankLauncherV2/ControllerUtils.js new file mode 100644 index 00000000..e63c9327 --- /dev/null +++ b/quickshell/Modals/DankLauncherV2/ControllerUtils.js @@ -0,0 +1,157 @@ +.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 evaluateCalculator(query) { + if (!query || query.length === 0) + return null; + + var mathExpr = query.replace(/[^0-9+\-*/().%\s^]/g, ""); + if (mathExpr.length < 2) + return null; + + var hasMath = /[+\-*/^%]/.test(query) && /\d/.test(query); + if (!hasMath) + return null; + + try { + var sanitized = mathExpr.replace(/\^/g, "**"); + var result = Function('"use strict"; return (' + sanitized + ')')(); + + if (typeof result === "number" && isFinite(result)) { + var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, ""); + return { + expression: query, + result: result, + displayResult: displayResult + }; + } + } catch (e) { } + + return null; +} + +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/quickshell/Modals/DankLauncherV2/ItemTransformers.js b/quickshell/Modals/DankLauncherV2/ItemTransformers.js new file mode 100644 index 00000000..703da7a5 --- /dev/null +++ b/quickshell/Modals/DankLauncherV2/ItemTransformers.js @@ -0,0 +1,223 @@ +.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" + } + }; +} + +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" + } + }; +} + +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" + } + }; +} + +function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel) { + var filename = file.path ? file.path.split("/").pop() : ""; + var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : ""; + + return { + id: file.path || "", + type: "file", + name: filename, + subtitle: dirname, + icon: Utils.getFileIcon(filename), + iconType: "material", + section: "files", + data: file, + actions: [ + { + name: openFolderLabel, + icon: "folder_open", + action: "open_folder" + }, + { + name: copyPathLabel, + icon: "content_copy", + action: "copy_path" + } + ], + primaryAction: { + name: openLabel, + icon: "open_in_new", + action: "open" + } + }; +} + +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" + } + }; +} + +function createCalculatorItem(calc, query, copyLabel) { + return { + id: "calculator_result", + type: "calculator", + name: calc.displayResult, + subtitle: query + " =", + icon: "calculate", + iconType: "material", + section: "calculator", + data: { + expression: calc.expression, + result: calc.result + }, + actions: [], + primaryAction: { + name: copyLabel, + icon: "content_copy", + action: "copy" + } + }; +} + +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" + } + }; +} diff --git a/quickshell/Modals/DankLauncherV2/NavigationHelpers.js b/quickshell/Modals/DankLauncherV2/NavigationHelpers.js new file mode 100644 index 00000000..f13e2bfd --- /dev/null +++ b/quickshell/Modals/DankLauncherV2/NavigationHelpers.js @@ -0,0 +1,245 @@ +.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) { + 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 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/quickshell/Modals/DankLauncherV2/ResultItem.qml b/quickshell/Modals/DankLauncherV2/ResultItem.qml index 3822e3ac..b6c2a37b 100644 --- a/quickshell/Modals/DankLauncherV2/ResultItem.qml +++ b/quickshell/Modals/DankLauncherV2/ResultItem.qml @@ -9,7 +9,7 @@ Rectangle { property var item: null property bool isSelected: false - property bool isHovered: itemArea.containsMouse + property bool isHovered: itemArea.containsMouse || allModeToggleArea.containsMouse property var controller: null property int flatIndex: -1 @@ -38,6 +38,29 @@ Rectangle { color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryPressed : "transparent" radius: Theme.cornerRadius + MouseArea { + id: itemArea + anchors.fill: parent + anchors.rightMargin: root.item?.type === "plugin_browse" ? 40 : 0 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + + 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 @@ -86,7 +109,47 @@ Rectangle { spacing: Theme.spacingS Rectangle { - visible: root.item?.type && root.item.type !== "app" + 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 @@ -116,27 +179,4 @@ Rectangle { } } } - - MouseArea { - id: itemArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton - - 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/quickshell/Modals/DankLauncherV2/Scorer.js b/quickshell/Modals/DankLauncherV2/Scorer.js index a233851c..6e2cfb23 100644 --- a/quickshell/Modals/DankLauncherV2/Scorer.js +++ b/quickshell/Modals/DankLauncherV2/Scorer.js @@ -16,7 +16,7 @@ const Weights = { } function tokenize(text) { - return text.toLowerCase().trim().split(/[\s\-_]+/).filter(function(w) { return w.length > 0 }) + return text.toLowerCase().trim().split(/[\s\-_]+/).filter(function (w) { return w.length > 0 }) } function hasWordBoundaryMatch(text, query) { @@ -164,7 +164,7 @@ function scoreItems(items, query, getFrecencyFn) { } } - scored.sort(function(a, b) { + scored.sort(function (a, b) { return b.score - a.score }) @@ -204,7 +204,7 @@ function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSec var section = sections[sectionOrder[i].id] if (section && section.items.length > 0) { if (sortAlphabetically && section.id === "apps") { - section.items.sort(function(a, b) { + section.items.sort(function (a, b) { return (a.name || "").localeCompare(b.name || "") }) }