add noctalia and fuzzel
This commit is contained in:
+654
@@ -0,0 +1,654 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.System
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var launcher: null
|
||||
property string name: I18n.tr("launcher.providers.applications")
|
||||
property bool handleSearch: true
|
||||
property var entries: []
|
||||
property string supportedLayouts: "both"
|
||||
property bool isDefaultProvider: true // This provider handles empty search
|
||||
property bool ignoreDensity: false // Apps should scale with launcher density
|
||||
property bool trackUsage: true // Track usage frequency for "most used" sorting
|
||||
|
||||
// Category support
|
||||
property string selectedCategory: "all"
|
||||
property bool showsCategories: true // Default to showing categories
|
||||
property var categories: ["all", "Pinned", "AudioVideo", "Chat", "Development", "Education", "Game", "Graphics", "Network", "Office", "System", "Misc", "WebBrowser"]
|
||||
property var availableCategories: ["all"] // Reactive property for available categories
|
||||
|
||||
property var categoryIcons: ({
|
||||
"all": "apps",
|
||||
"Pinned": "pin",
|
||||
"AudioVideo": "music",
|
||||
"Chat": "message-circle",
|
||||
"Development": "code",
|
||||
"Education": "school" // Includes Science
|
||||
,
|
||||
"Game": "device-gamepad",
|
||||
"Graphics": "brush",
|
||||
"Network": "wifi",
|
||||
"Office": "file-text",
|
||||
"System": "device-desktop" // Includes Settings and Utility
|
||||
,
|
||||
"Misc": "dots",
|
||||
"WebBrowser": "world"
|
||||
})
|
||||
|
||||
function getCategoryName(category) {
|
||||
const names = {
|
||||
"all": I18n.tr("launcher.categories.all"),
|
||||
"Pinned": I18n.tr("launcher.categories.pinned"),
|
||||
"AudioVideo": I18n.tr("launcher.categories.audiovideo"),
|
||||
"Chat": I18n.tr("launcher.categories.chat"),
|
||||
"Development": I18n.tr("launcher.categories.development"),
|
||||
"Education": I18n.tr("launcher.categories.education"),
|
||||
"Game": I18n.tr("launcher.categories.game"),
|
||||
"Graphics": I18n.tr("launcher.categories.graphics"),
|
||||
"Network": I18n.tr("common.network"),
|
||||
"Office": I18n.tr("launcher.categories.office"),
|
||||
"System": I18n.tr("launcher.categories.system"),
|
||||
"Misc": I18n.tr("launcher.categories.misc"),
|
||||
"WebBrowser": I18n.tr("launcher.categories.webbrowser")
|
||||
};
|
||||
return names[category] || category;
|
||||
}
|
||||
|
||||
function init() {
|
||||
loadApplications();
|
||||
migrateLegacyUsageKeys();
|
||||
}
|
||||
|
||||
function onOpened() {
|
||||
// Just update available categories in case pinned apps changed
|
||||
updateAvailableCategories();
|
||||
// Default to Pinned if there are pinned apps, otherwise all
|
||||
if (availableCategories.includes("Pinned")) {
|
||||
selectedCategory = "Pinned";
|
||||
} else {
|
||||
selectedCategory = "all";
|
||||
}
|
||||
// Set category mode initially (will be updated when getResults is called)
|
||||
showsCategories = true;
|
||||
}
|
||||
|
||||
// Reload applications when desktop entries change on disk
|
||||
Connections {
|
||||
target: typeof DesktopEntries !== 'undefined' ? DesktopEntries.applications : null
|
||||
function onValuesChanged() {
|
||||
Logger.d("ApplicationsProvider", "Desktop entries changed, reloading applications");
|
||||
loadApplications();
|
||||
}
|
||||
}
|
||||
|
||||
function selectCategory(category) {
|
||||
selectedCategory = category;
|
||||
if (launcher) {
|
||||
launcher.updateResults();
|
||||
}
|
||||
}
|
||||
|
||||
function getAppCategories(app) {
|
||||
if (!app)
|
||||
return [];
|
||||
|
||||
const result = [];
|
||||
|
||||
if (app.categories) {
|
||||
if (Array.isArray(app.categories)) {
|
||||
for (let cat of app.categories) {
|
||||
if (cat && cat.trim && cat.trim() !== '') {
|
||||
result.push(cat.trim());
|
||||
} else if (cat && typeof cat === 'string' && cat.trim() !== '') {
|
||||
result.push(cat.trim());
|
||||
}
|
||||
}
|
||||
} else if (typeof app.categories === 'string') {
|
||||
const cats = app.categories.split(';').filter(c => c && c.trim() !== '');
|
||||
for (let cat of cats) {
|
||||
const trimmed = cat.trim();
|
||||
if (trimmed && !result.includes(trimmed)) {
|
||||
result.push(trimmed);
|
||||
}
|
||||
}
|
||||
} else if (app.categories.length !== undefined) {
|
||||
try {
|
||||
for (let i = 0; i < app.categories.length; i++) {
|
||||
const cat = app.categories[i];
|
||||
if (cat && cat.trim && typeof cat.trim === 'function' && cat.trim() !== '') {
|
||||
result.push(cat.trim());
|
||||
} else if (cat && typeof cat === 'string' && cat.trim() !== '') {
|
||||
result.push(cat.trim());
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (app.Categories) {
|
||||
const cats = app.Categories.split(';').filter(c => c && c.trim() !== '');
|
||||
for (let cat of cats) {
|
||||
const trimmed = cat.trim();
|
||||
if (trimmed && !result.includes(trimmed)) {
|
||||
result.push(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getAppCategory(app) {
|
||||
const appCategories = getAppCategories(app);
|
||||
if (appCategories.length === 0)
|
||||
return null;
|
||||
|
||||
const priorityCategories = ["AudioVideo", "Chat", "WebBrowser", "Game", "Development", "Graphics", "Office", "Education", "System", "Network", "Misc"];
|
||||
|
||||
for (let cat of appCategories) {
|
||||
if (cat === "AudioVideo" || cat === "Audio" || cat === "Video") {
|
||||
return "AudioVideo";
|
||||
}
|
||||
}
|
||||
|
||||
if (appCategories.includes("Chat") || appCategories.includes("InstantMessaging")) {
|
||||
return "Chat";
|
||||
}
|
||||
|
||||
if (appCategories.includes("WebBrowser")) {
|
||||
return "WebBrowser";
|
||||
}
|
||||
|
||||
// Map Science to Education
|
||||
if (appCategories.includes("Science")) {
|
||||
return "Education";
|
||||
}
|
||||
|
||||
// Map Settings to System
|
||||
if (appCategories.includes("Settings")) {
|
||||
return "System";
|
||||
}
|
||||
|
||||
// Map Utility to System
|
||||
if (appCategories.includes("Utility")) {
|
||||
return "System";
|
||||
}
|
||||
|
||||
for (let priorityCat of priorityCategories) {
|
||||
if (appCategories.includes(priorityCat) && root.categories.includes(priorityCat)) {
|
||||
return priorityCat;
|
||||
}
|
||||
}
|
||||
|
||||
return "Misc";
|
||||
}
|
||||
|
||||
// Helper function to normalize app IDs for case-insensitive matching
|
||||
function normalizeAppId(appId) {
|
||||
if (!appId || typeof appId !== 'string')
|
||||
return "";
|
||||
return appId.toLowerCase().trim();
|
||||
}
|
||||
|
||||
// Helper function to check if an app is pinned
|
||||
function isAppPinned(app) {
|
||||
if (!app)
|
||||
return false;
|
||||
const pinnedApps = Settings.data.appLauncher.pinnedApps || [];
|
||||
const appId = getAppKey(app);
|
||||
const normalizedId = normalizeAppId(appId);
|
||||
return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId);
|
||||
}
|
||||
|
||||
function appMatchesCategory(app, category) {
|
||||
// Check if app matches the selected category
|
||||
if (category === "all")
|
||||
return true;
|
||||
|
||||
// Handle Pinned category separately
|
||||
if (category === "Pinned") {
|
||||
return isAppPinned(app);
|
||||
}
|
||||
|
||||
// Get the primary category for this app (first matching standard category)
|
||||
const primaryCategory = getAppCategory(app);
|
||||
|
||||
// If app has no matching standard category, don't show it in any category (only in "all")
|
||||
if (!primaryCategory)
|
||||
return false;
|
||||
|
||||
// Map Audio/Video to AudioVideo
|
||||
if (category === "AudioVideo") {
|
||||
const appCategories = getAppCategories(app);
|
||||
// Show if app has AudioVideo, Audio, or Video
|
||||
return appCategories.includes("AudioVideo") || appCategories.includes("Audio") || appCategories.includes("Video");
|
||||
}
|
||||
|
||||
// Map Science to Education
|
||||
if (category === "Education") {
|
||||
const appCategories = getAppCategories(app);
|
||||
return appCategories.includes("Education") || appCategories.includes("Science");
|
||||
}
|
||||
|
||||
// Map Settings and Utility to System
|
||||
if (category === "System") {
|
||||
const appCategories = getAppCategories(app);
|
||||
return appCategories.includes("System") || appCategories.includes("Settings") || appCategories.includes("Utility");
|
||||
}
|
||||
|
||||
// Only show app in its primary category to avoid overlap
|
||||
// This ensures each app appears in exactly one category tab
|
||||
return category === primaryCategory;
|
||||
}
|
||||
|
||||
function getAvailableCategories() {
|
||||
const categorySet = new Set();
|
||||
let hasAudioVideo = false;
|
||||
let hasEducation = false;
|
||||
let hasSystem = false;
|
||||
let hasPinned = false;
|
||||
|
||||
// Check if there are any pinned apps
|
||||
const pinnedApps = Settings.data.appLauncher.pinnedApps || [];
|
||||
if (pinnedApps.length > 0) {
|
||||
// Verify that at least one pinned app exists in entries
|
||||
for (let app of entries) {
|
||||
if (isAppPinned(app)) {
|
||||
hasPinned = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let app of entries) {
|
||||
const appCategories = getAppCategories(app);
|
||||
const primaryCategory = getAppCategory(app);
|
||||
|
||||
if (appCategories.includes("AudioVideo") || appCategories.includes("Audio") || appCategories.includes("Video")) {
|
||||
hasAudioVideo = true;
|
||||
} else if (appCategories.includes("Education") || appCategories.includes("Science")) {
|
||||
hasEducation = true;
|
||||
} else if (appCategories.includes("System") || appCategories.includes("Settings") || appCategories.includes("Utility")) {
|
||||
hasSystem = true;
|
||||
} else if (primaryCategory && root.categories.includes(primaryCategory)) {
|
||||
categorySet.add(primaryCategory);
|
||||
}
|
||||
}
|
||||
|
||||
const result = [];
|
||||
|
||||
// Add Pinned category first if there are pinned apps
|
||||
if (hasPinned) {
|
||||
result.push("Pinned");
|
||||
}
|
||||
|
||||
result.push("all");
|
||||
|
||||
if (hasAudioVideo) {
|
||||
categorySet.add("AudioVideo");
|
||||
}
|
||||
if (hasEducation) {
|
||||
categorySet.add("Education");
|
||||
}
|
||||
if (hasSystem) {
|
||||
categorySet.add("System");
|
||||
}
|
||||
|
||||
for (let cat of root.categories) {
|
||||
if (cat !== "all" && cat !== "Pinned" && cat !== "Misc" && categorySet.has(cat)) {
|
||||
result.push(cat);
|
||||
}
|
||||
}
|
||||
|
||||
if (categorySet.has("Misc")) {
|
||||
result.push("Misc");
|
||||
}
|
||||
|
||||
if (result.length === 1) {
|
||||
const fallback = root.categories.filter(c => c !== "Misc");
|
||||
fallback.push("Misc");
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function loadApplications() {
|
||||
if (typeof DesktopEntries === 'undefined') {
|
||||
Logger.w("ApplicationsProvider", "DesktopEntries service not available");
|
||||
return;
|
||||
}
|
||||
|
||||
const allApps = DesktopEntries.applications.values || [];
|
||||
const seen = new Map(); // Map of appId -> exec command
|
||||
|
||||
entries = allApps.filter(app => {
|
||||
if (!app || app.noDisplay || app.hidden)
|
||||
return false;
|
||||
|
||||
const appId = app.id || app.name;
|
||||
const execCmd = getExecutableName(app);
|
||||
|
||||
// Check if we've seen this app ID before
|
||||
if (seen.has(appId)) {
|
||||
const previousExec = seen.get(appId);
|
||||
|
||||
// If exec is different, it's a legitimate different entry - keep it
|
||||
if (previousExec !== execCmd) {
|
||||
Logger.d("ApplicationsProvider", `Keeping variant of ${appId}: ${execCmd} (differs from ${previousExec})`);
|
||||
// Add with modified ID to make it unique
|
||||
app.id = `${appId}_${execCmd}`;
|
||||
seen.set(app.id, execCmd);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Same appId AND same exec = true duplicate, skip it
|
||||
Logger.d("ApplicationsProvider", `Skipping duplicate: ${appId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
seen.set(appId, execCmd);
|
||||
return true;
|
||||
}).map(app => {
|
||||
app.executableName = getExecutableName(app);
|
||||
return app;
|
||||
});
|
||||
|
||||
Logger.d("ApplicationsProvider", `Loaded ${entries.length} applications`);
|
||||
updateAvailableCategories();
|
||||
}
|
||||
|
||||
function updateAvailableCategories() {
|
||||
availableCategories = getAvailableCategories();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings.data.appLauncher
|
||||
function onPinnedAppsChanged() {
|
||||
const wasViewingPinned = selectedCategory === "Pinned";
|
||||
updateAvailableCategories();
|
||||
|
||||
// If we were viewing Pinned category and it's no longer available, switch to "all"
|
||||
if (wasViewingPinned && !availableCategories.includes("Pinned")) {
|
||||
selectedCategory = "all";
|
||||
}
|
||||
|
||||
// Update results if we're currently viewing the Pinned category
|
||||
if (selectedCategory === "Pinned" && launcher) {
|
||||
launcher.updateResults();
|
||||
} else if (wasViewingPinned && selectedCategory === "all" && launcher) {
|
||||
// Also update results when switching to "all"
|
||||
launcher.updateResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getExecutableName(app) {
|
||||
if (!app)
|
||||
return "";
|
||||
|
||||
// Try to get executable name from command array
|
||||
if (app.command && Array.isArray(app.command) && app.command.length > 0) {
|
||||
const cmd = app.command[0];
|
||||
// Extract just the executable name from the full path
|
||||
const parts = cmd.split('/');
|
||||
const executable = parts[parts.length - 1];
|
||||
// Remove any arguments or parameters
|
||||
return executable.split(' ')[0];
|
||||
}
|
||||
|
||||
// Try to get from exec property if available
|
||||
if (app.exec) {
|
||||
const parts = app.exec.split('/');
|
||||
const executable = parts[parts.length - 1];
|
||||
return executable.split(' ')[0];
|
||||
}
|
||||
|
||||
// Fallback to app id (desktop file name without .desktop)
|
||||
if (app.id) {
|
||||
return app.id.replace('.desktop', '');
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function getResults(query) {
|
||||
if (!entries || entries.length === 0)
|
||||
return [];
|
||||
|
||||
// Set category mode based on whether there's a query
|
||||
const isSearching = !!(query && query.trim() !== "");
|
||||
showsCategories = !isSearching;
|
||||
|
||||
// Filter by category only when NOT searching
|
||||
let filteredEntries = entries;
|
||||
if (!isSearching && selectedCategory && selectedCategory !== "all") {
|
||||
filteredEntries = entries.filter(app => appMatchesCategory(app, selectedCategory));
|
||||
}
|
||||
|
||||
if (!query || query.trim() === "") {
|
||||
// Return filtered apps, optionally sorted by usage
|
||||
let sorted;
|
||||
if (Settings.data.appLauncher.sortByMostUsed) {
|
||||
sorted = filteredEntries.slice().sort((a, b) => {
|
||||
// Pinned first
|
||||
const aPinned = isAppPinned(a);
|
||||
const bPinned = isAppPinned(b);
|
||||
if (aPinned !== bPinned)
|
||||
return aPinned ? -1 : 1;
|
||||
|
||||
const ua = getUsageCount(a);
|
||||
const ub = getUsageCount(b);
|
||||
if (ub !== ua)
|
||||
return ub - ua;
|
||||
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase());
|
||||
});
|
||||
} else {
|
||||
sorted = filteredEntries.slice().sort((a, b) => {
|
||||
const aPinned = isAppPinned(a);
|
||||
const bPinned = isAppPinned(b);
|
||||
if (aPinned !== bPinned)
|
||||
return aPinned ? -1 : 1;
|
||||
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase());
|
||||
});
|
||||
}
|
||||
return sorted.map(app => createResultEntry(app));
|
||||
}
|
||||
|
||||
// Use fuzzy search if available, fallback to simple search
|
||||
if (typeof FuzzySort !== 'undefined') {
|
||||
const fuzzyResults = FuzzySort.go(query, filteredEntries, {
|
||||
"keys": ["name", "comment", "genericName", "executableName"],
|
||||
"limit": 20
|
||||
});
|
||||
|
||||
// Sort pinned first within fuzzy results while preserving fuzzysort order otherwise
|
||||
const pinned = [];
|
||||
const nonPinned = [];
|
||||
for (const r of fuzzyResults) {
|
||||
const app = r.obj;
|
||||
if (isAppPinned(app))
|
||||
pinned.push(r);
|
||||
else
|
||||
nonPinned.push(r);
|
||||
}
|
||||
return pinned.concat(nonPinned).map(result => createResultEntry(result.obj, result.score));
|
||||
} else {
|
||||
// Fallback to simple search
|
||||
const searchTerm = query.toLowerCase();
|
||||
return filteredEntries.filter(app => {
|
||||
const name = (app.name || "").toLowerCase();
|
||||
const comment = (app.comment || "").toLowerCase();
|
||||
const generic = (app.genericName || "").toLowerCase();
|
||||
const executable = getExecutableName(app).toLowerCase();
|
||||
return name.includes(searchTerm) || comment.includes(searchTerm) || generic.includes(searchTerm) || executable.includes(searchTerm);
|
||||
}).sort((a, b) => {
|
||||
// Prioritize name matches, then executable matches
|
||||
const aName = a.name.toLowerCase();
|
||||
const bName = b.name.toLowerCase();
|
||||
const aExecutable = getExecutableName(a).toLowerCase();
|
||||
const bExecutable = getExecutableName(b).toLowerCase();
|
||||
const aStarts = aName.startsWith(searchTerm);
|
||||
const bStarts = bName.startsWith(searchTerm);
|
||||
const aExecStarts = aExecutable.startsWith(searchTerm);
|
||||
const bExecStarts = bExecutable.startsWith(searchTerm);
|
||||
|
||||
// Prioritize name matches first
|
||||
if (aStarts && !bStarts)
|
||||
return -1;
|
||||
if (!aStarts && bStarts)
|
||||
return 1;
|
||||
|
||||
// Then prioritize executable matches
|
||||
if (aExecStarts && !bExecStarts)
|
||||
return -1;
|
||||
if (!aExecStarts && bExecStarts)
|
||||
return 1;
|
||||
|
||||
return aName.localeCompare(bName);
|
||||
}).slice(0, 20).map(app => createResultEntry(app));
|
||||
}
|
||||
}
|
||||
|
||||
function createResultEntry(app, score) {
|
||||
return {
|
||||
"appId": getAppKey(app),
|
||||
"usageKey": getAppKey(app),
|
||||
"name": app.name || "Unknown",
|
||||
"description": app.genericName || app.comment || "",
|
||||
"icon": app.icon || "application-x-executable",
|
||||
"isImage": false,
|
||||
"_score": (score !== undefined ? score : 0),
|
||||
"provider": root,
|
||||
"onActivate": function () {
|
||||
// Close the launcher/SmartPanel immediately without any animations.
|
||||
// Ensures we are not preventing the future focusing of the app
|
||||
launcher.closeImmediately();
|
||||
|
||||
// Defer execution to next event loop iteration to ensure panel is fully closed
|
||||
Qt.callLater(() => {
|
||||
Logger.d("ApplicationsProvider", `Launching: ${app.name} (App ID: ${app.id || "unknown"})`);
|
||||
|
||||
const execString = (app.exec !== undefined && app.exec !== null) ? String(app.exec) : "";
|
||||
const commandArgs = Array.isArray(app.command) ? app.command : (app.command && app.command.length !== undefined) ? Array.from(app.command) : [];
|
||||
let hasQuotedArgs = execString.includes("\"") || execString.includes("'");
|
||||
let hasSpaceArgs = false;
|
||||
if (!hasQuotedArgs) {
|
||||
hasQuotedArgs = commandArgs.some(arg => {
|
||||
const text = String(arg);
|
||||
return text.includes("\"") || text.includes("'");
|
||||
});
|
||||
}
|
||||
if (!hasSpaceArgs) {
|
||||
hasSpaceArgs = commandArgs.some(arg => String(arg).includes(" "));
|
||||
}
|
||||
if (app.execute && (hasQuotedArgs || hasSpaceArgs)) {
|
||||
Logger.d("ApplicationsProvider", `Detected quoted/space arguments in Exec for ${app.name}, using app.execute()`);
|
||||
app.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings.data.appLauncher.customLaunchPrefixEnabled && Settings.data.appLauncher.customLaunchPrefix) {
|
||||
// Use custom launch prefix
|
||||
const prefix = Settings.data.appLauncher.customLaunchPrefix.split(" ");
|
||||
Logger.d("ApplicationsProvider", `Using custom launch prefix: ${Settings.data.appLauncher.customLaunchPrefix}`);
|
||||
|
||||
if (app.runInTerminal) {
|
||||
const terminal = Settings.data.appLauncher.terminalCommand.split(" ");
|
||||
const command = prefix.concat(terminal.concat(app.command));
|
||||
Logger.d("ApplicationsProvider", `Executing command (with prefix and terminal): ${command.join(" ")}`);
|
||||
Quickshell.execDetached(command);
|
||||
} else {
|
||||
const command = prefix.concat(app.command);
|
||||
Logger.d("ApplicationsProvider", `Executing command (with prefix): ${command.join(" ")}`);
|
||||
Quickshell.execDetached(command);
|
||||
}
|
||||
} else {
|
||||
if (app.runInTerminal) {
|
||||
Logger.d("ApplicationsProvider", "Executing terminal app manually: " + app.name);
|
||||
const terminal = Settings.data.appLauncher.terminalCommand.split(" ");
|
||||
const command = terminal.concat(app.command);
|
||||
Logger.d("ApplicationsProvider", "Executing command (manual terminal): " + command.join(" "));
|
||||
CompositorService.spawn(command);
|
||||
} else if (app.command && app.command.length > 0) {
|
||||
Logger.d("ApplicationsProvider", "Executing command: " + app.command.join(" "));
|
||||
CompositorService.spawn(app.command);
|
||||
} else if (app.execute) {
|
||||
Logger.d("ApplicationsProvider", "Calling app.execute() for: " + app.name);
|
||||
app.execute();
|
||||
} else {
|
||||
Logger.w("ApplicationsProvider", `Could not launch: ${app.name}. No valid launch method.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// Item actions for launcher delegate
|
||||
function getItemActions(item) {
|
||||
if (!item || !item.appId)
|
||||
return [];
|
||||
return [
|
||||
{
|
||||
"icon": isAppPinned({
|
||||
"id": item.appId
|
||||
}) ? "unpin" : "pin",
|
||||
"tooltip": isAppPinned({
|
||||
"id": item.appId
|
||||
}) ? I18n.tr("common.unpin") : I18n.tr("common.pin"),
|
||||
"action": function () {
|
||||
togglePin(item.appId);
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function togglePin(appId) {
|
||||
if (!appId)
|
||||
return;
|
||||
const normalizedId = normalizeAppId(appId);
|
||||
let arr = (Settings.data.appLauncher.pinnedApps || []).slice();
|
||||
const idx = arr.findIndex(pinnedId => normalizeAppId(pinnedId) === normalizedId);
|
||||
if (idx >= 0)
|
||||
arr.splice(idx, 1);
|
||||
else
|
||||
arr.push(appId);
|
||||
Settings.data.appLauncher.pinnedApps = arr;
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// Usage tracking helpers
|
||||
function getAppKey(app) {
|
||||
if (app && app.id)
|
||||
return String(app.id);
|
||||
if (app && app.command && app.command.join)
|
||||
return app.command.join(" ");
|
||||
return String(app && app.name ? app.name : "unknown");
|
||||
}
|
||||
|
||||
function getUsageCount(app) {
|
||||
return ShellState.getLauncherUsageCount(getAppKey(app));
|
||||
}
|
||||
|
||||
// Migrate legacy command-based usage keys to canonical app-id keys at startup
|
||||
function migrateLegacyUsageKeys() {
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const app = entries[i];
|
||||
if (app && app.id && app.command && app.command.join) {
|
||||
const key = getAppKey(app);
|
||||
const legacyKey = app.command.join(" ");
|
||||
if (legacyKey !== key && ShellState.getLauncherUsageCount(legacyKey) > 0) {
|
||||
ShellState.migrateLauncherUsage(legacyKey, key);
|
||||
Logger.d("ApplicationsProvider", `Migrated usage: "${legacyKey}" → "${key}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user