1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-02 02:22:06 -04:00

Compare commits

..

19 Commits

Author SHA1 Message Date
purian23
2db5c1e2dd feat(Frame): Close the gaps 2026-04-14 23:27:09 -04:00
purian23
0e22a01a39 frame(Notifications): Update Arc path & Motion 2026-04-14 23:27:09 -04:00
purian23
4e17adb486 (frame): Update animation sync w/Dank Popouts 2026-04-14 23:27:09 -04:00
purian23
6e180ec928 (frame): Performance round 2026-04-14 23:27:09 -04:00
purian23
236191f1bd (frame): Update Connected blur Arcs & Enable shadow modes 2026-04-14 23:27:09 -04:00
purian23
3a1b5bdfd7 frame(ConnectedMode): Wire up Notifications 2026-04-14 23:27:09 -04:00
purian23
a345df49c7 (frame): Update connected mode animation & motion logic 2026-04-14 23:27:09 -04:00
purian23
66aa7c3b75 (frame): implement ConnectedModeState to better handle component sync 2026-04-14 23:27:09 -04:00
purian23
0f19679b21 (frameMode): Restore user settings when exiting frame mode
- Align blur settings in non-FrameMode motion settings
2026-04-14 23:27:09 -04:00
purian23
101c5c49fc (frame): Update connected mode with blur 2026-04-14 23:27:09 -04:00
purian23
7f0aa3beec (frame): Update connected mode & opacity connection settings 2026-04-14 23:27:09 -04:00
purian23
ff5baaf16b (frameInMotion): Initial Unified Frame Connected Mode 2026-04-14 23:27:09 -04:00
purian23
c969a2000d Add Directional Motion options 2026-04-14 23:27:09 -04:00
purian23
81f01c9402 Initial staging for Animation & Motion effects 2026-04-14 23:27:09 -04:00
purian23
f3e26919e6 (frame): Add blur support & cleanup 2026-04-14 23:27:08 -04:00
purian23
e190a62f0f (frame): Multi-monitor support 2026-04-14 23:27:08 -04:00
purian23
69a32ed41c Connected frames & defaults 2026-04-14 23:27:08 -04:00
purian23
eee7108d98 Continue frame implementation 2026-04-14 23:27:08 -04:00
purian23
20224c4cef Initial framework 2026-04-14 23:27:08 -04:00
23 changed files with 507 additions and 801 deletions

View File

