mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-07 14:08:29 -04:00
qs: numerous performance optimizations
- ClippingRectangle usages - Asynchronous loader usages - Replace cava visualizers with shaders
This commit is contained in:
@@ -9,7 +9,7 @@ hl.bind("SUPER + M", hl.dsp.exec_cmd("dms ipc call processlist focusOrToggle"))
|
|||||||
hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle"))
|
hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle"))
|
||||||
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle"))
|
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle"))
|
||||||
hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
|
hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
|
||||||
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dankdash wallpaper"))
|
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dash toggle wallpaper"))
|
||||||
hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
||||||
hl.bind("SUPER + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
hl.bind("SUPER + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
||||||
hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle"))
|
hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle"))
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ bind=SUPER,n,spawn,dms ipc call notifications toggle
|
|||||||
# Notepad
|
# Notepad
|
||||||
bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle
|
bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle
|
||||||
# Browse Wallpapers
|
# Browse Wallpapers
|
||||||
bind=SUPER,y,spawn,dms ipc call dankdash wallpaper
|
bind=SUPER,y,spawn,dms ipc call dash toggle wallpaper
|
||||||
# Power Menu
|
# Power Menu
|
||||||
bind=SUPER,x,spawn,dms ipc call powermenu toggle
|
bind=SUPER,x,spawn,dms ipc call powermenu toggle
|
||||||
# Cycle Display Profile
|
# Cycle Display Profile
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ binds {
|
|||||||
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
||||||
}
|
}
|
||||||
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
||||||
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
|
spawn "dms" "ipc" "call" "dash" "toggle" "wallpaper";
|
||||||
}
|
}
|
||||||
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
|
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
|
||||||
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
|
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
|
||||||
|
|||||||
@@ -712,12 +712,13 @@ Item {
|
|||||||
target: "hypr"
|
target: "hypr"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ! TODO - remove for v1.6
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function wallpaper(): string {
|
function wallpaper(): string {
|
||||||
const bar = root.getPreferredBar("clockButtonRef") || root.getPreferredBar();
|
const bar = root.getPreferredBar("clockButtonRef") || root.getPreferredBar();
|
||||||
if (bar) {
|
if (bar) {
|
||||||
bar.triggerWallpaperBrowser();
|
bar.triggerWallpaperBrowser();
|
||||||
return "SUCCESS: Toggled wallpaper browser";
|
return "WARN; deprecated, use dms ipc call dash toggle wallpaper instead";
|
||||||
}
|
}
|
||||||
return "ERROR: Failed to toggle wallpaper browser";
|
return "ERROR: Failed to toggle wallpaper browser";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
Item {
|
ClippingRectangle {
|
||||||
id: thumbnail
|
id: thumbnail
|
||||||
readonly property var log: Log.scoped("ClipboardThumbnail")
|
readonly property var log: Log.scoped("ClipboardThumbnail")
|
||||||
|
|
||||||
@@ -15,6 +15,10 @@ Item {
|
|||||||
required property int itemIndex
|
required property int itemIndex
|
||||||
property bool disposed: false
|
property bool disposed: false
|
||||||
|
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
color: "transparent"
|
||||||
|
antialiasing: true
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: thumbnailImage
|
id: thumbnailImage
|
||||||
|
|
||||||
@@ -34,7 +38,7 @@ Item {
|
|||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
smooth: true
|
smooth: true
|
||||||
cache: false
|
cache: false
|
||||||
visible: false
|
visible: entryType === "image" && status === Image.Ready && source != ""
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
sourceSize.width: 128
|
sourceSize.width: 128
|
||||||
sourceSize.height: 128
|
sourceSize.height: 128
|
||||||
@@ -236,33 +240,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MultiEffect {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 2
|
|
||||||
source: thumbnailImage
|
|
||||||
maskEnabled: true
|
|
||||||
maskSource: clipboardRoundedRectangularMask
|
|
||||||
visible: entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != ""
|
|
||||||
maskThresholdMin: 0.5
|
|
||||||
maskSpreadAtMin: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: clipboardRoundedRectangularMask
|
|
||||||
width: ClipboardConstants.thumbnailSize
|
|
||||||
height: ClipboardConstants.itemHeight - 4
|
|
||||||
layer.enabled: true
|
|
||||||
layer.smooth: true
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius / 2 // Thumbnail corner radius is divided by 2 so it doesnt look weird on large corner radius (eg: 32px)
|
|
||||||
color: "black"
|
|
||||||
antialiasing: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "")
|
visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "")
|
||||||
name: {
|
name: {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Rectangle {
|
|||||||
property string cachedMimeType: ""
|
property string cachedMimeType: ""
|
||||||
property var _requestedEntryId: null
|
property var _requestedEntryId: null
|
||||||
|
|
||||||
readonly property bool canLoadImage: !!entry?.isImage && (entry?.mimeType ?? "").startsWith("image/")
|
readonly property bool canLoadImage: typeof entry?.id === "number" && !!entry?.isImage && String(entry?.mimeType ?? "").startsWith("image/")
|
||||||
readonly property string sourceUrl: resolvedSourceUrl(cachedImageData, cachedMimeType || (entry?.mimeType ?? ""))
|
readonly property string sourceUrl: resolvedSourceUrl(cachedImageData, cachedMimeType || (entry?.mimeType ?? ""))
|
||||||
|
|
||||||
radius: Math.max(6, Theme.cornerRadius - 2)
|
radius: Math.max(6, Theme.cornerRadius - 2)
|
||||||
@@ -41,7 +41,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reloadPreview() {
|
function reloadPreview() {
|
||||||
if (!canLoadImage || !entry?.id) {
|
if (!canLoadImage || typeof entry?.id !== "number") {
|
||||||
_requestedEntryId = null;
|
_requestedEntryId = null;
|
||||||
cachedImageData = "";
|
cachedImageData = "";
|
||||||
cachedMimeType = "";
|
cachedMimeType = "";
|
||||||
|
|||||||
@@ -120,6 +120,18 @@ Item {
|
|||||||
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
||||||
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
|
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
|
||||||
|
|
||||||
|
// Blur region isn't auto-committed on geometry changes; kick twice to catch resize settling.
|
||||||
|
function _kickBlurCommit() {
|
||||||
|
launcherBlur.kick();
|
||||||
|
Qt.callLater(launcherBlur.kick);
|
||||||
|
}
|
||||||
|
|
||||||
|
onAlignedXChanged: _kickBlurCommit()
|
||||||
|
onAlignedYChanged: _kickBlurCommit()
|
||||||
|
onAlignedWidthChanged: _kickBlurCommit()
|
||||||
|
on_ContentImplicitHChanged: _kickBlurCommit()
|
||||||
|
onContentVisibleChanged: _kickBlurCommit()
|
||||||
|
|
||||||
signal dialogClosed
|
signal dialogClosed
|
||||||
|
|
||||||
function _ensureContentLoadedAndInitialize(query, mode) {
|
function _ensureContentLoadedAndInitialize(query, mode) {
|
||||||
@@ -328,6 +340,7 @@ Item {
|
|||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
WindowBlur {
|
WindowBlur {
|
||||||
|
id: launcherBlur
|
||||||
targetWindow: launcherWindow
|
targetWindow: launcherWindow
|
||||||
readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2))
|
readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2))
|
||||||
blurX: modalContainer.x
|
blurX: modalContainer.x
|
||||||
@@ -337,6 +350,9 @@ Item {
|
|||||||
blurRadius: root.cornerRadius
|
blurRadius: root.cornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onWidthChanged: root._kickBlurCommit()
|
||||||
|
onHeightChanged: root._kickBlurCommit()
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:spotlight"
|
WlrLayershell.namespace: "dms:spotlight"
|
||||||
WlrLayershell.layer: root.effectiveLauncherLayer
|
WlrLayershell.layer: root.effectiveLauncherLayer
|
||||||
WlrLayershell.exclusiveZone: -1
|
WlrLayershell.exclusiveZone: -1
|
||||||
@@ -421,6 +437,9 @@ Item {
|
|||||||
|
|
||||||
opacity: contentVisible ? 1 : 0
|
opacity: contentVisible ? 1 : 0
|
||||||
|
|
||||||
|
onOpacityChanged: root._kickBlurCommit()
|
||||||
|
onSlideOffsetChanged: root._kickBlurCommit()
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: contentVisible ? root._openDuration : root._closeDuration
|
duration: contentVisible ? root._openDuration : root._closeDuration
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Rectangle {
|
|||||||
return item.icon || "";
|
return item.icon || "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
readonly property bool hasClipboardPreview: item?.type === "clipboard" && item?.data?.isImage === true && (item?.data?.mimeType ?? "").startsWith("image/")
|
readonly property bool hasClipboardPreview: item?.type === "clipboard" && !!item?.data?.isImage && String(item?.data?.mimeType ?? "").startsWith("image/")
|
||||||
|
|
||||||
width: parent?.width ?? 200
|
width: parent?.width ?? 200
|
||||||
height: 52
|
height: 52
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ Rectangle {
|
|||||||
return "file://" + raw;
|
return "file://" + raw;
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
readonly property bool hasClipboardPreview: item?.type === "clipboard" && item?.data?.isImage === true && (item?.data?.mimeType ?? "").startsWith("image/")
|
readonly property bool hasClipboardPreview: item?.type === "clipboard" && !!item?.data?.isImage && String(item?.data?.mimeType ?? "").startsWith("image/")
|
||||||
readonly property bool hasMediaPreview: previewSource.length > 0 || hasClipboardPreview
|
readonly property bool hasMediaPreview: previewSource.length > 0 || hasClipboardPreview
|
||||||
|
|
||||||
readonly property string typeLabel: {
|
readonly property string typeLabel: {
|
||||||
|
|||||||
@@ -7,15 +7,24 @@ Item {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
||||||
readonly property bool hasActiveMedia: activePlayer !== null
|
readonly property bool isPlaying: activePlayer !== null && activePlayer.playbackState === MprisPlaybackState.Playing
|
||||||
readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
|
readonly property bool live: visible && isPlaying
|
||||||
|
|
||||||
width: 20
|
width: 20
|
||||||
height: Theme.iconSize
|
height: Theme.iconSize
|
||||||
|
|
||||||
Loader {
|
readonly property real maxBarHeight: Theme.iconSize - 2
|
||||||
active: isPlaying
|
readonly property real minBarHeight: 3
|
||||||
|
|
||||||
|
onLiveChanged: {
|
||||||
|
if (!live) {
|
||||||
|
bars.bandsA = Qt.vector4d(0, 0, 0, 0);
|
||||||
|
bars.bandsB = Qt.vector2d(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: root.live
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
Ref {
|
Ref {
|
||||||
service: CavaService
|
service: CavaService
|
||||||
@@ -23,15 +32,8 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property real maxBarHeight: Theme.iconSize - 2
|
|
||||||
readonly property real minBarHeight: 3
|
|
||||||
readonly property real heightRange: maxBarHeight - minBarHeight
|
|
||||||
property var barHeights: [minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight]
|
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: fallbackTimer
|
running: !CavaService.cavaAvailable && root.live
|
||||||
|
|
||||||
running: !CavaService.cavaAvailable && isPlaying
|
|
||||||
interval: 500
|
interval: 500
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
@@ -41,54 +43,32 @@ Item {
|
|||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: CavaService
|
target: CavaService
|
||||||
|
enabled: root.live
|
||||||
function onValuesChanged() {
|
function onValuesChanged() {
|
||||||
if (!root.isPlaying) {
|
const v = CavaService.values;
|
||||||
root.barHeights = [root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight];
|
if (v.length < 6)
|
||||||
return;
|
return;
|
||||||
}
|
const n = i => {
|
||||||
|
const x = v[i];
|
||||||
const newHeights = [];
|
return x <= 0 ? 0 : x >= 100 ? 1 : Math.sqrt(x * 0.01);
|
||||||
for (let i = 0; i < 6; i++) {
|
};
|
||||||
if (CavaService.values.length <= i) {
|
bars.bandsA = Qt.vector4d(n(0), n(1), n(2), n(3));
|
||||||
newHeights.push(root.minBarHeight);
|
bars.bandsB = Qt.vector2d(n(4), n(5));
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawLevel = CavaService.values[i];
|
|
||||||
if (rawLevel <= 0) {
|
|
||||||
newHeights.push(root.minBarHeight);
|
|
||||||
} else if (rawLevel >= 100) {
|
|
||||||
newHeights.push(root.maxBarHeight);
|
|
||||||
} else {
|
|
||||||
newHeights.push(root.minBarHeight + Math.sqrt(rawLevel * 0.01) * root.heightRange);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
root.barHeights = newHeights;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
ShaderEffect {
|
||||||
anchors.centerIn: parent
|
id: bars
|
||||||
spacing: 1.5
|
anchors.fill: parent
|
||||||
|
|
||||||
Repeater {
|
property real widthPx: width
|
||||||
model: 6
|
property real heightPx: height
|
||||||
|
property real minH: root.minBarHeight
|
||||||
|
property real maxH: root.maxBarHeight
|
||||||
|
property vector4d bandsA: Qt.vector4d(0, 0, 0, 0)
|
||||||
|
property vector2d bandsB: Qt.vector2d(0, 0)
|
||||||
|
property vector4d fillColor: Qt.vector4d(Theme.primary.r, Theme.primary.g, Theme.primary.b, Theme.primary.a)
|
||||||
|
|
||||||
Rectangle {
|
fragmentShader: Qt.resolvedUrl("../../../Shaders/qsb/viz_bars.frag.qsb")
|
||||||
width: 2
|
|
||||||
height: root.barHeights[index]
|
|
||||||
radius: 1.5
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
enabled: root.isPlaying && !CavaService.cavaAvailable
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 100
|
|
||||||
easing.type: Easing.Linear
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
@@ -234,16 +233,19 @@ BasePill {
|
|||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
Row {
|
||||||
id: contentRow
|
id: contentRow
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
visible: !root.isVerticalOrientation
|
visible: !root.isVerticalOrientation
|
||||||
|
|
||||||
|
readonly property real iconSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
id: horizontalAppIcon
|
id: horizontalAppIcon
|
||||||
Layout.preferredWidth: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
width: contentRow.iconSize
|
||||||
Layout.preferredHeight: Layout.preferredWidth
|
height: contentRow.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.showIcon && activeWindow && status === Image.Ready
|
visible: root.showIcon && activeWindow && status === Image.Ready
|
||||||
source: {
|
source: {
|
||||||
if (!activeWindow || !activeWindow.appId)
|
if (!activeWindow || !activeWindow.appId)
|
||||||
@@ -264,8 +266,10 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
Layout.preferredWidth: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
id: horizontalSteamIcon
|
||||||
size: Layout.preferredWidth
|
width: contentRow.iconSize
|
||||||
|
size: contentRow.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
name: "sports_esports"
|
name: "sports_esports"
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
visible: root.showIcon && activeWindow && activeWindow.appId && horizontalAppIcon.status !== Image.Ready && Paths.isSteamApp(activeWindow.appId)
|
visible: root.showIcon && activeWindow && activeWindow.appId && horizontalAppIcon.status !== Image.Ready && Paths.isSteamApp(activeWindow.appId)
|
||||||
@@ -280,9 +284,11 @@ BasePill {
|
|||||||
}
|
}
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
Layout.maximumWidth: compactMode ? 80 : 180
|
width: Math.min(implicitWidth, compactMode ? 80 : 180)
|
||||||
visible: text.length > 0
|
visible: text.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,6 +297,7 @@ BasePill {
|
|||||||
text: compactMode ? "" : "•"
|
text: compactMode ? "" : "•"
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.outlineButton
|
color: Theme.outlineButton
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: !compactMode && appText.text && titleText.text
|
visible: !compactMode && appText.text && titleText.text
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,9 +325,24 @@ BasePill {
|
|||||||
}
|
}
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
Layout.maximumWidth: maxWidth - appText.implicitWidth - appSeparator.implicitWidth
|
width: {
|
||||||
|
const sp = contentRow.spacing;
|
||||||
|
let used = 0;
|
||||||
|
if (horizontalAppIcon.visible)
|
||||||
|
used += horizontalAppIcon.width + sp;
|
||||||
|
else if (horizontalSteamIcon.visible)
|
||||||
|
used += horizontalSteamIcon.width + sp;
|
||||||
|
if (appText.visible)
|
||||||
|
used += appText.width + sp;
|
||||||
|
if (appSeparator.visible)
|
||||||
|
used += appSeparator.width + sp;
|
||||||
|
const budget = root.maxWidth - root.horizontalPadding * 2 - used;
|
||||||
|
return Math.min(implicitWidth, Math.max(0, budget));
|
||||||
|
}
|
||||||
visible: text.length > 0
|
visible: text.length > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -411,6 +411,7 @@ DankPopout {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentTabId === "media"
|
active: root.currentTabId === "media"
|
||||||
visible: active
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
MediaPlayerTab {
|
MediaPlayerTab {
|
||||||
targetScreen: root.screen
|
targetScreen: root.screen
|
||||||
@@ -462,7 +463,7 @@ DankPopout {
|
|||||||
DankSpinner {
|
DankSpinner {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
size: 40
|
size: 40
|
||||||
visible: wallpaperLoader.active && wallpaperLoader.status === Loader.Loading
|
visible: (wallpaperLoader.active && wallpaperLoader.status === Loader.Loading) || (mediaLoader.active && mediaLoader.status === Loader.Loading) || (weatherLoader.active && weatherLoader.status === Loader.Loading)
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
@@ -470,6 +471,7 @@ DankPopout {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentTabId === "weather"
|
active: root.currentTabId === "weather"
|
||||||
visible: active
|
visible: active
|
||||||
|
asynchronous: true
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
WeatherTab {}
|
WeatherTab {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ Item {
|
|||||||
|
|
||||||
property int __panelHoverCount: 0
|
property int __panelHoverCount: 0
|
||||||
|
|
||||||
|
readonly property bool overlayBlurActive: dropdownBlur.active
|
||||||
|
|
||||||
onDropdownTypeChanged: {
|
onDropdownTypeChanged: {
|
||||||
if (dropdownType === 0) {
|
if (dropdownType === 0) {
|
||||||
__panelHoverCount = 0;
|
__panelHoverCount = 0;
|
||||||
@@ -75,8 +77,10 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WindowBlur {
|
WindowBlur {
|
||||||
|
id: dropdownBlur
|
||||||
targetWindow: root.targetWindow
|
targetWindow: root.targetWindow
|
||||||
readonly property bool active: root.__activePanel !== null && root.__activePanel.visible && root.__activePanel.opacity > 0
|
readonly property bool active: root.__activePanel !== null && root.__activePanel.visible && root.__activePanel.opacity > 0
|
||||||
|
blurEnabled: active && Theme.connectedSurfaceBlurEnabled
|
||||||
readonly property real s: root.__activePanel ? Math.min(1, root.__activePanel.scale) : 1
|
readonly property real s: root.__activePanel ? Math.min(1, root.__activePanel.scale) : 1
|
||||||
blurX: root.__activePanel ? root.__activePanel.x + root.__activePanel.width * (1 - s) * 0.5 : 0
|
blurX: root.__activePanel ? root.__activePanel.x + root.__activePanel.width * (1 - s) * 0.5 : 0
|
||||||
blurY: root.__activePanel ? root.__activePanel.y + root.__activePanel.height * (1 - s) * 0.5 : 0
|
blurY: root.__activePanel ? root.__activePanel.y + root.__activePanel.height * (1 - s) * 0.5 : 0
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import QtQuick
|
|||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -70,7 +71,7 @@ Item {
|
|||||||
property bool _switchHold: false
|
property bool _switchHold: false
|
||||||
Timer {
|
Timer {
|
||||||
id: _switchHoldTimer
|
id: _switchHoldTimer
|
||||||
interval: 650
|
interval: 1500
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: _switchHold = false
|
onTriggered: _switchHold = false
|
||||||
}
|
}
|
||||||
@@ -78,7 +79,8 @@ Item {
|
|||||||
onActivePlayerChanged: {
|
onActivePlayerChanged: {
|
||||||
if (!activePlayer) {
|
if (!activePlayer) {
|
||||||
isSwitching = false;
|
isSwitching = false;
|
||||||
_switchHold = false;
|
_switchHold = true;
|
||||||
|
_switchHoldTimer.restart();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isSwitching = true;
|
isSwitching = true;
|
||||||
@@ -117,16 +119,12 @@ Item {
|
|||||||
Connections {
|
Connections {
|
||||||
target: MprisController
|
target: MprisController
|
||||||
function onAvailablePlayersChanged() {
|
function onAvailablePlayersChanged() {
|
||||||
const count = (MprisController.availablePlayers?.length || 0);
|
if ((MprisController.availablePlayers?.length || 0) === 0)
|
||||||
if (count === 0) {
|
|
||||||
isSwitching = false;
|
isSwitching = false;
|
||||||
_switchHold = false;
|
|
||||||
} else {
|
|
||||||
_switchHold = true;
|
_switchHold = true;
|
||||||
_switchHoldTimer.restart();
|
_switchHoldTimer.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function getAudioDeviceIcon(device) {
|
function getAudioDeviceIcon(device) {
|
||||||
if (!device || !device.name)
|
if (!device || !device.name)
|
||||||
@@ -290,34 +288,64 @@ Item {
|
|||||||
Item {
|
Item {
|
||||||
id: bgContainer
|
id: bgContainer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: TrackArtService.resolvedArtUrl !== ""
|
|
||||||
|
readonly property string curArt: TrackArtService.resolvedArtUrl
|
||||||
|
// Two layers crossfade: new art loads into the hidden one and fades in once decoded.
|
||||||
|
property bool _showA: true
|
||||||
|
visible: layerA.ready || layerB.ready
|
||||||
|
|
||||||
|
onCurArtChanged: syncArt()
|
||||||
|
Component.onCompleted: syncArt()
|
||||||
|
|
||||||
|
function syncArt() {
|
||||||
|
if (curArt === "" || layerA.art == curArt || layerB.art == curArt)
|
||||||
|
return;
|
||||||
|
if (_showA)
|
||||||
|
layerB.art = curArt;
|
||||||
|
else
|
||||||
|
layerA.art = curArt;
|
||||||
|
}
|
||||||
|
|
||||||
|
component BgBlurLayer: ClippingRectangle {
|
||||||
|
id: layer
|
||||||
|
property alias art: layerImg.source
|
||||||
|
readonly property bool ready: layerImg.status === Image.Ready && layerImg.source != ""
|
||||||
|
property bool front: false
|
||||||
|
signal loaded
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
antialiasing: true
|
||||||
|
opacity: front ? 0.7 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 350
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: bgImage
|
id: layerImg
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: Math.max(parent.width, parent.height) * 1.1
|
width: Math.max(parent.width, parent.height) * 1.1
|
||||||
height: width
|
height: width
|
||||||
source: TrackArtService.resolvedArtUrl
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
cache: true
|
cache: true
|
||||||
visible: false
|
visible: false
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
if (status === Image.Ready)
|
if (status === Image.Ready && source != "")
|
||||||
maybeFinishSwitch();
|
layer.loaded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
|
||||||
id: blurredBg
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
MultiEffect {
|
MultiEffect {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: bgImage.width
|
width: layerImg.width
|
||||||
height: bgImage.height
|
height: layerImg.height
|
||||||
source: bgImage
|
source: layerImg
|
||||||
blurEnabled: true
|
blurEnabled: true
|
||||||
blurMax: 64
|
blurMax: 64
|
||||||
blur: 0.8
|
blur: 0.8
|
||||||
@@ -326,22 +354,26 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
BgBlurLayer {
|
||||||
id: bgMask
|
id: layerA
|
||||||
anchors.fill: parent
|
front: bgContainer._showA
|
||||||
radius: Theme.cornerRadius
|
onLoaded: {
|
||||||
visible: false
|
if (!bgContainer._showA) {
|
||||||
layer.enabled: true
|
bgContainer._showA = true;
|
||||||
|
root.maybeFinishSwitch();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MultiEffect {
|
BgBlurLayer {
|
||||||
anchors.fill: parent
|
id: layerB
|
||||||
source: blurredBg
|
front: !bgContainer._showA
|
||||||
maskEnabled: true
|
onLoaded: {
|
||||||
maskSource: bgMask
|
if (bgContainer._showA) {
|
||||||
maskThresholdMin: 0.5
|
bgContainer._showA = false;
|
||||||
maskSpreadAtMin: 1.0
|
root.maybeFinishSwitch();
|
||||||
opacity: 0.7
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -442,7 +474,8 @@ Item {
|
|||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
visible: text.length > 0
|
// Reserve the line so late album metadata doesn't shift the seekbar.
|
||||||
|
height: Math.max(implicitHeight, Theme.fontSizeSmall * 1.4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modules.DankDash.Overview
|
import qs.Modules.DankDash.Overview
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
@@ -17,7 +18,7 @@ Item {
|
|||||||
signal navFocusRequested
|
signal navFocusRequested
|
||||||
|
|
||||||
function handleKeyEvent(event) {
|
function handleKeyEvent(event) {
|
||||||
return calendarCard.handleKeyEvent(event);
|
return calendarLoader.item ? calendarLoader.item.handleKeyEvent(event) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -57,17 +58,27 @@ Item {
|
|||||||
height: 220
|
height: 220
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calendar - bottom middle (wider and taller)
|
// Calendar - bottom middle; deferred so the grid stays off the emerge frame.
|
||||||
CalendarOverviewCard {
|
Loader {
|
||||||
id: calendarCard
|
id: calendarLoader
|
||||||
x: parent.width * 0.2 - Theme.spacingM
|
x: parent.width * 0.2 - Theme.spacingM
|
||||||
y: 100 + Theme.spacingM
|
y: 100 + Theme.spacingM
|
||||||
width: parent.width * 0.6
|
width: parent.width * 0.6
|
||||||
height: 300
|
height: 300
|
||||||
|
asynchronous: true
|
||||||
|
sourceComponent: Component {
|
||||||
|
CalendarOverviewCard {
|
||||||
onCloseDash: root.closeDash()
|
onCloseDash: root.closeDash()
|
||||||
onNavFocusRequested: root.navFocusRequested()
|
onNavFocusRequested: root.navFocusRequested()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSpinner {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
size: 32
|
||||||
|
visible: calendarLoader.status === Loader.Loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Media - bottom right (narrow and taller)
|
// Media - bottom right (narrow and taller)
|
||||||
MediaOverviewCard {
|
MediaOverviewCard {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
|
||||||
import QtQuick.Shapes
|
import QtQuick.Shapes
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -245,17 +244,6 @@ Item {
|
|||||||
size: Theme.iconSize * 2
|
size: Theme.iconSize * 2
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
layer.enabled: Theme.elevationEnabled
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: Theme.elevationEnabled
|
|
||||||
shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel1)
|
|
||||||
shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel1, 1)
|
|
||||||
shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined ? Theme.elevationLevel1.blurPx : 4) / Theme.elevationBlurMax)) : 0
|
|
||||||
blurMax: Theme.elevationBlurMax
|
|
||||||
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel1)
|
|
||||||
shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -820,16 +808,6 @@ Item {
|
|||||||
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, false)
|
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, false)
|
||||||
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (moonPhase.width / 2) + skyBox.hMargin
|
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (moonPhase.width / 2) + skyBox.hMargin
|
||||||
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (moonPhase.height / 2) + skyBox.vMargin
|
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (moonPhase.height / 2) + skyBox.vMargin
|
||||||
|
|
||||||
layer.enabled: Theme.elevationEnabled
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: Theme.elevationEnabled
|
|
||||||
shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2)
|
|
||||||
shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4)
|
|
||||||
shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0
|
|
||||||
blurMax: Theme.elevationBlurMax
|
|
||||||
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
@@ -842,16 +820,6 @@ Item {
|
|||||||
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, true)
|
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, true)
|
||||||
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (sun.width / 2) + skyBox.hMargin
|
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (sun.width / 2) + skyBox.hMargin
|
||||||
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (sun.height / 2) + skyBox.vMargin
|
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (sun.height / 2) + skyBox.vMargin
|
||||||
|
|
||||||
layer.enabled: Theme.elevationEnabled
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
shadowEnabled: Theme.elevationEnabled
|
|
||||||
shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2)
|
|
||||||
shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4)
|
|
||||||
shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0
|
|
||||||
blurMax: Theme.elevationBlurMax
|
|
||||||
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -993,6 +961,7 @@ Item {
|
|||||||
ListView {
|
ListView {
|
||||||
id: hourlyList
|
id: hourlyList
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
reuseItems: true
|
||||||
orientation: ListView.Horizontal
|
orientation: ListView.Horizontal
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
clip: true
|
clip: true
|
||||||
@@ -1079,6 +1048,7 @@ Item {
|
|||||||
ListView {
|
ListView {
|
||||||
id: dailyList
|
id: dailyList
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
reuseItems: true
|
||||||
orientation: ListView.Horizontal
|
orientation: ListView.Horizontal
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
clip: true
|
clip: true
|
||||||
|
|||||||
@@ -84,31 +84,30 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateFilteredModel() {
|
function updateFilteredModel() {
|
||||||
let filtered = internalEntries;
|
const query = searchText.trim().toLowerCase();
|
||||||
|
const filterAll = activeFilter === "all";
|
||||||
|
const unpinned = [];
|
||||||
|
const pinned = [];
|
||||||
|
|
||||||
if (activeFilter !== "all") {
|
for (let i = 0; i < internalEntries.length; i++) {
|
||||||
filtered = filtered.filter(entry => getEntryType(entry) === activeFilter);
|
const entry = internalEntries[i];
|
||||||
|
if (!filterAll && getEntryType(entry) !== activeFilter)
|
||||||
|
continue;
|
||||||
|
if (query.length > 0 && !entry.preview.toLowerCase().includes(query))
|
||||||
|
continue;
|
||||||
|
(entry.pinned ? pinned : unpinned).push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = searchText.trim();
|
const byIdDesc = (a, b) => b.id - a.id;
|
||||||
|
pinned.sort(byIdDesc);
|
||||||
|
unpinned.sort(byIdDesc);
|
||||||
|
|
||||||
if (query.length > 0) {
|
pinnedEntries = pinned;
|
||||||
const lowerQuery = query.toLowerCase();
|
unpinnedEntries = unpinned;
|
||||||
filtered = filtered.filter(entry => entry.preview.toLowerCase().includes(lowerQuery));
|
clipboardEntries = pinned.concat(unpinned);
|
||||||
}
|
|
||||||
|
|
||||||
filtered.sort((a, b) => {
|
|
||||||
if (a.pinned !== b.pinned)
|
|
||||||
return b.pinned ? 1 : -1;
|
|
||||||
return b.id - a.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
clipboardEntries = filtered;
|
|
||||||
unpinnedEntries = filtered.filter(e => !e.pinned);
|
|
||||||
pinnedEntries = filtered.filter(e => e.pinned);
|
|
||||||
totalCount = clipboardEntries.length;
|
totalCount = clipboardEntries.length;
|
||||||
|
|
||||||
const activeCount = Math.max(unpinnedEntries.length, pinnedEntries.length);
|
const activeCount = Math.max(unpinned.length, pinned.length);
|
||||||
|
|
||||||
if (activeCount === 0) {
|
if (activeCount === 0) {
|
||||||
keyboardNavigationActive = false;
|
keyboardNavigationActive = false;
|
||||||
@@ -116,10 +115,9 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedIndex >= activeCount) {
|
if (selectedIndex >= activeCount)
|
||||||
selectedIndex = activeCount - 1;
|
selectedIndex = activeCount - 1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
if (!clipboardAvailable) {
|
if (!clipboardAvailable) {
|
||||||
|
|||||||
@@ -56,18 +56,19 @@ Singleton {
|
|||||||
|
|
||||||
function loadArtwork(url) {
|
function loadArtwork(url) {
|
||||||
if (!url || url === "") {
|
if (!url || url === "") {
|
||||||
resolvedArtUrl = "";
|
// Keep stale art; only blank once the empty url debounce settles.
|
||||||
_lastArtUrl = "";
|
_lastArtUrl = "";
|
||||||
loading = false;
|
loading = false;
|
||||||
|
_clearDebounce.restart();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_clearDebounce.stop();
|
||||||
if (url === _lastArtUrl)
|
if (url === _lastArtUrl)
|
||||||
return;
|
return;
|
||||||
_lastArtUrl = url;
|
_lastArtUrl = url;
|
||||||
|
|
||||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||||
loading = true;
|
loading = true;
|
||||||
resolvedArtUrl = ""; // Clear stale artwork immediately while loading
|
|
||||||
const targetUrl = url;
|
const targetUrl = url;
|
||||||
const hash = djb2Hash(url);
|
const hash = djb2Hash(url);
|
||||||
const cacheDir = Paths.strip(Paths.imagecache);
|
const cacheDir = Paths.strip(Paths.imagecache);
|
||||||
@@ -134,19 +135,30 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
resolvedArtUrl = ""; // Clear stale artwork immediately while verifying local file
|
|
||||||
const localUrl = url;
|
const localUrl = url;
|
||||||
const filePath = url.startsWith("file://") ? url.substring(7) : url;
|
const filePath = url.startsWith("file://") ? url.substring(7) : url;
|
||||||
Proc.runCommand(null, ["test", "-f", filePath], (output, exitCode) => {
|
// Cover file often lands after the metadata update, so poll briefly.
|
||||||
|
Proc.runCommand(null, ["sh", "-c", "for i in $(seq 20); do [ -f \"$1\" ] && exit 0; sleep 0.15; done; exit 1", "sh", filePath], (output, exitCode) => {
|
||||||
if (_lastArtUrl !== localUrl)
|
if (_lastArtUrl !== localUrl)
|
||||||
return;
|
return;
|
||||||
resolvedArtUrl = exitCode === 0 ? localUrl : "";
|
resolvedArtUrl = exitCode === 0 ? localUrl : "";
|
||||||
loading = false;
|
loading = false;
|
||||||
}, 200);
|
}, 50, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: _clearDebounce
|
||||||
|
interval: 800
|
||||||
|
onTriggered: {
|
||||||
|
if (root._lastArtUrl === "")
|
||||||
|
root.resolvedArtUrl = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property MprisPlayer activePlayer: MprisController.activePlayer
|
property MprisPlayer activePlayer: MprisController.activePlayer
|
||||||
|
|
||||||
|
property string _resolvedTrackKey: ""
|
||||||
|
|
||||||
onActivePlayerChanged: _updateArtUrl()
|
onActivePlayerChanged: _updateArtUrl()
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -157,8 +169,21 @@ Singleton {
|
|||||||
function onMetadataChanged() { root._updateArtUrl(); }
|
function onMetadataChanged() { root._updateArtUrl(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _trackKey() {
|
||||||
|
const p = activePlayer;
|
||||||
|
if (!p)
|
||||||
|
return "";
|
||||||
|
return (p.trackTitle || "") + "" + (p.trackArtist || "") + "" + (p.trackAlbum || "");
|
||||||
|
}
|
||||||
|
|
||||||
function _updateArtUrl() {
|
function _updateArtUrl() {
|
||||||
const url = getArtworkUrl(activePlayer);
|
const url = getArtworkUrl(activePlayer);
|
||||||
|
const key = _trackKey();
|
||||||
|
// Ignore metadata jitter once resolved, but only when the url still matches
|
||||||
|
// (trackArtUrl can update before the title, and skipping then wedges old art).
|
||||||
|
if (key !== "" && key === _resolvedTrackKey && url === _lastArtUrl && resolvedArtUrl !== "")
|
||||||
|
return;
|
||||||
|
_resolvedTrackKey = key;
|
||||||
loadArtwork(url);
|
loadArtwork(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 qt_TexCoord0;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
layout(std140, binding = 0) uniform buf {
|
||||||
|
mat4 qt_Matrix;
|
||||||
|
float qt_Opacity;
|
||||||
|
float phase;
|
||||||
|
float spin;
|
||||||
|
float sizePx;
|
||||||
|
float baseRadiusPx;
|
||||||
|
float amplitudePx;
|
||||||
|
float activation;
|
||||||
|
float energy;
|
||||||
|
vec2 bandsB;
|
||||||
|
vec4 bandsA;
|
||||||
|
vec4 fillColor;
|
||||||
|
} ubuf;
|
||||||
|
|
||||||
|
const float TAU = 6.28318530718;
|
||||||
|
const float NOISE_FREQ = 1.25;
|
||||||
|
const float ORBIT_RADIUS = 1.0;
|
||||||
|
const float WARP_STRENGTH = 0.75;
|
||||||
|
const float RING_BIAS = 0.25;
|
||||||
|
const float MID_FREQ = 2.4;
|
||||||
|
const float MID_GAIN = 0.34;
|
||||||
|
const float HIGH_FREQ = 5.0;
|
||||||
|
const float HIGH_GAIN = 0.16;
|
||||||
|
const float LOBE_MAX = 1.3;
|
||||||
|
|
||||||
|
float hash(vec2 p) {
|
||||||
|
p = fract(p * vec2(123.34, 456.21));
|
||||||
|
p += dot(p, p + 45.32);
|
||||||
|
return fract(p.x * p.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float vnoise(vec2 p) {
|
||||||
|
vec2 i = floor(p);
|
||||||
|
vec2 f = fract(p);
|
||||||
|
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||||
|
float a = hash(i);
|
||||||
|
float b = hash(i + vec2(1.0, 0.0));
|
||||||
|
float c = hash(i + vec2(0.0, 1.0));
|
||||||
|
float d = hash(i + vec2(1.0, 1.0));
|
||||||
|
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float fbm(vec2 p) {
|
||||||
|
return 0.55 * vnoise(p) + 0.3 * vnoise(p * 2.0 + vec2(17.1, 9.2)) + 0.15 * vnoise(p * 3.9 + vec2(4.3, 21.7));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 p = (qt_TexCoord0 - 0.5) * ubuf.sizePx;
|
||||||
|
float r = length(p);
|
||||||
|
float aa = max(fwidth(r), 0.75);
|
||||||
|
|
||||||
|
float T = ubuf.phase * TAU;
|
||||||
|
vec2 dir = r > 0.001 ? p / r : vec2(1.0, 0.0);
|
||||||
|
float cs = cos(ubuf.spin);
|
||||||
|
float sn = sin(ubuf.spin);
|
||||||
|
dir = vec2(dir.x * cs - dir.y * sn, dir.x * sn + dir.y * cs);
|
||||||
|
vec2 q = dir * NOISE_FREQ;
|
||||||
|
|
||||||
|
vec2 orbitA = vec2(cos(T), sin(T)) * ORBIT_RADIUS;
|
||||||
|
vec2 orbitB = vec2(cos(2.0 * T + 2.1), sin(2.0 * T + 2.1)) * (ORBIT_RADIUS * 0.6);
|
||||||
|
float w1 = fbm(q + orbitA);
|
||||||
|
float w2 = fbm(q * 1.3 + orbitB + vec2(3.7, 7.3));
|
||||||
|
float n = fbm(q + orbitA + WARP_STRENGTH * (vec2(w1, w2) - 0.5));
|
||||||
|
|
||||||
|
float mid = max(ubuf.bandsA.z, ubuf.bandsA.w);
|
||||||
|
float high = max(ubuf.bandsB.x, ubuf.bandsB.y);
|
||||||
|
|
||||||
|
vec2 orbitM = vec2(cos(3.0 * T + 0.7), sin(3.0 * T + 0.7)) * 0.9;
|
||||||
|
vec2 orbitH = vec2(cos(5.0 * T + 4.2), sin(5.0 * T + 4.2)) * 1.2;
|
||||||
|
float midTerm = (vnoise(dir * MID_FREQ + orbitM) - 0.5) * MID_GAIN * mid;
|
||||||
|
float highTerm = (vnoise(dir * HIGH_FREQ + orbitH) - 0.5) * HIGH_GAIN * high;
|
||||||
|
|
||||||
|
float shaped = smoothstep(0.34, 0.7, n);
|
||||||
|
float body = ubuf.energy * (RING_BIAS + (1.0 - RING_BIAS) * shaped);
|
||||||
|
float lobe = clamp(body + midTerm + highTerm, 0.0, LOBE_MAX);
|
||||||
|
float blobR = ubuf.baseRadiusPx + ubuf.activation * ubuf.amplitudePx * lobe;
|
||||||
|
|
||||||
|
float mask = 1.0 - smoothstep(-aa, aa, r - blobR);
|
||||||
|
float a = ubuf.fillColor.a * mask * ubuf.qt_Opacity;
|
||||||
|
fragColor = vec4(ubuf.fillColor.rgb * a, a);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 qt_TexCoord0;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
layout(std140, binding = 0) uniform buf {
|
||||||
|
mat4 qt_Matrix;
|
||||||
|
float qt_Opacity;
|
||||||
|
float widthPx;
|
||||||
|
float heightPx;
|
||||||
|
float minH;
|
||||||
|
float maxH;
|
||||||
|
vec2 bandsB;
|
||||||
|
vec4 bandsA;
|
||||||
|
vec4 fillColor;
|
||||||
|
} ubuf;
|
||||||
|
|
||||||
|
const float BAR_W = 2.0;
|
||||||
|
const float GAP = 1.5;
|
||||||
|
const float RADIUS = 1.0;
|
||||||
|
|
||||||
|
float sdRoundBar(vec2 p, vec2 halfSize, float r) {
|
||||||
|
vec2 q = abs(p) - halfSize + vec2(r);
|
||||||
|
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 px = vec2(qt_TexCoord0.x * ubuf.widthPx, qt_TexCoord0.y * ubuf.heightPx);
|
||||||
|
float total = 6.0 * BAR_W + 5.0 * GAP;
|
||||||
|
float x0 = (ubuf.widthPx - total) * 0.5;
|
||||||
|
float slot = BAR_W + GAP;
|
||||||
|
float fi = floor((px.x - x0) / slot);
|
||||||
|
|
||||||
|
if (fi < 0.0 || fi > 5.0) {
|
||||||
|
fragColor = vec4(0.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int i = int(fi);
|
||||||
|
float lvl = i == 0 ? ubuf.bandsA.x : i == 1 ? ubuf.bandsA.y : i == 2 ? ubuf.bandsA.z : i == 3 ? ubuf.bandsA.w : i == 4 ? ubuf.bandsB.x : ubuf.bandsB.y;
|
||||||
|
float h = ubuf.minH + clamp(lvl, 0.0, 1.0) * (ubuf.maxH - ubuf.minH);
|
||||||
|
|
||||||
|
float lx = px.x - x0 - fi * slot;
|
||||||
|
vec2 p = vec2(lx - BAR_W * 0.5, px.y - ubuf.heightPx * 0.5);
|
||||||
|
float d = sdRoundBar(p, vec2(BAR_W * 0.5, h * 0.5), RADIUS);
|
||||||
|
float mask = 1.0 - smoothstep(-0.6, 0.6, d);
|
||||||
|
|
||||||
|
float a = ubuf.fillColor.a * mask * ubuf.qt_Opacity;
|
||||||
|
fragColor = vec4(ubuf.fillColor.rgb * a, a);
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
+122
-107
@@ -1,5 +1,4 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Shapes
|
|
||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -15,6 +14,36 @@ Item {
|
|||||||
property bool showAnimation: true
|
property bool showAnimation: true
|
||||||
property real animationScale: 1.0
|
property real animationScale: 1.0
|
||||||
|
|
||||||
|
readonly property real blobBaseRadiusFactor: 0.43
|
||||||
|
readonly property real blobAmplitudeFactor: 0.115
|
||||||
|
readonly property real blobOvershoot: 1.15
|
||||||
|
readonly property real blobEnergySensitivity: 1.15
|
||||||
|
readonly property real cavaFullScale: 45
|
||||||
|
readonly property real blobAttack: 0.75
|
||||||
|
readonly property real blobRelease: 0.2
|
||||||
|
readonly property real blobBeatBoost: 2.5
|
||||||
|
readonly property real blobBeatKick: 4
|
||||||
|
readonly property real blobOnsetThreshold: 1.4
|
||||||
|
readonly property real blobSpringStiffness: 220
|
||||||
|
readonly property real blobSpringDamping: 19
|
||||||
|
readonly property real blobMorphSpeed: 0.05
|
||||||
|
readonly property real blobMorphBoost: 1.7
|
||||||
|
readonly property real blobSpinSpeed: 0.03
|
||||||
|
|
||||||
|
readonly property bool blobActive: CavaService.cavaAvailable && activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation && albumArtStatus === Image.Ready
|
||||||
|
property var smoothedBands: [0, 0, 0, 0, 0, 0]
|
||||||
|
property var slowBands: [0, 0, 0, 0, 0, 0]
|
||||||
|
property var bandTargets: [0, 0, 0, 0, 0, 0]
|
||||||
|
property var bandDisplay: [0, 0, 0, 0, 0, 0]
|
||||||
|
property var prevLevels: [0, 0, 0, 0, 0, 0]
|
||||||
|
readonly property var fluxWeights: [1.0, 1.0, 0.6, 0.6, 0.35, 0.35]
|
||||||
|
property real fluxAvg: 0.02
|
||||||
|
property real loudCtx: 0.1
|
||||||
|
property int beatCooldown: 0
|
||||||
|
property real energyTarget: 0
|
||||||
|
property real energyPos: 0
|
||||||
|
property real energyVel: 0
|
||||||
|
|
||||||
onActivePlayerChanged: {
|
onActivePlayerChanged: {
|
||||||
lastValidArtUrl = "";
|
lastValidArtUrl = "";
|
||||||
}
|
}
|
||||||
@@ -25,6 +54,62 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateBands() {
|
||||||
|
const vals = CavaService.values;
|
||||||
|
if (!vals || vals.length < 6)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const s = smoothedBands;
|
||||||
|
const slow = slowBands;
|
||||||
|
const out = bandTargets;
|
||||||
|
const prev = prevLevels;
|
||||||
|
const w = fluxWeights;
|
||||||
|
let flux = 0;
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const level = Math.min(Math.max(vals[i], 0), cavaFullScale) / cavaFullScale;
|
||||||
|
flux += Math.max(0, level - prev[i]) * w[i];
|
||||||
|
prev[i] = level;
|
||||||
|
const alpha = level > s[i] ? blobAttack : blobRelease;
|
||||||
|
s[i] += alpha * (level - s[i]);
|
||||||
|
slow[i] += 0.05 * (level - slow[i]);
|
||||||
|
const punch = Math.max(0, s[i] - slow[i]) * blobBeatBoost;
|
||||||
|
out[i] = Math.min(1, (0.55 * s[i] + punch) * blobEnergySensitivity);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio = flux / Math.max(fluxAvg, 0.004);
|
||||||
|
fluxAvg += 0.06 * (flux - fluxAvg);
|
||||||
|
if (beatCooldown > 0) {
|
||||||
|
beatCooldown--;
|
||||||
|
} else if (ratio > blobOnsetThreshold && flux > 0.008) {
|
||||||
|
energyVel += blobBeatKick * Math.min(2.5, ratio - 1);
|
||||||
|
beatCooldown = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loud = 0.7 * Math.max(prev[0], prev[1]) + 0.3 * Math.max(prev[2], prev[3]);
|
||||||
|
loudCtx += 0.03 * (loud - loudCtx);
|
||||||
|
const surge = Math.max(0, loud / Math.max(loudCtx, 0.05) - 1);
|
||||||
|
energyTarget = Math.min(1, 0.5 * loud + 0.6 * Math.min(1, surge));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stepBlob(dt) {
|
||||||
|
energyVel += (blobSpringStiffness * (energyTarget - energyPos) - blobSpringDamping * energyVel) * dt;
|
||||||
|
energyPos = Math.max(0, Math.min(blobOvershoot, energyPos + energyVel * dt));
|
||||||
|
blobEffect.energy = energyPos;
|
||||||
|
|
||||||
|
const d = bandDisplay;
|
||||||
|
const t = bandTargets;
|
||||||
|
const f = Math.min(1, dt * 14);
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
d[i] += f * (t[i] - d[i]);
|
||||||
|
}
|
||||||
|
blobEffect.bandsA = Qt.vector4d(d[0], d[1], d[2], d[3]);
|
||||||
|
blobEffect.bandsB = Qt.vector2d(d[4], d[5]);
|
||||||
|
|
||||||
|
const speed = 1 + energyPos * blobMorphBoost;
|
||||||
|
blobEffect.phase = (blobEffect.phase + dt * blobMorphSpeed * speed) % 1;
|
||||||
|
blobEffect.spin = (blobEffect.spin + dt * blobSpinSpeed) % 6.28318530718;
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
active: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
|
active: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
@@ -34,119 +119,49 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Shape {
|
|
||||||
id: morphingBlob
|
|
||||||
width: parent.width * 1.1
|
|
||||||
height: parent.height * 1.1
|
|
||||||
anchors.centerIn: parent
|
|
||||||
visible: CavaService.cavaAvailable && activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
|
|
||||||
asynchronous: false
|
|
||||||
antialiasing: true
|
|
||||||
preferredRendererType: Shape.CurveRenderer
|
|
||||||
z: 0
|
|
||||||
layer.enabled: false
|
|
||||||
|
|
||||||
readonly property real centerX: width / 2
|
|
||||||
readonly property real centerY: height / 2
|
|
||||||
readonly property real baseRadius: Math.min(width, height) * 0.41 * root.animationScale
|
|
||||||
readonly property int segments: 28
|
|
||||||
|
|
||||||
property var audioLevels: {
|
|
||||||
if (!CavaService.cavaAvailable || CavaService.values.length === 0) {
|
|
||||||
return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6];
|
|
||||||
}
|
|
||||||
return CavaService.values;
|
|
||||||
}
|
|
||||||
|
|
||||||
property var smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6]
|
|
||||||
property var cubics: []
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: CavaService
|
target: CavaService
|
||||||
|
enabled: blobEffect.visible
|
||||||
function onValuesChanged() {
|
function onValuesChanged() {
|
||||||
if (morphingBlob.visible) {
|
root.updateBands();
|
||||||
morphingBlob.updatePath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: cubicSegment
|
|
||||||
PathCubic {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: pathMoveComp
|
|
||||||
PathMove {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
shapePath.pathElements.push(pathMoveComp.createObject(shapePath));
|
|
||||||
|
|
||||||
for (let i = 0; i < segments; i++) {
|
|
||||||
const seg = cubicSegment.createObject(shapePath);
|
|
||||||
shapePath.pathElements.push(seg);
|
|
||||||
cubics.push(seg);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePath() {
|
|
||||||
if (cubics.length === 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const alpha = 0.35;
|
|
||||||
const minLen = Math.min(smoothedLevels.length, audioLevels.length);
|
|
||||||
for (let i = 0; i < minLen; i++) {
|
|
||||||
smoothedLevels[i] += alpha * (audioLevels[i] - smoothedLevels[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const angleStep = 2 * Math.PI / segments;
|
|
||||||
const tension3 = 0.16666667;
|
|
||||||
const startMove = shapePath.pathElements[0];
|
|
||||||
|
|
||||||
const points = new Array(segments);
|
|
||||||
for (let i = 0; i < segments; i++) {
|
|
||||||
const angle = i * angleStep;
|
|
||||||
const audioIndex = i % 10;
|
|
||||||
const rawLevel = smoothedLevels[audioIndex] || 0;
|
|
||||||
const clampedLevel = rawLevel < 0 ? 0 : (rawLevel > 100 ? 100 : rawLevel);
|
|
||||||
const audioLevel = Math.max(0.15, Math.sqrt(clampedLevel * 0.01)) * 0.5;
|
|
||||||
const radius = baseRadius * (1.0 + audioLevel);
|
|
||||||
points[i] = {
|
|
||||||
x: centerX + Math.cos(angle) * radius,
|
|
||||||
y: centerY + Math.sin(angle) * radius
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
startMove.x = points[0].x;
|
|
||||||
startMove.y = points[0].y;
|
|
||||||
|
|
||||||
for (let i = 0; i < segments; i++) {
|
|
||||||
const p0 = points[(i + segments - 1) % segments];
|
|
||||||
const p1 = points[i];
|
|
||||||
const p2 = points[(i + 1) % segments];
|
|
||||||
const p3 = points[(i + 2) % segments];
|
|
||||||
|
|
||||||
const seg = cubics[i];
|
|
||||||
seg.control1X = p1.x + (p2.x - p0.x) * tension3;
|
|
||||||
seg.control1Y = p1.y + (p2.y - p0.y) * tension3;
|
|
||||||
seg.control2X = p2.x - (p3.x - p1.x) * tension3;
|
|
||||||
seg.control2Y = p2.y - (p3.y - p1.y) * tension3;
|
|
||||||
seg.x = p2.x;
|
|
||||||
seg.y = p2.y;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ShapePath {
|
FrameAnimation {
|
||||||
id: shapePath
|
running: blobEffect.visible
|
||||||
fillColor: Theme.primary
|
onTriggered: root.stepBlob(Math.min(frameTime, 0.05))
|
||||||
strokeColor: "transparent"
|
|
||||||
strokeWidth: 0
|
|
||||||
joinStyle: ShapePath.RoundJoin
|
|
||||||
fillRule: ShapePath.WindingFill
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ShaderEffect {
|
||||||
|
id: blobEffect
|
||||||
|
|
||||||
|
readonly property real span: Math.min(root.width, root.height)
|
||||||
|
|
||||||
|
width: span * (root.blobBaseRadiusFactor + root.blobAmplitudeFactor * root.blobOvershoot) * 2 * root.animationScale + 4
|
||||||
|
height: width
|
||||||
|
anchors.centerIn: parent
|
||||||
|
z: 0
|
||||||
|
visible: root.blobActive || activation > 0.004
|
||||||
|
|
||||||
|
property real phase: 0
|
||||||
|
property real spin: 0
|
||||||
|
property real sizePx: width
|
||||||
|
property real baseRadiusPx: span * root.blobBaseRadiusFactor * root.animationScale
|
||||||
|
property real amplitudePx: span * root.blobAmplitudeFactor * root.animationScale
|
||||||
|
property real activation: root.blobActive ? 1 : 0
|
||||||
|
property real energy: 0
|
||||||
|
property vector4d bandsA: Qt.vector4d(0, 0, 0, 0)
|
||||||
|
property vector2d bandsB: Qt.vector2d(0, 0)
|
||||||
|
property vector4d fillColor: Qt.vector4d(Theme.primary.r, Theme.primary.g, Theme.primary.b, Theme.primary.a)
|
||||||
|
|
||||||
|
Behavior on activation {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 550
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/blob.frag.qsb")
|
||||||
}
|
}
|
||||||
|
|
||||||
DankCircularImage {
|
DankCircularImage {
|
||||||
|
|||||||
@@ -13,9 +13,17 @@ Rectangle {
|
|||||||
property bool cacheImages: true
|
property bool cacheImages: true
|
||||||
property bool hasImage: imageSource !== ""
|
property bool hasImage: imageSource !== ""
|
||||||
readonly property bool shouldProbe: imageSource !== "" && !imageSource.startsWith("image://")
|
readonly property bool shouldProbe: imageSource !== "" && !imageSource.startsWith("image://")
|
||||||
// Probe with AnimatedImage first; once loaded, check frameCount to decide.
|
|
||||||
readonly property bool isAnimated: shouldProbe && probe.status === Image.Ready && probe.frameCount > 1
|
readonly property bool isAnimated: shouldProbe && probe.status === Image.Ready && probe.frameCount > 1
|
||||||
readonly property var activeImage: isAnimated ? probe : staticImage
|
readonly property bool probeSettled: probe.status === Image.Ready || probe.status === Image.Error
|
||||||
|
readonly property var activeImage: {
|
||||||
|
if (isAnimated)
|
||||||
|
return probe;
|
||||||
|
if (staticImage.status === Image.Ready)
|
||||||
|
return staticImage;
|
||||||
|
if (probe.status === Image.Ready && probe.source !== "")
|
||||||
|
return probe;
|
||||||
|
return staticImage;
|
||||||
|
}
|
||||||
property int imageStatus: activeImage.status
|
property int imageStatus: activeImage.status
|
||||||
|
|
||||||
signal imageSaved(string filePath)
|
signal imageSaved(string filePath)
|
||||||
@@ -57,7 +65,7 @@ Rectangle {
|
|||||||
border.color: "transparent"
|
border.color: "transparent"
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
// Probe: loads as AnimatedImage to detect frame count.
|
// Probes as AnimatedImage to read frameCount; retires once staticImage is ready.
|
||||||
AnimatedImage {
|
AnimatedImage {
|
||||||
id: probe
|
id: probe
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -68,10 +76,10 @@ Rectangle {
|
|||||||
mipmap: true
|
mipmap: true
|
||||||
cache: root.cacheImages
|
cache: root.cacheImages
|
||||||
visible: false
|
visible: false
|
||||||
source: root.shouldProbe ? root.imageSource : ""
|
source: root.shouldProbe && (root.isAnimated || staticImage.status !== Image.Ready) ? root.imageSource : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static fallback: used once probe confirms the image is not animated.
|
// Takes over once the probe settles on a non-animated image, then latches.
|
||||||
Image {
|
Image {
|
||||||
id: staticImage
|
id: staticImage
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -84,38 +92,12 @@ Rectangle {
|
|||||||
visible: false
|
visible: false
|
||||||
sourceSize.width: Math.max(width * 2, 128)
|
sourceSize.width: Math.max(width * 2, 128)
|
||||||
sourceSize.height: Math.max(height * 2, 128)
|
sourceSize.height: Math.max(height * 2, 128)
|
||||||
source: !root.shouldProbe ? root.imageSource : ""
|
source: {
|
||||||
}
|
|
||||||
|
|
||||||
// Once the probe loads, if not animated, hand off to Image and unload probe.
|
|
||||||
Connections {
|
|
||||||
target: probe
|
|
||||||
function onStatusChanged() {
|
|
||||||
if (!root.shouldProbe)
|
if (!root.shouldProbe)
|
||||||
return;
|
return root.imageSource;
|
||||||
switch (probe.status) {
|
if ((root.probeSettled && !root.isAnimated) || staticImage.status !== Image.Null)
|
||||||
case Image.Ready:
|
return root.imageSource;
|
||||||
if (probe.frameCount <= 1) {
|
return "";
|
||||||
staticImage.source = root.imageSource;
|
|
||||||
probe.source = "";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Image.Error:
|
|
||||||
staticImage.source = root.imageSource;
|
|
||||||
probe.source = "";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If imageSource changes, reset: re-probe with AnimatedImage.
|
|
||||||
onImageSourceChanged: {
|
|
||||||
if (root.shouldProbe) {
|
|
||||||
staticImage.source = "";
|
|
||||||
probe.source = root.imageSource;
|
|
||||||
} else {
|
|
||||||
probe.source = "";
|
|
||||||
staticImage.source = root.imageSource;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1249,14 +1249,14 @@ Item {
|
|||||||
// Fast fade duration for superseded close.
|
// Fast fade duration for superseded close.
|
||||||
readonly property bool _supersededFade: root._supersededClose && !root.shouldBeVisible
|
readonly property bool _supersededFade: root._supersededClose && !root.shouldBeVisible
|
||||||
readonly property real _targetOpacity: root._supersededClose ? (root.shouldBeVisible ? 1 : 0) : (Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0))
|
readonly property real _targetOpacity: root._supersededClose ? (root.shouldBeVisible ? 1 : 0) : (Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0))
|
||||||
property real publishedOpacity: _targetOpacity
|
readonly property real publishedOpacity: opacity
|
||||||
|
|
||||||
opacity: _targetOpacity
|
opacity: _targetOpacity
|
||||||
visible: _renderActive
|
visible: _renderActive
|
||||||
|
|
||||||
scale: contentContainer.scaleValue
|
scale: contentContainer.scaleValue
|
||||||
x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - scale) * 0.5, root.dpr)
|
x: Theme.snap(contentContainer.animX, root.dpr)
|
||||||
y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - scale) * 0.5, root.dpr)
|
y: Theme.snap(contentContainer.animY, root.dpr)
|
||||||
|
|
||||||
layer.enabled: _animating || (_fadeWithOpacity && publishedOpacity < 1)
|
layer.enabled: _animating || (_fadeWithOpacity && publishedOpacity < 1)
|
||||||
layer.smooth: false
|
layer.smooth: false
|
||||||
@@ -1276,15 +1276,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on publishedOpacity {
|
|
||||||
enabled: contentWrapper._fadeWithOpacity
|
|
||||||
NumberAnimation {
|
|
||||||
duration: contentWrapper._supersededFade ? Theme.shorterDuration : Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale)
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root
|
target: root
|
||||||
function onShouldBeVisibleChanged() {
|
function onShouldBeVisibleChanged() {
|
||||||
|
|||||||
@@ -220,6 +220,31 @@ Item {
|
|||||||
onTriggered: root._bgCommitWindow = false
|
onTriggered: root._bgCommitWindow = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An idle layer surface won't commit the cleared blur region on auto-close, so the
|
||||||
|
// blur sticks; pulse updatesEnabled false->true to force a re-commit.
|
||||||
|
property bool _blurCommitSuppress: false
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: blurCommitPulseTimer
|
||||||
|
interval: 16
|
||||||
|
onTriggered: root._blurCommitSuppress = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function _pulseBlurCommit() {
|
||||||
|
if (!backgroundWindow.visible)
|
||||||
|
return;
|
||||||
|
_blurCommitSuppress = true;
|
||||||
|
blurCommitPulseTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: overlayLoader.item
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
function onOverlayBlurActiveChanged() {
|
||||||
|
root._pulseBlurCommit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function _setSurfaceGeometry(bodyX, bodyY, bodyW, bodyH) {
|
function _setSurfaceGeometry(bodyX, bodyY, bodyW, bodyH) {
|
||||||
const newX = Theme.snap(bodyX, dpr);
|
const newX = Theme.snap(bodyX, dpr);
|
||||||
const newY = Theme.snap(bodyY, dpr);
|
const newY = Theme.snap(bodyY, dpr);
|
||||||
@@ -540,7 +565,7 @@ Item {
|
|||||||
// Skip buffer updates when there's nothing to render. Briefly flipped
|
// Skip buffer updates when there's nothing to render. Briefly flipped
|
||||||
// true via _bgCommitWindow when _surfaceBodyW/H changes so the
|
// true via _bgCommitWindow when _surfaceBodyW/H changes so the
|
||||||
// contentHoleRect mask carve-out actually commits to the compositor.
|
// contentHoleRect mask carve-out actually commits to the compositor.
|
||||||
updatesEnabled: root.overlayContent !== null || root._bgCommitWindow
|
updatesEnabled: !root._blurCommitSuppress && (root.overlayContent !== null || root._bgCommitWindow)
|
||||||
|
|
||||||
WlrLayershell.namespace: root.layerNamespace + ":background"
|
WlrLayershell.namespace: root.layerNamespace + ":background"
|
||||||
WlrLayershell.layer: root.effectivePopoutLayer
|
WlrLayershell.layer: root.effectivePopoutLayer
|
||||||
|
|||||||
Reference in New Issue
Block a user