1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -05:00

meta: support async launcher plugins, cached GIFs, paste on launcher v2

action
- Preparations for DankGifSearch plugin
This commit is contained in:
bbedward
2026-01-23 12:02:12 -05:00
parent 808ee66e11
commit 972fc534a4
8 changed files with 141 additions and 34 deletions

View File

@@ -29,16 +29,7 @@ DankModal {
property int activeImageLoads: 0
readonly property int maxConcurrentLoads: 3
readonly property bool clipboardAvailable: DMSService.isConnected && (DMSService.capabilities.length === 0 || DMSService.capabilities.includes("clipboard"))
property bool wtypeAvailable: false
Process {
id: wtypeCheck
command: ["which", "wtype"]
running: true
onExited: exitCode => {
clipboardHistoryModal.wtypeAvailable = (exitCode === 0);
}
}
readonly property bool wtypeAvailable: SessionService.wtypeAvailable
Process {
id: wtypeProcess

View File

@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import "Scorer.js" as Scorer
@@ -41,6 +42,47 @@ Item {
}
}
Connections {
target: PluginService
function onRequestLauncherUpdate(pluginId) {
if (activePluginId === pluginId || searchQuery) {
performSearch();
}
}
}
Process {
id: wtypeProcess
command: ["wtype", "-M", "ctrl", "-P", "v", "-p", "v", "-m", "ctrl"]
running: false
}
Timer {
id: pasteTimer
interval: 200
repeat: false
onTriggered: wtypeProcess.running = true
}
function pasteSelected() {
if (!selectedItem)
return;
if (!SessionService.wtypeAvailable) {
ToastService.showError("wtype not available - install wtype for paste support");
return;
}
const pluginId = selectedItem.pluginId;
if (!pluginId)
return;
const pasteText = AppSearchService.getPluginPasteText(pluginId, selectedItem.data);
if (!pasteText)
return;
Quickshell.execDetached(["dms", "cl", "copy", pasteText]);
itemExecuted();
pasteTimer.start();
}
readonly property var sectionDefinitions: [
{
id: "calculator",

View File

@@ -209,6 +209,10 @@ FocusScope {
return;
case Qt.Key_Return:
case Qt.Key_Enter:
if (event.modifiers & Qt.ShiftModifier) {
controller.pasteSelected();
return;
}
if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) {
actionPanel.executeSelectedAction();
} else {

View File

@@ -109,6 +109,18 @@ Rectangle {
color: Theme.primaryText
}
}
Image {
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: Theme.spacingXS
width: 40
height: 16
fillMode: Image.PreserveAspectFit
source: root.item?.data?.attribution || ""
visible: source !== ""
opacity: 0.9
}
}
}

View File

@@ -855,6 +855,21 @@ Singleton {
return false;
}
function getPluginPasteText(pluginId, item) {
if (typeof PluginService === "undefined")
return null;
const instance = PluginService.pluginInstances[pluginId];
if (!instance)
return null;
if (typeof instance.getPasteText === "function") {
return instance.getPasteText(item);
}
return null;
}
function searchPluginItems(query) {
if (typeof PluginService === "undefined")
return [];

View File

@@ -592,6 +592,13 @@ Singleton {
return SettingsData.getPluginSetting(pluginId, key, defaultValue);
}
function getPluginPath(pluginId) {
const plugin = availablePlugins[pluginId];
if (!plugin)
return "";
return plugin.pluginDirectory || "";
}
function saveAllPluginSettings() {
SettingsData.savePluginSettings();
}

View File

@@ -29,6 +29,7 @@ Singleton {
}
property bool loginctlAvailable: false
property bool wtypeAvailable: false
property string sessionId: ""
property string sessionPath: ""
property bool locked: false
@@ -59,6 +60,7 @@ Singleton {
detectElogindProcess.running = true;
detectHibernateProcess.running = true;
detectPrimeRunProcess.running = true;
detectWtypeProcess.running = true;
console.info("SessionService: Native inhibitor available:", nativeInhibitorAvailable);
if (!SettingsData.loginctlLockIntegration) {
console.log("SessionService: loginctl lock integration disabled by user");
@@ -124,6 +126,15 @@ Singleton {
}
}
Process {
id: detectWtypeProcess
running: false
command: ["which", "wtype"]
onExited: exitCode => {
wtypeAvailable = (exitCode === 0);
}
}
Process {
id: detectPrimeRunProcess
running: false

View File

@@ -1,13 +1,21 @@
import QtQuick
import qs.Common
Image {
Item {
id: root
property string imagePath: ""
property int maxCacheSize: 512
property int status: isAnimated ? animatedImg.status : staticImg.status
property int fillMode: Image.PreserveAspectCrop
readonly property bool isRemoteUrl: imagePath.startsWith("http://") || imagePath.startsWith("https://")
readonly property bool isAnimated: {
if (!imagePath)
return false;
const lower = imagePath.toLowerCase();
return lower.endsWith(".gif") || lower.endsWith(".webp");
}
readonly property string normalizedPath: {
if (!imagePath)
return "";
@@ -30,7 +38,7 @@ Image {
}
readonly property string imageHash: normalizedPath ? djb2Hash(normalizedPath) : ""
readonly property string cachePath: imageHash && !isRemoteUrl ? `${Paths.stringify(Paths.imagecache)}/${imageHash}@${maxCacheSize}x${maxCacheSize}.png` : ""
readonly property string cachePath: imageHash && !isRemoteUrl && !isAnimated ? `${Paths.stringify(Paths.imagecache)}/${imageHash}@${maxCacheSize}x${maxCacheSize}.png` : ""
readonly property string encodedImagePath: {
if (!normalizedPath)
return "";
@@ -39,39 +47,56 @@ Image {
return "file://" + normalizedPath.split('/').map(s => encodeURIComponent(s)).join('/');
}
asynchronous: true
fillMode: Image.PreserveAspectCrop
sourceSize.width: maxCacheSize
sourceSize.height: maxCacheSize
smooth: true
AnimatedImage {
id: animatedImg
anchors.fill: parent
visible: root.isAnimated
asynchronous: true
fillMode: root.fillMode
source: root.isAnimated ? root.imagePath : ""
playing: visible && status === AnimatedImage.Ready
}
Image {
id: staticImg
anchors.fill: parent
visible: !root.isAnimated
asynchronous: true
fillMode: root.fillMode
sourceSize.width: root.maxCacheSize
sourceSize.height: root.maxCacheSize
smooth: true
onStatusChanged: {
if (source == root.cachePath && status === Image.Error) {
source = root.encodedImagePath;
return;
}
if (root.isRemoteUrl || source != root.encodedImagePath || status !== Image.Ready || !root.cachePath)
return;
Paths.mkdir(Paths.imagecache);
const grabPath = root.cachePath;
if (visible && width > 0 && height > 0 && Window.window?.visible) {
grabToImage(res => res.saveToFile(grabPath));
}
}
}
onImagePathChanged: {
if (!imagePath) {
source = "";
staticImg.source = "";
return;
}
if (isAnimated)
return;
if (isRemoteUrl) {
source = imagePath;
staticImg.source = imagePath;
return;
}
Paths.mkdir(Paths.imagecache);
const hash = djb2Hash(normalizedPath);
const cPath = hash ? `${Paths.stringify(Paths.imagecache)}/${hash}@${maxCacheSize}x${maxCacheSize}.png` : "";
const encoded = "file://" + normalizedPath.split('/').map(s => encodeURIComponent(s)).join('/');
source = cPath || encoded;
}
onStatusChanged: {
if (source == cachePath && status === Image.Error) {
source = encodedImagePath;
return;
}
if (isRemoteUrl || source != encodedImagePath || status !== Image.Ready || !cachePath)
return;
Paths.mkdir(Paths.imagecache);
const grabPath = cachePath;
if (visible && width > 0 && height > 0 && Window.window?.visible) {
grabToImage(res => res.saveToFile(grabPath));
}
staticImg.source = cPath || encoded;
}
}