@@ -13,7 +13,7 @@ Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
[![GitHub stars](https://img.shields.io/github/stars/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=ffd700)](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
[![GitHub License](https://img.shields.io/github/license/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=b9c8da)](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/v/release/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://github.com/AvengeMedia/DankMaterialShell/releases)
[![Arch version](https://img.shields.io/archlinux/v/extra/x86_64/dms-shell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://archlinux.org/packages/extra/x86_64/dms-shell/)
[![AUR version](https://img.shields.io/aur/version/dms-shell-bin?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://aur.archlinux.org/packages/dms-shell-bin)
[![AUR version (git)](<https://img.shields.io/aur/version/dms-shell-git?style=for-the-badge&labelColor=101418&color=9ccbfb&label=AUR%20(git)>)](https://aur.archlinux.org/packages/dms-shell-git)
[![Ko-Fi donate](https://img.shields.io/badge/donate-kofi?style=for-the-badge&logo=ko-fi&logoColor=ffffff&label=ko-fi&labelColor=101418&color=f16061&link=https%3A%2F%2Fko-fi.com%2Fdanklinux)](https://ko-fi.com/danklinux)

View File

@@ -8,7 +8,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/godbus/dbus/v5"
"golang.org/x/sys/unix"
)
const resumeDelay = 3 * time.Second
@@ -30,14 +29,11 @@ func NewManager() (*Manager, error) {
stopChan: make(chan struct{}),
}
// Only run a startup scan when the system has been suspended at least once.
// On a fresh boot CLOCK_BOOTTIME ≈ CLOCK_MONOTONIC (difference ~0).
// After any suspend/resume cycle the difference grows by the time spent
// sleeping. This avoids duplicate registrations on normal boot where apps
// are still starting up and will register their own tray icons shortly.
if timeSuspended() > 5*time.Second {
// Run a startup scan after a delay — covers the case where the process
// was killed during suspend and restarted by systemd (Type=dbus).
// The fresh process never sees the PrepareForSleep true→false transition,
// so the loginctl watcher alone is not enough.
go m.scheduleRecovery()
}
return m, nil
}
@@ -95,21 +91,3 @@ func (m *Manager) Close() {
}
log.Info("TrayRecovery manager closed")
}
// timeSuspended returns how long the system has spent in suspend since boot.
// It is the difference between CLOCK_BOOTTIME (includes suspend) and
// CLOCK_MONOTONIC (excludes suspend).
func timeSuspended() time.Duration {
var bt, mt unix.Timespec
if err := unix.ClockGettime(unix.CLOCK_BOOTTIME, &bt); err != nil {
return 0
}
if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &mt); err != nil {
return 0
}
diff := (bt.Sec-mt.Sec)*int64(time.Second) + (bt.Nsec - mt.Nsec)
if diff < 0 {
return 0
}
return time.Duration(diff)
}

View File

@@ -502,9 +502,6 @@ Notepad/scratchpad modal control for quick note-taking.
- `open` - Show notepad modal
- `close` - Hide notepad modal
- `toggle` - Toggle notepad modal visibility
- `expand` - Expand the active notepad width and open it if hidden
- `collapse` - Collapse the active notepad width without changing visibility
- `toggleExpand` - Toggle the active notepad width between collapsed and expanded
### Target: `dash`
Dashboard popup control with tab selection for overview, media, and weather information.
@@ -613,15 +610,6 @@ dms ipc call powermenu toggle
# Open notepad
dms ipc call notepad toggle
# Open the active notepad expanded
dms ipc call notepad expand
# Collapse the active notepad width
dms ipc call notepad collapse
# Toggle the active notepad width
dms ipc call notepad toggleExpand
# Show dashboard with specific tabs
dms ipc call dash open overview
dms ipc call dash toggle media
@@ -659,8 +647,6 @@ binds {
Mod+Space { spawn "qs" "-c" "dms" "ipc" "call" "spotlight" "toggle"; }
Mod+V { spawn "qs" "-c" "dms" "ipc" "call" "clipboard" "toggle"; }
Mod+P { spawn "qs" "-c" "dms" "ipc" "call" "notepad" "toggle"; }
Mod+Shift+P { spawn "qs" "-c" "dms" "ipc" "call" "notepad" "expand"; }
Mod+Ctrl+P { spawn "qs" "-c" "dms" "ipc" "call" "notepad" "toggleExpand"; }
Mod+X { spawn "qs" "-c" "dms" "ipc" "call" "powermenu" "toggle"; }
XF86AudioRaiseVolume { spawn "qs" "-c" "dms" "ipc" "call" "audio" "increment" "3"; }
XF86MonBrightnessUp { spawn "qs" "-c" "dms" "ipc" "call" "brightness" "increment" "5" ""; }
@@ -672,8 +658,6 @@ binds {
bind = SUPER, Space, exec, qs -c dms ipc call spotlight toggle
bind = SUPER, V, exec, qs -c dms ipc call clipboard toggle
bind = SUPER, P, exec, qs -c dms ipc call notepad toggle
bind = SUPER SHIFT, P, exec, qs -c dms ipc call notepad expand
bind = SUPER CTRL, P, exec, qs -c dms ipc call notepad toggleExpand
bind = SUPER, X, exec, qs -c dms ipc call powermenu toggle
bind = SUPER, slash, exec, qs -c dms ipc call hypr toggleBinds
bind = SUPER, Tab, exec, qs -c dms ipc call hypr toggleOverview

View File

@@ -115,32 +115,6 @@ Singleton {
return true;
}
function setPopoutBody(claimId, bodyX, bodyY, bodyW, bodyH) {
if (!hasPopoutOwner(claimId))
return false;
if (bodyX !== undefined) {
const nextX = Number(bodyX);
if (!isNaN(nextX) && popoutBodyX !== nextX)
popoutBodyX = nextX;
}
if (bodyY !== undefined) {
const nextY = Number(bodyY);
if (!isNaN(nextY) && popoutBodyY !== nextY)
popoutBodyY = nextY;
}
if (bodyW !== undefined) {
const nextW = Number(bodyW);
if (!isNaN(nextW) && popoutBodyW !== nextW)
popoutBodyW = nextW;
}
if (bodyH !== undefined) {
const nextH = Number(bodyH);
if (!isNaN(nextH) && popoutBodyH !== nextH)
popoutBodyH = nextH;
}
return true;
}
function _cloneDockStates() {
const next = {};
for (const screenName in dockStates)

View File

@@ -34,9 +34,6 @@ const DMS_ACTIONS = [
{ id: "spawn dms ipc call notepad toggle", label: "Notepad: Toggle" },
{ id: "spawn dms ipc call notepad open", label: "Notepad: Open" },
{ id: "spawn dms ipc call notepad close", label: "Notepad: Close" },
{ id: "spawn dms ipc call notepad expand", label: "Notepad: Expand" },
{ id: "spawn dms ipc call notepad collapse", label: "Notepad: Collapse" },
{ id: "spawn dms ipc call notepad toggleExpand", label: "Notepad: Toggle Expand" },
{ id: "spawn dms ipc call dash toggle \"\"", label: "Dashboard: Toggle" },
{ id: "spawn dms ipc call dash open overview", label: "Dashboard: Overview" },
{ id: "spawn dms ipc call dash open media", label: "Dashboard: Media" },

View File

@@ -310,37 +310,6 @@ Item {
return "NOTEPAD_TOGGLE_FAILED";
}
function expand(): string {
var instance = getActiveNotepadInstance();
if (instance) {
instance.expandedWidth = true;
if (!instance.isVisible)
instance.show();
return "NOTEPAD_EXPAND_SUCCESS";
}
return "NOTEPAD_EXPAND_FAILED";
}
function collapse(): string {
var instance = getActiveNotepadInstance();
if (instance) {
instance.expandedWidth = false;
if (!instance.isVisible)
instance.show();
return "NOTEPAD_COLLAPSE_SUCCESS";
}
return "NOTEPAD_COLLAPSE_FAILED";
}
function toggleExpand(): string {
var instance = getActiveNotepadInstance();
if (instance) {
instance.expandedWidth = !instance.expandedWidth;
return "NOTEPAD_TOGGLE_EXPAND_SUCCESS";
}
return "NOTEPAD_TOGGLE_EXPAND_FAILED";
}
target: "notepad"
}

View File

@@ -37,7 +37,7 @@ Item {
Loader {
id: pluginDetailLoader
width: parent.width
height: Math.max(0, parent.height - Theme.spacingS)
height: parent.height - Theme.spacingS
y: Theme.spacingS
active: false
sourceComponent: null
@@ -46,7 +46,7 @@ Item {
Loader {
id: coreDetailLoader
width: parent.width
height: Math.max(0, parent.height - Theme.spacingS)
height: parent.height - Theme.spacingS
y: Theme.spacingS
active: false
sourceComponent: null
@@ -134,7 +134,7 @@ Item {
}
pluginDetailLoader.sourceComponent = builtinInstance.ccDetailContent;
pluginDetailLoader.active = true;
pluginDetailLoader.active = parent.height > 0;
return;
}
@@ -155,19 +155,19 @@ Item {
}
pluginDetailLoader.sourceComponent = pluginDetailInstance.ccDetailContent;
pluginDetailLoader.active = true;
pluginDetailLoader.active = parent.height > 0;
return;
}
if (root.expandedSection.startsWith("diskUsage_")) {
coreDetailLoader.sourceComponent = diskUsageDetailComponent;
coreDetailLoader.active = true;
coreDetailLoader.active = parent.height > 0;
return;
}
if (root.expandedSection.startsWith("brightnessSlider_")) {
coreDetailLoader.sourceComponent = brightnessDetailComponent;
coreDetailLoader.active = true;
coreDetailLoader.active = parent.height > 0;
return;
}
@@ -192,7 +192,7 @@ Item {
return;
}
coreDetailLoader.active = true;
coreDetailLoader.active = parent.height > 0;
}
Component {

View File

@@ -51,35 +51,6 @@ Column {
return Math.max(100, maxPopoutHeight - totalRowHeight - rowSpacing);
}
readonly property real targetImplicitHeight: {
const rows = layoutResult.rows;
let totalHeight = 0;
for (let i = 0; i < rows.length; i++) {
const widgets = rows[i] || [];
const sliderOnly = widgets.length > 0 && widgets.every(w => {
const id = w.id || "";
return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider";
});
totalHeight += sliderOnly ? (editMode ? 56 : 36) : 60;
if (expandedSection !== "" && i === expandedRowIndex)
totalHeight += detailHeightForSection(expandedSection) + Theme.spacingS;
}
totalHeight += Math.max(0, rows.length - 1) * spacing;
return totalHeight;
}
function detailHeightForSection(section) {
if (!section)
return 0;
if (section === "wifi" || section === "bluetooth" || section === "builtin_vpn")
return Math.min(350, _maxDetailHeight);
if (section.startsWith("brightnessSlider_"))
return Math.min(400, _maxDetailHeight);
if (section.startsWith("plugin_"))
return Math.min(250, _maxDetailHeight);
return Math.min(250, _maxDetailHeight);
}
function calculateRowsAndWidgets() {
return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex);
}
@@ -208,10 +179,7 @@ Column {
id: detailHost
width: parent.width
maxAvailableHeight: root._maxDetailHeight
height: active ? (root.detailHeightForSection(root.expandedSection) + Theme.spacingS) : 0
clip: true
property string retainedSection: ""
property var retainedWidgetData: null
height: active ? (getDetailHeight(root.expandedSection) + Theme.spacingS) : 0
property bool active: {
if (root.expandedSection === "")
return false;
@@ -228,47 +196,14 @@ Column {
return rowIndex === root.expandedRowIndex;
}
visible: active || height > 0.5
expandedSection: active ? root.expandedSection : retainedSection
expandedWidgetData: active ? root.expandedWidgetData : retainedWidgetData
visible: active
expandedSection: root.expandedSection
expandedWidgetData: root.expandedWidgetData
bluetoothCodecSelector: root.bluetoothCodecSelector
widgetModel: root.model
collapseCallback: root.requestCollapse
screenName: root.screenName
screenModel: root.screenModel
function retainActiveDetail() {
if (!active || !root.expandedSection)
return;
retainedSection = root.expandedSection;
retainedWidgetData = root.expandedWidgetData;
}
onActiveChanged: retainActiveDetail()
onHeightChanged: {
if (!active && height <= 0.5) {
retainedSection = "";
retainedWidgetData = null;
}
}
Connections {
target: root
function onExpandedSectionChanged() {
detailHost.retainActiveDetail();
}
function onExpandedWidgetDataChanged() {
detailHost.retainActiveDetail();
}
}
Behavior on height {
NumberAnimation {
duration: Theme.variantDuration(Theme.popoutAnimationDuration, detailHost.active)
easing.type: Easing.BezierSpline
easing.bezierCurve: detailHost.active ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
}
}
}
@@ -325,7 +260,7 @@ Column {
}
case "audioOutput":
{
if (!AudioService.sink?.audio)
if (!AudioService.sink)
return "volume_off";
let volume = AudioService.sink.audio.volume;
let muted = AudioService.sink.audio.muted;
@@ -341,7 +276,7 @@ Column {
}
case "audioInput":
{
if (!AudioService.source?.audio)
if (!AudioService.source)
return "mic_off";
let muted = AudioService.source.audio.muted;
return muted ? "mic_off" : "mic";
@@ -434,7 +369,7 @@ Column {
}
case "audioOutput":
{
if (!AudioService.sink?.audio)
if (!AudioService.sink)
return I18n.tr("Select device", "audio status");
if (AudioService.sink.audio.muted)
return I18n.tr("Muted", "audio status");
@@ -445,7 +380,7 @@ Column {
}
case "audioInput":
{
if (!AudioService.source?.audio)
if (!AudioService.source)
return I18n.tr("Select device", "audio status");
if (AudioService.source.audio.muted)
return I18n.tr("Muted", "audio status");
@@ -477,9 +412,9 @@ Column {
case "bluetooth":
return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled);
case "audioOutput":
return !!(AudioService.sink?.audio && !AudioService.sink.audio.muted);
return !!(AudioService.sink && !AudioService.sink.audio.muted);
case "audioInput":
return !!(AudioService.source?.audio && !AudioService.source.audio.muted);
return !!(AudioService.source && !AudioService.source.audio.muted);
default:
return false;
}

View File

@@ -20,53 +20,19 @@ DankPopout {
property int expandedWidgetIndex: -1
property var expandedWidgetData: null
property bool powerMenuOpen: powerMenuModalLoader?.item?.shouldBeVisible ?? false
property real targetPopupHeight: 400
property bool _heightUpdatePending: false
signal lockRequested
function _maxPopupHeight() {
const screenHeight = (triggerScreen?.height ?? 1080);
return screenHeight - 100;
}
function _contentTargetHeight() {
const item = contentLoader.item;
if (!item)
return 400;
const naturalHeight = item.targetImplicitHeight !== undefined ? item.targetImplicitHeight : item.implicitHeight;
return Math.max(300, naturalHeight + 20);
}
function updateTargetPopupHeight() {
const target = Math.min(_maxPopupHeight(), _contentTargetHeight());
if (Math.abs(targetPopupHeight - target) < 0.5)
return;
targetPopupHeight = target;
}
function queueTargetPopupHeightUpdate() {
if (_heightUpdatePending)
return;
_heightUpdatePending = true;
Qt.callLater(() => {
_heightUpdatePending = false;
updateTargetPopupHeight();
});
}
function collapseAll() {
expandedSection = "";
expandedWidgetIndex = -1;
expandedWidgetData = null;
queueTargetPopupHeightUpdate();
}
onEditModeChanged: {
if (editMode) {
collapseAll();
}
queueTargetPopupHeightUpdate();
}
onVisibleChanged: {
@@ -86,7 +52,12 @@ DankPopout {
}
popupWidth: 550
popupHeight: targetPopupHeight
popupHeight: {
const screenHeight = (triggerScreen?.height ?? 1080);
const maxHeight = screenHeight - 100;
const contentHeight = contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400;
return Math.min(maxHeight, contentHeight);
}
triggerWidth: 80
positioning: ""
screen: triggerScreen
@@ -124,7 +95,6 @@ DankPopout {
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
collapseAll();
queueTargetPopupHeightUpdate();
Qt.callLater(() => {
if (NetworkService.activeService)
NetworkService.activeService.autoRefreshEnabled = NetworkService.wifiEnabled;
@@ -141,28 +111,6 @@ DankPopout {
}
}
onExpandedSectionChanged: queueTargetPopupHeightUpdate()
onExpandedWidgetIndexChanged: queueTargetPopupHeightUpdate()
onTriggerScreenChanged: queueTargetPopupHeightUpdate()
Connections {
target: contentLoader
function onLoaded() {
root.queueTargetPopupHeightUpdate();
}
}
Connections {
target: contentLoader.item
ignoreUnknownSignals: true
function onTargetImplicitHeightChanged() {
root.queueTargetPopupHeightUpdate();
}
function onImplicitHeightChanged() {
root.queueTargetPopupHeightUpdate();
}
}
WidgetModel {
id: widgetModel
}
@@ -174,13 +122,7 @@ DankPopout {
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
readonly property real targetImplicitHeight: {
let total = headerPane.implicitHeight + Theme.spacingS + widgetGrid.targetImplicitHeight;
if (editControls.visible)
total += Theme.spacingS + editControls.height;
return total + Theme.spacingM;
}
implicitHeight: targetImplicitHeight
implicitHeight: mainColumn.implicitHeight + Theme.spacingM
property alias bluetoothCodecSelector: bluetoothCodecSelector
color: "transparent"
@@ -203,17 +145,9 @@ DankPopout {
}
}
DankFlickable {
id: contentFlickable
anchors.fill: parent
clip: true
contentWidth: width
contentHeight: Math.max(height, mainColumn.implicitHeight + Theme.spacingM)
interactive: contentHeight > height
Column {
id: mainColumn
width: contentFlickable.width - Theme.spacingL * 2
width: parent.width - Theme.spacingL * 2
x: Theme.spacingL
y: Theme.spacingL
spacing: Theme.spacingS
@@ -247,7 +181,7 @@ DankPopout {
editMode: root.editMode
maxPopoutHeight: {
const screenHeight = (root.triggerScreen?.height ?? 1080);
return screenHeight - 100 - Theme.spacingL - headerPane.implicitHeight - Theme.spacingS;
return screenHeight - 100 - Theme.spacingL - headerPane.height - Theme.spacingS;
}
expandedSection: root.expandedSection
expandedWidgetIndex: root.expandedWidgetIndex
@@ -276,7 +210,6 @@ DankPopout {
}
EditControls {
id: editControls
width: parent.width
visible: editMode
popoutContent: controlContent
@@ -292,7 +225,6 @@ DankPopout {
onClearAll: () => widgetModel.clearAll()
}
}
}
BluetoothCodecSelector {
id: bluetoothCodecSelector

View File

@@ -351,8 +351,8 @@ Rectangle {
deviceRipple.trigger(mapped.x, mapped.y);
}
onClicked: {
if (modelData && modelData.name) {
AudioService.setDefaultSourceByName(modelData.name);
if (modelData) {
Pipewire.preferredDefaultAudioSource = modelData;
}
}
}

View File

@@ -355,8 +355,8 @@ Rectangle {
deviceRipple.trigger(mapped.x, mapped.y);
}
onClicked: {
if (modelData && modelData.name) {
AudioService.setDefaultSinkByName(modelData.name);
if (modelData) {
Pipewire.preferredDefaultAudioSink = modelData;
}
}
}

View File

@@ -35,7 +35,7 @@ Row {
cursorShape: Qt.PointingHandCursor
onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y)
onClicked: {
if (defaultSink?.audio) {
if (defaultSink) {
SessionData.suppressOSDTemporarily();
defaultSink.audio.muted = !defaultSink.audio.muted;
}
@@ -45,7 +45,7 @@ Row {
DankIcon {
anchors.centerIn: parent
name: {
if (!defaultSink?.audio)
if (!defaultSink)
return "volume_off";
let volume = defaultSink.audio.volume;
@@ -62,18 +62,18 @@ Row {
return "volume_up";
}
size: Theme.iconSize
color: defaultSink?.audio && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
}
}
DankSlider {
id: volumeSlider
readonly property real actualVolumePercent: defaultSink?.audio ? Math.round(defaultSink.audio.volume * 100) : 0
readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
enabled: defaultSink?.audio != null
enabled: defaultSink !== null
minimum: 0
maximum: AudioService.sinkMaxVolume
showValue: true
@@ -83,7 +83,7 @@ Row {
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
onSliderValueChanged: function (newValue) {
if (defaultSink?.audio) {
if (defaultSink) {
SessionData.suppressOSDTemporarily();
defaultSink.audio.volume = newValue / 100.0;
if (newValue > 0 && defaultSink.audio.muted) {
@@ -97,7 +97,7 @@ Row {
Binding {
target: volumeSlider
property: "value"
value: defaultSink?.audio ? Math.min(AudioService.sinkMaxVolume, Math.round(defaultSink.audio.volume * 100)) : 0
value: defaultSink ? Math.min(AudioService.sinkMaxVolume, Math.round(defaultSink.audio.volume * 100)) : 0
when: !volumeSlider.isDragging
}
}

View File

@@ -35,7 +35,7 @@ Row {
cursorShape: Qt.PointingHandCursor
onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y)
onClicked: {
if (defaultSource?.audio) {
if (defaultSource) {
SessionData.suppressOSDTemporarily();
defaultSource.audio.muted = !defaultSource.audio.muted;
}
@@ -45,7 +45,7 @@ Row {
DankIcon {
anchors.centerIn: parent
name: {
if (!defaultSource?.audio)
if (!defaultSource)
return "mic_off";
let volume = defaultSource.audio.volume;
@@ -56,26 +56,26 @@ Row {
return "mic";
}
size: Theme.iconSize
color: defaultSource?.audio && !defaultSource.audio.muted && defaultSource.audio.volume > 0 ? Theme.primary : Theme.surfaceText
color: defaultSource && !defaultSource.audio.muted && defaultSource.audio.volume > 0 ? Theme.primary : Theme.surfaceText
}
}
DankSlider {
readonly property real actualVolumePercent: defaultSource?.audio ? Math.round(defaultSource.audio.volume * 100) : 0
readonly property real actualVolumePercent: defaultSource ? Math.round(defaultSource.audio.volume * 100) : 0
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
enabled: defaultSource?.audio != null
enabled: defaultSource !== null
minimum: 0
maximum: 100
value: defaultSource?.audio ? Math.min(100, Math.round(defaultSource.audio.volume * 100)) : 0
value: defaultSource ? Math.min(100, Math.round(defaultSource.audio.volume * 100)) : 0
showValue: true
unit: "%"
valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceContainer
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
onSliderValueChanged: function (newValue) {
if (defaultSource?.audio) {
if (defaultSource) {
SessionData.suppressOSDTemporarily();
defaultSource.audio.volume = newValue / 100.0;
if (newValue > 0 && defaultSource.audio.muted) {

View File

@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
@@ -24,7 +25,7 @@ Item {
}
property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
property bool volumeAvailable: !!((activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio))
property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio)
property var availableDevices: {
const hidden = SessionData.hiddenOutputDeviceNames ?? [];
return Pipewire.nodes.values.filter(node => {
@@ -406,8 +407,8 @@ Item {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && modelData.name) {
AudioService.setDefaultSinkByName(modelData.name);
if (modelData) {
Pipewire.preferredDefaultAudioSink = modelData;
root.deviceSelected(modelData);
}
}

View File

@@ -57,7 +57,7 @@ Item {
const id = activePlayer.identity.toLowerCase();
return id.includes("chrome") || id.includes("chromium");
}
readonly property bool volumeAvailable: !!((activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio))
readonly property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio)
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)

View File

@@ -16,7 +16,8 @@ DankListView {
property bool listInitialized: false
property int swipingCardIndex: -1
property real swipingCardOffset: 0
property bool _stableHeightUpdatePending: false
property real __pendingStableHeight: 0
property real __heightUpdateThreshold: 20
readonly property real shadowBlurPx: Theme.elevationEnabled ? ((Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined) ? Theme.elevationLevel1.blurPx : 4) : 0
readonly property real shadowHorizontalGutter: Theme.snap(Math.max(Theme.spacingS, Math.min(32, shadowBlurPx * 1.5 + 6)), 1)
readonly property real shadowVerticalGutter: Theme.snap(Math.max(Theme.spacingXS, 6), 1)
@@ -26,52 +27,51 @@ DankListView {
Qt.callLater(() => {
if (listView) {
listView.listInitialized = true;
listView.syncStableContentHeight(false);
listView.stableContentHeight = listView.contentHeight;
}
});
}
function targetContentHeight() {
if (count <= 0)
return contentHeight;
let total = topMargin + bottomMargin + Math.max(0, count - 1) * spacing;
for (let i = 0; i < count; i++) {
const item = itemAtIndex(i);
if (!item || item.nonAnimHeight === undefined)
return contentHeight;
total += item.nonAnimHeight;
Timer {
id: heightUpdateDebounce
interval: Theme.mediumDuration + 20
repeat: false
onTriggered: {
if (!listView.isAnimatingExpansion && Math.abs(listView.__pendingStableHeight - listView.stableContentHeight) > listView.__heightUpdateThreshold) {
listView.stableContentHeight = listView.__pendingStableHeight;
}
return Math.max(0, total);
}
function syncStableContentHeight(useTarget) {
const nextHeight = useTarget ? targetContentHeight() : contentHeight;
if (Math.abs(nextHeight - stableContentHeight) <= 0.5)
return;
stableContentHeight = nextHeight;
}
function queueStableContentHeightUpdate(useTarget) {
if (_stableHeightUpdatePending)
return;
_stableHeightUpdatePending = true;
Qt.callLater(() => {
_stableHeightUpdatePending = false;
syncStableContentHeight(useTarget || isAnimatingExpansion);
});
}
onContentHeightChanged: {
if (!isAnimatingExpansion)
queueStableContentHeightUpdate(false);
if (!isAnimatingExpansion) {
__pendingStableHeight = contentHeight;
if (Math.abs(contentHeight - stableContentHeight) > __heightUpdateThreshold) {
heightUpdateDebounce.restart();
} else {
stableContentHeight = contentHeight;
}
}
}
onIsAnimatingExpansionChanged: {
if (isAnimatingExpansion) {
syncStableContentHeight(true);
heightUpdateDebounce.stop();
let delta = 0;
for (let i = 0; i < count; i++) {
const item = itemAtIndex(i);
if (item && item.children[0] && item.children[0].isAnimating) {
const targetDelegateHeight = item.children[0].targetHeight + listView.delegateShadowGutter;
delta += targetDelegateHeight - item.height;
}
}
const targetHeight = contentHeight + delta;
// During expansion, always update immediately without threshold check
stableContentHeight = targetHeight;
} else {
queueStableContentHeightUpdate(false);
__pendingStableHeight = contentHeight;
heightUpdateDebounce.stop();
stableContentHeight = __pendingStableHeight;
}
}
@@ -148,14 +148,11 @@ DankListView {
readonly property real adjacentScaleInfluence: isAdjacentToSwipe ? 1.0 - Math.abs(listView.swipingCardOffset) / width * 0.02 : 1.0
readonly property real swipeFadeStartOffset: width * 0.75
readonly property real swipeFadeDistance: Math.max(1, width - swipeFadeStartOffset)
readonly property real nonAnimHeight: notificationCard.targetHeight + listView.delegateShadowGutter
Component.onCompleted: {
Qt.callLater(() => {
if (delegateRoot) {
if (delegateRoot)
delegateRoot.__delegateInitialized = true;
listView.queueStableContentHeightUpdate(listView.isAnimatingExpansion);
}
});
}
@@ -183,7 +180,6 @@ DankListView {
onIsAnimatingChanged: {
if (isAnimating) {
listView.isAnimatingExpansion = true;
listView.syncStableContentHeight(true);
} else {
Qt.callLater(() => {
if (!notificationCard || !listView)
@@ -201,13 +197,6 @@ DankListView {
}
}
onTargetHeightChanged: {
if (isAnimating || listView.isAnimatingExpansion)
listView.syncStableContentHeight(true);
else
listView.queueStableContentHeightUpdate(false);
}
isGroupSelected: {
if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive)
return false;

View File

@@ -16,12 +16,6 @@ Rectangle {
property bool userInitiatedExpansion: false
property bool isAnimating: false
property bool animateExpansion: true
property bool _retainedExpandedContent: false
property bool _clipAnimatedContent: false
property real expandedContentOpacity: expanded ? 1 : 0
property real collapsedContentOpacity: expanded ? 0 : 1
readonly property bool renderExpandedContent: expanded || _retainedExpandedContent
readonly property bool renderCollapsedContent: !expanded
property bool isGroupSelected: false
property int selectedNotificationIndex: -1
@@ -63,14 +57,6 @@ Rectangle {
});
}
function expansionMotionDuration() {
return root.connectedFrameMode ? Theme.variantDuration(Theme.popoutAnimationDuration, root.expanded) : (root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration);
}
function expansionMotionCurve() {
return root.connectedFrameMode ? (root.expanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : Theme.expressiveCurves.emphasized;
}
Behavior on scale {
enabled: listLevelScaleAnimationsEnabled
NumberAnimation {
@@ -80,7 +66,6 @@ Rectangle {
}
Behavior on shadowBlurPx {
enabled: !root.connectedFrameMode
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -88,7 +73,6 @@ Rectangle {
}
Behavior on shadowOffsetXPx {
enabled: !root.connectedFrameMode
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -96,7 +80,6 @@ Rectangle {
}
Behavior on shadowOffsetYPx {
enabled: !root.connectedFrameMode
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
@@ -111,24 +94,6 @@ Rectangle {
}
}
Behavior on expandedContentOpacity {
enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion
NumberAnimation {
duration: root.expansionMotionDuration()
easing.type: Easing.BezierSpline
easing.bezierCurve: root.expansionMotionCurve()
}
}
Behavior on collapsedContentOpacity {
enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion
NumberAnimation {
duration: root.expansionMotionDuration()
easing.type: Easing.BezierSpline
easing.bezierCurve: root.expansionMotionCurve()
}
}
color: {
if (isGroupSelected && keyboardNavigationActive) {
return Theme.primaryPressed;
@@ -164,31 +129,7 @@ Rectangle {
}
return 0;
}
clip: connectedFrameMode && _clipAnimatedContent
onExpandedChanged: {
if (connectedFrameMode && __initialized && userInitiatedExpansion && animateExpansion)
_clipAnimatedContent = true;
if (expanded) {
_retainedExpandedContent = false;
return;
}
if (connectedFrameMode && __initialized && userInitiatedExpansion && animateExpansion)
_retainedExpandedContent = true;
}
onHeightChanged: {
if (Math.abs(height - targetHeight) > 0.5)
return;
_clipAnimatedContent = false;
if (!expanded && _retainedExpandedContent)
_retainedExpandedContent = false;
}
onExpandedContentOpacityChanged: {
if (!expanded && _retainedExpandedContent && expandedContentOpacity <= 0.01)
_retainedExpandedContent = false;
}
clip: false
HoverHandler {
id: cardHoverHandler
@@ -208,7 +149,7 @@ Rectangle {
shadowOffsetX: root.shadowOffsetXPx
shadowOffsetY: root.shadowOffsetYPx
shadowColor: root.shadowElevation ? Theme.elevationShadowColor(root.shadowElevation) : "transparent"
shadowEnabled: root.shadowsAllowed && !root.connectedFrameMode
shadowEnabled: root.shadowsAllowed
}
Rectangle {
@@ -248,8 +189,7 @@ Rectangle {
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin
height: collapsedContentHeight + extraHeight
visible: renderCollapsedContent
opacity: root.collapsedContentOpacity
visible: !expanded
DankCircularImage {
id: iconContainer
@@ -448,8 +388,7 @@ Rectangle {
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
spacing: compactMode ? Theme.spacingXS : Theme.spacingS
visible: renderExpandedContent
opacity: root.expandedContentOpacity
visible: expanded
Item {
width: parent.width
@@ -892,8 +831,7 @@ Rectangle {
}
Row {
visible: renderCollapsedContent
opacity: root.collapsedContentOpacity
visible: !expanded
anchors.right: clearButton.visible ? clearButton.left : parent.right
anchors.rightMargin: clearButton.visible ? contentSpacing : Theme.spacingL
anchors.top: collapsedContent.bottom
@@ -949,8 +887,7 @@ Rectangle {
property bool isHovered: false
readonly property int actionCount: (notificationGroup?.latestNotification?.actions || []).length
visible: renderCollapsedContent && actionCount < 3
opacity: root.collapsedContentOpacity
visible: !expanded && actionCount < 3
anchors.right: parent.right
anchors.rightMargin: Theme.spacingL
anchors.top: collapsedContent.bottom
@@ -981,7 +918,7 @@ Rectangle {
MouseArea {
anchors.fill: parent
visible: renderCollapsedContent && (notificationGroup?.count || 0) > 1 && !descriptionExpanded
visible: !expanded && (notificationGroup?.count || 0) > 1 && !descriptionExpanded
cursorShape: Qt.PointingHandCursor
onClicked: {
root.userInitiatedExpansion = true;
@@ -1025,17 +962,15 @@ Rectangle {
Behavior on height {
enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion
NumberAnimation {
duration: root.expansionMotionDuration()
duration: root.connectedFrameMode ? Theme.variantDuration(Theme.popoutAnimationDuration, root.expanded) : (root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.expansionMotionCurve()
easing.bezierCurve: root.connectedFrameMode ? (root.expanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : Theme.expressiveCurves.emphasized
onRunningChanged: {
if (running) {
root.isAnimating = true;
} else {
root.isAnimating = false;
root.userInitiatedExpansion = false;
root._retainedExpandedContent = false;
root._clipAnimatedContent = false;
}
}
}

View File

@@ -14,7 +14,6 @@ DankPopout {
property real stablePopupHeight: 400
property real _lastAlignedContentHeight: -1
property bool _pendingSizedOpen: false
property bool _heightUpdatePending: false
function updateStablePopupHeight() {
const item = contentLoader.item;
@@ -31,16 +30,6 @@ DankPopout {
stablePopupHeight = target;
}
function queueStablePopupHeightUpdate() {
if (_heightUpdatePending)
return;
_heightUpdatePending = true;
Qt.callLater(() => {
_heightUpdatePending = false;
updateStablePopupHeight();
});
}
NotificationKeyboardController {
id: keyboardController
listView: null
@@ -139,7 +128,7 @@ DankPopout {
Connections {
target: contentLoader.item
function onImplicitHeightChanged() {
root.queueStablePopupHeightUpdate();
root.updateStablePopupHeight();
}
}

View File

@@ -537,21 +537,21 @@ PanelWindow {
Behavior on shadowBlurPx {
NumberAnimation {
duration: win.descriptionExpanded ? Theme.notificationExpandDuration : Theme.shortDuration
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on shadowOffsetX {
NumberAnimation {
duration: win.descriptionExpanded ? Theme.notificationExpandDuration : Theme.shortDuration
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on shadowOffsetY {
NumberAnimation {
duration: win.descriptionExpanded ? Theme.notificationExpandDuration : Theme.shortDuration
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
@@ -566,7 +566,7 @@ PanelWindow {
shadowOffsetX: content.shadowOffsetX
shadowOffsetY: content.shadowOffsetY
shadowColor: content.shadowsAllowed && content.elevLevel ? Theme.elevationShadowColor(content.elevLevel) : "transparent"
shadowEnabled: !win._isDestroying && win.screenValid && content.shadowsAllowed && !win.connectedFrameMode
shadowEnabled: !win._isDestroying && win.screenValid && content.shadowsAllowed
layer.textureSize: Qt.size(Math.round(width * win.dpr), Math.round(height * win.dpr))
layer.textureMirroring: ShaderEffectSource.MirrorVertically
@@ -578,20 +578,19 @@ PanelWindow {
sourceRect.radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
sourceRect.color: win.connectedFrameMode ? Theme.popupLayerColor(Theme.surfaceContainer) : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
sourceRect.antialiasing: true
sourceRect.layer.enabled: false
sourceRect.layer.textureSize: Qt.size(0, 0)
sourceRect.layer.enabled: win.connectedFrameMode
sourceRect.layer.smooth: true
sourceRect.layer.textureSize: win.connectedFrameMode && win.dpr > 1 ? Qt.size(Math.ceil(sourceRect.width * win.dpr), Math.ceil(sourceRect.height * win.dpr)) : Qt.size(0, 0)
sourceRect.border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.outline, 0.08)
sourceRect.border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 0
}
// Keep critical accent outside shadow rendering so connected mode still shows it.
Rectangle {
x: content.cardInset
y: content.cardInset
width: Math.max(0, content.width - content.cardInset * 2)
height: Math.max(0, content.height - content.cardInset * 2)
radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
visible: win.notificationData && win.notificationData.urgency === NotificationUrgency.Critical
x: bgShadowLayer.sourceRect.x
y: bgShadowLayer.sourceRect.y
width: bgShadowLayer.sourceRect.width
height: bgShadowLayer.sourceRect.height
radius: bgShadowLayer.sourceRect.radius
visible: notificationData && notificationData.urgency === NotificationUrgency.Critical
opacity: 1
clip: true
@@ -614,6 +613,7 @@ PanelWindow {
}
}
}
}
Rectangle {
anchors.fill: parent

View File

@@ -36,7 +36,6 @@ QtObject {
property var pendingDestroys: []
property int destroyDelayMs: 100
property bool _chromeSyncPending: false
property bool _syncingVisibleNotifications: false
readonly property real chromeOpenProgressThreshold: 0.10
readonly property real chromeReleaseTailStart: 0.90
readonly property real chromeReleaseDropProgress: 0.995
@@ -161,7 +160,6 @@ QtObject {
function _sync(newWrappers) {
let needsReposition = false;
_syncingVisibleNotifications = true;
for (const p of popupWindows.slice()) {
if (!_isValidWindow(p) || p.exiting)
continue;
@@ -173,10 +171,10 @@ QtObject {
}
for (const w of newWrappers) {
if (w && !_hasWindowFor(w) && _isFocusedScreen()) {
needsReposition = _insertAtTop(w, true) || needsReposition;
_insertAtTop(w);
needsReposition = false;
}
}
_syncingVisibleNotifications = false;
if (needsReposition)
_repositionAll();
}
@@ -185,9 +183,9 @@ QtObject {
return (p.alignedHeight || p.implicitHeight || (baseNotificationHeight - popupSpacing)) + popupSpacing;
}
function _insertAtTop(wrapper, deferReposition) {
function _insertAtTop(wrapper) {
if (!wrapper)
return false;
return;
const notificationId = wrapper?.notification ? wrapper.notification.id : "";
const win = popupComponent.createObject(null, {
"notificationData": wrapper,
@@ -196,17 +194,15 @@ QtObject {
"screen": manager.modelData
});
if (!win)
return false;
return;
if (!win.hasValidData) {
win.destroy();
return false;
return;
}
popupWindows.unshift(win);
if (!deferReposition)
_repositionAll();
if (!sweeper.running)
sweeper.start();
return true;
}
function _repositionAll() {
@@ -325,10 +321,6 @@ QtObject {
if (!rect || p !== trailing || !p.popupChromeReleaseProgress)
return rect;
// Keep maxed-stack chrome anchored while a replacement tail exits.
if (p.exiting && p.notificationData?.removedByLimit && _layoutWindows().length > 0)
return rect;
const progress = _chromeReleaseTailProgress(p.popupChromeReleaseProgress());
if (progress <= 0)
return rect;
@@ -497,34 +489,17 @@ QtObject {
_scheduleNotificationChromeSync();
}
// Coalesce resize repositioning; exit-path moves remain immediate.
property bool _repositionPending: false
function _queueReposition() {
if (_repositionPending)
return;
_repositionPending = true;
Qt.callLater(_flushReposition);
}
function _flushReposition() {
_repositionPending = false;
_repositionAll();
}
function _onPopupHeightChanged(p) {
if (!p || p.exiting || p._isDestroying)
return;
if (popupWindows.indexOf(p) === -1)
return;
_queueReposition();
_repositionAll();
}
function _onPopupExitStarted(p) {
if (!p || popupWindows.indexOf(p) === -1)
return;
if (_syncingVisibleNotifications)
return;
_repositionAll();
}

View File

@@ -71,7 +71,7 @@ Singleton {
// Used in playLoginSoundIfApplicable()
Process {
id: loginSoundChecker
onExited: exitCode => {
onExited: (exitCode) => {
if (exitCode === 0) {
playLoginSound();
}
@@ -83,36 +83,6 @@ Singleton {
return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream && !hidden.includes(node.name));
}
// Resolve a PwNode by name from the live typed list and assign it as the
// default sink. Going through Pipewire.nodes.values directly (no .filter
// / spread / .sort / property var) avoids QML type erasure to QObject*,
// which newer quickshell rejects when assigning to preferredDefaultAudioSink.
function setDefaultSinkByName(name) {
if (!name)
return false;
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
const node = Pipewire.nodes.values[i];
if (node && node.name === name && node.audio && node.isSink && !node.isStream) {
Pipewire.preferredDefaultAudioSink = node;
return true;
}
}
return false;
}
function setDefaultSourceByName(name) {
if (!name)
return false;
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
const node = Pipewire.nodes.values[i];
if (node && node.name === name && node.audio && !node.isSink && !node.isStream) {
Pipewire.preferredDefaultAudioSource = node;
return true;
}
}
return false;
}
function cycleAudioOutput() {
const sinks = getAvailableSinks();
if (sinks.length < 2)
@@ -122,7 +92,6 @@ Singleton {
const currentIndex = sinks.findIndex(s => s.name === currentName);
const nextIndex = (currentIndex + 1) % sinks.length;
const nextSink = sinks[nextIndex];
if (!setDefaultSinkByName(nextSink.name))
Pipewire.preferredDefaultAudioSink = nextSink;
const name = displayName(nextSink);
audioOutputCycled(name, sinkIcon(nextSink));
@@ -681,6 +650,7 @@ EOFCONFIG
}
}
`, root, "AudioService.LoginSound");
} catch (e) {
console.warn("AudioService: Error creating sound players:", e);
}
@@ -734,8 +704,7 @@ EOFCONFIG
const runtimeDir = Quickshell.env("XDG_RUNTIME_DIR");
const sessionId = Quickshell.env("XDG_SESSION_ID") || "0";
if (!runtimeDir)
return;
if (!runtimeDir) return;
const loginFile = `${runtimeDir}/danklinux.login-${sessionId}`;

View File

@@ -32,6 +32,8 @@ Item {
property bool fullHeightSurface: false
property bool _primeContent: false
property bool _resizeActive: false
property real _surfaceMarginLeft: 0
property real _surfaceW: 0
property string _chromeClaimId: ""
property int _connectedChromeSerial: 0
property real _chromeAnimTravelX: 1
@@ -47,8 +49,6 @@ Item {
"rightBar": 0
})
property var screen: null
// Connected resize uses one full-screen surface; body-sized regions are masks.
readonly property bool useBackgroundWindow: false
readonly property real effectiveBarThickness: {
if (Theme.isConnectedEffect)
@@ -118,6 +118,12 @@ Item {
}
readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
// Snapshot mask geometry to prevent background damage on bar updates
property real _frozenMaskX: 0
property real _frozenMaskY: 0
property real _frozenMaskWidth: 0
property real _frozenMaskHeight: 0
function setBarContext(position, bottomGap) {
effectiveBarPosition = position !== undefined ? position : 0;
effectiveBarBottomGap = bottomGap !== undefined ? bottomGap : 0;
@@ -175,7 +181,7 @@ Item {
if (barSide !== "top" && barSide !== "bottom")
return contentContainer.animY;
const extent = Math.max(0, root.renderedAlignedHeight);
const extent = Math.max(0, root.alignedHeight);
const progress = Math.min(1, Math.abs(contentContainer.animY) / Math.max(1, _chromeAnimTravelY));
const offset = Theme.snap(extent * progress, root.dpr);
return contentContainer.animY < 0 ? -offset : offset;
@@ -187,9 +193,9 @@ Item {
"visible": visible,
"barSide": contentContainer.connectedBarSide,
"bodyX": root.alignedX,
"bodyY": root.renderedAlignedY,
"bodyY": root.alignedY,
"bodyW": root.alignedWidth,
"bodyH": root.renderedAlignedHeight,
"bodyH": root.alignedHeight,
"animX": _connectedChromeAnimX(),
"animY": _connectedChromeAnimY(),
"screen": root.screen ? root.screen.name : "",
@@ -252,28 +258,16 @@ Item {
ConnectedModeState.setPopoutAnim(_chromeClaimId, syncX ? _connectedChromeAnimX() : undefined, syncY ? _connectedChromeAnimY() : undefined);
}
function _syncPopoutBody() {
if (!root.frameOwnsConnectedChrome || !_chromeClaimId)
return;
if (!contentWindow.visible && !shouldBeVisible)
return;
ConnectedModeState.setPopoutBody(_chromeClaimId, root.alignedX, root.renderedAlignedY, root.alignedWidth, root.renderedAlignedHeight);
}
function _flushFullSync() {
_fullSyncPending = false;
if (root && typeof root._syncPopoutChromeState === "function")
root._syncPopoutChromeState();
_syncPopoutChromeState();
}
function _queueFullSync() {
if (_fullSyncPending)
return;
_fullSyncPending = true;
Qt.callLater(() => {
if (root && typeof root._flushFullSync === "function")
root._flushFullSync();
});
Qt.callLater(root._flushFullSync);
}
onAlignedXChanged: _queueFullSync()
@@ -281,8 +275,6 @@ Item {
onAlignedWidthChanged: _queueFullSync()
onContentAnimXChanged: _syncPopoutAnim("x")
onContentAnimYChanged: _syncPopoutAnim("y")
onRenderedAlignedYChanged: _syncPopoutBody()
onRenderedAlignedHeightChanged: _syncPopoutBody()
onScreenChanged: _syncPopoutChromeState()
onEffectiveBarPositionChanged: _syncPopoutChromeState()
@@ -314,10 +306,18 @@ Item {
}
}
readonly property bool useBackgroundWindow: !CompositorService.isHyprland || CompositorService.useHyprlandFocusGrab
readonly property bool frameOwnsConnectedChrome: SettingsData.connectedFrameModeActive
&& !!root.screen
&& SettingsData.isScreenInPreferences(root.screen, SettingsData.frameScreenPreferences)
function updateSurfacePosition() {
if (useBackgroundWindow && shouldBeVisible) {
_surfaceMarginLeft = alignedX - shadowBuffer;
_surfaceW = alignedWidth + shadowBuffer * 2;
}
}
property bool animationsEnabled: true
function open() {
@@ -328,8 +328,16 @@ Item {
animationsEnabled = false;
_primeContent = true;
// Snapshot mask geometry
_frozenMaskX = maskX;
_frozenMaskY = maskY;
_frozenMaskWidth = maskWidth;
_frozenMaskHeight = maskHeight;
if (_lastOpenedScreen !== null && _lastOpenedScreen !== screen) {
contentWindow.visible = false;
if (useBackgroundWindow)
backgroundWindow.visible = false;
}
_lastOpenedScreen = screen;
@@ -347,12 +355,19 @@ Item {
_chromeClaimId = "";
}
if (useBackgroundWindow) {
_surfaceMarginLeft = alignedX - shadowBuffer;
_surfaceW = alignedWidth + shadowBuffer * 2;
backgroundWindow.visible = true;
}
contentWindow.visible = true;
Qt.callLater(() => {
animationsEnabled = true;
shouldBeVisible = true;
if (shouldBeVisible && screen) {
if (useBackgroundWindow)
backgroundWindow.visible = true;
contentWindow.visible = true;
PopoutManager.showPopout(root);
opened();
@@ -398,6 +413,8 @@ Item {
if (!shouldBeVisible) {
isClosing = false;
contentWindow.visible = false;
if (useBackgroundWindow)
backgroundWindow.visible = false;
PopoutManager.hidePopout(root);
popoutClosed();
}
@@ -519,27 +536,6 @@ Item {
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
readonly property real alignedWidth: Theme.px(popupWidth, dpr)
readonly property real alignedHeight: Theme.px(popupHeight, dpr)
property real renderedAlignedY: alignedY
property real renderedAlignedHeight: alignedHeight
readonly property bool renderedGeometryGrowing: alignedHeight >= renderedAlignedHeight
Behavior on renderedAlignedY {
enabled: root.animationsEnabled && contentWindow.visible && root.shouldBeVisible
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.renderedGeometryGrowing)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.renderedGeometryGrowing ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on renderedAlignedHeight {
enabled: root.animationsEnabled && contentWindow.visible && root.shouldBeVisible
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.renderedGeometryGrowing)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.renderedGeometryGrowing ? root.animationEnterCurve : root.animationExitCurve
}
}
readonly property real connectedAnchorX: {
if (!Theme.isConnectedEffect)
return triggerX;
@@ -576,7 +572,7 @@ Item {
}
onAlignedHeightChanged: {
_queueFullSync();
_syncPopoutChromeState();
if (!suspendShadowWhileResizing || !shouldBeVisible)
return;
_resizeActive = true;
@@ -665,6 +661,117 @@ Item {
return Math.max(100, screenHeight - maskY - bottomExclusion);
}
PanelWindow {
id: backgroundWindow
screen: root.screen
visible: false
color: "transparent"
Component.onCompleted: {
if (typeof updatesEnabled !== "undefined" && !root.overlayContent)
updatesEnabled = false;
}
WlrLayershell.namespace: root.layerNamespace + ":background"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
bottom: true
}
mask: Region {
item: maskRect
Region {
item: contentExclusionRect
intersection: Intersection.Subtract
}
}
Rectangle {
id: maskRect
visible: false
color: "transparent"
x: root._frozenMaskX
y: root._frozenMaskY
width: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskWidth : 0
height: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskHeight : 0
}
Item {
id: contentExclusionRect
visible: false
x: root.alignedX
y: root.alignedY
width: root.alignedWidth
height: root.alignedHeight
}
Item {
id: outsideClickCatcher
x: root._frozenMaskX
y: root._frozenMaskY
width: root._frozenMaskWidth
height: root._frozenMaskHeight
enabled: root.shouldBeVisible && root.backgroundInteractive
readonly property real contentLeft: Math.max(0, root.alignedX - x)
readonly property real contentTop: Math.max(0, root.alignedY - y)
readonly property real contentRight: Math.min(width, contentLeft + root.alignedWidth)
readonly property real contentBottom: Math.min(height, contentTop + root.alignedHeight)
MouseArea {
x: 0
y: 0
width: outsideClickCatcher.width
height: Math.max(0, outsideClickCatcher.contentTop)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.backgroundClicked()
}
MouseArea {
x: 0
y: outsideClickCatcher.contentBottom
width: outsideClickCatcher.width
height: Math.max(0, outsideClickCatcher.height - outsideClickCatcher.contentBottom)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.backgroundClicked()
}
MouseArea {
x: 0
y: outsideClickCatcher.contentTop
width: Math.max(0, outsideClickCatcher.contentLeft)
height: Math.max(0, outsideClickCatcher.contentBottom - outsideClickCatcher.contentTop)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.backgroundClicked()
}
MouseArea {
x: outsideClickCatcher.contentRight
y: outsideClickCatcher.contentTop
width: Math.max(0, outsideClickCatcher.width - outsideClickCatcher.contentRight)
height: Math.max(0, outsideClickCatcher.contentBottom - outsideClickCatcher.contentTop)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.backgroundClicked()
}
}
Loader {
id: overlayLoader
anchors.fill: parent
active: root.overlayContent !== null && backgroundWindow.visible
sourceComponent: root.overlayContent
}
}
PanelWindow {
id: contentWindow
screen: root.screen
@@ -717,37 +824,27 @@ Item {
return WlrKeyboardFocus.Exclusive;
}
readonly property bool _fullHeight: root.fullHeightSurface
readonly property bool _fullHeight: useBackgroundWindow && root.fullHeightSurface
anchors {
left: true
top: true
right: true
bottom: true
right: !useBackgroundWindow
bottom: _fullHeight || !useBackgroundWindow
}
WlrLayershell.margins {
left: 0
top: 0
left: useBackgroundWindow ? root._surfaceMarginLeft : 0
top: (useBackgroundWindow && !_fullHeight) ? (root.alignedY - shadowBuffer) : 0
}
implicitWidth: 0
implicitHeight: 0
implicitWidth: useBackgroundWindow ? root._surfaceW : 0
implicitHeight: (useBackgroundWindow && !_fullHeight) ? (root.alignedHeight + shadowBuffer * 2) : 0
mask: contentInputMask
mask: useBackgroundWindow ? contentInputMask : null
Region {
id: contentInputMask
// Outside-click dismissal needs full-screen input only while interactive.
item: (shouldBeVisible && backgroundInteractive) ? fullScreenMaskItem : contentMaskRect
}
Item {
id: fullScreenMaskItem
visible: false
x: 0
y: 0
width: 32767
height: 32767
item: contentMaskRect
}
Item {
@@ -756,18 +853,18 @@ Item {
x: contentContainer.x - contentContainer.horizontalConnectorExtent
y: contentContainer.y - contentContainer.verticalConnectorExtent
width: root.alignedWidth + contentContainer.horizontalConnectorExtent * 2
height: root.renderedAlignedHeight + contentContainer.verticalConnectorExtent * 2
height: root.alignedHeight + contentContainer.verticalConnectorExtent * 2
}
MouseArea {
anchors.fill: parent
enabled: shouldBeVisible && backgroundInteractive
enabled: !useBackgroundWindow && shouldBeVisible && backgroundInteractive
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
z: -1
onClicked: mouse => {
const clickX = mouse.x;
const clickY = mouse.y;
const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.renderedAlignedY || clickY > root.renderedAlignedY + root.renderedAlignedHeight;
const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight;
if (!outsideContent)
return;
backgroundClicked();
@@ -776,10 +873,10 @@ Item {
Item {
id: contentContainer
x: root.alignedX
y: root.renderedAlignedY
x: useBackgroundWindow ? shadowBuffer : root.alignedX
y: (useBackgroundWindow && !contentWindow._fullHeight) ? shadowBuffer : root.alignedY
width: root.alignedWidth
height: root.renderedAlignedHeight
height: root.alignedHeight
readonly property bool barTop: effectiveBarPosition === SettingsData.Position.Top
readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom
@@ -985,20 +1082,26 @@ Item {
return parent.height + clipOversize * 2;
}
// Roll-out clips a wrapper while content and shadow keep full-size geometry.
Item {
id: rollOutAdjuster
id: aligner
readonly property real baseWidth: contentContainer.width
readonly property real baseHeight: contentContainer.height
readonly property bool isRollOut: typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect
x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0
y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0
x: (directionalClipMask.x !== 0 ? -directionalClipMask.x : 0) + (isRollOut && contentContainer.barRight ? baseWidth * (1 - contentContainer.scaleValue) : 0)
y: (directionalClipMask.y !== 0 ? -directionalClipMask.y : 0) + (isRollOut && contentContainer.barBottom ? baseHeight * (1 - contentContainer.scaleValue) : 0)
width: isRollOut && (contentContainer.barLeft || contentContainer.barRight) ? Math.max(0, baseWidth * contentContainer.scaleValue) : baseWidth
height: isRollOut && (contentContainer.barTop || contentContainer.barBottom) ? Math.max(0, baseHeight * contentContainer.scaleValue) : baseHeight
clip: isRollOut
Item {
id: unrollCounteract
x: aligner.isRollOut && contentContainer.barRight ? -(aligner.baseWidth * (1 - contentContainer.scaleValue)) : 0
y: aligner.isRollOut && contentContainer.barBottom ? -(aligner.baseHeight * (1 - contentContainer.scaleValue)) : 0
width: aligner.baseWidth
height: aligner.baseHeight
ElevationShadow {
id: shadowSource
readonly property real connectorExtent: Theme.isConnectedEffect ? Theme.connectedCornerRadius : 0
@@ -1008,11 +1111,11 @@ Item {
readonly property real extraBottom: Theme.isConnectedEffect && (contentContainer.barLeft || contentContainer.barRight) ? connectorExtent : 0
readonly property real bodyX: extraLeft
readonly property real bodyY: extraTop
readonly property real bodyWidth: rollOutAdjuster.baseWidth
readonly property real bodyHeight: rollOutAdjuster.baseHeight
readonly property real bodyWidth: parent.width
readonly property real bodyHeight: parent.height
width: rollOutAdjuster.baseWidth + extraLeft + extraRight
height: rollOutAdjuster.baseHeight + extraTop + extraBottom
width: parent.width + extraLeft + extraRight
height: parent.height + extraTop + extraBottom
opacity: contentWrapper.opacity
scale: contentWrapper.scale
x: contentWrapper.x - extraLeft
@@ -1076,14 +1179,14 @@ Item {
Item {
id: contentWrapper
width: rollOutAdjuster.baseWidth
height: rollOutAdjuster.baseHeight
width: parent.width
height: parent.height
opacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0)
visible: opacity > 0
scale: rollOutAdjuster.isRollOut ? 1.0 : contentContainer.scaleValue
x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - scale) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - scale) * 0.5, root.dpr)
scale: aligner.isRollOut ? 1.0 : contentContainer.scaleValue
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - scale) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - scale) * 0.5, root.dpr)
layer.enabled: contentWrapper.opacity < 1
layer.smooth: false
@@ -1122,10 +1225,11 @@ Item {
active: root._primeContent || shouldBeVisible || contentWindow.visible
asynchronous: false
}
}
}
}
}
} // closes contentWrapper
} // closes unrollCounteract
} // closes aligner
} // closes directionalClipMask
} // closes contentContainer
Item {
id: focusHelper
@@ -1143,12 +1247,5 @@ Item {
}
}
}
Loader {
id: overlayLoader
anchors.fill: parent
active: root.overlayContent !== null && contentWindow.visible
sourceComponent: root.overlayContent
}
}
}

View File

@@ -52,14 +52,11 @@ PanelWindow {
anchors.bottom: true
anchors.right: true
// Expandable: fixed max surface width; strip width is slideContainer only (keeps blur/mask aligned).
implicitWidth: expandable ? expandedWidthValue : slideoutWidth
implicitHeight: modelData ? modelData.height : 800
color: "transparent"
readonly property bool slideoutBlurActive: root.visible && BlurService.enabled
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: 0
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
@@ -67,13 +64,12 @@ PanelWindow {
readonly property real dpr: CompositorService.getScreenScale(root.screen)
readonly property real alignedWidth: Theme.px(expandable && expandedWidth ? expandedWidthValue : slideoutWidth, dpr)
readonly property real alignedHeight: Theme.px(modelData ? modelData.height : 800, dpr)
readonly property real slideoutSlideSnapX: Theme.snap(slideContainer.slideOffset, dpr)
mask: Region {
item: Rectangle {
x: root.width - slideContainer.width
x: root.width - alignedWidth
y: 0
width: slideContainer.width
width: alignedWidth
height: root.height
}
}
@@ -109,11 +105,9 @@ PanelWindow {
}
}
// Expandable only; mask/blur bind to slideContainer geometry so they track this animation.
Behavior on width {
enabled: root.expandable
NumberAnimation {
duration: Theme.popoutAnimationDuration
duration: 250
easing.type: Easing.OutCubic
}
}
@@ -130,14 +124,12 @@ PanelWindow {
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width
x: root.slideoutSlideSnapX
x: Theme.snap(slideContainer.slideOffset, root.dpr)
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, contentRect.effectiveTransparency)
radius: Theme.cornerRadius
border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
border.width: BlurService.borderWidth
}
Column {
@@ -214,14 +206,4 @@ PanelWindow {
}
}
}
// Blur region from slideContainer (not layered contentRect); position uses x + slideoutSlideSnapX, not mapToItem(root).
WindowBlur {
targetWindow: root
blurX: root.slideoutBlurActive ? slideContainer.x + root.slideoutSlideSnapX : 0
blurY: root.slideoutBlurActive ? slideContainer.y : 0
blurWidth: root.slideoutBlurActive ? slideContainer.width : 0
blurHeight: root.slideoutBlurActive ? slideContainer.height : 0
blurRadius: Theme.cornerRadius
}
}