mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 13:32:50 -05:00
457 lines
16 KiB
QML
457 lines
16 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import qs.Common
|
|
import qs.Services
|
|
|
|
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: []
|
|
readonly property var categoryIcons: categories.map(category => AppSearchService.getCategoryIcon(category))
|
|
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
|
|
property alias model: filteredModel
|
|
property var _uniqueApps: []
|
|
property bool _initialized: false
|
|
property bool _isTriggered: false
|
|
property string _triggeredCategory: ""
|
|
property bool _updatingFromTrigger: false
|
|
|
|
signal appLaunched(var app)
|
|
signal categorySelected(string category)
|
|
signal viewModeSelected(string mode)
|
|
|
|
function ensureInitialized() {
|
|
if (_initialized)
|
|
return;
|
|
_initialized = true;
|
|
updateCategories();
|
|
updateFilteredModel();
|
|
}
|
|
|
|
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();
|
|
}
|
|
function onRequestLauncherUpdate(pluginId) {
|
|
// Only update if we are actually looking at this plugin or in All category
|
|
updateFilteredModel();
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: SettingsData
|
|
function onSortAppsAlphabeticallyChanged() {
|
|
updateFilteredModel();
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: SessionData
|
|
function onHiddenAppsChanged() {
|
|
updateFilteredModel();
|
|
}
|
|
function onAppOverridesChanged() {
|
|
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;
|
|
if (triggerResult.isBuiltIn) {
|
|
apps = AppSearchService.getBuiltInLauncherItems(triggerResult.pluginId, triggerResult.query);
|
|
} else {
|
|
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);
|
|
});
|
|
const builtInEmptyTrigger = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger();
|
|
builtInEmptyTrigger.forEach(pluginId => {
|
|
const items = AppSearchService.getBuiltInLauncherItems(pluginId, "");
|
|
emptyTriggerItems = emptyTriggerItems.concat(items);
|
|
});
|
|
const coreItems = AppSearchService.getCoreApps("");
|
|
apps = AppSearchService.getVisibleApplications().concat(emptyTriggerItems).concat(coreItems);
|
|
} else {
|
|
apps = AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults);
|
|
const coreItems = AppSearchService.getCoreApps("").filter(app => app.categories.includes(selectedCategory));
|
|
apps = apps.concat(coreItems);
|
|
}
|
|
} 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);
|
|
});
|
|
const builtInEmptyTrigger = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger();
|
|
builtInEmptyTrigger.forEach(pluginId => {
|
|
const items = AppSearchService.getBuiltInLauncherItems(pluginId, searchQuery);
|
|
emptyTriggerItems = emptyTriggerItems.concat(items);
|
|
});
|
|
|
|
const coreItems = AppSearchService.getCoreApps(searchQuery);
|
|
apps = apps.concat(emptyTriggerItems).concat(coreItems);
|
|
} 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 = [];
|
|
}
|
|
|
|
const coreItems = AppSearchService.getCoreApps(searchQuery).filter(app => app.categories.includes(selectedCategory));
|
|
apps = apps.concat(coreItems);
|
|
}
|
|
}
|
|
}
|
|
|
|
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.isCore ? false : (app.action !== undefined);
|
|
filteredModel.append({
|
|
"name": app.name || "",
|
|
"exec": app.execString || app.exec || app.action || "",
|
|
"icon": app.icon !== undefined ? String(app.icon) : (isPluginItem ? "" : "application-x-executable"),
|
|
"comment": app.comment || "",
|
|
"categories": app.categories || [],
|
|
"isPlugin": isPluginItem,
|
|
"isCore": app.isCore === true,
|
|
"isBuiltInLauncher": app.isBuiltInLauncher === true,
|
|
"isAction": app.isAction === true,
|
|
"appIndex": uniqueApps.length - 1,
|
|
"pinned": app._pinned === true
|
|
});
|
|
}
|
|
});
|
|
|
|
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.isBuiltInLauncher) {
|
|
AppSearchService.executeBuiltInLauncherItem(actualApp);
|
|
appLaunched(appData);
|
|
return;
|
|
}
|
|
|
|
if (appData.isCore) {
|
|
AppSearchService.executeCoreApp(actualApp);
|
|
appLaunched(appData);
|
|
return;
|
|
}
|
|
|
|
if (appData.isPlugin) {
|
|
const pluginId = getPluginIdForItem(actualApp);
|
|
if (pluginId) {
|
|
AppSearchService.executePluginItem(actualApp, pluginId);
|
|
appLaunched(appData);
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (appData.isAction && actualApp.parentApp && actualApp.actionData) {
|
|
SessionService.launchDesktopAction(actualApp.parentApp, actualApp.actionData);
|
|
appLaunched(appData);
|
|
AppUsageHistoryData.addAppUsage(actualApp.parentApp);
|
|
return;
|
|
}
|
|
|
|
SessionService.launchDesktopEntry(actualApp);
|
|
appLaunched(appData);
|
|
AppUsageHistoryData.addAppUsage(actualApp);
|
|
}
|
|
|
|
function reset() {
|
|
suppressUpdatesWhileLaunching = false;
|
|
searchQuery = "";
|
|
selectedIndex = 0;
|
|
setCategory(I18n.tr("All"));
|
|
updateFilteredModel();
|
|
}
|
|
|
|
function setCategory(category) {
|
|
selectedCategory = category;
|
|
categorySelected(category);
|
|
}
|
|
|
|
function setViewMode(mode) {
|
|
viewMode = mode;
|
|
viewModeSelected(mode);
|
|
}
|
|
|
|
onSearchQueryChanged: {
|
|
if (!_initialized)
|
|
return;
|
|
if (debounceSearch) {
|
|
searchDebounceTimer.restart();
|
|
} else {
|
|
updateFilteredModel();
|
|
}
|
|
}
|
|
|
|
onSelectedCategoryChanged: {
|
|
if (_updatingFromTrigger || !_initialized)
|
|
return;
|
|
updateFilteredModel();
|
|
}
|
|
|
|
onAppUsageRankingChanged: {
|
|
if (_initialized)
|
|
updateFilteredModel();
|
|
}
|
|
|
|
Connections {
|
|
target: DesktopEntries
|
|
function onApplicationsChanged() {
|
|
if (!root._initialized)
|
|
return;
|
|
root.updateCategories();
|
|
root.updateFilteredModel();
|
|
}
|
|
}
|
|
|
|
ListModel {
|
|
id: filteredModel
|
|
}
|
|
|
|
Timer {
|
|
id: searchDebounceTimer
|
|
|
|
interval: root.debounceInterval
|
|
repeat: false
|
|
onTriggered: updateFilteredModel()
|
|
}
|
|
|
|
function checkPluginTriggers(query) {
|
|
if (!query)
|
|
return {
|
|
triggered: false,
|
|
pluginCategory: "",
|
|
query: ""
|
|
};
|
|
|
|
const builtInTriggers = AppSearchService.getBuiltInLauncherTriggers();
|
|
for (const trigger in builtInTriggers) {
|
|
if (!query.startsWith(trigger))
|
|
continue;
|
|
const pluginId = builtInTriggers[trigger];
|
|
const plugin = AppSearchService.builtInPlugins[pluginId];
|
|
if (!plugin)
|
|
continue;
|
|
return {
|
|
triggered: true,
|
|
pluginId: pluginId,
|
|
pluginCategory: plugin.name,
|
|
query: query.substring(trigger.length).trim(),
|
|
trigger: trigger,
|
|
isBuiltIn: true
|
|
};
|
|
}
|
|
|
|
if (typeof PluginService === "undefined")
|
|
return {
|
|
triggered: false,
|
|
pluginCategory: "",
|
|
query: ""
|
|
};
|
|
|
|
const triggers = PluginService.getAllPluginTriggers();
|
|
for (const trigger in triggers) {
|
|
if (!query.startsWith(trigger))
|
|
continue;
|
|
const pluginId = triggers[trigger];
|
|
const plugin = PluginService.getLauncherPlugin(pluginId);
|
|
if (!plugin)
|
|
continue;
|
|
return {
|
|
triggered: true,
|
|
pluginId: pluginId,
|
|
pluginCategory: plugin.name || pluginId,
|
|
query: query.substring(trigger.length).trim(),
|
|
trigger: trigger,
|
|
isBuiltIn: false
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|