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

Compare commits

...

3 Commits

Author SHA1 Message Date
bbedward
972fc534a4 meta: support async launcher plugins, cached GIFs, paste on launcher v2
action
- Preparations for DankGifSearch plugin
2026-01-23 12:03:05 -05:00
purian23
808ee66e11 feat: AppsDock Widget on the Dankbar
- Pinnable apps independent from the main dock
- Drag & Drop support
2026-01-23 11:49:45 -05:00
bbedward
3936a516f8 lock: fix loginctl lock integration disabled setting
fixes #1471
2026-01-23 09:56:43 -05:00
16 changed files with 1497 additions and 38 deletions

View File

@@ -83,6 +83,7 @@ Singleton {
property string nightModeLocationProvider: ""
property var pinnedApps: []
property var barPinnedApps: []
property int dockLauncherPosition: 0
property var hiddenTrayIds: []
property var recentColors: []
@@ -784,6 +785,32 @@ Singleton {
return appId && pinnedApps.indexOf(appId) !== -1;
}
function setBarPinnedApps(apps) {
barPinnedApps = apps;
saveSettings();
}
function addBarPinnedApp(appId) {
if (!appId)
return;
var currentPinned = [...barPinnedApps];
if (currentPinned.indexOf(appId) === -1) {
currentPinned.push(appId);
setBarPinnedApps(currentPinned);
}
}
function removeBarPinnedApp(appId) {
if (!appId)
return;
var currentPinned = barPinnedApps.filter(id => id !== appId);
setBarPinnedApps(currentPinned);
}
function isBarPinnedApp(appId) {
return appId && barPinnedApps.indexOf(appId) !== -1;
}
function hideTrayId(trayId) {
if (!trayId)
return;

View File

@@ -39,6 +39,7 @@ var SPEC = {
weatherCoordinates: { def: "40.7128,-74.0060" },
pinnedApps: { def: [] },
barPinnedApps: { def: [] },
dockLauncherPosition: { def: 0 },
hiddenTrayIds: { def: [] },
recentColors: { def: [] },

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

@@ -302,6 +302,7 @@ Item {
"workspaceSwitcher": workspaceSwitcherComponent,
"focusedWindow": focusedWindowComponent,
"runningApps": runningAppsComponent,
"appsDock": appsDockComponent,
"clock": clockComponent,
"music": mediaComponent,
"weather": weatherComponent,
@@ -343,6 +344,7 @@ Item {
"workspaceSwitcherComponent": workspaceSwitcherComponent,
"focusedWindowComponent": focusedWindowComponent,
"runningAppsComponent": runningAppsComponent,
"appsDockComponent": appsDockComponent,
"clockComponent": clockComponent,
"mediaComponent": mediaComponent,
"weatherComponent": weatherComponent,
@@ -660,6 +662,21 @@ Item {
}
}
Component {
id: appsDockComponent
AppsDock {
widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4
section: topBarContent.getWidgetSection(parent)
parentScreen: barWindow.screen
topBar: topBarContent
barConfig: topBarContent.barConfig
isAutoHideBar: topBarContent.barConfig?.autoHide ?? false
}
}
Component {
id: clockComponent

View File

@@ -242,7 +242,8 @@ Loader {
"colorPicker": components.colorPickerComponent,
"systemUpdate": components.systemUpdateComponent,
"layout": components.layoutComponent,
"powerMenuButton": components.powerMenuButtonComponent
"powerMenuButton": components.powerMenuButtonComponent,
"appsDock": components.appsDockComponent
};
if (componentMap[widgetId]) {

View File

@@ -0,0 +1,867 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var widgetData: null
property var barConfig: null
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property string section: "left"
property var parentScreen
property var hoveredItem: null
property var topBar: null
property real widgetThickness: 30
property real barThickness: 48
property real barSpacing: 4
property bool isAutoHideBar: false
readonly property real horizontalPadding: (barConfig?.noBackground ?? false) ? 2 : Theme.spacingS
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
property int draggedIndex: -1
property int dropTargetIndex: -1
property bool suppressShiftAnimation: false
property int pinnedAppCount: 0
readonly property real effectiveBarThickness: {
if (barThickness > 0 && barSpacing > 0) {
return barThickness + barSpacing;
}
const innerPadding = barConfig?.innerPadding ?? 4;
const spacing = barConfig?.spacing ?? 4;
return Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding)) + spacing;
}
readonly property var barBounds: {
if (!parentScreen || !barConfig) {
return {
"x": 0,
"y": 0,
"width": 0,
"height": 0,
"wingSize": 0
};
}
const barPosition = axis.edge === "left" ? 2 : (axis.edge === "right" ? 3 : (axis.edge === "top" ? 0 : 1));
return SettingsData.getBarBounds(parentScreen, effectiveBarThickness, barPosition, barConfig);
}
readonly property real barY: barBounds.y
readonly property real minTooltipY: {
if (!parentScreen || !isVertical) {
return 0;
}
if (isAutoHideBar) {
return 0;
}
if (parentScreen.y > 0) {
return effectiveBarThickness;
}
return 0;
}
// --- Dock Logic Helpers ---
function movePinnedApp(fromDockIndex, toDockIndex) {
if (fromDockIndex === toDockIndex)
return;
const currentPinned = [...(SessionData.barPinnedApps || [])];
if (fromDockIndex < 0 || fromDockIndex >= currentPinned.length || toDockIndex < 0 || toDockIndex >= currentPinned.length) {
return;
}
const movedApp = currentPinned.splice(fromDockIndex, 1)[0];
currentPinned.splice(toDockIndex, 0, movedApp);
SessionData.setBarPinnedApps(currentPinned);
}
property int _desktopEntriesUpdateTrigger: 0
property int _toplevelsUpdateTrigger: 0
property int _appIdSubstitutionsTrigger: 0
Connections {
target: CompositorService
function onToplevelsChanged() {
_toplevelsUpdateTrigger++;
updateModel();
}
}
Connections {
target: DesktopEntries
function onApplicationsChanged() {
_desktopEntriesUpdateTrigger++;
}
}
Connections {
target: SettingsData
function onAppIdSubstitutionsChanged() {
_appIdSubstitutionsTrigger++;
updateModel();
}
function onRunningAppsCurrentWorkspaceChanged() {
updateModel();
}
}
Connections {
target: SessionData
function onBarPinnedAppsChanged() {
root.suppressShiftAnimation = true;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
updateModel();
Qt.callLater(() => {
root.suppressShiftAnimation = false;
});
}
}
property var dockItems: []
function isOnScreen(toplevel, screenName) {
if (!toplevel.screens)
return false;
for (let i = 0; i < toplevel.screens.length; i++) {
if (toplevel.screens[i]?.name === screenName)
return true;
}
return false;
}
function getCoreAppData(appId) {
if (typeof AppSearchService === "undefined")
return null;
const coreApps = AppSearchService.coreApps || [];
for (let i = 0; i < coreApps.length; i++) {
if (coreApps[i].builtInPluginId === appId)
return coreApps[i];
}
return null;
}
function getCoreAppDataByTitle(windowTitle) {
if (typeof AppSearchService === "undefined" || !windowTitle)
return null;
const coreApps = AppSearchService.coreApps || [];
for (let i = 0; i < coreApps.length; i++) {
if (coreApps[i].name === windowTitle)
return coreApps[i];
}
return null;
}
function updateModel() {
const items = [];
const pinnedApps = [...(SessionData.barPinnedApps || [])];
_toplevelsUpdateTrigger;
const allToplevels = CompositorService.sortedToplevels;
let sortedToplevels = allToplevels;
if (SettingsData.runningAppsCurrentWorkspace && parentScreen) {
sortedToplevels = CompositorService.filterCurrentWorkspace(allToplevels, parentScreen.name) || [];
}
const appGroups = new Map();
pinnedApps.forEach(rawAppId => {
const appId = Paths.moddedAppId(rawAppId);
const coreAppData = getCoreAppData(appId);
appGroups.set(appId, {
appId: appId,
isPinned: true,
windows: [],
isCoreApp: coreAppData !== null,
coreAppData: coreAppData
});
});
sortedToplevels.forEach((toplevel, index) => {
const rawAppId = toplevel.appId || "unknown";
let appId = Paths.moddedAppId(rawAppId);
let coreAppData = null;
if (rawAppId === "org.quickshell") {
coreAppData = getCoreAppDataByTitle(toplevel.title);
if (coreAppData) {
appId = coreAppData.builtInPluginId;
}
}
if (!appGroups.has(appId)) {
appGroups.set(appId, {
appId: appId,
isPinned: false,
windows: [],
isCoreApp: coreAppData !== null,
coreAppData: coreAppData
});
}
appGroups.get(appId).windows.push({
toplevel: toplevel,
index: index,
windowTitle: toplevel.title
});
});
const pinnedGroups = [];
const unpinnedGroups = [];
Array.from(appGroups.entries()).forEach(([appId, group]) => {
const firstWindow = group.windows.length > 0 ? group.windows[0] : null;
const item = {
uniqueKey: "grouped_" + appId,
type: "grouped",
appId: appId,
toplevel: firstWindow ? firstWindow.toplevel : null,
isPinned: group.isPinned,
isRunning: group.windows.length > 0,
windowCount: group.windows.length,
allWindows: group.windows,
isCoreApp: group.isCoreApp || false,
coreAppData: group.coreAppData || null
};
if (group.isPinned) {
pinnedGroups.push(item);
} else {
unpinnedGroups.push(item);
}
});
pinnedGroups.forEach(item => items.push(item));
if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
items.push({
uniqueKey: "separator_grouped",
type: "separator",
appId: "__SEPARATOR__",
toplevel: null,
isPinned: false,
isRunning: false
});
}
unpinnedGroups.forEach(item => items.push(item));
root.pinnedAppCount = pinnedGroups.length;
dockItems = items;
}
Component.onCompleted: updateModel()
readonly property int calculatedSize: {
const count = dockItems.length;
if (count === 0)
return 0;
if (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) {
return count * 24 + (count - 1) * Theme.spacingXS + horizontalPadding * 2;
} else {
return count * (24 + Theme.spacingXS + 120) + (count - 1) * Theme.spacingXS + horizontalPadding * 2;
}
}
readonly property real realCalculatedSize: {
let total = horizontalPadding * 2;
const compact = (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode);
for (let i = 0; i < dockItems.length; i++) {
const item = dockItems[i];
let itemSize = 0;
if (item.type === "separator") {
itemSize = 8;
} else {
itemSize = compact ? 24 : (24 + Theme.spacingXS + 120);
}
total += itemSize;
if (i < dockItems.length - 1)
total += Theme.spacingXS;
}
return total;
}
width: dockItems.length > 0 ? (isVertical ? barThickness : realCalculatedSize) : 0
height: dockItems.length > 0 ? (isVertical ? realCalculatedSize : barThickness) : 0
visible: dockItems.length > 0
Item {
id: visualBackground
width: root.isVertical ? root.widgetThickness : root.realCalculatedSize
height: root.isVertical ? root.realCalculatedSize : root.widgetThickness
anchors.centerIn: parent
clip: false
Rectangle {
id: outline
anchors.centerIn: parent
width: {
const borderWidth = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0;
return parent.width + borderWidth * 2;
}
height: {
const borderWidth = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0;
return parent.height + borderWidth * 2;
}
radius: (barConfig?.noBackground ?? false) ? 0 : Theme.cornerRadius
color: "transparent"
border.width: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0
border.color: {
if (!(barConfig?.widgetOutlineEnabled ?? false)) {
return "transparent";
}
const colorOption = barConfig?.widgetOutlineColor || "primary";
const opacity = barConfig?.widgetOutlineOpacity ?? 1.0;
switch (colorOption) {
case "surfaceText":
return Theme.withAlpha(Theme.surfaceText, opacity);
case "secondary":
return Theme.withAlpha(Theme.secondary, opacity);
case "primary":
return Theme.withAlpha(Theme.primary, opacity);
default:
return Theme.withAlpha(Theme.primary, opacity);
}
}
}
Rectangle {
id: background
anchors.fill: parent
radius: (barConfig?.noBackground ?? false) ? 0 : Theme.cornerRadius
color: {
if (dockItems.length === 0)
return "transparent";
if ((barConfig?.noBackground ?? false))
return "transparent";
const baseColor = Theme.widgetBaseBackgroundColor;
const transparency = (root.barConfig && root.barConfig.widgetTransparency !== undefined) ? root.barConfig.widgetTransparency : 1.0;
if (Theme.widgetBackgroundHasAlpha) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * transparency);
}
return Theme.withAlpha(baseColor, transparency);
}
}
}
Loader {
id: layoutLoader
anchors.centerIn: parent
sourceComponent: root.isVertical ? columnLayout : rowLayout
}
Component {
id: rowLayout
Row {
spacing: Theme.spacingXS
Repeater {
id: repeater
model: ScriptModel {
values: root.dockItems
objectProp: "uniqueKey"
}
delegate: dockDelegate
}
}
}
Component {
id: columnLayout
Column {
spacing: Theme.spacingXS
Repeater {
model: ScriptModel {
values: root.dockItems
objectProp: "uniqueKey"
}
delegate: dockDelegate
}
}
}
Loader {
id: tooltipLoader
active: false
sourceComponent: DankTooltip {}
}
Component {
id: dockDelegate
Item {
id: delegateItem
property bool isSeparator: modelData.type === "separator"
readonly property real visualSize: isSeparator ? 8 : ((widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? 24 : (24 + Theme.spacingXS + 120))
readonly property real visualWidth: root.isVertical ? root.barThickness : visualSize
readonly property real visualHeight: root.isVertical ? visualSize : root.barThickness
width: visualWidth
height: visualHeight
z: (dragHandler.dragging) ? 100 : 0
// --- Drag and Drop Shift Animation Logic ---
property real shiftOffset: {
if (root.draggedIndex < 0 || !modelData.isPinned || isSeparator)
return 0;
if (index === root.draggedIndex)
return 0;
const dragIdx = root.draggedIndex;
const dropIdx = root.dropTargetIndex;
const myIdx = index;
const shiftAmount = visualSize + Theme.spacingXS;
if (dropIdx < 0)
return 0;
if (dragIdx < dropIdx && myIdx > dragIdx && myIdx <= dropIdx)
return -shiftAmount;
if (dragIdx > dropIdx && myIdx >= dropIdx && myIdx < dragIdx)
return shiftAmount;
return 0;
}
transform: Translate {
x: root.isVertical ? 0 : delegateItem.shiftOffset
y: root.isVertical ? delegateItem.shiftOffset : 0
Behavior on x {
enabled: !root.suppressShiftAnimation
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
Behavior on y {
enabled: !root.suppressShiftAnimation
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
}
Rectangle {
visible: isSeparator
width: root.isVertical ? root.barThickness * 0.6 : 2
height: root.isVertical ? 2 : root.barThickness * 0.6
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
radius: 1
anchors.centerIn: parent
}
Item {
id: appItem
visible: !isSeparator
anchors.fill: parent
property bool isFocused: {
if (modelData.type === "grouped") {
return modelData.allWindows.some(w => w.toplevel && w.toplevel.activated);
}
return modelData.toplevel ? modelData.toplevel.activated : false;
}
property var appId: modelData.appId
property int windowCount: modelData.windowCount || (modelData.isRunning ? 1 : 0)
property string windowTitle: {
if (modelData.type === "grouped") {
const active = modelData.allWindows.find(w => w.toplevel && w.toplevel.activated);
if (active)
return active.windowTitle || "(Unnamed)";
if (modelData.allWindows.length > 0)
return modelData.allWindows[0].windowTitle || "(Unnamed)";
return "";
}
return modelData.toplevel ? (modelData.toplevel.title || "(Unnamed)") : "";
}
property string tooltipText: {
root._desktopEntriesUpdateTrigger;
const moddedId = Paths.moddedAppId(appId);
const desktopEntry = moddedId ? DesktopEntries.heuristicLookup(moddedId) : null;
const appName = appId ? Paths.getAppName(appId, desktopEntry) : "Unknown";
if (modelData.type === "grouped" && windowCount > 1) {
return appName + " (" + windowCount + " windows)";
}
return appName + (windowTitle ? " • " + windowTitle : "");
}
transform: Translate {
x: (dragHandler.dragging && !root.isVertical) ? dragHandler.dragAxisOffset : 0
y: (dragHandler.dragging && root.isVertical) ? dragHandler.dragAxisOffset : 0
}
Rectangle {
id: visualContent
width: root.isVertical ? 24 : delegateItem.visualSize
height: root.isVertical ? delegateItem.visualSize : 24
anchors.centerIn: parent
radius: Theme.cornerRadius
color: {
if (appItem.isFocused) {
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
}
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent";
}
border.width: dragHandler.dragging ? 2 : 0
border.color: Theme.primary
opacity: dragHandler.dragging ? 0.8 : 1.0
AppIconRenderer {
id: coreIcon
readonly property bool isCompact: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode)
anchors.left: (root.isVertical || isCompact) ? undefined : parent.left
anchors.leftMargin: (root.isVertical || isCompact) ? 0 : Theme.spacingXS
anchors.top: (root.isVertical && !isCompact) ? parent.top : undefined
anchors.topMargin: (root.isVertical && !isCompact) ? Theme.spacingXS : 0
anchors.centerIn: (root.isVertical || isCompact) ? parent : undefined
iconSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
materialIconSizeAdjustment: 0
iconValue: {
if (!modelData || !modelData.isCoreApp || !modelData.coreAppData)
return "";
const appId = modelData.coreAppData.id || modelData.coreAppData.builtInPluginId;
if ((appId === "dms_settings" || appId === "dms_notepad" || appId === "dms_sysmon") && modelData.coreAppData.cornerIcon) {
return "material:" + modelData.coreAppData.cornerIcon;
}
return modelData.coreAppData.icon || "";
}
colorOverride: Theme.widgetIconColor
fallbackText: "?"
visible: iconValue !== ""
z: 2
}
IconImage {
id: iconImg
readonly property bool isCompact: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode)
anchors.left: (root.isVertical || isCompact) ? undefined : parent.left
anchors.leftMargin: (root.isVertical || isCompact) ? 0 : Theme.spacingXS
anchors.top: (root.isVertical && !isCompact) ? parent.top : undefined
anchors.topMargin: (root.isVertical && !isCompact) ? Theme.spacingXS : 0
anchors.centerIn: (root.isVertical || isCompact) ? parent : undefined
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
source: {
root._desktopEntriesUpdateTrigger;
root._appIdSubstitutionsTrigger;
if (!appItem.appId)
return "";
if (modelData.isCoreApp)
return ""; // Explicitly skip if core app to avoid flickering or wrong look ups
const moddedId = Paths.moddedAppId(appItem.appId);
const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
return Paths.getAppIcon(appItem.appId, desktopEntry);
}
smooth: true
mipmap: true
asynchronous: true
visible: status === Image.Ready && !coreIcon.visible
layer.enabled: appItem.appId === "org.quickshell"
layer.smooth: true
layer.mipmap: true
layer.effect: MultiEffect {
saturation: 0
colorization: 1
colorizationColor: Theme.primary
}
z: 2
}
DankIcon {
readonly property bool isCompact: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode)
anchors.left: (root.isVertical || isCompact) ? undefined : parent.left
anchors.leftMargin: (root.isVertical || isCompact) ? 0 : Theme.spacingXS
anchors.top: (root.isVertical && !isCompact) ? parent.top : undefined
anchors.topMargin: (root.isVertical && !isCompact) ? Theme.spacingXS : 0
anchors.centerIn: (root.isVertical || isCompact) ? parent : undefined
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground)
name: "sports_esports"
color: Theme.widgetTextColor
visible: !iconImg.visible && !coreIcon.visible && Paths.isSteamApp(appItem.appId)
}
Text {
anchors.centerIn: parent
visible: !iconImg.visible && !coreIcon.visible && !Paths.isSteamApp(appItem.appId)
text: {
root._desktopEntriesUpdateTrigger;
if (!appItem.appId)
return "?";
const moddedId = Paths.moddedAppId(appItem.appId);
const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
const appName = Paths.getAppName(appItem.appId, desktopEntry);
return appName.charAt(0).toUpperCase();
}
font.pixelSize: 10
color: Theme.widgetTextColor
}
Rectangle {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? -2 : 2
anchors.bottomMargin: -2
width: 14
height: 14
radius: 7
color: Theme.primary
visible: modelData.type === "grouped" && appItem.windowCount > 1
z: 10
StyledText {
anchors.centerIn: parent
text: appItem.windowCount > 9 ? "9+" : appItem.windowCount
font.pixelSize: 9
color: Theme.surface
}
}
StyledText {
visible: !root.isVertical && !(widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode)
anchors.left: iconImg.right
anchors.leftMargin: Theme.spacingXS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: appItem.windowTitle || appItem.appId
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
color: Theme.widgetTextColor
elide: Text.ElideRight
maximumLineCount: 1
}
Rectangle {
visible: modelData.isRunning
width: root.isVertical ? 2 : 20
height: root.isVertical ? 20 : 2
radius: 1
color: appItem.isFocused ? Theme.primary : Theme.surfaceText
opacity: appItem.isFocused ? 1 : 0.5
anchors.bottom: root.isVertical ? undefined : parent.bottom
anchors.right: root.isVertical ? parent.right : undefined
anchors.horizontalCenter: root.isVertical ? undefined : parent.horizontalCenter
anchors.verticalCenter: root.isVertical ? parent.verticalCenter : undefined
anchors.margins: 0
z: 5
}
}
}
// Handler for Drag Logic
Item {
id: dragHandler
anchors.fill: parent
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property real dragAxisOffset: 0
property bool longPressing: false
Timer {
id: longPressTimer
interval: 500
repeat: false
onTriggered: {
if (modelData.isPinned) {
dragHandler.longPressing = true;
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: mouse => {
if (mouse.button === Qt.LeftButton && modelData.isPinned) {
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
longPressTimer.start();
}
}
onReleased: mouse => {
longPressTimer.stop();
const wasDragging = dragHandler.dragging;
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
if (didReorder) {
root.movePinnedApp(root.draggedIndex, root.dropTargetIndex);
}
dragHandler.longPressing = false;
dragHandler.dragging = false;
dragHandler.dragAxisOffset = 0;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
if (wasDragging || mouse.button !== Qt.LeftButton)
return;
if (wasDragging || mouse.button !== Qt.LeftButton)
return;
if (modelData.type === "grouped") {
if (modelData.windowCount === 0) {
if (modelData.isCoreApp && modelData.coreAppData) {
AppSearchService.executeCoreApp(modelData.coreAppData);
} else {
const moddedId = Paths.moddedAppId(modelData.appId);
const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
if (desktopEntry)
SessionService.launchDesktopEntry(desktopEntry);
}
} else if (modelData.windowCount === 1) {
if (modelData.allWindows[0].toplevel)
modelData.allWindows[0].toplevel.activate();
} else {
let currentIndex = -1;
for (var i = 0; i < modelData.allWindows.length; i++) {
if (modelData.allWindows[i].toplevel.activated) {
currentIndex = i;
break;
}
}
const nextIndex = (currentIndex + 1) % modelData.allWindows.length;
modelData.allWindows[nextIndex].toplevel.activate();
}
}
}
onPositionChanged: mouse => {
if (dragHandler.longPressing && !dragHandler.dragging) {
const distance = Math.sqrt(Math.pow(mouse.x - dragHandler.dragStartPos.x, 2) + Math.pow(mouse.y - dragHandler.dragStartPos.y, 2));
if (distance > 5) {
dragHandler.dragging = true;
root.draggedIndex = index;
root.dropTargetIndex = index;
}
}
if (!dragHandler.dragging)
return;
const axisOffset = root.isVertical ? (mouse.y - dragHandler.dragStartPos.y) : (mouse.x - dragHandler.dragStartPos.x);
dragHandler.dragAxisOffset = axisOffset;
const itemSize = (root.isVertical ? delegateItem.height : delegateItem.width) + Theme.spacingXS;
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.pinnedAppCount - 1, index + slotOffset));
if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex;
}
}
onEntered: {
root.hoveredItem = delegateItem;
if (isSeparator)
return;
tooltipLoader.active = true;
if (tooltipLoader.item) {
if (root.isVertical) {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
const screenX = root.parentScreen ? root.parentScreen.x : 0;
const screenY = root.parentScreen ? root.parentScreen.y : 0;
const relativeY = globalPos.y - screenY;
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + (barConfig?.spacing ?? 4) + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - (barConfig?.spacing ?? 4) - Theme.spacingXS);
const isLeft = root.axis?.edge === "left";
const adjustedY = relativeY + root.minTooltipY;
const finalX = screenX + tooltipX;
tooltipLoader.item.show(appItem.tooltipText, finalX, adjustedY, root.parentScreen, isLeft, !isLeft);
} else {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height);
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
const isBottom = root.axis?.edge === "bottom";
const tooltipY = isBottom ? (screenHeight - Theme.barHeight - (barConfig?.spacing ?? 4) - Theme.spacingXS - 35) : (Theme.barHeight + (barConfig?.spacing ?? 4) + Theme.spacingXS);
tooltipLoader.item.show(appItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false);
}
}
}
onExited: {
if (root.hoveredItem === delegateItem) {
root.hoveredItem = null;
if (tooltipLoader.item)
tooltipLoader.item.hide();
tooltipLoader.active = false;
}
}
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
if (tooltipLoader.item) {
tooltipLoader.item.hide();
}
tooltipLoader.active = false;
contextMenuLoader.active = true;
if (contextMenuLoader.item) {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
const isBarVertical = root.axis?.isVertical ?? false;
const barEdge = root.axis?.edge ?? "top";
let x = globalPos.x;
let y = globalPos.y;
if (barEdge === "bottom") {
y = (root.parentScreen ? root.parentScreen.height : Screen.height) - root.effectiveBarThickness;
} else if (barEdge === "top") {
y = root.effectiveBarThickness;
} else if (barEdge === "left") {
x = root.effectiveBarThickness;
} else if (barEdge === "right") {
x = (root.parentScreen ? root.parentScreen.width : Screen.width) - root.effectiveBarThickness;
}
const shouldHidePin = modelData.appId === "org.quickshell";
const moddedId = Paths.moddedAppId(modelData.appId);
const desktopEntry = moddedId ? DesktopEntries.heuristicLookup(moddedId) : null;
contextMenuLoader.item.showAt(x, y, isBarVertical, barEdge, modelData, shouldHidePin, desktopEntry, root.parentScreen);
}
}
}
}
}
}
}
Loader {
id: contextMenuLoader
active: false
source: "AppsDockContextMenu.qml"
}
}

