feat: added widgets capture, music visualiser, bluetooth, notes

This commit is contained in:
2026-09-22 01:22:27 +02:00
parent 9b023a0993
commit d15bb4687c
25 changed files with 1675 additions and 55 deletions
+49
View File
@@ -0,0 +1,49 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Modals.Common
DankModal {
id: overlay
property var daemon: null
readonly property var pluginData: (daemon && daemon.pluginData) ? daemon.pluginData : ({})
readonly property real availableWidth: screenWidth > 0 ? screenWidth : 1920
readonly property real availableHeight: screenHeight > 0 ? screenHeight : 1080
function requestClose() {
if (overlay.daemon)
overlay.daemon.closeOverlay();
else
overlay.close();
}
layerNamespace: "dms:plugins:gameBar"
// Overlay layer puts the widgets above fullscreen windows, like the Xbox Game Bar.
useOverlayLayer: true
positioning: "center"
shouldBeVisible: false
closeOnEscapeKey: true
closeOnBackgroundClick: true
showBackground: true
backgroundOpacity: Math.max(0, Math.min(0.95, (pluginData.backdropOpacity ?? 70) / 100))
onBackgroundClicked: overlay.requestClose()
// The modal itself is the full screen; the widgets inside are the visible chrome.
backgroundColor: "transparent"
borderWidth: 0
cornerRadius: 0
enableShadow: false
// A full-screen surface should fade, not scale/slide in.
animationScaleCollapsed: 1
animationOffset: 0
modalWidth: availableWidth
modalHeight: availableHeight
content: Component {
Panel {
overlayRef: overlay
}
}
}
+189
View File
@@ -0,0 +1,189 @@
import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
import "../widgets"
FocusScope {
id: root
property var overlayRef: null
readonly property var daemon: overlayRef ? overlayRef.daemon : null
readonly property var pluginData: (daemon && daemon.pluginData) ? daemon.pluginData : ({})
readonly property var targetScreen: overlayRef ? overlayRef.effectiveScreen : null
readonly property real edgeMargin: Theme.spacingM
// Changes when the overlay lands on a different monitor (or that monitor
// changes resolution); widgets re-read their saved spot when it does.
readonly property string screenKey: daemon ? daemon.screenKey(root.targetScreen) : ""
readonly property real topLimit: topBar.y + topBar.height + Theme.spacingM
property int windowZCounter: 1
function nextWindowZ() {
root.windowZCounter = root.windowZCounter + 1;
return root.windowZCounter;
}
function requestClose() {
if (overlayRef)
overlayRef.requestClose();
}
function isWidgetVisible(widgetId) {
return daemon ? daemon.isWidgetVisible(widgetId) : false;
}
function toggleWidget(widgetId) {
if (daemon)
daemon.toggleWidget(widgetId);
}
function widgetPosition(widgetId) {
if (daemon)
return daemon.widgetPosition(root.targetScreen, widgetId, root.width, root.height);
return {
"x": root.width * 0.1,
"y": root.height * 0.15
};
}
function saveWidgetPosition(widgetId, x, y) {
if (daemon)
daemon.saveWidgetPosition(root.targetScreen, widgetId, x, y);
}
anchors.fill: parent
focus: true
// The modal's own Escape handler sits on a sibling focus scope, so the key
// never reaches it once this content holds focus - handle it here instead.
Keys.onEscapePressed: event => {
root.requestClose();
event.accepted = true;
}
Component.onCompleted: root.forceActiveFocus()
// Keeps the default sink/source properties live while the overlay is open.
PwObjectTracker {
objects: [AudioService.sink, AudioService.source].filter(node => !!node)
}
// Clicking empty space closes, like clicking outside a dialog.
MouseArea {
anchors.fill: parent
onClicked: root.requestClose()
}
TopBar {
id: topBar
panel: root
anchors.top: parent.top
anchors.topMargin: Theme.spacingL
anchors.horizontalCenter: parent.horizontalCenter
}
Repeater {
model: root.daemon ? root.daemon.widgetDefs : []
delegate: WidgetWindow {
required property var modelData
panel: root
widgetId: modelData.id
title: modelData.title
icon: modelData.icon
widgetWidth: modelData.width
widgetHeight: modelData.height
minimumY: root.topLimit
shown: root.isWidgetVisible(modelData.id)
contentComponent: {
switch (modelData.id) {
case "audio":
return audioWidgetComponent;
case "apps":
return appsWidgetComponent;
case "capture":
return captureWidgetComponent;
case "media":
return mediaWidgetComponent;
case "visualizer":
return visualizerWidgetComponent;
case "bluetooth":
return bluetoothWidgetComponent;
case "notepad":
return notepadWidgetComponent;
case "home":
return homeWidgetComponent;
case "system":
return systemWidgetComponent;
}
return null;
}
}
}
Component {
id: audioWidgetComponent
AudioWidget {}
}
Component {
id: appsWidgetComponent
AppMixer {}
}
Component {
id: captureWidgetComponent
CaptureWidget {
service: root.daemon ? root.daemon.captureService : null
panel: root
}
}
Component {
id: visualizerWidgetComponent
VisualizerWidget {}
}
Component {
id: bluetoothWidgetComponent
BluetoothWidget {}
}
Component {
id: notepadWidgetComponent
NotepadWidget {}
}
Component {
id: mediaWidgetComponent
MediaWidget {}
}
Component {
id: homeWidgetComponent
HomeWidget {
service: root.daemon ? root.daemon.homeService : null
}
}
Component {
id: systemWidgetComponent
SystemWidget {}
}
}
+156
View File
@@ -0,0 +1,156 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
import "../components"
Rectangle {
id: bar
property var panel: null
readonly property var widgetDefs: (panel && panel.daemon) ? panel.daemon.widgetDefs : []
// Labelled chips stop fitting once there are a lot of widgets on a narrow
// panel, so fall back to icon-only toggles.
readonly property bool compact: (panel ? panel.width : 1920) < widgetDefs.length * 110 + 560
width: barRow.width + Theme.spacingL * 2
height: 56
radius: height / 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
SystemClock {
id: clock
precision: SystemClock.Minutes
}
component BarSeparator: Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 26
color: Theme.outlineMedium
}
Row {
id: barRow
anchors.centerIn: parent
spacing: Theme.spacingM
Row {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: "sports_esports"
size: Theme.iconSize
color: Theme.primary
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 0
StyledText {
text: Qt.formatDateTime(clock.date, "HH:mm")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: Qt.formatDateTime(clock.date, "ddd d MMM")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
BarSeparator {}
BrightnessControl {
anchors.verticalCenter: parent.verticalCenter
daemon: bar.panel ? bar.panel.daemon : null
screenName: (bar.panel && bar.panel.targetScreen && bar.panel.targetScreen.name) ? bar.panel.targetScreen.name : ""
}
BarSeparator {}
Row {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Repeater {
model: bar.widgetDefs
delegate: Rectangle {
id: toggleButton
required property var modelData
readonly property bool active: bar.panel ? bar.panel.isWidgetVisible(modelData.id) : false
width: bar.compact ? 40 : toggleRow.width + Theme.spacingM * 2
height: 38
radius: Theme.cornerRadius
color: active ? Theme.primarySelected : (toggleArea.containsMouse ? Theme.surfaceHover : "transparent")
border.color: active ? Theme.primary : "transparent"
border.width: active ? 1 : 0
Row {
id: toggleRow
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: toggleButton.modelData.icon
size: Theme.iconSize - 6
color: toggleButton.active ? Theme.primary : Theme.surfaceText
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: toggleButton.modelData.title
font.pixelSize: Theme.fontSizeSmall
font.weight: toggleButton.active ? Font.Medium : Font.Normal
color: toggleButton.active ? Theme.primary : Theme.surfaceText
visible: !bar.compact
}
}
MouseArea {
id: toggleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (bar.panel)
bar.panel.toggleWidget(toggleButton.modelData.id);
}
}
}
}
}
BarSeparator {}
DankActionButton {
anchors.verticalCenter: parent.verticalCenter
iconName: "close"
buttonSize: 34
iconSize: 18
iconColor: Theme.surfaceText
onClicked: {
if (bar.panel)
bar.panel.requestClose();
}
}
}
}
+204
View File
@@ -0,0 +1,204 @@
import QtQuick
import qs.Common
import qs.Widgets
Item {
id: win
property var panel: null
property string widgetId: ""
property string title: ""
property string icon: ""
property real widgetWidth: 360
property real widgetHeight: 320
property real minimumY: 0
property bool shown: false
property Component contentComponent: null
readonly property real margin: panel ? panel.edgeMargin : 12
readonly property real panelWidth: panel ? panel.width : 0
readonly property real panelHeight: panel ? panel.height : 0
readonly property string screenKey: panel ? panel.screenKey : ""
// Which monitor the current x/y were resolved for; guards against saving a
// position under a screen it was never arranged on.
property string appliedKey: ""
readonly property real maxX: Math.max(margin, (panel ? panel.width : width) - width - margin)
readonly property real maxY: Math.max(minimumY, (panel ? panel.height : height) - height - margin)
function clampX(value) {
return Math.max(margin, Math.min(maxX, value));
}
function clampY(value) {
return Math.max(minimumY, Math.min(maxY, value));
}
// Each monitor keeps its own absolute arrangement, so moving between a 4K
// and a 1440p screen restores what was arranged there rather than a scaled
// copy of the other one.
function applyStoredPosition() {
if (!panel || panel.width <= 0 || panel.height <= 0)
return;
const stored = panel.widgetPosition(win.widgetId);
win.x = win.clampX(stored.x);
win.y = win.clampY(stored.y);
win.appliedKey = win.screenKey;
}
function raise() {
if (panel)
win.z = panel.nextWindowZ();
}
function storePosition() {
if (!panel || panel.width <= 0 || panel.height <= 0)
return;
// Mid-transition to another monitor: the position on screen does not
// belong to the new screen yet.
if (win.screenKey === "" || win.screenKey !== win.appliedKey)
return;
panel.saveWidgetPosition(win.widgetId, win.x, win.y);
}
width: widgetWidth
height: widgetHeight
visible: shown && opacity > 0
opacity: shown ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
Component.onCompleted: win.applyStoredPosition()
onShownChanged: {
if (shown) {
win.applyStoredPosition();
win.raise();
}
}
// The panel has neither size nor a resolved screen while the delegate is
// created, and the two settle independently - the screen can change without
// a resize (same resolution) and vice versa, so react to both.
onPanelWidthChanged: win.applyStoredPosition()
onPanelHeightChanged: win.applyStoredPosition()
onScreenKeyChanged: win.applyStoredPosition()
Rectangle {
id: frame
anchors.fill: parent
radius: Theme.cornerRadius * 1.25
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: dragArea.pressed ? Theme.primary : Theme.outlineMedium
border.width: dragArea.pressed ? 2 : Theme.layerOutlineWidth
Rectangle {
id: titleBar
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 38
radius: frame.radius
color: dragArea.containsMouse ? Theme.surfaceContainerHigh : "transparent"
// Square off the bottom corners so the title bar blends into the body.
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: parent.radius
color: parent.color
}
DankIcon {
id: titleIcon
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
name: win.icon
size: Theme.iconSize - 6
color: Theme.primary
}
StyledText {
anchors.left: titleIcon.right
anchors.leftMargin: Theme.spacingS
anchors.right: closeButton.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: win.title
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
}
DankActionButton {
id: closeButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
iconName: "close"
buttonSize: 26
iconSize: 15
iconColor: Theme.surfaceVariantText
onClicked: {
if (win.panel)
win.panel.toggleWidget(win.widgetId);
}
}
MouseArea {
id: dragArea
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: closeButton.left
hoverEnabled: true
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
drag.target: win
drag.axis: Drag.XAndYAxis
drag.minimumX: win.margin
drag.maximumX: win.maxX
drag.minimumY: win.minimumY
drag.maximumY: win.maxY
drag.threshold: 0
onPressed: win.raise()
onReleased: win.storePosition()
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.AllButtons
propagateComposedEvents: true
z: -1
onPressed: mouse => {
win.raise();
mouse.accepted = false;
}
}
Loader {
anchors.top: titleBar.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingM
active: win.shown
sourceComponent: win.contentComponent
}
}
}