mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-15 08:42:47 -04:00
app picker: extend App Picker to integrate with mime overrides
- Adds "DMS Opener" as an option (dms-open.desktop) - Add mime type GO utils - Add rememberance to App Picker modal
This commit is contained in:
@@ -850,6 +850,8 @@ Item {
|
||||
|
||||
filePickerModal.targetData = data.target;
|
||||
filePickerModal.targetDataLabel = data.requestType || "file";
|
||||
filePickerModal.mimeType = data.mimeType || "";
|
||||
filePickerModal.rememberMimeTypes = [];
|
||||
|
||||
if (data.categories && data.categories.length > 0) {
|
||||
filePickerModal.categoryFilter = data.categories;
|
||||
|
||||
@@ -19,9 +19,19 @@ DankModal {
|
||||
property var categoryFilter: []
|
||||
property var usageHistoryKey: ""
|
||||
property bool showTargetData: true
|
||||
property string mimeType: ""
|
||||
property var rememberMimeTypes: []
|
||||
property bool rememberChoice: false
|
||||
property var mimeMatchedAppIds: []
|
||||
|
||||
signal applicationSelected(var app, string targetData)
|
||||
|
||||
function _normAppId(id) {
|
||||
if (!id)
|
||||
return "";
|
||||
return id.replace(/\.desktop$/, "").toLowerCase();
|
||||
}
|
||||
|
||||
shouldBeVisible: false
|
||||
allowStacking: true
|
||||
modalWidth: 520
|
||||
@@ -37,6 +47,8 @@ DankModal {
|
||||
|
||||
onOpened: {
|
||||
searchQuery = "";
|
||||
rememberChoice = false;
|
||||
fetchMimeMatches();
|
||||
updateApplicationList();
|
||||
selectedIndex = 0;
|
||||
Qt.callLater(() => {
|
||||
@@ -47,22 +59,55 @@ DankModal {
|
||||
});
|
||||
}
|
||||
|
||||
function fetchMimeMatches() {
|
||||
mimeMatchedAppIds = [];
|
||||
const queriedMime = mimeType;
|
||||
if (queriedMime.length === 0)
|
||||
return;
|
||||
DMSService.sendRequest("mime.appsForMime", {
|
||||
"mimeType": queriedMime
|
||||
}, response => {
|
||||
if (queriedMime !== root.mimeType)
|
||||
return;
|
||||
if (response.error) {
|
||||
log.warn("mime.appsForMime failed:", response.error);
|
||||
return;
|
||||
}
|
||||
const ids = (response.result && response.result.desktopIds) || [];
|
||||
mimeMatchedAppIds = ids.map(_normAppId);
|
||||
updateApplicationList();
|
||||
});
|
||||
}
|
||||
|
||||
function _appMatchesMime(app, mime) {
|
||||
const list = app && (app.mimeTypes || app.mimeType);
|
||||
return !!list && !!list.includes && list.includes(mime);
|
||||
}
|
||||
|
||||
function updateApplicationList() {
|
||||
applicationsModel.clear();
|
||||
const apps = AppSearchService.applications;
|
||||
const usageHistory = usageHistoryKey && SettingsData[usageHistoryKey] ? SettingsData[usageHistoryKey] : {};
|
||||
const hasCategoryFilter = categoryFilter.length > 0;
|
||||
const hasMime = mimeType.length > 0;
|
||||
const hasMimeMatches = mimeMatchedAppIds.length > 0;
|
||||
const lowerQuery = searchQuery.toLowerCase();
|
||||
let filteredApps = [];
|
||||
|
||||
for (const app of apps) {
|
||||
if (!app || !app.categories)
|
||||
if (!app)
|
||||
continue;
|
||||
let matchesCategory = categoryFilter.length === 0;
|
||||
const appId = _normAppId(app.id || app.execString || app.exec || "");
|
||||
const mimeIdMatch = hasMimeMatches && mimeMatchedAppIds.includes(appId);
|
||||
const mimeFieldMatch = hasMime && _appMatchesMime(app, mimeType);
|
||||
const mimeMatch = mimeIdMatch || mimeFieldMatch;
|
||||
|
||||
if (categoryFilter.length > 0) {
|
||||
let categoryMatch = false;
|
||||
if (hasCategoryFilter && app.categories) {
|
||||
try {
|
||||
for (const cat of app.categories) {
|
||||
if (categoryFilter.includes(cat)) {
|
||||
matchesCategory = true;
|
||||
categoryMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -72,24 +117,28 @@ DankModal {
|
||||
}
|
||||
}
|
||||
|
||||
if (matchesCategory) {
|
||||
const name = app.name || "";
|
||||
const lowerName = name.toLowerCase();
|
||||
const lowerQuery = searchQuery.toLowerCase();
|
||||
const include = (!hasCategoryFilter && !hasMime) || mimeMatch || categoryMatch;
|
||||
if (!include)
|
||||
continue;
|
||||
|
||||
if (searchQuery === "" || lowerName.includes(lowerQuery)) {
|
||||
filteredApps.push({
|
||||
name: name,
|
||||
icon: app.icon || "application-x-executable",
|
||||
exec: app.exec || app.execString || "",
|
||||
startupClass: app.startupWMClass || "",
|
||||
appData: app
|
||||
});
|
||||
}
|
||||
}
|
||||
const name = app.name || "";
|
||||
if (searchQuery !== "" && !name.toLowerCase().includes(lowerQuery))
|
||||
continue;
|
||||
|
||||
filteredApps.push({
|
||||
name: name,
|
||||
icon: app.icon || "application-x-executable",
|
||||
exec: app.exec || app.execString || "",
|
||||
startupClass: app.startupWMClass || "",
|
||||
appData: app,
|
||||
mimeMatch: mimeMatch
|
||||
});
|
||||
}
|
||||
|
||||
filteredApps.sort((a, b) => {
|
||||
if (a.mimeMatch !== b.mimeMatch) {
|
||||
return a.mimeMatch ? -1 : 1;
|
||||
}
|
||||
const aId = a.appData.id || a.appData.execString || a.appData.exec || "";
|
||||
const bId = b.appData.id || b.appData.execString || b.appData.exec || "";
|
||||
const aUsage = usageHistory[aId] ? usageHistory[aId].count : 0;
|
||||
@@ -134,16 +183,15 @@ DankModal {
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (applicationsModel.count === 0)
|
||||
return;
|
||||
|
||||
// Toggle view mode with Tab key
|
||||
if (event.key === Qt.Key_Tab) {
|
||||
root.viewMode = root.viewMode === "grid" ? "list" : "grid";
|
||||
if (event.key === Qt.Key_Tab && root.mimeType.length > 0) {
|
||||
root.rememberChoice = !root.rememberChoice;
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (applicationsModel.count === 0)
|
||||
return;
|
||||
|
||||
if (root.viewMode === "grid") {
|
||||
if (event.key === Qt.Key_Left) {
|
||||
root.keyboardNavigationActive = true;
|
||||
@@ -309,6 +357,9 @@ DankModal {
|
||||
if (root.showTargetData) {
|
||||
usedHeight += 36 + Theme.spacingS;
|
||||
}
|
||||
if (root.mimeType && root.mimeType.length > 0) {
|
||||
usedHeight += 36 + Theme.spacingS;
|
||||
}
|
||||
return parent.height - usedHeight;
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
@@ -447,11 +498,38 @@ DankModal {
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 36
|
||||
visible: root.mimeType.length > 0
|
||||
|
||||
DankToggle {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: root.rememberChoice
|
||||
text: I18n.tr("Always use this app for %1").arg(root.mimeType)
|
||||
onToggled: checked => {
|
||||
root.rememberChoice = checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function launchApplication(app) {
|
||||
if (!app)
|
||||
return;
|
||||
|
||||
if (root.rememberChoice && app.appId) {
|
||||
const targets = (root.rememberMimeTypes && root.rememberMimeTypes.length > 0) ? root.rememberMimeTypes : (root.mimeType ? [root.mimeType] : []);
|
||||
if (targets.length > 0) {
|
||||
DesktopService.setDefaultAppForMimes(targets, app.appId);
|
||||
}
|
||||
}
|
||||
|
||||
root.applicationSelected(app, root.targetData);
|
||||
|
||||
if (usageHistoryKey && app.appId) {
|
||||
|
||||
@@ -17,6 +17,8 @@ AppPickerModal {
|
||||
viewMode: SettingsData.browserPickerViewMode || "grid"
|
||||
usageHistoryKey: "browserUsageHistory"
|
||||
showTargetData: true
|
||||
mimeType: url.startsWith("https://") ? "x-scheme-handler/https" : (url.startsWith("http://") ? "x-scheme-handler/http" : "")
|
||||
rememberMimeTypes: ["x-scheme-handler/http", "x-scheme-handler/https", "text/html", "application/xhtml+xml"]
|
||||
|
||||
function shellEscape(str) {
|
||||
return "'" + str.replace(/'/g, "'\\''") + "'";
|
||||
|
||||
@@ -244,8 +244,7 @@ Rectangle {
|
||||
"id": "default_apps",
|
||||
"text": I18n.tr("Default Apps"),
|
||||
"icon": "star",
|
||||
"tabIndex": 34,
|
||||
"gioOnly": true
|
||||
"tabIndex": 34
|
||||
},
|
||||
{
|
||||
"id": "running_apps",
|
||||
@@ -364,8 +363,6 @@ Rectangle {
|
||||
return false;
|
||||
if (item.updaterOnly && !SystemUpdateService.sysupdateAvailable)
|
||||
return false;
|
||||
if (item.gioOnly && !DesktopService.gioAvailable)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ Item {
|
||||
PDFReader: 6,
|
||||
Mail: 7,
|
||||
Terminal: 8,
|
||||
Calendar: 9
|
||||
Calendar: 9
|
||||
})
|
||||
|
||||
property string currentWebBrowserAppId: ""
|
||||
@@ -35,63 +35,17 @@ Item {
|
||||
|
||||
property var categoryModels: ({})
|
||||
|
||||
// A curated list of MIME types for each category.
|
||||
// The first one is used for fetching the apps list and current default,
|
||||
// the rest are for setting the default app.
|
||||
// A curated list of MIME types for each category.
|
||||
// The first one is used for fetching the apps list and current default,
|
||||
// the rest are for setting the default app.
|
||||
readonly property var mimeMapping: ({
|
||||
[root.appCategory.WebBrowser]: [
|
||||
"x-scheme-handler/https",
|
||||
"x-scheme-handler/http",
|
||||
"text/html",
|
||||
"application/xhtml+xml"
|
||||
],
|
||||
[root.appCategory.FileManager]: [
|
||||
"inode/directory",
|
||||
"x-scheme-handler/file"
|
||||
],
|
||||
[root.appCategory.TextEditor]: [
|
||||
"text/plain",
|
||||
"application/x-zerosize",
|
||||
"text/x-c++src",
|
||||
"text/x-csrc",
|
||||
"text/x-python",
|
||||
"text/x-shellscript",
|
||||
"application/json"
|
||||
],
|
||||
[root.appCategory.ImageViewer]: [
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
"image/bmp",
|
||||
"image/webp",
|
||||
"image/avif",
|
||||
"image/svg+xml"
|
||||
],
|
||||
[root.appCategory.VideoPlayer]: [
|
||||
"video/mp4",
|
||||
"video/x-matroska",
|
||||
"video/webm",
|
||||
"video/avi",
|
||||
"video/mpeg",
|
||||
"video/quicktime",
|
||||
"video/x-msvideo"
|
||||
],
|
||||
[root.appCategory.MusicPlayer]: [
|
||||
"audio/mpeg",
|
||||
"audio/x-flac",
|
||||
"audio/wav",
|
||||
"audio/ogg",
|
||||
"audio/aac",
|
||||
"audio/webm"
|
||||
],
|
||||
[root.appCategory.PDFReader]: [
|
||||
"application/pdf",
|
||||
"application/x-ext-pdf",
|
||||
"application/x-bzpdf",
|
||||
"application/x-gzpdf",
|
||||
"application/vnd.comicbook-rar",
|
||||
"application/vnd.comicbook+zip"
|
||||
],
|
||||
[root.appCategory.WebBrowser]: ["x-scheme-handler/https", "x-scheme-handler/http", "text/html", "application/xhtml+xml"],
|
||||
[root.appCategory.FileManager]: ["inode/directory", "x-scheme-handler/file"],
|
||||
[root.appCategory.TextEditor]: ["text/plain", "application/x-zerosize", "text/x-c++src", "text/x-csrc", "text/x-python", "text/x-shellscript", "application/json"],
|
||||
[root.appCategory.ImageViewer]: ["image/png", "image/jpeg", "image/gif", "image/bmp", "image/webp", "image/avif", "image/svg+xml"],
|
||||
[root.appCategory.VideoPlayer]: ["video/mp4", "video/x-matroska", "video/webm", "video/avi", "video/mpeg", "video/quicktime", "video/x-msvideo"],
|
||||
[root.appCategory.MusicPlayer]: ["audio/mpeg", "audio/x-flac", "audio/wav", "audio/ogg", "audio/aac", "audio/webm"],
|
||||
[root.appCategory.PDFReader]: ["application/pdf", "application/x-ext-pdf", "application/x-bzpdf", "application/x-gzpdf", "application/vnd.comicbook-rar", "application/vnd.comicbook+zip"],
|
||||
[root.appCategory.Mail]: ["x-scheme-handler/mailto"],
|
||||
[root.appCategory.Calendar]: ["x-scheme-handler/calendar"],
|
||||
[root.appCategory.Terminal]: ["terminal"] // Special
|
||||
@@ -111,11 +65,13 @@ Item {
|
||||
}
|
||||
|
||||
function getAppDisplayName(appId) {
|
||||
if (appId === root.dmsChooserId || appId === "dms-open") {
|
||||
return root.dmsChooserLabel;
|
||||
}
|
||||
let entry = DesktopEntries.heuristicLookup(appId);
|
||||
if (entry && entry.name) {
|
||||
return entry.name;
|
||||
}
|
||||
// If the appname can't be found, show the appID
|
||||
const withoutSuffix = appId.replace(/\.desktop$/, "");
|
||||
if (withoutSuffix !== appId) {
|
||||
entry = DesktopEntries.heuristicLookup(withoutSuffix);
|
||||
@@ -126,14 +82,28 @@ Item {
|
||||
return appId;
|
||||
}
|
||||
|
||||
readonly property string dmsChooserId: "dms-open.desktop"
|
||||
readonly property string dmsChooserLabel: I18n.tr("DMS Chooser")
|
||||
|
||||
function withDmsChooser(entries) {
|
||||
const filtered = (entries || []).filter(e => e.value !== root.dmsChooserId && e.value !== "dms-open");
|
||||
return [
|
||||
{
|
||||
text: root.dmsChooserLabel,
|
||||
value: root.dmsChooserId
|
||||
}
|
||||
].concat(filtered);
|
||||
}
|
||||
|
||||
function loadCategoryModel(categoryKey, categorySearchName) {
|
||||
const apps = loadAppSearchCategory(categorySearchName);
|
||||
const appIds = apps.map(app => app.id || app.execString || "").filter(id => id);
|
||||
let models = Object.assign({}, root.categoryModels);
|
||||
models[categoryKey] = appIds.map(id => ({
|
||||
text: root.getAppDisplayName(id),
|
||||
value: id
|
||||
}));
|
||||
const entries = appIds.map(id => ({
|
||||
text: root.getAppDisplayName(id),
|
||||
value: id
|
||||
}));
|
||||
models[categoryKey] = categoryKey === root.appCategory.Terminal ? entries : root.withDmsChooser(entries);
|
||||
root.categoryModels = models;
|
||||
}
|
||||
|
||||
@@ -147,9 +117,9 @@ Item {
|
||||
loadCategoryModel(root.appCategory.Terminal, "TerminalEmulator");
|
||||
getDefaultTerminal();
|
||||
break;
|
||||
case root.appCategory.WebBrowser:
|
||||
case root.appCategory.WebBrowser:
|
||||
// When using the MIME type, stuff like dms-run shows up.
|
||||
// It's probably better to use the category.
|
||||
// It's probably better to use the category.
|
||||
loadCategoryModel(root.appCategory.WebBrowser, "WebBrowser");
|
||||
DesktopService.getDefaultApp(mimeMapping[category][0], category.toString());
|
||||
break;
|
||||
@@ -201,7 +171,7 @@ Item {
|
||||
Component {
|
||||
id: xdgGetDefaultTerminal
|
||||
Process {
|
||||
property string configPath: Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config")
|
||||
property string configPath: Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config")
|
||||
|
||||
command: ["sh", "-c", `cat '${configPath}/xdg-terminals.list'`]
|
||||
stdout: StdioCollector {
|
||||
@@ -231,30 +201,24 @@ Item {
|
||||
target: DesktopService
|
||||
|
||||
function onGetAppsForMimeResult(mimeType, appIds, callbackId) {
|
||||
if (!appIds || appIds.length === 0) {
|
||||
log.info("No apps found for MIME type:", mimeType);
|
||||
return;
|
||||
}
|
||||
|
||||
let categoryIndex = parseInt(callbackId);
|
||||
let models = Object.assign({}, root.categoryModels);
|
||||
|
||||
models[categoryIndex] = appIds.map(id => {
|
||||
return {
|
||||
text: root.getAppDisplayName(id),
|
||||
value: id
|
||||
};
|
||||
});
|
||||
const entries = (appIds || []).map(id => ({
|
||||
text: root.getAppDisplayName(id),
|
||||
value: id
|
||||
}));
|
||||
|
||||
models[categoryIndex] = root.withDmsChooser(entries);
|
||||
root.categoryModels = models;
|
||||
}
|
||||
|
||||
function onGetDefaultAppResult(mimeType, desktopFileId, callbackId) {
|
||||
if (!desktopFileId) {
|
||||
log.info("No default app found for MIME type:", mimeType);
|
||||
return
|
||||
return;
|
||||
}
|
||||
root[propertyName(parseInt(callbackId))] = desktopFileId;
|
||||
root[propertyName(parseInt(callbackId))] = desktopFileId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,11 +227,11 @@ Item {
|
||||
options: (root.categoryModels[category] || []).map(opt => opt.text)
|
||||
enabled: options.length > 0
|
||||
emptyText: options.length > 0 ? I18n.tr("Unset", "Unset") : ""
|
||||
opacity: options.length > 0 ? 1 : 0.5
|
||||
opacity: options.length > 0 ? 1 : 0.5
|
||||
currentValue: {
|
||||
let id = root[propertyName(category)];
|
||||
if (!id || id.length === 0) {
|
||||
return ""
|
||||
return "";
|
||||
}
|
||||
return root.getAppDisplayName(id);
|
||||
}
|
||||
@@ -278,11 +242,7 @@ Item {
|
||||
if (category === root.appCategory.Terminal) {
|
||||
root.setDefaultTerminal(found.value);
|
||||
} else {
|
||||
// Set the default app for all MIME types in the category
|
||||
// If the app doesn't support a MIME type, it will be ignored
|
||||
root.mimeMapping[category].forEach(mimeType => {
|
||||
DesktopService.setDefaultApp(mimeType, found.value, category.toString());
|
||||
});
|
||||
DesktopService.setDefaultAppForMimes(root.mimeMapping[category], found.value, category.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,16 +269,16 @@ Item {
|
||||
|
||||
AppSelector {
|
||||
text: I18n.tr("Web Browser", "Web Browser")
|
||||
tags: ["web", "browser", "internet"]
|
||||
tags: ["web", "browser", "internet"]
|
||||
category: root.appCategory.WebBrowser
|
||||
description: I18n.tr("Handles links and opens HTML files", "Handles links and opens HTML files")
|
||||
description: I18n.tr("Handles links and opens HTML files", "Handles links and opens HTML files")
|
||||
}
|
||||
|
||||
AppSelector {
|
||||
text: I18n.tr("Mail", "Mail")
|
||||
category: root.appCategory.Mail
|
||||
tags: ["mail", "email"]
|
||||
description: I18n.tr("Handles mailto links", "Handles mailto links")
|
||||
tags: ["mail", "email"]
|
||||
description: I18n.tr("Handles mailto links", "Handles mailto links")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,21 +288,21 @@ Item {
|
||||
|
||||
AppSelector {
|
||||
text: I18n.tr("File Manager", "File Manager")
|
||||
tags: ["file", "manager"]
|
||||
tags: ["file", "manager"]
|
||||
category: root.appCategory.FileManager
|
||||
description: I18n.tr("Manages files and directories", "Manages files and directories")
|
||||
description: I18n.tr("Manages files and directories", "Manages files and directories")
|
||||
}
|
||||
AppSelector {
|
||||
text: I18n.tr("Terminal", "Terminal")
|
||||
category: root.appCategory.Terminal
|
||||
tags: ["terminal", "console"]
|
||||
description: I18n.tr("Used for xdg-terminal-exec", "Used for xdg-terminal-exec")
|
||||
tags: ["terminal", "console"]
|
||||
description: I18n.tr("Used for xdg-terminal-exec", "Used for xdg-terminal-exec")
|
||||
}
|
||||
AppSelector {
|
||||
AppSelector {
|
||||
text: I18n.tr("Calendar", "Calendar")
|
||||
category: root.appCategory.Calendar
|
||||
tags: ["calendar", "events"]
|
||||
description: I18n.tr("Manages calendar events", "Manages calendar events")
|
||||
tags: ["calendar", "events"]
|
||||
description: I18n.tr("Manages calendar events", "Manages calendar events")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,14 +313,14 @@ Item {
|
||||
AppSelector {
|
||||
text: I18n.tr("Text Editor", "Text Editor")
|
||||
category: root.appCategory.TextEditor
|
||||
tags: ["text", "editor"]
|
||||
description: I18n.tr("For editing plain text files", "For editing plain text files")
|
||||
tags: ["text", "editor"]
|
||||
description: I18n.tr("For editing plain text files", "For editing plain text files")
|
||||
}
|
||||
AppSelector {
|
||||
text: I18n.tr("PDF Reader", "PDF Reader")
|
||||
category: root.appCategory.PDFReader
|
||||
tags: ["pdf", "reader"]
|
||||
description: I18n.tr("For reading PDF files", "For reading PDF files")
|
||||
tags: ["pdf", "reader"]
|
||||
description: I18n.tr("For reading PDF files", "For reading PDF files")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,20 +330,20 @@ Item {
|
||||
AppSelector {
|
||||
text: I18n.tr("Image Viewer", "Image Viewer")
|
||||
category: root.appCategory.ImageViewer
|
||||
tags: ["image", "viewer"]
|
||||
description: I18n.tr("Opens image files", "Opens image files")
|
||||
tags: ["image", "viewer"]
|
||||
description: I18n.tr("Opens image files", "Opens image files")
|
||||
}
|
||||
AppSelector {
|
||||
text: I18n.tr("Video Player", "Video Player")
|
||||
category: root.appCategory.VideoPlayer
|
||||
tags: ["video", "player"]
|
||||
description: I18n.tr("Plays video files", "Plays video files")
|
||||
tags: ["video", "player"]
|
||||
description: I18n.tr("Plays video files", "Plays video files")
|
||||
}
|
||||
AppSelector {
|
||||
text: I18n.tr("Music Player", "Music Player")
|
||||
category: root.appCategory.MusicPlayer
|
||||
tags: ["music", "player"]
|
||||
description: I18n.tr("Plays audio files", "Plays audio files")
|
||||
tags: ["music", "player"]
|
||||
description: I18n.tr("Plays audio files", "Plays audio files")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,16 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
import Quickshell.Io
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var log: Log.scoped("DesktopService")
|
||||
property var _cache: ({})
|
||||
property bool gioAvailable: false;
|
||||
// For the queue that setDefaultApp uses
|
||||
property var _setDefaultAppQueue: []
|
||||
property bool _isProcessingQueue: false
|
||||
|
||||
Component.onCompleted: {
|
||||
checkGioAndXdgMime.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: checkGioAndXdgMime
|
||||
command: ["sh", "-c", "which gio && which xdg-mime"]
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
root.gioAvailable = true;
|
||||
} else {
|
||||
root.gioAvailable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveIconPath(moddedAppId) {
|
||||
if (!moddedAppId)
|
||||
@@ -39,18 +20,15 @@ Singleton {
|
||||
return _cache[moddedAppId];
|
||||
|
||||
const result = (function () {
|
||||
// 1. Try heuristic lookup (standard)
|
||||
const entry = DesktopEntries.heuristicLookup(moddedAppId);
|
||||
let icon = Quickshell.iconPath(entry?.icon, true);
|
||||
if (icon && icon !== "")
|
||||
return icon;
|
||||
|
||||
// 2. Try the appId itself as an icon name
|
||||
icon = Quickshell.iconPath(moddedAppId, true);
|
||||
if (icon && icon !== "")
|
||||
return icon;
|
||||
|
||||
// 3. Try variations of the appId (lowercase, last part)
|
||||
const appIds = [moddedAppId.toLowerCase()];
|
||||
const lastPart = moddedAppId.split('.').pop();
|
||||
if (lastPart && lastPart !== moddedAppId) {
|
||||
@@ -64,8 +42,6 @@ Singleton {
|
||||
return icon;
|
||||
}
|
||||
|
||||
// 4. Deep search in all desktop entries (if the above fail)
|
||||
// This is slow-ish but only happens once for failed icons
|
||||
const strippedId = moddedAppId.replace(/-bin$/, "").toLowerCase();
|
||||
const allEntries = DesktopEntries.applications.values;
|
||||
for (let i = 0; i < allEntries.length; i++) {
|
||||
@@ -81,7 +57,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Nix/Guix specific store check (as a last resort)
|
||||
for (const appId of appIds) {
|
||||
let execPath = entry?.execString?.replace(/\/bin.*/, "");
|
||||
if (!execPath)
|
||||
@@ -112,180 +87,53 @@ Singleton {
|
||||
return result;
|
||||
}
|
||||
|
||||
signal getDefaultAppResult(string mimeType, string desktopFileId, string callbackId)
|
||||
signal getAppsForMimeResult(string mimeType, var appIds, string callbackId)
|
||||
|
||||
// Set default app for a MIME type
|
||||
Component {
|
||||
id: gioSetDefaultApp
|
||||
function setDefaultApp(mimeType, desktopFileId, callbackId = "") {
|
||||
setDefaultAppForMimes([mimeType], desktopFileId, callbackId);
|
||||
}
|
||||
|
||||
Process {
|
||||
property string targetMimeType: ""
|
||||
property string targetDesktopFileId: ""
|
||||
property string callbackId: ""
|
||||
|
||||
// Check if the app actually supports the MIME type before setting it as default
|
||||
// This uses a shell script
|
||||
command: ["sh", "-c", `
|
||||
apps=$(gio mime "${targetMimeType}" 2>/dev/null | grep -v "^Default" | awk '{print $1}')
|
||||
if echo "$apps" | grep -Fxq "${targetDesktopFileId}"; then
|
||||
xdg-mime default "${targetDesktopFileId}" "${targetMimeType}"
|
||||
gio mime "${targetMimeType}" "${targetDesktopFileId}"
|
||||
fi
|
||||
`]
|
||||
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
const success = (exitCode === 0)
|
||||
if (!success) {
|
||||
log.error("DesktopService: failed to set default app for", targetMimeType, "to", targetDesktopFileId, "(exit code:", exitCode + ")")
|
||||
}
|
||||
root._processDefaultAppQueue()
|
||||
destroy()
|
||||
function setDefaultAppForMimes(mimeTypes, desktopFileId, callbackId = "") {
|
||||
if (!desktopFileId.endsWith(".desktop")) {
|
||||
desktopFileId += ".desktop";
|
||||
}
|
||||
const filtered = (mimeTypes || []).filter(m => m && m.length > 0);
|
||||
if (filtered.length === 0)
|
||||
return;
|
||||
DMSService.sendRequest("mime.setDefaults", {
|
||||
"mimeTypes": filtered,
|
||||
"desktopId": desktopFileId
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
log.warn("DesktopService.setDefaultApp failed:", response.error, "mimes:", filtered, "app:", desktopFileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setDefaultApp(mimeType, desktopFileId, callbackId = "") {
|
||||
// Add .desktop in case it's missing, xdg-mime needs it
|
||||
if (!desktopFileId.endsWith(".desktop")) {
|
||||
desktopFileId += ".desktop";
|
||||
}
|
||||
|
||||
// Queue the request to avoid race conditions
|
||||
_setDefaultAppQueue.push({
|
||||
mimeType: mimeType,
|
||||
desktopFileId: desktopFileId,
|
||||
callbackId: callbackId
|
||||
})
|
||||
|
||||
// Start processing the queue if not already running
|
||||
if (!_isProcessingQueue) {
|
||||
_processDefaultAppQueue()
|
||||
}
|
||||
}
|
||||
|
||||
function _processDefaultAppQueue() {
|
||||
if (_setDefaultAppQueue.length === 0) {
|
||||
_isProcessingQueue = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_isProcessingQueue = true;
|
||||
const request = _setDefaultAppQueue.shift();
|
||||
|
||||
const proc = gioSetDefaultApp.createObject(root, {
|
||||
targetMimeType: request.mimeType,
|
||||
targetDesktopFileId: request.desktopFileId,
|
||||
callbackId: request.callbackId,
|
||||
running: true
|
||||
})
|
||||
|
||||
if (!proc) {
|
||||
log.warn("DesktopService: couldn't create process for", request.mimeType, request.desktopFileId)
|
||||
_processDefaultAppQueue()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Get default app for a MIME type
|
||||
Component {
|
||||
id: xdgGetDefaultApp
|
||||
|
||||
Process {
|
||||
property string targetMimeType: ""
|
||||
property string callbackId: ""
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const desktopFileId = text.trim();
|
||||
root.getDefaultAppResult(targetMimeType, desktopFileId, callbackId);
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim().length > 0) {
|
||||
log.error("DesktopService: xdg-mime query error:", text, "mime:", targetMimeType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode, exitStatus) => { destroy() }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getDefaultApp(mimeType, callbackId = "") {
|
||||
const proc = xdgGetDefaultApp.createObject(root, {
|
||||
targetMimeType: mimeType,
|
||||
callbackId: callbackId,
|
||||
command: ["xdg-mime", "query", "default", mimeType],
|
||||
running: true
|
||||
})
|
||||
|
||||
if (!proc) {
|
||||
log.warn("DesktopService: couldn't create process for", mimeType)
|
||||
}
|
||||
DMSService.sendRequest("mime.getDefault", {
|
||||
"mimeType": mimeType
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
log.warn("DesktopService.getDefaultApp failed:", response.error, "mime:", mimeType);
|
||||
return;
|
||||
}
|
||||
const result = response.result || {};
|
||||
root.getDefaultAppResult(mimeType, result.desktopId || "", callbackId);
|
||||
});
|
||||
}
|
||||
|
||||
signal getDefaultAppResult(string mimeType, string desktopFileId, string callbackId)
|
||||
|
||||
|
||||
|
||||
// Get apps that support a MIME type
|
||||
Component {
|
||||
id: gioGetAppsForMime
|
||||
|
||||
Process {
|
||||
property string targetMimeType: ""
|
||||
property string callbackId: ""
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const lines = text.split("\n");
|
||||
let appIds = [];
|
||||
let seen = {};
|
||||
|
||||
for (let line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (
|
||||
trimmed &&
|
||||
trimmed.endsWith(".desktop") &&
|
||||
!trimmed.startsWith("Default") &&
|
||||
!trimmed.startsWith("default=")
|
||||
) {
|
||||
if (!seen[trimmed]) {
|
||||
seen[trimmed] = true;
|
||||
appIds.push(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
root.getAppsForMimeResult(targetMimeType, appIds, callbackId);
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim().length > 0) {
|
||||
log.error("DesktopService: gio mime query error:", text, "command:", command, "mime:", targetMimeType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode, exitStatus) => { destroy() }
|
||||
}
|
||||
function getAppsForMimeType(mimeType, callbackId = "") {
|
||||
DMSService.sendRequest("mime.appsForMime", {
|
||||
"mimeType": mimeType
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
log.warn("DesktopService.getAppsForMimeType failed:", response.error, "mime:", mimeType);
|
||||
return;
|
||||
}
|
||||
const result = response.result || {};
|
||||
root.getAppsForMimeResult(mimeType, result.desktopIds || [], callbackId);
|
||||
});
|
||||
}
|
||||
|
||||
function getAppsForMimeType(mimeType, callbackId = "") {
|
||||
const proc = gioGetAppsForMime.createObject(root, {
|
||||
targetMimeType: mimeType,
|
||||
callbackId: callbackId,
|
||||
command: ["gio", "mime", mimeType],
|
||||
running: true
|
||||
});
|
||||
|
||||
if (!proc) {
|
||||
log.warn("DesktopService: couldn't create process for", mimeType)
|
||||
}
|
||||
}
|
||||
|
||||
signal getAppsForMimeResult(string mimeType, var appIds, string callbackId)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user