import QtQuick import QtQuick.Controls import Quickshell import qs.Common import qs.Services import qs.Widgets Item { id: root // DEVELOPER NOTE: This component manages the AppDrawer launcher (accessed via DankBar icon). // Changes to launcher behavior, especially item rendering, filtering, or model structure, // likely require corresponding updates in Modals/Spotlight/SpotlightResults.qml and vice versa. property string searchQuery: "" property string selectedCategory: I18n.tr("All") property string viewMode: "list" // "list" or "grid" property int selectedIndex: 0 property int maxResults: 50 property int gridColumns: 4 property bool debounceSearch: true property int debounceInterval: 50 property bool keyboardNavigationActive: false property bool suppressUpdatesWhileLaunching: false property var categories: { const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science") const result = [I18n.tr("All")] return result.concat(allCategories.filter(cat => cat !== I18n.tr("All"))) } readonly property var categoryIcons: categories.map(category => AppSearchService.getCategoryIcon(category)) property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {} property alias model: filteredModel property var _watchApplications: AppSearchService.applications property var _uniqueApps: [] property bool _isTriggered: false property string _triggeredCategory: "" property bool _updatingFromTrigger: false signal appLaunched(var app) signal categorySelected(string category) signal viewModeSelected(string mode) function updateCategories() { const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science") const result = [I18n.tr("All")] categories = result.concat(allCategories.filter(cat => cat !== I18n.tr("All"))) } Connections { target: PluginService function onPluginLoaded() { updateCategories() } function onPluginUnloaded() { updateCategories() } function onPluginListUpdated() { updateCategories() } } Connections { target: SettingsData function onSortAppsAlphabeticallyChanged() { updateFilteredModel() } } function updateFilteredModel() { if (suppressUpdatesWhileLaunching) { suppressUpdatesWhileLaunching = false return } filteredModel.clear() selectedIndex = 0 keyboardNavigationActive = false const triggerResult = checkPluginTriggers(searchQuery) if (triggerResult.triggered) { console.log("AppLauncher: Plugin trigger detected:", triggerResult.trigger, "for plugin:", triggerResult.pluginId) } let apps = [] const allCategory = I18n.tr("All") const emptyTriggerPlugins = typeof PluginService !== "undefined" ? PluginService.getPluginsWithEmptyTrigger() : [] if (triggerResult.triggered) { _isTriggered = true _triggeredCategory = triggerResult.pluginCategory _updatingFromTrigger = true selectedCategory = triggerResult.pluginCategory _updatingFromTrigger = false apps = AppSearchService.getPluginItems(triggerResult.pluginCategory, triggerResult.query) } else { if (_isTriggered) { _updatingFromTrigger = true selectedCategory = allCategory _updatingFromTrigger = false _isTriggered = false _triggeredCategory = "" } if (searchQuery.length === 0) { if (selectedCategory === allCategory) { let emptyTriggerItems = [] emptyTriggerPlugins.forEach(pluginId => { const plugin = PluginService.getLauncherPlugin(pluginId) const pluginCategory = plugin.name || pluginId const items = AppSearchService.getPluginItems(pluginCategory, "") emptyTriggerItems = emptyTriggerItems.concat(items) }) apps = AppSearchService.applications.concat(emptyTriggerItems) } else { apps = AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults) } } else { if (selectedCategory === allCategory) { apps = AppSearchService.searchApplications(searchQuery) let emptyTriggerItems = [] emptyTriggerPlugins.forEach(pluginId => { const plugin = PluginService.getLauncherPlugin(pluginId) const pluginCategory = plugin.name || pluginId const items = AppSearchService.getPluginItems(pluginCategory, searchQuery) emptyTriggerItems = emptyTriggerItems.concat(items) }) apps = apps.concat(emptyTriggerItems) } else { const categoryApps = AppSearchService.getAppsInCategory(selectedCategory) if (categoryApps.length > 0) { const allSearchResults = AppSearchService.searchApplications(searchQuery) const categoryNames = new Set(categoryApps.map(app => app.name)) apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults) } else { apps = [] } } } } if (searchQuery.length === 0) { if (SettingsData.sortAppsAlphabetically) { apps = apps.sort((a, b) => { return (a.name || "").localeCompare(b.name || "") }) } else { apps = apps.sort((a, b) => { const aId = a.id || a.execString || a.exec || "" const bId = b.id || b.execString || b.exec || "" const aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0 const bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0 if (aUsage !== bUsage) { return bUsage - aUsage } return (a.name || "").localeCompare(b.name || "") }) } } const seenNames = new Set() const uniqueApps = [] apps.forEach(app => { if (app) { const itemKey = app.name + "|" + (app.execString || app.exec || app.action || "") if (seenNames.has(itemKey)) { return } seenNames.add(itemKey) uniqueApps.push(app) const isPluginItem = app.action !== undefined filteredModel.append({ "name": app.name || "", "exec": app.execString || app.exec || app.action || "", "icon": app.icon !== undefined ? app.icon : (isPluginItem ? "" : "application-x-executable"), "comment": app.comment || "", "categories": app.categories || [], "isPlugin": isPluginItem, "appIndex": uniqueApps.length - 1 }) } }) root._uniqueApps = uniqueApps } function selectNext() { if (filteredModel.count === 0) { return } keyboardNavigationActive = true selectedIndex = viewMode === "grid" ? Math.min(selectedIndex + gridColumns, filteredModel.count - 1) : Math.min(selectedIndex + 1, filteredModel.count - 1) } function selectPrevious() { if (filteredModel.count === 0) { return } keyboardNavigationActive = true selectedIndex = viewMode === "grid" ? Math.max(selectedIndex - gridColumns, 0) : Math.max(selectedIndex - 1, 0) } function selectNextInRow() { if (filteredModel.count === 0 || viewMode !== "grid") { return } keyboardNavigationActive = true selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1) } function selectPreviousInRow() { if (filteredModel.count === 0 || viewMode !== "grid") { return } keyboardNavigationActive = true selectedIndex = Math.max(selectedIndex - 1, 0) } function launchSelected() { if (filteredModel.count === 0 || selectedIndex < 0 || selectedIndex >= filteredModel.count) { return } const selectedApp = filteredModel.get(selectedIndex) launchApp(selectedApp) } function launchApp(appData) { if (!appData || typeof appData.appIndex === "undefined" || appData.appIndex < 0 || appData.appIndex >= _uniqueApps.length) { return } suppressUpdatesWhileLaunching = true const actualApp = _uniqueApps[appData.appIndex] if (appData.isPlugin) { const pluginId = getPluginIdForItem(actualApp) if (pluginId) { AppSearchService.executePluginItem(actualApp, pluginId) appLaunched(appData) return } } else { SessionService.launchDesktopEntry(actualApp) appLaunched(appData) AppUsageHistoryData.addAppUsage(actualApp) } } function setCategory(category) { selectedCategory = category categorySelected(category) } function setViewMode(mode) { viewMode = mode viewModeSelected(mode) } onSearchQueryChanged: { if (debounceSearch) { searchDebounceTimer.restart() } else { updateFilteredModel() } } onSelectedCategoryChanged: { if (_updatingFromTrigger) { return } updateFilteredModel() } onAppUsageRankingChanged: updateFilteredModel() on_WatchApplicationsChanged: updateFilteredModel() Component.onCompleted: { updateFilteredModel() } ListModel { id: filteredModel } Timer { id: searchDebounceTimer interval: root.debounceInterval repeat: false onTriggered: updateFilteredModel() } // Plugin trigger system functions function checkPluginTriggers(query) { if (!query || typeof PluginService === "undefined") { return { triggered: false, pluginCategory: "", query: "" } } const triggers = PluginService.getAllPluginTriggers() for (const trigger in triggers) { if (query.startsWith(trigger)) { const pluginId = triggers[trigger] const plugin = PluginService.getLauncherPlugin(pluginId) if (plugin) { const remainingQuery = query.substring(trigger.length).trim() const result = { triggered: true, pluginId: pluginId, pluginCategory: plugin.name || pluginId, query: remainingQuery, trigger: trigger } return result } } } return { triggered: false, pluginCategory: "", query: "" } } function getPluginIdForItem(item) { if (!item || !item.categories || typeof PluginService === "undefined") { return null } const launchers = PluginService.getLauncherPlugins() for (const pluginId in launchers) { const plugin = launchers[pluginId] const pluginCategory = plugin.name || pluginId let hasCategory = false if (Array.isArray(item.categories)) { hasCategory = item.categories.includes(pluginCategory) } else if (item.categories && typeof item.categories.count !== "undefined") { for (let i = 0; i < item.categories.count; i++) { if (item.categories.get(i) === pluginCategory) { hasCategory = true break } } } if (hasCategory) { return pluginId } } return null } }