View File

@@ -0,0 +1,431 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: root
WlrLayershell.namespace: "dms:dock-context-menu"
property var appData: null
property var anchorItem: null
property int margin: 10
property bool hidePin: false
property var desktopEntry: null
property bool isDmsWindow: appData?.appId === "org.quickshell"
property bool isVertical: false
property string edge: "top"
property point anchorPos: Qt.point(0, 0)
function showAt(x, y, vertical, barEdge, data, hidePinOption, entry, targetScreen) {
if (targetScreen) {
root.screen = targetScreen;
}
anchorPos = Qt.point(x, y);
isVertical = vertical ?? false;
edge = barEdge ?? "top";
appData = data;
hidePin = hidePinOption || false;
desktopEntry = entry || null;
visible = true;
}
function close() {
visible = false;
}
screen: null
visible: false
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
id: menuContainer
x: {
if (root.isVertical) {
if (root.edge === "left") {
return Math.min(root.width - width - 10, root.anchorPos.x);
} else {
return Math.max(10, root.anchorPos.x - width);
}
} else {
const left = 10;
const right = root.width - width - 10;
const want = root.anchorPos.x - width / 2;
return Math.max(left, Math.min(right, want));
}
}
y: {
if (root.isVertical) {
const top = 10;
const bottom = root.height - height - 10;
const want = root.anchorPos.y - height / 2;
return Math.max(top, Math.min(bottom, want));
} else {
if (root.edge === "top") {
return Math.min(root.height - height - 10, root.anchorPos.y);
} else {
return Math.max(10, root.anchorPos.y - height);
}
}
}
width: Math.min(400, Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2))
height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2)
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: root.visible ? 1 : 0
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: -1
}
Column {
id: menuColumn
width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
spacing: 1
// Window list for grouped apps
Repeater {
model: {
if (!root.appData || root.appData.type !== "grouped")
return [];
const toplevels = [];
const allToplevels = ToplevelManager.toplevels.values;
for (let i = 0; i < allToplevels.length; i++) {
const toplevel = allToplevels[i];
if (toplevel.appId === root.appData.appId) {
toplevels.push(toplevel);
}
}
return toplevels;
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
color: windowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: closeButton.left
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
text: (modelData && modelData.title) ? modelData.title : I18n.tr("(Unnamed)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Rectangle {
id: closeButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
width: 20
height: 20
radius: 10
color: closeMouseArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "close"
size: 12
color: closeMouseArea.containsMouse ? Theme.error : Theme.surfaceText
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && modelData.close) {
modelData.close();
}
root.close();
}
}
}
MouseArea {
id: windowArea
anchors.fill: parent
anchors.rightMargin: 24
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && modelData.activate) {
modelData.activate();
}
root.close();
}
}
}
}
Rectangle {
visible: {
if (!root.appData)
return false;
if (root.appData.type !== "grouped")
return false;
return root.appData.windowCount > 0;
}
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Repeater {
model: root.desktopEntry && root.desktopEntry.actions ? root.desktopEntry.actions : []
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
color: actionArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Item {
anchors.verticalCenter: parent.verticalCenter
width: 16
height: 16
visible: modelData.icon && modelData.icon !== ""
IconImage {
anchors.fill: parent
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: modelData.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
MouseArea {
id: actionArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData) {
SessionService.launchDesktopAction(root.desktopEntry, modelData);
}
root.close();
}
}
}
}
Rectangle {
visible: {
if (!root.desktopEntry?.actions || root.desktopEntry.actions.length === 0) {
return false;
}
return !root.hidePin || (!root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand);
}
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Rectangle {
visible: !root.hidePin
width: parent.width
height: 28
radius: Theme.cornerRadius
color: pinArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: root.appData && root.appData.isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: pinArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!root.appData) {
return;
}
if (root.appData.isPinned) {
SessionData.removeBarPinnedApp(root.appData.appId);
} else {
SessionData.addBarPinnedApp(root.appData.appId);
}
root.close();
}
}
}
Rectangle {
visible: {
const hasNvidia = !root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand;
const hasWindow = root.appData && (root.appData.type === "window" || (root.appData.type === "grouped" && root.appData.windowCount > 0));
const hasPinOption = !root.hidePin;
const hasContentAbove = hasPinOption || hasNvidia;
return hasContentAbove && hasWindow;
}
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Rectangle {
visible: !root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand
width: parent.width
height: 28
radius: Theme.cornerRadius
color: nvidiaArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Launch on dGPU")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: nvidiaArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.desktopEntry) {
SessionService.launchDesktopEntry(root.desktopEntry, true);
}
root.close();
}
}
}
Rectangle {
visible: root.appData && (root.appData.type === "window" || (root.appData.type === "grouped" && root.appData.windowCount > 0))
width: parent.width
height: 28
radius: Theme.cornerRadius
color: closeArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: {
if (root.appData && root.appData.type === "grouped") {
return I18n.tr("Close All Windows");
}
return I18n.tr("Close Window");
}
font.pixelSize: Theme.fontSizeSmall
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.appData?.type === "window") {
root.appData?.toplevel?.close();
} else if (root.appData?.type === "grouped") {
root.appData?.allWindows?.forEach(window => window.toplevel?.close());
}
root.close();
}
}
}
}
}
MouseArea {
anchors.fill: parent
z: -1
onClicked: root.close()
}
}

