feat: added widgets capture, music visualiser, bluetooth, notes
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user