diff --git a/Common/Prefs.qml b/Common/Prefs.qml index 1b79fca4..8e287a72 100644 --- a/Common/Prefs.qml +++ b/Common/Prefs.qml @@ -15,7 +15,7 @@ Singleton { property bool isLightMode: false property real topBarTransparency: 0.75 property real popupTransparency: 0.92 - property var recentlyUsedApps: [] + property var appUsageRanking: {} property bool use24HourClock: true property bool useFahrenheit: false property bool nightModeEnabled: false @@ -58,7 +58,7 @@ Singleton { isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false; topBarTransparency = settings.topBarTransparency !== undefined ? (settings.topBarTransparency > 1 ? settings.topBarTransparency / 100 : settings.topBarTransparency) : 0.75; popupTransparency = settings.popupTransparency !== undefined ? (settings.popupTransparency > 1 ? settings.popupTransparency / 100 : settings.popupTransparency) : 0.92; - recentlyUsedApps = settings.recentlyUsedApps || []; + appUsageRanking = settings.appUsageRanking || {}; use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true; useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false; nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false; @@ -104,7 +104,7 @@ Singleton { "isLightMode": isLightMode, "topBarTransparency": topBarTransparency, "popupTransparency": popupTransparency, - "recentlyUsedApps": recentlyUsedApps, + "appUsageRanking": appUsageRanking, "use24HourClock": use24HourClock, "useFahrenheit": useFahrenheit, "nightModeEnabled": nightModeEnabled, @@ -179,56 +179,77 @@ Singleton { saveSettings(); } - function addRecentApp(app) { + function addAppUsage(app) { if (!app) - return ; + return; - var execProp = app.execString || app.exec || ""; - if (!execProp) - return ; + var appId = app.id || (app.execString || app.exec || ""); + if (!appId) + return; - var existingIndex = -1; - for (var i = 0; i < recentlyUsedApps.length; i++) { - if (recentlyUsedApps[i].exec === execProp) { - existingIndex = i; - break; - } - } - if (existingIndex >= 0) { - // App exists, increment usage count - recentlyUsedApps[existingIndex].usageCount = (recentlyUsedApps[existingIndex].usageCount || 1) + 1; - recentlyUsedApps[existingIndex].lastUsed = Date.now(); + var currentRanking = Object.assign({}, appUsageRanking); + + if (currentRanking[appId]) { + currentRanking[appId].usageCount = (currentRanking[appId].usageCount || 1) + 1; + currentRanking[appId].lastUsed = Date.now(); + currentRanking[appId].icon = app.icon || currentRanking[appId].icon || "application-x-executable"; + currentRanking[appId].name = app.name || currentRanking[appId].name || ""; } else { - // New app, create entry - var appData = { + currentRanking[appId] = { "name": app.name || "", - "exec": execProp, + "exec": app.execString || app.exec || "", "icon": app.icon || "application-x-executable", "comment": app.comment || "", "usageCount": 1, "lastUsed": Date.now() }; - recentlyUsedApps.push(appData); } - // Sort by usage count (descending), then alphabetically by name - var sortedApps = recentlyUsedApps.sort(function(a, b) { - if (a.usageCount !== b.usageCount) - return b.usageCount - a.usageCount; - - // Higher usage count first - return a.name.localeCompare(b.name); - }); - // Limit to 10 apps - if (sortedApps.length > 10) - sortedApps = sortedApps.slice(0, 10); - - // Reassign to trigger property change signal - recentlyUsedApps = sortedApps; + + appUsageRanking = currentRanking; saveSettings(); } - function getRecentApps() { - return recentlyUsedApps; + function getAppUsageRanking() { + return appUsageRanking; + } + + function getRankedApps() { + var apps = []; + for (var appId in appUsageRanking) { + var appData = appUsageRanking[appId]; + apps.push({ + id: appId, + name: appData.name, + exec: appData.exec, + icon: appData.icon, + comment: appData.comment, + usageCount: appData.usageCount, + lastUsed: appData.lastUsed + }); + } + + return apps.sort(function(a, b) { + if (a.usageCount !== b.usageCount) + return b.usageCount - a.usageCount; + return a.name.localeCompare(b.name); + }); + } + + function cleanupAppUsageRanking(availableAppIds) { + var currentRanking = Object.assign({}, appUsageRanking); + var hasChanges = false; + + for (var appId in currentRanking) { + if (availableAppIds.indexOf(appId) === -1) { + delete currentRanking[appId]; + hasChanges = true; + } + } + + if (hasChanges) { + appUsageRanking = currentRanking; + saveSettings(); + } } // New preference setters diff --git a/Modals/ClipboardHistoryModal.qml b/Modals/ClipboardHistoryModal.qml index 9d710662..47ea4481 100644 --- a/Modals/ClipboardHistoryModal.qml +++ b/Modals/ClipboardHistoryModal.qml @@ -550,8 +550,8 @@ DankModal { anchors.right: parent.right anchors.rightMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter - iconName: "dangerous" - iconSize: Theme.iconSize - 4 + iconName: "close" + iconSize: Theme.iconSize - 6 iconColor: Theme.error hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) onClicked: { diff --git a/Modals/SettingsModal.qml b/Modals/SettingsModal.qml index ffa8a9fb..e9183bc2 100644 --- a/Modals/SettingsModal.qml +++ b/Modals/SettingsModal.qml @@ -20,7 +20,7 @@ DankModal { } // DankModal configuration visible: settingsVisible - width: 650 + width: 750 height: 750 keyboardFocus: "ondemand" onBackgroundClicked: { @@ -92,6 +92,7 @@ DankModal { { text: "Personalization", icon: "person" }, { text: "Time & Weather", icon: "schedule" }, { text: "Widgets", icon: "widgets" }, + { text: "Launcher", icon: "apps" }, { text: "Appearance", icon: "palette" } ] } @@ -128,7 +129,7 @@ DankModal { } } - // System Tab + // Widgets Tab Loader { anchors.fill: parent active: settingsTabBar.currentIndex === 2 @@ -139,12 +140,23 @@ DankModal { } } - // Appearance Tab + // Launcher Tab Loader { anchors.fill: parent active: settingsTabBar.currentIndex === 3 visible: active asynchronous: true + sourceComponent: Component { + LauncherTab {} + } + } + + // Appearance Tab + Loader { + anchors.fill: parent + active: settingsTabBar.currentIndex === 4 + visible: active + asynchronous: true sourceComponent: Component { AppearanceTab {} } diff --git a/Modals/SpotlightModal.qml b/Modals/SpotlightModal.qml index 15a9f98f..28c1815d 100644 --- a/Modals/SpotlightModal.qml +++ b/Modals/SpotlightModal.qml @@ -93,6 +93,7 @@ DankModal { content: Component { Item { + id: spotlightKeyHandler anchors.fill: parent focus: true // Handle keyboard shortcuts @@ -115,9 +116,9 @@ DankModal { } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { appLauncher.launchSelected(); event.accepted = true; - } else if (event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) { - searchField.text = event.text; + } else if (!searchField.activeFocus && event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) { searchField.forceActiveFocus(); + searchField.insertText(event.text); event.accepted = true; } } @@ -162,6 +163,8 @@ DankModal { font.pixelSize: Theme.fontSizeLarge enabled: spotlightOpen placeholderText: "Search applications..." + ignoreLeftRightKeys: true + keyForwardTargets: [spotlightKeyHandler] text: appLauncher.searchQuery onTextEdited: { appLauncher.searchQuery = text; @@ -170,14 +173,14 @@ DankModal { if (event.key === Qt.Key_Escape) { hide(); event.accepted = true; - } else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && appLauncher.searchQuery.length > 0) { - // Launch first app when typing in search field - if (appLauncher.model.count > 0) + } else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) { + if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) { + appLauncher.launchSelected(); + } else if (appLauncher.model.count > 0) { appLauncher.launchApp(appLauncher.model.get(0)); - + } event.accepted = true; - } else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") || (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && appLauncher.searchQuery.length === 0)) { - // Pass navigation keys and enter (when not searching) to main handler + } else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) { event.accepted = false; } } diff --git a/Modules/AppDrawer/AppDrawerPopout.qml b/Modules/AppDrawer/AppDrawerPopout.qml index 42723f48..1b6904ab 100644 --- a/Modules/AppDrawer/AppDrawerPopout.qml +++ b/Modules/AppDrawer/AppDrawerPopout.qml @@ -145,6 +145,7 @@ PanelWindow { // Content with focus management Item { + id: keyHandler anchors.fill: parent focus: true Component.onCompleted: { @@ -172,10 +173,10 @@ PanelWindow { } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { appLauncher.launchSelected(); event.accepted = true; - } else if (event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) { + } else if (!searchField.activeFocus && event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) { // User started typing, focus search field and pass the character searchField.forceActiveFocus(); - searchField.text = event.text; + searchField.insertText(event.text); event.accepted = true; } } @@ -233,17 +234,21 @@ PanelWindow { font.pixelSize: Theme.fontSizeLarge enabled: appDrawerPopout.isVisible placeholderText: "Search applications..." + ignoreLeftRightKeys: true + keyForwardTargets: [keyHandler] onTextEdited: { appLauncher.searchQuery = text; } Keys.onPressed: function(event) { - if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && appLauncher.model.count && text.length > 0) { - // Launch first app when typing in search field - var firstApp = appLauncher.model.get(0); - appLauncher.launchApp(firstApp); + if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) { + if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) { + appLauncher.launchSelected(); + } else if (appLauncher.model.count > 0) { + var firstApp = appLauncher.model.get(0); + appLauncher.launchApp(firstApp); + } event.accepted = true; - } else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") || (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) { - // Pass navigation keys and enter (when not searching) to main handler + } else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) { event.accepted = false; } } diff --git a/Modules/AppDrawer/AppLauncher.qml b/Modules/AppDrawer/AppLauncher.qml index c7a71c9d..e60178af 100644 --- a/Modules/AppDrawer/AppLauncher.qml +++ b/Modules/AppDrawer/AppLauncher.qml @@ -24,7 +24,7 @@ Item { var allCategories = AppSearchService.getAllCategories().filter(cat => { return cat !== "Education" && cat !== "Science"; }); - var result = ["All", "Recents"]; + var result = ["All"]; return result.concat(allCategories.filter(cat => { return cat !== "All"; })); @@ -33,13 +33,8 @@ Item { // Category icons (computed from AppSearchService) property var categoryIcons: categories.map(category => AppSearchService.getCategoryIcon(category)) - // Recent apps helper - property var recentApps: Prefs.recentlyUsedApps.map(recentApp => { - var app = AppSearchService.getAppByExec(recentApp.exec); - return app && !app.noDisplay ? app : null; - }).filter(app => { - return app !== null; - }) + // App usage ranking helper + property var appUsageRanking: Prefs.appUsageRanking // Signals signal appLaunched(var app) @@ -70,10 +65,12 @@ Item { } } onSelectedCategoryChanged: updateFilteredModel() + onAppUsageRankingChanged: updateFilteredModel() function updateFilteredModel() { filteredModel.clear(); selectedIndex = 0; + keyboardNavigationActive = false; var apps = []; @@ -81,8 +78,6 @@ Item { // Show apps from category if (selectedCategory === "All") { apps = AppSearchService.applications || []; - } else if (selectedCategory === "Recents") { - apps = recentApps; } else { var categoryApps = AppSearchService.getAppsInCategory(selectedCategory); apps = categoryApps.slice(0, maxResults); @@ -91,16 +86,6 @@ Item { // Search with category filter if (selectedCategory === "All") { apps = AppSearchService.searchApplications(searchQuery); - } else if (selectedCategory === "Recents") { - if (recentApps.length > 0) { - var allSearchResults = AppSearchService.searchApplications(searchQuery); - var recentNames = new Set(recentApps.map(app => app.name)); - apps = allSearchResults.filter(searchApp => { - return recentNames.has(searchApp.name); - }); - } else { - apps = []; - } } else { var categoryApps = AppSearchService.getAppsInCategory(selectedCategory); if (categoryApps.length > 0) { @@ -114,6 +99,21 @@ Item { } } } + + // Sort apps by usage ranking, then alphabetically + apps = apps.sort(function(a, b) { + var aId = a.id || (a.execString || a.exec || ""); + var bId = b.id || (b.execString || b.exec || ""); + + var aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0; + var bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0; + + if (aUsage !== bUsage) { + return bUsage - aUsage; // Higher usage first + } + + return (a.name || "").localeCompare(b.name || ""); // Alphabetical fallback + }); // Convert to model format and populate apps.forEach(app => { @@ -178,16 +178,14 @@ Item { } function launchApp(appData) { - if (appData.desktopEntry) { - Prefs.addRecentApp(appData.desktopEntry); - appData.desktopEntry.execute(); - } else { - // Fallback to direct execution - var cleanExec = appData.exec.replace(/%[fFuU]/g, "").trim(); - console.log("AppLauncher: Launching app directly:", cleanExec); - Quickshell.execDetached(["sh", "-c", cleanExec]); + if (!appData) { + console.warn("AppLauncher: No app data provided"); + return; } + + appData.desktopEntry.execute(); appLaunched(appData); + Prefs.addAppUsage(appData.desktopEntry); } // Category management diff --git a/Modules/AppDrawer/CategorySelector.qml b/Modules/AppDrawer/CategorySelector.qml index 4b5510db..095c7640 100644 --- a/Modules/AppDrawer/CategorySelector.qml +++ b/Modules/AppDrawer/CategorySelector.qml @@ -58,9 +58,9 @@ Item { width: parent.width spacing: Theme.spacingS - // Top row: All, Recents, Development, Graphics (4 items) + // Top row: All, Development, Graphics, Games (4 items) Row { - property var topRowCategories: ["All", "Recents", "Development", "Graphics"] + property var topRowCategories: ["All", "Development", "Graphics", "Games"] width: parent.width spacing: Theme.spacingS diff --git a/Modules/Lock/LockScreenContent.qml b/Modules/Lock/LockScreenContent.qml index 28cf9005..bab4d8af 100644 --- a/Modules/Lock/LockScreenContent.qml +++ b/Modules/Lock/LockScreenContent.qml @@ -598,13 +598,13 @@ Item { anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.margins: Theme.spacingL - width: Math.min(600, parent.width - Theme.spacingXL * 2) + width: Math.min(parent.width - Theme.spacingXL * 2, implicitWidth) text: randomFact font.pixelSize: Theme.fontSizeSmall color: "white" opacity: 0.8 horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap + wrapMode: Text.NoWrap visible: randomFact !== "" } } diff --git a/Modules/Settings/LauncherTab.qml b/Modules/Settings/LauncherTab.qml new file mode 100644 index 00000000..985119a7 --- /dev/null +++ b/Modules/Settings/LauncherTab.qml @@ -0,0 +1,399 @@ +import QtQuick +import QtQuick.Controls +import Quickshell.Widgets +import qs.Common +import qs.Widgets + +ScrollView { + id: launcherTab + + contentWidth: availableWidth + contentHeight: column.implicitHeight + Theme.spacingXL + clip: true + + Column { + id: column + + width: parent.width + spacing: Theme.spacingXL + topPadding: Theme.spacingL + bottomPadding: Theme.spacingXL + + StyledRect { + width: parent.width + height: appLauncherSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadiusLarge + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: appLauncherSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "apps" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "App Launcher" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + } + + DankToggle { + width: parent.width + text: "Use OS Logo for App Launcher" + description: "Display operating system logo instead of apps icon" + checked: Prefs.useOSLogo + onToggled: (checked) => { + return Prefs.setUseOSLogo(checked); + } + } + + StyledRect { + width: parent.width + height: logoCustomization.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) + border.width: 1 + visible: Prefs.useOSLogo + opacity: visible ? 1 : 0 + + Column { + id: logoCustomization + + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + StyledText { + text: "OS Logo Customization" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: "Color Override" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankTextField { + width: 160 + height: 36 + placeholderText: "#ffffff" + text: Prefs.osLogoColorOverride + maximumLength: 7 + font.pixelSize: Theme.fontSizeMedium + topPadding: Theme.spacingS + bottomPadding: Theme.spacingS + onEditingFinished: { + var color = text.trim(); + if (color === "" || /^#[0-9A-Fa-f]{6}$/.test(color)) + Prefs.setOSLogoColorOverride(color); + else + text = Prefs.osLogoColorOverride; + } + } + + } + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: "Brightness" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankSlider { + width: parent.width + height: 24 + minimum: 0 + maximum: 100 + value: Math.round(Prefs.osLogoBrightness * 100) + unit: "" + showValue: true + onSliderValueChanged: (newValue) => { + Prefs.setOSLogoBrightness(newValue / 100); + } + } + + } + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: "Contrast" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + + DankSlider { + width: parent.width + height: 24 + minimum: 0 + maximum: 200 + value: Math.round(Prefs.osLogoContrast * 100) + unit: "" + showValue: true + onSliderValueChanged: (newValue) => { + Prefs.setOSLogoContrast(newValue / 100); + } + } + + } + + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + + } + + } + + } + + } + + StyledRect { + width: parent.width + height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadiusLarge + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: recentlyUsedSection + + property var rankedAppsModel: { + var apps = []; + for (var appId in Prefs.appUsageRanking) { + var appData = Prefs.appUsageRanking[appId]; + apps.push({ + "id": appId, + "name": appData.name, + "exec": appData.exec, + "icon": appData.icon, + "comment": appData.comment, + "usageCount": appData.usageCount, + "lastUsed": appData.lastUsed + }); + } + apps.sort(function(a, b) { + if (a.usageCount !== b.usageCount) + return b.usageCount - a.usageCount; + + return a.name.localeCompare(b.name); + }); + return apps.slice(0, 20); + } + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "history" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Recently Used Apps" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - parent.children[0].width - parent.children[1].width - clearAllButton.width - Theme.spacingM * 3 + height: 1 + } + + DankActionButton { + id: clearAllButton + + iconName: "delete_sweep" + iconSize: Theme.iconSize - 2 + iconColor: Theme.error + hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) + anchors.verticalCenter: parent.verticalCenter + onClicked: { + Prefs.appUsageRanking = { + }; + Prefs.saveSettings(); + } + } + + } + + StyledText { + width: parent.width + text: "Apps are ordered by usage frequency, then alphabetically." + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + + Column { + id: rankedAppsList + + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: recentlyUsedSection.rankedAppsModel + + delegate: Rectangle { + width: rankedAppsList.width + height: 48 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) + border.width: 1 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + StyledText { + text: (index + 1).toString() + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.primary + width: 20 + anchors.verticalCenter: parent.verticalCenter + } + + Image { + width: 24 + height: 24 + source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable" + sourceSize.width: 24 + sourceSize.height: 24 + fillMode: Image.PreserveAspectFit + anchors.verticalCenter: parent.verticalCenter + onStatusChanged: { + if (status === Image.Error) + source = "image://icon/application-x-executable"; + + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: modelData.name || "Unknown App" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: { + if (!modelData.lastUsed) return "Never used"; + var date = new Date(modelData.lastUsed); + var now = new Date(); + var diffMs = now - date; + var diffMins = Math.floor(diffMs / (1000 * 60)); + var diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + var diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffMins < 1) return "Last launched just now"; + if (diffMins < 60) return "Last launched " + diffMins + " minute" + (diffMins === 1 ? "" : "s") + " ago"; + if (diffHours < 24) return "Last launched " + diffHours + " hour" + (diffHours === 1 ? "" : "s") + " ago"; + if (diffDays < 7) return "Last launched " + diffDays + " day" + (diffDays === 1 ? "" : "s") + " ago"; + + return "Last launched " + date.toLocaleDateString(); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + } + + } + + DankActionButton { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + circular: true + iconName: "close" + iconSize: 16 + iconColor: Theme.error + hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) + onClicked: { + var currentRanking = Object.assign({}, Prefs.appUsageRanking); + delete currentRanking[modelData.id]; + Prefs.appUsageRanking = currentRanking; + Prefs.saveSettings(); + } + } + + } + + } + + StyledText { + width: parent.width + text: recentlyUsedSection.rankedAppsModel.length === 0 ? "No apps have been launched yet." : "" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + visible: recentlyUsedSection.rankedAppsModel.length === 0 + } + + } + + } + + } + + } + +} diff --git a/Modules/Settings/WidgetsTab.qml b/Modules/Settings/WidgetsTab.qml index df16aca0..8c615d4a 100644 --- a/Modules/Settings/WidgetsTab.qml +++ b/Modules/Settings/WidgetsTab.qml @@ -170,165 +170,5 @@ ScrollView { } } } - - // App Launcher Section - StyledRect { - width: parent.width - height: appLauncherSection.implicitHeight + Theme.spacingL * 2 - radius: Theme.cornerRadiusLarge - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) - border.width: 1 - - Column { - id: appLauncherSection - anchors.fill: parent - anchors.margins: Theme.spacingL - spacing: Theme.spacingM - - Row { - width: parent.width - spacing: Theme.spacingM - - DankIcon { - name: "apps" - size: Theme.iconSize - color: Theme.primary - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: "App Launcher" - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Medium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - } - - DankToggle { - width: parent.width - text: "Use OS Logo for App Launcher" - description: "Display operating system logo instead of apps icon" - checked: Prefs.useOSLogo - onToggled: (checked) => { - return Prefs.setUseOSLogo(checked); - } - } - - // OS Logo Customization - only visible when OS logo is enabled - StyledRect { - width: parent.width - height: logoCustomization.implicitHeight + Theme.spacingM * 2 - radius: Theme.cornerRadius - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) - border.width: 1 - visible: Prefs.useOSLogo - opacity: visible ? 1 : 0 - - Column { - id: logoCustomization - anchors.fill: parent - anchors.margins: Theme.spacingM - spacing: Theme.spacingM - - StyledText { - text: "OS Logo Customization" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - font.weight: Font.Medium - } - - Column { - width: parent.width - spacing: Theme.spacingS - - StyledText { - text: "Color Override" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Medium - } - - DankTextField { - width: 160 - height: 36 - placeholderText: "#ffffff" - text: Prefs.osLogoColorOverride - maximumLength: 7 - font.pixelSize: Theme.fontSizeMedium - topPadding: Theme.spacingS - bottomPadding: Theme.spacingS - onEditingFinished: { - var color = text.trim(); - if (color === "" || /^#[0-9A-Fa-f]{6}$/.test(color)) - Prefs.setOSLogoColorOverride(color); - else - text = Prefs.osLogoColorOverride; - } - } - } - - Column { - width: parent.width - spacing: Theme.spacingS - - StyledText { - text: "Brightness" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Medium - } - - DankSlider { - width: parent.width - height: 24 - minimum: 0 - maximum: 100 - value: Math.round(Prefs.osLogoBrightness * 100) - unit: "" - showValue: true - onSliderValueChanged: (newValue) => { - Prefs.setOSLogoBrightness(newValue / 100); - } - } - } - - Column { - width: parent.width - spacing: Theme.spacingS - - StyledText { - text: "Contrast" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Medium - } - - DankSlider { - width: parent.width - height: 24 - minimum: 0 - maximum: 200 - value: Math.round(Prefs.osLogoContrast * 100) - unit: "" - showValue: true - onSliderValueChanged: (newValue) => { - Prefs.setOSLogoContrast(newValue / 100); - } - } - } - } - - Behavior on opacity { - NumberAnimation { - duration: Theme.mediumDuration - easing.type: Theme.emphasizedEasing - } - } - } - } - } } } \ No newline at end of file diff --git a/Modules/TopBar/TopBar.qml b/Modules/TopBar/TopBar.qml index e2751915..4ba71248 100644 --- a/Modules/TopBar/TopBar.qml +++ b/Modules/TopBar/TopBar.qml @@ -116,20 +116,20 @@ PanelWindow { // Use estimated fixed widths to break circular dependencies readonly property int launcherButtonWidth: 40 readonly property int workspaceSwitcherWidth: 120 // Approximate - readonly property int focusedAppMaxWidth: focusedApp.visible ? (topBarContent.spacingTight ? 288 : 456) : 0 + readonly property int focusedAppMaxWidth: focusedApp.visible ? 456 : 0 readonly property int estimatedLeftSectionWidth: launcherButtonWidth + workspaceSwitcherWidth + focusedAppMaxWidth + (Theme.spacingXS * 2) readonly property int rightSectionWidth: rightSection.width readonly property int clockWidth: clock.width readonly property int mediaMaxWidth: media.visible ? 280 : 0 // Normal max width readonly property int weatherWidth: weather.visible ? weather.width : 0 readonly property bool validLayout: availableWidth > 100 && estimatedLeftSectionWidth > 0 && rightSectionWidth > 0 - readonly property int clockLeftEdge: validLayout ? (availableWidth - clockWidth) / 2 : 0 + readonly property int clockLeftEdge: (availableWidth - clockWidth) / 2 readonly property int clockRightEdge: clockLeftEdge + clockWidth readonly property int leftSectionRightEdge: estimatedLeftSectionWidth readonly property int mediaLeftEdge: clockLeftEdge - mediaMaxWidth - Theme.spacingS readonly property int rightSectionLeftEdge: availableWidth - rightSectionWidth - readonly property int leftToClockGap: validLayout ? Math.max(0, clockLeftEdge - leftSectionRightEdge) : 1000 - readonly property int leftToMediaGap: (mediaMaxWidth > 0 && validLayout) ? Math.max(0, mediaLeftEdge - leftSectionRightEdge) : leftToClockGap + readonly property int leftToClockGap: Math.max(0, clockLeftEdge - leftSectionRightEdge) + readonly property int leftToMediaGap: mediaMaxWidth > 0 ? Math.max(0, mediaLeftEdge - leftSectionRightEdge) : leftToClockGap readonly property int mediaToClockGap: mediaMaxWidth > 0 ? Theme.spacingS : 0 readonly property int clockToRightGap: validLayout ? Math.max(0, rightSectionLeftEdge - clockRightEdge) : 1000 readonly property bool spacingTight: validLayout && (leftToMediaGap < 150 || clockToRightGap < 100) diff --git a/Services/AppSearchService.qml b/Services/AppSearchService.qml index 86660d1a..8d56cc0b 100644 --- a/Services/AppSearchService.qml +++ b/Services/AppSearchService.qml @@ -119,7 +119,6 @@ Singleton { // Category icon mappings property var categoryIcons: ({ "All": "apps", - "Recents": "history", "Media": "music_video", "Development": "code", "Games": "sports_esports", diff --git a/Widgets/DankTextField.qml b/Widgets/DankTextField.qml index 91933516..a2a2aa9c 100644 --- a/Widgets/DankTextField.qml +++ b/Widgets/DankTextField.qml @@ -39,6 +39,10 @@ Rectangle { property real topPadding: Theme.spacingM property real bottomPadding: Theme.spacingM + // Behavior control + property bool ignoreLeftRightKeys: false + property var keyForwardTargets: [] + // Signals signal textEdited() signal editingFinished() @@ -83,6 +87,10 @@ Rectangle { textInput.cut(); } + function insertText(str) { + textInput.insert(textInput.cursorPosition, str); + } + function clearFocus() { textInput.focus = false; } @@ -119,12 +127,26 @@ Rectangle { font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceText verticalAlignment: TextInput.AlignVCenter - selectByMouse: true + selectByMouse: !root.ignoreLeftRightKeys clip: true onTextChanged: root.textEdited() onEditingFinished: root.editingFinished() onAccepted: root.accepted() onActiveFocusChanged: root.focusStateChanged(activeFocus) + + Keys.forwardTo: root.ignoreLeftRightKeys ? root.keyForwardTargets : [] + + Keys.onLeftPressed: function(event) { + if (root.ignoreLeftRightKeys) { + event.accepted = true; + } + } + + Keys.onRightPressed: function(event) { + if (root.ignoreLeftRightKeys) { + event.accepted = true; + } + } MouseArea { anchors.fill: parent