View File

@@ -65,7 +65,7 @@ Scope {
lockInitiatedLocally = true;
lockPowerOffArmed = SettingsData.lockScreenPowerOffMonitorsOnLock;
if (!SessionService.active && SessionService.loginctlAvailable) {
if (!SessionService.active && SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration) {
pendingLock = true;
notifyLoginctl(true);
return;
@@ -99,7 +99,7 @@ Scope {
function onSessionLocked() {
if (shouldLock || pendingLock)
return;
if (!SessionService.active && SessionService.loginctlAvailable) {
if (!SessionService.active && SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration) {
pendingLock = true;
lockInitiatedLocally = false;
return;

View File

@@ -68,6 +68,13 @@ Item {
"icon": "apps",
"enabled": true
},
{
"id": "appsDock",
"text": I18n.tr("Apps Dock"),
"description": I18n.tr("Pinned and running apps with drag-and-drop"),
"icon": "dock_to_bottom",
"enabled": true
},
{
"id": "clock",
"text": I18n.tr("Clock"),

View File

@@ -219,7 +219,8 @@ Singleton {
action: plugin.action,
categories: plugin.categories,
isCore: true,
builtInPluginId: pluginId
builtInPluginId: pluginId,
cornerIcon: plugin.cornerIcon
});
}
return apps;
@@ -854,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;
}
}