mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Launcher: add app categories
NIconTabButton: add tooltip support
This commit is contained in:
@@ -494,6 +494,20 @@
|
||||
"unknown": "Unbekannt"
|
||||
},
|
||||
"launcher": {
|
||||
"categories": {
|
||||
"all": "Alle",
|
||||
"audiovideo": "Audio & Video",
|
||||
"chat": "Chat",
|
||||
"development": "Entwicklung",
|
||||
"education": "Bildung",
|
||||
"game": "Spiele",
|
||||
"graphics": "Grafiken",
|
||||
"misc": "Verschiedenes",
|
||||
"network": "Netzwerk",
|
||||
"office": "Büro",
|
||||
"system": "System",
|
||||
"webbrowser": "Webbrowser"
|
||||
},
|
||||
"pin": "An das Dock anheften",
|
||||
"unpin": "Vom Dock lösen"
|
||||
},
|
||||
|
||||
@@ -494,6 +494,20 @@
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"launcher": {
|
||||
"categories": {
|
||||
"all": "All",
|
||||
"audiovideo": "Audio & Video",
|
||||
"chat": "Chat",
|
||||
"development": "Development",
|
||||
"education": "Education",
|
||||
"game": "Games",
|
||||
"graphics": "Graphics",
|
||||
"misc": "Misc",
|
||||
"network": "Network",
|
||||
"office": "Office",
|
||||
"system": "System",
|
||||
"webbrowser": "Web Browser"
|
||||
},
|
||||
"pin": "Pin to dock",
|
||||
"unpin": "Unpin from dock"
|
||||
},
|
||||
|
||||
@@ -494,6 +494,20 @@
|
||||
"unknown": "Desconocido"
|
||||
},
|
||||
"launcher": {
|
||||
"categories": {
|
||||
"all": "Todo",
|
||||
"audiovideo": "Audio y video",
|
||||
"chat": "Chat",
|
||||
"development": "Desarrollo",
|
||||
"education": "Educación",
|
||||
"game": "Juegos",
|
||||
"graphics": "Gráficos",
|
||||
"misc": "Varios",
|
||||
"network": "Red",
|
||||
"office": "Oficina",
|
||||
"system": "Sistema",
|
||||
"webbrowser": "Navegador web"
|
||||
},
|
||||
"pin": "Anclar al dock",
|
||||
"unpin": "Desanclar del dock"
|
||||
},
|
||||
|
||||
@@ -494,6 +494,20 @@
|
||||
"unknown": "Inconnu"
|
||||
},
|
||||
"launcher": {
|
||||
"categories": {
|
||||
"all": "Tout",
|
||||
"audiovideo": "Audio et vidéo",
|
||||
"chat": "Conversation",
|
||||
"development": "Développement",
|
||||
"education": "Éducation",
|
||||
"game": "Jeux",
|
||||
"graphics": "Graphiques",
|
||||
"misc": "Divers",
|
||||
"network": "Réseau",
|
||||
"office": "Bureau",
|
||||
"system": "Système",
|
||||
"webbrowser": "Navigateur web"
|
||||
},
|
||||
"pin": "Épingler au dock",
|
||||
"unpin": "Retirer du dock"
|
||||
},
|
||||
|
||||
@@ -494,6 +494,20 @@
|
||||
"unknown": "不明"
|
||||
},
|
||||
"launcher": {
|
||||
"categories": {
|
||||
"all": "すべて",
|
||||
"audiovideo": "音声 & ビデオ",
|
||||
"chat": "チャット",
|
||||
"development": "開発",
|
||||
"education": "教育",
|
||||
"game": "ゲーム",
|
||||
"graphics": "グラフィックス",
|
||||
"misc": "その他",
|
||||
"network": "ネットワーク",
|
||||
"office": "オフィス",
|
||||
"system": "システム",
|
||||
"webbrowser": "ウェブブラウザ"
|
||||
},
|
||||
"pin": "ドックにピン留め",
|
||||
"unpin": "ドックからピン留めを解除"
|
||||
},
|
||||
|
||||
@@ -494,6 +494,20 @@
|
||||
"unknown": "Onbekend"
|
||||
},
|
||||
"launcher": {
|
||||
"categories": {
|
||||
"all": "Alle",
|
||||
"audiovideo": "Audio & Video",
|
||||
"chat": "Chat",
|
||||
"development": "Ontwikkeling",
|
||||
"education": "Onderwijs",
|
||||
"game": "Spellen",
|
||||
"graphics": "Grafische weergave",
|
||||
"misc": "Diversen",
|
||||
"network": "Netwerk",
|
||||
"office": "Kantoor",
|
||||
"system": "Systeem",
|
||||
"webbrowser": "Webbrowser"
|
||||
},
|
||||
"pin": "Aan dock vastmaken",
|
||||
"unpin": "Van dock losmaken"
|
||||
},
|
||||
|
||||
@@ -494,6 +494,20 @@
|
||||
"unknown": "Desconhecido"
|
||||
},
|
||||
"launcher": {
|
||||
"categories": {
|
||||
"all": "Tudo",
|
||||
"audiovideo": "Áudio e Vídeo",
|
||||
"chat": "Bate-papo",
|
||||
"development": "Desenvolvimento",
|
||||
"education": "Educação",
|
||||
"game": "Jogos",
|
||||
"graphics": "Gráficos",
|
||||
"misc": "Diversos",
|
||||
"network": "Rede",
|
||||
"office": "Escritório",
|
||||
"system": "Sistema",
|
||||
"webbrowser": "Navegador web"
|
||||
},
|
||||
"pin": "Fixar no dock",
|
||||
"unpin": "Desafixar do dock"
|
||||
},
|
||||
|
||||
@@ -494,6 +494,20 @@
|
||||
"unknown": "Неизвестно"
|
||||
},
|
||||
"launcher": {
|
||||
"categories": {
|
||||
"all": "Всё",
|
||||
"audiovideo": "Аудио и видео",
|
||||
"chat": "Чат",
|
||||
"development": "Разработка",
|
||||
"education": "Образование",
|
||||
"game": "Игры",
|
||||
"graphics": "Графика",
|
||||
"misc": "Разное",
|
||||
"network": "Сеть",
|
||||
"office": "Офис",
|
||||
"system": "Система",
|
||||
"webbrowser": "Веб-браузер"
|
||||
},
|
||||
"pin": "Закрепить на панели",
|
||||
"unpin": "Открепить от панели"
|
||||
},
|
||||
|
||||
@@ -494,6 +494,20 @@
|
||||
"unknown": "Bilinmiyor"
|
||||
},
|
||||
"launcher": {
|
||||
"categories": {
|
||||
"all": "Tümü",
|
||||
"audiovideo": "Ses ve Video",
|
||||
"chat": "Sohbet",
|
||||
"development": "Gelişim",
|
||||
"education": "Eğitim",
|
||||
"game": "Oyunlar",
|
||||
"graphics": "Grafikler",
|
||||
"misc": "Çeşitli",
|
||||
"network": "Ağ",
|
||||
"office": "Ofis",
|
||||
"system": "Sistem",
|
||||
"webbrowser": "Web tarayıcı"
|
||||
},
|
||||
"pin": "Dock'a sabitle",
|
||||
"unpin": "Dock'dan sabitlemeyi kaldır"
|
||||
},
|
||||
|
||||
@@ -494,6 +494,20 @@
|
||||
"unknown": "Невідомо"
|
||||
},
|
||||
"launcher": {
|
||||
"categories": {
|
||||
"all": "Все",
|
||||
"audiovideo": "Аудіо та відео",
|
||||
"chat": "Чат",
|
||||
"development": "Розвиток",
|
||||
"education": "Освіта",
|
||||
"game": "Ігри",
|
||||
"graphics": "Графіка",
|
||||
"misc": "Різне",
|
||||
"network": "Мережа",
|
||||
"office": "Офіс",
|
||||
"system": "Система",
|
||||
"webbrowser": "Веб-браузер"
|
||||
},
|
||||
"pin": "Закріпити в доці",
|
||||
"unpin": "Відкріпити з доку"
|
||||
},
|
||||
|
||||
@@ -494,6 +494,20 @@
|
||||
"unknown": "未知"
|
||||
},
|
||||
"launcher": {
|
||||
"categories": {
|
||||
"all": "全部",
|
||||
"audiovideo": "音频和视频",
|
||||
"chat": "聊天",
|
||||
"development": "发展",
|
||||
"education": "教育",
|
||||
"game": "游戏",
|
||||
"graphics": "图形",
|
||||
"misc": "杂项",
|
||||
"network": "网络",
|
||||
"office": "办公室",
|
||||
"system": "系统",
|
||||
"webbrowser": "网页浏览器"
|
||||
},
|
||||
"pin": "固定到 Dock",
|
||||
"unpin": "从 Dock 取消固定"
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ SmartPanel {
|
||||
readonly property bool previewActive: !!(searchText && searchText.startsWith(">clip") && Settings.data.appLauncher.enableClipPreview && ClipboardService.items && ClipboardService.items.length > 0 && selectedIndex >= 0 && results && results[selectedIndex] && results[selectedIndex].clipboardId)
|
||||
|
||||
// Panel configuration
|
||||
readonly property int listPanelWidth: Math.round(600 * Style.uiScaleRatio)
|
||||
readonly property int listPanelWidth: Math.round(500 * Style.uiScaleRatio)
|
||||
readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio)
|
||||
readonly property int totalBaseWidth: listPanelWidth + (Style.marginL * 2)
|
||||
|
||||
@@ -68,8 +68,10 @@ SmartPanel {
|
||||
}
|
||||
|
||||
// Target columns, but actual columns may vary based on available width
|
||||
// Account for NTabBar margins (Style.marginXS on each side) to match category tabs width
|
||||
readonly property int targetGridColumns: 5
|
||||
readonly property int gridCellSize: Math.floor((listPanelWidth - Style.marginS - (targetGridColumns * Style.marginXXS)) / targetGridColumns)
|
||||
readonly property int gridContentWidth: listPanelWidth - (2 * Style.marginXS)
|
||||
readonly property int gridCellSize: Math.floor((gridContentWidth - ((targetGridColumns - 1) * Style.marginS)) / targetGridColumns)
|
||||
|
||||
// Actual columns that fit in the GridView
|
||||
// This gets updated dynamically by the GridView when its actual width is known
|
||||
@@ -85,6 +87,12 @@ SmartPanel {
|
||||
var currentIndex = emojiPlugin.categories.indexOf(emojiPlugin.selectedCategory);
|
||||
var nextIndex = (currentIndex + 1) % emojiPlugin.categories.length;
|
||||
emojiPlugin.selectCategory(emojiPlugin.categories[nextIndex]);
|
||||
} else if ((activePlugin === null || activePlugin === appsPlugin) && appsPlugin.isBrowsingMode) {
|
||||
// In apps browsing mode (no search), Tab navigates between categories
|
||||
var availableCategories = appsPlugin.availableCategories || ["all"];
|
||||
var currentIndex = availableCategories.indexOf(appsPlugin.selectedCategory);
|
||||
var nextIndex = (currentIndex + 1) % availableCategories.length;
|
||||
appsPlugin.selectCategory(availableCategories[nextIndex]);
|
||||
} else {
|
||||
selectNextWrapped();
|
||||
}
|
||||
@@ -653,6 +661,34 @@ SmartPanel {
|
||||
}
|
||||
}
|
||||
|
||||
// App category tabs (shown when browsing apps without search)
|
||||
NTabBar {
|
||||
id: appCategoryTabs
|
||||
visible: (root.activePlugin === null || root.activePlugin === appsPlugin) && appsPlugin.isBrowsingMode
|
||||
Layout.fillWidth: true
|
||||
currentIndex: {
|
||||
if (visible && appsPlugin.availableCategories) {
|
||||
return appsPlugin.availableCategories.indexOf(appsPlugin.selectedCategory);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: appsPlugin.availableCategories || []
|
||||
NIconTabButton {
|
||||
required property string modelData
|
||||
required property int index
|
||||
icon: appsPlugin.categoryIcons[modelData] || "apps"
|
||||
tooltipText: appsPlugin.getCategoryName ? appsPlugin.getCategoryName(modelData) : modelData
|
||||
tabIndex: index
|
||||
checked: appCategoryTabs.currentIndex === index
|
||||
onClicked: {
|
||||
appsPlugin.selectCategory(modelData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: resultsViewLoader
|
||||
Layout.fillWidth: true
|
||||
@@ -918,14 +954,31 @@ SmartPanel {
|
||||
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
|
||||
return parent.width / 5;
|
||||
}
|
||||
return gridCellSize + Style.marginXXS;
|
||||
// Use gridCellSize which already accounts for NTabBar margins
|
||||
return root.gridCellSize + Style.marginXL;
|
||||
}
|
||||
cellHeight: {
|
||||
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
|
||||
return (parent.width / 5) * 1.2;
|
||||
}
|
||||
return gridCellSize + Style.marginXXS;
|
||||
return gridCellSize + Style.marginXL;
|
||||
}
|
||||
leftMargin: {
|
||||
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
|
||||
return 0;
|
||||
}
|
||||
// Match NTabBar margins (Style.marginXS on each side) to align with category tabs
|
||||
return Style.marginXS;
|
||||
}
|
||||
rightMargin: {
|
||||
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
|
||||
return 0;
|
||||
}
|
||||
// Match NTabBar margins (Style.marginXS on each side) to align with category tabs
|
||||
return Style.marginXS;
|
||||
}
|
||||
topMargin: 0
|
||||
bottomMargin: 0
|
||||
model: results
|
||||
cacheBuffer: resultsGrid.height * 2
|
||||
keyNavigationEnabled: false
|
||||
@@ -1018,13 +1071,13 @@ SmartPanel {
|
||||
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
|
||||
return resultsGrid.width / 5;
|
||||
}
|
||||
return gridCellSize;
|
||||
return resultsGrid.cellWidth;
|
||||
}
|
||||
height: {
|
||||
if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) {
|
||||
return (resultsGrid.width / 5) * 1.2;
|
||||
}
|
||||
return gridCellSize;
|
||||
return resultsGrid.cellHeight;
|
||||
}
|
||||
radius: Style.radiusM
|
||||
color: gridEntry.isSelected ? Color.mHover : Color.mSurface
|
||||
@@ -1044,7 +1097,7 @@ SmartPanel {
|
||||
}
|
||||
return Style.marginM;
|
||||
}
|
||||
spacing: Style.marginS
|
||||
spacing: Style.marginM
|
||||
|
||||
// Icon badge or Image preview or Emoji
|
||||
Rectangle {
|
||||
|
||||
@@ -5,11 +5,54 @@ import "../../../../Helpers/FuzzySort.js" as Fuzzysort
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var launcher: null
|
||||
property string name: I18n.tr("plugins.applications")
|
||||
property bool handleSearch: true
|
||||
property var entries: []
|
||||
|
||||
// Category support
|
||||
property string selectedCategory: "all"
|
||||
property bool isBrowsingMode: false
|
||||
property var categories: ["all", "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",
|
||||
"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"),
|
||||
"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("launcher.categories.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;
|
||||
}
|
||||
|
||||
// Persistent usage tracking stored in cacheDir
|
||||
property string usageFilePath: Settings.cacheDir + "launcher_app_usage.json"
|
||||
|
||||
@@ -49,6 +92,199 @@ Item {
|
||||
function onOpened() {
|
||||
// Refresh apps when launcher opens
|
||||
loadApplications();
|
||||
// Reset to "all" category when opening
|
||||
selectedCategory = "all";
|
||||
// Set browsing mode initially (will be updated when getResults is called)
|
||||
isBrowsingMode = true;
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
function appMatchesCategory(app, category) {
|
||||
// Check if app matches the selected category
|
||||
if (category === "all")
|
||||
return true;
|
||||
|
||||
// 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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAudioVideo) {
|
||||
categorySet.add("AudioVideo");
|
||||
}
|
||||
if (hasEducation) {
|
||||
categorySet.add("Education");
|
||||
}
|
||||
if (hasSystem) {
|
||||
categorySet.add("System");
|
||||
}
|
||||
|
||||
const result = ["all"];
|
||||
for (let cat of root.categories) {
|
||||
if (cat !== "all" && 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() {
|
||||
@@ -64,6 +300,12 @@ Item {
|
||||
return app;
|
||||
});
|
||||
Logger.d("ApplicationsPlugin", `Loaded ${entries.length} applications`);
|
||||
// Update available categories when apps are loaded
|
||||
updateAvailableCategories();
|
||||
}
|
||||
|
||||
function updateAvailableCategories() {
|
||||
availableCategories = getAvailableCategories();
|
||||
}
|
||||
|
||||
function getExecutableName(app) {
|
||||
@@ -99,38 +341,47 @@ Item {
|
||||
if (!entries || entries.length === 0)
|
||||
return [];
|
||||
|
||||
// Set browsing mode based on whether there's a query
|
||||
isBrowsingMode = !query || query.trim() === "";
|
||||
|
||||
// Filter by category first
|
||||
let filteredEntries = entries;
|
||||
if (selectedCategory && selectedCategory !== "all") {
|
||||
filteredEntries = entries.filter(app => appMatchesCategory(app, selectedCategory));
|
||||
}
|
||||
|
||||
if (!query || query.trim() === "") {
|
||||
// Return all apps, optionally sorted by usage
|
||||
// Return filtered apps, optionally sorted by usage
|
||||
const favoriteApps = Settings.data.appLauncher.favoriteApps || [];
|
||||
let sorted;
|
||||
if (Settings.data.appLauncher.sortByMostUsed) {
|
||||
sorted = entries.slice().sort((a, b) => {
|
||||
// Favorites first
|
||||
const aFav = favoriteApps.includes(getAppKey(a));
|
||||
const bFav = favoriteApps.includes(getAppKey(b));
|
||||
if (aFav !== bFav)
|
||||
return aFav ? -1 : 1;
|
||||
const ua = getUsageCount(a);
|
||||
const ub = getUsageCount(b);
|
||||
if (ub !== ua)
|
||||
return ub - ua;
|
||||
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase());
|
||||
});
|
||||
sorted = filteredEntries.slice().sort((a, b) => {
|
||||
// Favorites first
|
||||
const aFav = favoriteApps.includes(getAppKey(a));
|
||||
const bFav = favoriteApps.includes(getAppKey(b));
|
||||
if (aFav !== bFav)
|
||||
return aFav ? -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 = entries.slice().sort((a, b) => {
|
||||
const aFav = favoriteApps.includes(getAppKey(a));
|
||||
const bFav = favoriteApps.includes(getAppKey(b));
|
||||
if (aFav !== bFav)
|
||||
return aFav ? -1 : 1;
|
||||
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase());
|
||||
});
|
||||
sorted = filteredEntries.slice().sort((a, b) => {
|
||||
const aFav = favoriteApps.includes(getAppKey(a));
|
||||
const bFav = favoriteApps.includes(getAppKey(b));
|
||||
if (aFav !== bFav)
|
||||
return aFav ? -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, entries, {
|
||||
const fuzzyResults = Fuzzysort.go(query, filteredEntries, {
|
||||
"keys": ["name", "comment", "genericName", "executableName"],
|
||||
"threshold": -1000,
|
||||
"limit": 20
|
||||
@@ -151,37 +402,37 @@ Item {
|
||||
} else {
|
||||
// Fallback to simple search
|
||||
const searchTerm = query.toLowerCase();
|
||||
return entries.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);
|
||||
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;
|
||||
// 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;
|
||||
// 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));
|
||||
return aName.localeCompare(bName);
|
||||
}).slice(0, 20).map(app => createResultEntry(app));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -897,9 +897,9 @@ ColumnLayout {
|
||||
|
||||
NCheckbox {
|
||||
label: "Emacs"
|
||||
description: ProgramCheckerService.emacsAvailable ? "Doom: ~/.config/doom/themes/noctalia.el\nStandard: ~/.emacs.d/themes/noctalia.el\n\nApply manually: (load-theme 'noctalia)" : I18n.tr("settings.color-scheme.templates.programs.emacs.description-missing", {
|
||||
"app": "emacs"
|
||||
})
|
||||
description: ProgramCheckerService.emacsAvailable ? "Doom: ~/.config/doom/themes/noctalia.el\nStandard: ~/.emacs.d/themes/noctalia.el\n\nApply manually: (load-theme 'noctalia t)" : I18n.tr("settings.color-scheme.templates.programs.emacs.description-missing", {
|
||||
"app": "emacs"
|
||||
})
|
||||
checked: Settings.data.templates.emacs
|
||||
enabled: ProgramCheckerService.emacsAvailable
|
||||
opacity: ProgramCheckerService.emacsAvailable ? 1.0 : 0.6
|
||||
|
||||
@@ -36,6 +36,10 @@ Item {
|
||||
property alias delegate: gridView.delegate
|
||||
property alias cellWidth: gridView.cellWidth
|
||||
property alias cellHeight: gridView.cellHeight
|
||||
property alias leftMargin: gridView.leftMargin
|
||||
property alias rightMargin: gridView.rightMargin
|
||||
property alias topMargin: gridView.topMargin
|
||||
property alias bottomMargin: gridView.bottomMargin
|
||||
property alias currentIndex: gridView.currentIndex
|
||||
property alias count: gridView.count
|
||||
property alias contentHeight: gridView.contentHeight
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
@@ -9,6 +11,7 @@ Rectangle {
|
||||
|
||||
// Public properties
|
||||
property string icon: ""
|
||||
property string tooltipText: ""
|
||||
property bool checked: false
|
||||
property int tabIndex: 0
|
||||
|
||||
@@ -51,9 +54,22 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: root.isHovered = true
|
||||
onExited: root.isHovered = false
|
||||
onEntered: {
|
||||
root.isHovered = true;
|
||||
if (root.tooltipText) {
|
||||
TooltipService.show(parent, root.tooltipText);
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
root.isHovered = false;
|
||||
if (root.tooltipText) {
|
||||
TooltipService.hide();
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
if (root.tooltipText) {
|
||||
TooltipService.hide();
|
||||
}
|
||||
root.clicked();
|
||||
// Update parent NTabBar's currentIndex
|
||||
if (root.parent && root.parent.parent && root.parent.parent.currentIndex !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user