diff --git a/Helpers/ThemeIcons.qml b/Helpers/ThemeIcons.qml new file mode 100644 index 0000000..714f114 --- /dev/null +++ b/Helpers/ThemeIcons.qml @@ -0,0 +1,286 @@ +pragma Singleton + +import QtQuick +import Quickshell + +Singleton { + id: root + + property real scoreThreshold: 0.2 + + // Manual overrides for tricky apps + property var substitutions: ({ + "code-url-handler": "visual-studio-code", + "Code": "visual-studio-code", + "gnome-tweaks": "org.gnome.tweaks", + "pavucontrol-qt": "pavucontrol", + "wps": "wps-office2019-kprometheus", + "wpsoffice": "wps-office2019-kprometheus", + "footclient": "foot" + }) + + // Dynamic fixups + property var regexSubstitutions: [ + { + "regex": /^steam_app_(\d+)$/, + "replace": "steam_icon_$1" + }, + { + "regex": /Minecraft.*/, + "replace": "minecraft-launcher" + }, + { + "regex": /.*polkit.*/, + "replace": "system-lock-screen" + }, + { + "regex": /gcr.prompter/, + "replace": "system-lock-screen" + } + ] + + property list entryList: [] + property var preppedNames: [] + property var preppedIcons: [] + property var preppedIds: [] + + Component.onCompleted: refreshEntries() + + Connections { + target: DesktopEntries.applications + function onValuesChanged() { + refreshEntries(); + } + } + + function refreshEntries() { + if (typeof DesktopEntries === 'undefined') + return; + + const values = Array.from(DesktopEntries.applications.values); + if (values) { + entryList = values.sort((a, b) => a.name.localeCompare(b.name)); + updatePreppedData(); + } + } + + function updatePreppedData() { + if (typeof FuzzySort === 'undefined') + return; + + const list = Array.from(entryList); + preppedNames = list.map(a => ({ + name: FuzzySort.prepare(`${a.name} `), entry: a})); + preppedIcons = list.map(a => ({ + name: FuzzySort.prepare(`${a.icon} `), + entry: a + })); + preppedIds = list.map(a => ({ + name: FuzzySort.prepare(`${a.id} `), + entry: a + })); + } + + function iconForAppId(appId, fallbackName) { + const fallback = fallbackName || "application-x-executable"; + if (!appId) + return iconFromName(fallback, fallback); + + const entry = findAppEntry(appId); + if (entry) { + return iconFromName(entry.icon, fallback); + } + + return iconFromName(appId, fallback); + } + + // Robust lookup strategy + function findAppEntry(str) { + if (!str || str.length === 0) + return null; + + let result = null; + + if (result = checkHeuristic(str)) + return result; + if (result = checkSubstitutions(str)) + return result; + if (result = checkRegex(str)) + return result; + if (result = checkSimpleTransforms(str)) + return result; + if (result = checkFuzzySearch(str)) + return result; + if (result = checkCleanMatch(str)) + return result; + + return null; + } + + function iconFromName(iconName, fallbackName) { + const fallback = fallbackName || "application-x-executable"; + try { + if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) { + const p = Quickshell.iconPath(iconName, fallback); + if (p && p !== "") + return p; + } + } catch (e) {} + + try { + return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : ""; + } catch (e2) { + return ""; + } + } + + function distroLogoPath() { + try { + return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : ""; + } catch (e) { + return ""; + } + } + + // --- Lookup Helpers --- + + function checkHeuristic(str) { + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) { + const entry = DesktopEntries.heuristicLookup(str); + if (entry) + return entry; + } + return null; + } + + function checkSubstitutions(str) { + let effectiveStr = substitutions[str]; + if (!effectiveStr) + effectiveStr = substitutions[str.toLowerCase()]; + + if (effectiveStr && effectiveStr !== str) { + return findAppEntry(effectiveStr); + } + return null; + } + + function checkRegex(str) { + for (let i = 0; i < regexSubstitutions.length; i++) { + const sub = regexSubstitutions[i]; + const replaced = str.replace(sub.regex, sub.replace); + if (replaced !== str) { + return findAppEntry(replaced); + } + } + return null; + } + + function checkSimpleTransforms(str) { + if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId) + return null; + + const lower = str.toLowerCase(); + + const variants = [str, lower, getFromReverseDomain(str), getFromReverseDomain(str)?.toLowerCase(), normalizeWithHyphens(str), str.replace(/_/g, '-').toLowerCase(), str.replace(/-/g, '_').toLowerCase()]; + + for (let i = 0; i < variants.length; i++) { + const variant = variants[i]; + if (variant) { + const entry = DesktopEntries.byId(variant); + if (entry) + return entry; + } + } + return null; + } + + function checkFuzzySearch(str) { + if (typeof FuzzySort === 'undefined') + return null; + + // Check filenames (IDs) first + if (preppedIds.length > 0) { + let results = fuzzyQuery(str, preppedIds); + if (results.length === 0) { + const underscored = str.replace(/-/g, '_').toLowerCase(); + if (underscored !== str) + results = fuzzyQuery(underscored, preppedIds); + } + if (results.length > 0) + return results[0]; + } + + // Then icons + if (preppedIcons.length > 0) { + const results = fuzzyQuery(str, preppedIcons); + if (results.length > 0) + return results[0]; + } + + // Then names + if (preppedNames.length > 0) { + const results = fuzzyQuery(str, preppedNames); + if (results.length > 0) + return results[0]; + } + + return null; + } + + function checkCleanMatch(str) { + if (!str || str.length <= 3) + return null; + if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId) + return null; + + // Aggressive fallback: strip all separators + const cleanStr = str.toLowerCase().replace(/[\.\-_]/g, ''); + const list = Array.from(entryList); + + for (let i = 0; i < list.length; i++) { + const entry = list[i]; + const cleanId = (entry.id || "").toLowerCase().replace(/[\.\-_]/g, ''); + if (cleanId.includes(cleanStr) || cleanStr.includes(cleanId)) { + return entry; + } + } + return null; + } + + function fuzzyQuery(search, preppedData) { + if (!search || !preppedData || preppedData.length === 0) + return []; + return FuzzySort.go(search, preppedData, { + all: true, + key: "name" + }).map(r => r.obj.entry); + } + + function iconExists(iconName) { + if (!iconName || iconName.length === 0) + return false; + if (iconName.startsWith("/")) + return true; + + const path = Quickshell.iconPath(iconName, true); + return path && path.length > 0 && !path.includes("image-missing"); + } + + function getFromReverseDomain(str) { + if (!str) + return ""; + return str.split('.').slice(-1)[0]; + } + + function normalizeWithHyphens(str) { + if (!str) + return ""; + return str.toLowerCase().replace(/\s+/g, "-"); + } + + // Deprecated shim + function guessIcon(str) { + const entry = findAppEntry(str); + return entry ? entry.icon : "image-missing"; + } +} diff --git a/Modules/AudioPopup.qml b/Modules/AudioPopup.qml index 02d32cc..8544e41 100644 --- a/Modules/AudioPopup.qml +++ b/Modules/AudioPopup.qml @@ -9,6 +9,7 @@ import QtQuick.Controls import qs.Config import qs.Components import qs.Daemons +import qs.Helpers Item { id: root @@ -126,69 +127,116 @@ Item { spacing: 12 RowLayout { - Layout.fillWidth: true - Layout.fillHeight: true + Layout.topMargin: 10 + spacing: 15 + Rectangle { + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + Layout.alignment: Qt.AlignVCenter + color: DynamicColors.tPalette.m3primaryContainer + radius: 1000 + MaterialIcon { + anchors.centerIn: parent + color: DynamicColors.palette.m3onPrimaryContainer + text: "volume_up" + font.pointSize: 22 + } + } - CustomText { - text: "Output Volume" - elide: Text.ElideRight + ColumnLayout { Layout.fillWidth: true - Layout.fillHeight: true - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - } + RowLayout { + Layout.fillWidth: true - CustomText { - text: qsTr("%1").arg(Audio.muted ? qsTr("Muted") : `${Math.round(Audio.volume * 100)}%`); - font.bold: true - Layout.fillHeight: true - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + CustomText { + text: "Output Volume" + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + } + + CustomText { + text: qsTr("%1").arg(Audio.muted ? qsTr("Muted") : `${Math.round(Audio.volume * 100)}%`); + font.bold: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + } + } + + CustomMouseArea { + Layout.fillWidth: true + Layout.preferredHeight: 10 + Layout.bottomMargin: 5 + + CustomSlider { + anchors.fill: parent + value: Audio.volume + onMoved: Audio.setVolume(value) + + Behavior on value { Anim {} } + } + } } } - CustomMouseArea { - Layout.fillWidth: true - implicitHeight: 10 - - CustomSlider { - anchors.fill: parent - value: Audio.volume - onMoved: Audio.setVolume(value) - - Behavior on value { Anim {} } - } - } RowLayout { - Layout.fillWidth: true - Layout.fillHeight: true - - CustomText { - text: "Input Volume" - elide: Text.ElideRight - Layout.fillWidth: true - Layout.fillHeight: true - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.topMargin: 10 + spacing: 15 + Rectangle { + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + Layout.alignment: Qt.AlignVCenter + color: DynamicColors.tPalette.m3primaryContainer + radius: 1000 + MaterialIcon { + anchors.centerIn: parent + anchors.alignWhenCentered: false + color: DynamicColors.palette.m3onPrimaryContainer + text: "mic" + font.pointSize: 22 + } } - CustomText { - text: qsTr("%1").arg(Audio.sourceMuted ? qsTr("Muted") : `${Math.round(Audio.sourceVolume * 100)}%`); - font.bold: true - Layout.fillHeight: true - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + ColumnLayout { + Layout.fillWidth: true + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + CustomText { + text: "Input Volume" + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + } + + CustomText { + text: qsTr("%1").arg(Audio.sourceMuted ? qsTr("Muted") : `${Math.round(Audio.sourceVolume * 100)}%`); + font.bold: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + } + } + + CustomMouseArea { + Layout.fillWidth: true + Layout.bottomMargin: 5 + implicitHeight: 10 + + CustomSlider { + anchors.fill: parent + value: Audio.sourceVolume + onMoved: Audio.setSourceVolume(value) + + Behavior on value { Anim {} } + } + } } } - CustomMouseArea { + Rectangle { + Layout.topMargin: 10 Layout.fillWidth: true - implicitHeight: 10 + Layout.preferredHeight: 1 - CustomSlider { - anchors.fill: parent - value: Audio.sourceVolume - onMoved: Audio.setSourceVolume(value) - - Behavior on value { Anim {} } - } + color: DynamicColors.tPalette.m3outline } Repeater { @@ -203,6 +251,90 @@ Item { visible: !isCaptureStream required property PwNode modelData + function isValidMatch(searchTerm, entry) { + if (!entry) + return false; + var search = searchTerm.toLowerCase(); + var id = (entry.id || "").toLowerCase(); + var name = (entry.name || "").toLowerCase(); + var icon = (entry.icon || "").toLowerCase(); + // Match is valid if search term appears in entry or entry appears in search + return id.includes(search) || name.includes(search) || icon.includes(search) || search.includes(id.split('.').pop()) || search.includes(name.replace(/\s+/g, '')); + } + + readonly property string appName: { + if (!modelData) + return "Unknown App"; + + var props = modelData.properties; + var desc = modelData.description || ""; + var name = modelData.name || ""; + var mediaName = props["media.name"] || ""; + + if ( mediaName !== "playStream" ) { + return mediaName; + } + + if (!props) { + if (desc) + return desc; + if (name) { + var nameParts = name.split(/[-_]/); + if (nameParts.length > 0 && nameParts[0]) + return nameParts[0].charAt(0).toUpperCase() + nameParts[0].slice(1); + return name; + } + return "Unknown App"; + } + + var binaryName = props["application.process.binary"] || ""; + + // Try binary name first (fixes Electron apps like vesktop) + if (binaryName) { + var binParts = binaryName.split("/"); + if (binParts.length > 0) { + var binName = binParts[binParts.length - 1].toLowerCase(); + var entry = ThemeIcons.findAppEntry(binName); + // Only use entry if it's actually related to binary name + if (entry && entry.name && isValidMatch(binName, entry)) + return entry.name; + } + } + + var computedAppName = props["application.name"] || ""; + var mediaName = props["media.name"] || ""; + var appId = props["application.id"] || ""; + + if (appId) { + var entry = ThemeIcons.findAppEntry(appId); + if (entry && entry.name && isValidMatch(appId, entry)) + return entry.name; + if (!computedAppName) { + var parts = appId.split("."); + if (parts.length > 0 && parts[0]) + computedAppName = parts[0].charAt(0).toUpperCase() + parts[0].slice(1); + } + } + + if (!computedAppName && binaryName) { + var binParts = binaryName.split("/"); + if (binParts.length > 0 && binParts[binParts.length - 1]) + computedAppName = binParts[binParts.length - 1].charAt(0).toUpperCase() + binParts[binParts.length - 1].slice(1); + } + + var result = computedAppName || mediaTitle || mediaName || binaryName || desc || name; + + if (!result || result === "" || result === "Unknown App") { + if (name) { + var nameParts = name.split(/[-_]/); + if (nameParts.length > 0 && nameParts[0]) + result = nameParts[0].charAt(0).toUpperCase() + nameParts[0].slice(1); + } + } + + return result || "Unknown App"; + } + PwObjectTracker { objects: appBox.modelData ? [appBox.modelData] : [] } @@ -233,8 +365,10 @@ Item { spacing: 15 IconImage { - property string iconPath: Quickshell.iconPath(DesktopEntries.byId(appBox.modelData.name).icon) - source: iconPath !== "" ? iconPath : Quickshell.iconPath("application-x-executable") + id: icon + property string iconPath1: Quickshell.iconPath(DesktopEntries.byId(appBox.modelData.name).icon); + property string iconPath2: Quickshell.iconPath(DesktopEntries.byId(appBox.appName).icon); + source: iconPath1 !== "" ? iconPath1 : iconPath2 Layout.alignment: Qt.AlignVCenter implicitSize: 42 } @@ -245,7 +379,7 @@ Item { TextMetrics { id: metrics - text: appBox.modelData.properties["media.name"] + text: appBox.appName elide: Text.ElideRight elideWidth: root.width - 50 } @@ -280,7 +414,7 @@ Item { value: appBox.modelData.audio.volume onMoved: { Audio.setAppAudioVolume(appBox.modelData, value) - console.log(layoutVolume.implicitHeight) + console.log(icon.iconPath1, icon.iconPath2) } } }