letsss GOOO
This commit is contained in:
@@ -0,0 +1,156 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: mixer
|
||||||
|
|
||||||
|
readonly property var streams: Pipewire.nodes.values.filter(node => node.audio && node.isSink && node.isStream)
|
||||||
|
readonly property int streamCount: streams.length
|
||||||
|
|
||||||
|
function appName(node) {
|
||||||
|
if (!node)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
const props = node.properties || {};
|
||||||
|
return props["application.name"] || AudioService.displayName(node) || "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function mediaName(node) {
|
||||||
|
const props = (node && node.properties) ? node.properties : {};
|
||||||
|
return props["media.name"] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function appIconValue(node) {
|
||||||
|
const props = (node && node.properties) ? node.properties : {};
|
||||||
|
const candidates = [props["application.icon-name"], props["application.process.binary"], props["application.name"]];
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (!candidate)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const entry = DesktopEntries.heuristicLookup(String(candidate));
|
||||||
|
if (entry && entry.icon)
|
||||||
|
return entry.icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "material:graphic_eq";
|
||||||
|
}
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
contentHeight: streamColumn.height
|
||||||
|
contentWidth: width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: streamColumn
|
||||||
|
|
||||||
|
width: mixer.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ScriptModel {
|
||||||
|
values: mixer.streams
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: streamItem
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
width: streamColumn.width
|
||||||
|
height: 86
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.surfaceLight, Theme.popupTransparency)
|
||||||
|
border.color: Theme.outlineLight
|
||||||
|
border.width: Theme.layerOutlineWidth
|
||||||
|
|
||||||
|
AppIconRenderer {
|
||||||
|
id: streamIcon
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
width: Theme.iconSize
|
||||||
|
height: Theme.iconSize
|
||||||
|
iconValue: mixer.appIconValue(streamItem.modelData)
|
||||||
|
iconSize: Theme.iconSize
|
||||||
|
fallbackText: mixer.appName(streamItem.modelData).charAt(0).toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: streamIcon.right
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: mixer.appName(streamItem.modelData)
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: mixer.mediaName(streamItem.modelData)
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
visible: text !== ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GameBarVolumeRow {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.bottomMargin: Theme.spacingXS
|
||||||
|
node: streamItem.modelData
|
||||||
|
maxPercent: 100
|
||||||
|
activeIcon: "volume_up"
|
||||||
|
mutedIcon: "volume_off"
|
||||||
|
}
|
||||||
|
|
||||||
|
PwObjectTracker {
|
||||||
|
objects: [streamItem.modelData]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: streamColumn.width
|
||||||
|
height: 120
|
||||||
|
visible: mixer.streamCount === 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
name: "music_off"
|
||||||
|
size: Theme.iconSizeLarge
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "No apps are playing audio"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: widget
|
||||||
|
|
||||||
|
readonly property real sectionSpacing: Theme.spacingS
|
||||||
|
|
||||||
|
GameBarVolumeRow {
|
||||||
|
id: outputVolume
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
node: AudioService.sink
|
||||||
|
maxPercent: AudioService.sinkMaxVolume
|
||||||
|
activeIcon: "volume_up"
|
||||||
|
mutedIcon: "volume_off"
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: outputLabel
|
||||||
|
|
||||||
|
anchors.top: outputVolume.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
text: "Output"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
GameBarDeviceList {
|
||||||
|
id: outputDevices
|
||||||
|
|
||||||
|
anchors.top: outputLabel.bottom
|
||||||
|
anchors.topMargin: Theme.spacingXS
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
height: Math.max(52, (parent.height - outputVolume.height - inputVolume.height - outputLabel.height - inputLabel.height - widget.sectionSpacing * 3 - Theme.spacingXS * 2) * 0.5)
|
||||||
|
isSink: true
|
||||||
|
}
|
||||||
|
|
||||||
|
GameBarVolumeRow {
|
||||||
|
id: inputVolume
|
||||||
|
|
||||||
|
anchors.top: outputDevices.bottom
|
||||||
|
anchors.topMargin: widget.sectionSpacing
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
node: AudioService.source
|
||||||
|
maxPercent: 100
|
||||||
|
activeIcon: "mic"
|
||||||
|
mutedIcon: "mic_off"
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: inputLabel
|
||||||
|
|
||||||
|
anchors.top: inputVolume.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
text: "Input"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
GameBarDeviceList {
|
||||||
|
anchors.top: inputLabel.bottom
|
||||||
|
anchors.topMargin: Theme.spacingXS
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
isSink: false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modules.Plugins
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
PluginComponent {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
pluginId: "gameBar"
|
||||||
|
pluginService: PluginService
|
||||||
|
|
||||||
|
readonly property bool overlayOpen: overlay.shouldBeVisible
|
||||||
|
|
||||||
|
// Widget catalogue. `id` is the persistence key, the rest drives the top bar
|
||||||
|
// toggles and the default placement (fractions of the screen size).
|
||||||
|
readonly property var widgetDefs: [
|
||||||
|
{
|
||||||
|
"id": "audio",
|
||||||
|
"title": "Audio",
|
||||||
|
"icon": "volume_up",
|
||||||
|
"width": 430,
|
||||||
|
"height": 470,
|
||||||
|
"x": 0.04,
|
||||||
|
"y": 0.14,
|
||||||
|
"defaultVisible": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "apps",
|
||||||
|
"title": "Apps",
|
||||||
|
"icon": "tune",
|
||||||
|
"width": 400,
|
||||||
|
"height": 360,
|
||||||
|
"x": 0.30,
|
||||||
|
"y": 0.14,
|
||||||
|
"defaultVisible": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "system",
|
||||||
|
"title": "Performance",
|
||||||
|
"icon": "speed",
|
||||||
|
"width": 380,
|
||||||
|
"height": 330,
|
||||||
|
"x": 0.56,
|
||||||
|
"y": 0.14,
|
||||||
|
"defaultVisible": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function widgetDef(widgetId) {
|
||||||
|
return widgetDefs.find(def => def.id === widgetId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Visibility ───────────────────────────────────────────────────────────
|
||||||
|
function savePluginValue(key, value) {
|
||||||
|
if (pluginService)
|
||||||
|
pluginService.savePluginData(pluginId, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWidgetVisible(widgetId) {
|
||||||
|
const stored = pluginData.visibleWidgets;
|
||||||
|
if (stored && stored[widgetId] !== undefined)
|
||||||
|
return stored[widgetId] === true;
|
||||||
|
|
||||||
|
const def = widgetDef(widgetId);
|
||||||
|
return def ? def.defaultVisible === true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWidgetVisible(widgetId, visible) {
|
||||||
|
const stored = Object.assign({}, pluginData.visibleWidgets || {});
|
||||||
|
stored[widgetId] = visible === true;
|
||||||
|
root.savePluginValue("visibleWidgets", stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleWidget(widgetId) {
|
||||||
|
root.setWidgetVisible(widgetId, !root.isWidgetVisible(widgetId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Positions ────────────────────────────────────────────────────────────
|
||||||
|
// Stored per screen as fractions of the screen size, so the same layout
|
||||||
|
// survives monitors of different resolutions (and is re-clamped on load).
|
||||||
|
function screenKey(screen) {
|
||||||
|
if (!screen)
|
||||||
|
return "default";
|
||||||
|
|
||||||
|
return SettingsData.getScreenDisplayName(screen) || screen.name || "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
function widgetPosition(screen, widgetId) {
|
||||||
|
const layout = pluginData.layout || {};
|
||||||
|
const perScreen = layout[root.screenKey(screen)];
|
||||||
|
const saved = perScreen ? perScreen[widgetId] : null;
|
||||||
|
if (saved && saved.x !== undefined && saved.y !== undefined)
|
||||||
|
return saved;
|
||||||
|
|
||||||
|
const def = widgetDef(widgetId);
|
||||||
|
return def ? ({
|
||||||
|
"x": def.x,
|
||||||
|
"y": def.y
|
||||||
|
}) : ({
|
||||||
|
"x": 0.1,
|
||||||
|
"y": 0.15
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveWidgetPosition(screen, widgetId, fractionX, fractionY) {
|
||||||
|
const layout = JSON.parse(JSON.stringify(pluginData.layout || {}));
|
||||||
|
const key = root.screenKey(screen);
|
||||||
|
if (!layout[key])
|
||||||
|
layout[key] = {};
|
||||||
|
|
||||||
|
layout[key][widgetId] = {
|
||||||
|
"x": fractionX,
|
||||||
|
"y": fractionY
|
||||||
|
};
|
||||||
|
root.savePluginValue("layout", layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetLayout() {
|
||||||
|
root.savePluginValue("layout", {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Overlay ──────────────────────────────────────────────────────────────
|
||||||
|
function openOverlay() {
|
||||||
|
if (overlay.shouldBeVisible)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (typeof PopoutService !== "undefined" && PopoutService)
|
||||||
|
PopoutService.closeControlCenter();
|
||||||
|
|
||||||
|
overlay.shouldBeVisible = true;
|
||||||
|
overlay.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeOverlay() {
|
||||||
|
if (!overlay.shouldBeVisible)
|
||||||
|
return;
|
||||||
|
|
||||||
|
overlay.shouldBeVisible = false;
|
||||||
|
overlay.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOverlay() {
|
||||||
|
if (overlay.shouldBeVisible)
|
||||||
|
root.closeOverlay();
|
||||||
|
else
|
||||||
|
root.openOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "gameBar"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
function toggle() : string {
|
||||||
|
root.toggleOverlay();
|
||||||
|
return "SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function open() : string {
|
||||||
|
root.openOverlay();
|
||||||
|
return "SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() : string {
|
||||||
|
root.closeOverlay();
|
||||||
|
return "SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function widget(name: string) : string {
|
||||||
|
if (!root.widgetDef(name))
|
||||||
|
return "UNKNOWN_WIDGET: " + name;
|
||||||
|
|
||||||
|
root.toggleWidget(name);
|
||||||
|
return "SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetLayout() : string {
|
||||||
|
root.resetLayout();
|
||||||
|
return "SUCCESS";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GameBarOverlay {
|
||||||
|
id: overlay
|
||||||
|
|
||||||
|
daemon: root
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (pluginService && pluginId) {
|
||||||
|
const instances = Object.assign({}, pluginService.pluginInstances);
|
||||||
|
instances[pluginId] = root;
|
||||||
|
pluginService.pluginInstances = instances;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
if (pluginService && pluginService.pluginInstances[pluginId] === root) {
|
||||||
|
const instances = Object.assign({}, pluginService.pluginInstances);
|
||||||
|
delete instances[pluginId];
|
||||||
|
pluginService.pluginInstances = instances;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: deviceList
|
||||||
|
|
||||||
|
property bool isSink: true
|
||||||
|
|
||||||
|
readonly property var activeNode: isSink ? AudioService.sink : AudioService.source
|
||||||
|
readonly property var deviceNodes: {
|
||||||
|
const hidden = isSink ? (SessionData.hiddenOutputDeviceNames ?? []) : (SessionData.hiddenInputDeviceNames ?? []);
|
||||||
|
const nodes = Pipewire.nodes.values.filter(node => {
|
||||||
|
if (!node.audio || node.isStream)
|
||||||
|
return false;
|
||||||
|
if (node.isSink !== isSink)
|
||||||
|
return false;
|
||||||
|
return !hidden.includes(node.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
const active = activeNode;
|
||||||
|
return [...nodes].sort((a, b) => {
|
||||||
|
if (a === active)
|
||||||
|
return -1;
|
||||||
|
if (b === active)
|
||||||
|
return 1;
|
||||||
|
return AudioService.displayName(a).localeCompare(AudioService.displayName(b));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function activate(node) {
|
||||||
|
if (!node || !node.name)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (deviceList.isSink)
|
||||||
|
AudioService.setDefaultSinkByName(node.name);
|
||||||
|
else
|
||||||
|
AudioService.setDefaultSourceByName(node.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
contentHeight: deviceColumn.height
|
||||||
|
contentWidth: width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: deviceColumn
|
||||||
|
|
||||||
|
width: deviceList.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ScriptModel {
|
||||||
|
values: deviceList.deviceNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: deviceItem
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
readonly property bool isActive: modelData === deviceList.activeNode
|
||||||
|
|
||||||
|
width: deviceColumn.width
|
||||||
|
height: 46
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: isActive ? Theme.primarySelected : (deviceArea.containsMouse ? Theme.surfaceHover : Theme.withAlpha(Theme.surfaceLight, Theme.popupTransparency))
|
||||||
|
border.color: isActive ? Theme.primary : Theme.outlineLight
|
||||||
|
border.width: isActive ? 2 : Theme.layerOutlineWidth
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
id: deviceIcon
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
name: deviceList.isSink ? AudioService.sinkIcon(deviceItem.modelData) : "mic"
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: deviceItem.isActive ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
id: activeCheck
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
name: "check_circle"
|
||||||
|
size: Theme.iconSize - 6
|
||||||
|
color: Theme.primary
|
||||||
|
visible: deviceItem.isActive
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: deviceIcon.right
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.right: activeCheck.visible ? activeCheck.left : parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: AudioService.displayName(deviceItem.modelData)
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: deviceItem.isActive ? Font.Medium : Font.Normal
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: AudioService.subtitle(deviceItem.modelData.name)
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
visible: text !== ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deviceArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: deviceList.activate(deviceItem.modelData)
|
||||||
|
}
|
||||||
|
|
||||||
|
PwObjectTracker {
|
||||||
|
objects: [deviceItem.modelData]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: deviceColumn.width
|
||||||
|
text: deviceList.isSink ? "No output devices found" : "No input devices found"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
visible: deviceList.deviceNodes.length === 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
GameBarPanel {
|
||||||
|
overlayRef: overlay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.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
|
||||||
|
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);
|
||||||
|
|
||||||
|
return {
|
||||||
|
"x": 0.1,
|
||||||
|
"y": 0.15
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveWidgetPosition(widgetId, fractionX, fractionY) {
|
||||||
|
if (daemon)
|
||||||
|
daemon.saveWidgetPosition(root.targetScreen, widgetId, fractionX, fractionY);
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
GameBarTopBar {
|
||||||
|
id: topBar
|
||||||
|
|
||||||
|
panel: root
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: Theme.spacingL
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.daemon ? root.daemon.widgetDefs : []
|
||||||
|
|
||||||
|
delegate: GameBarWindow {
|
||||||
|
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 "system":
|
||||||
|
return systemWidgetComponent;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: audioWidgetComponent
|
||||||
|
|
||||||
|
GameBarAudioWidget {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: appsWidgetComponent
|
||||||
|
|
||||||
|
GameBarAppMixer {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: systemWidgetComponent
|
||||||
|
|
||||||
|
GameBarSystemWidget {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modules.Plugins
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
PluginSettings {
|
||||||
|
id: settings
|
||||||
|
|
||||||
|
pluginId: "gameBar"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: hintColumn.implicitHeight + Theme.spacingM * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
|
border.color: Theme.outlineMedium
|
||||||
|
border.width: Theme.layerOutlineWidth
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: hintColumn
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Keybind setup"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: "Commands: dms ipc call gameBar toggle / open / close / widget <audio|apps|system> / resetLayout"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.family: Theme.monoFontFamily
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SliderSetting {
|
||||||
|
settingKey: "backdropOpacity"
|
||||||
|
label: "Background dimming"
|
||||||
|
description: "How much the desktop behind the overlay is darkened"
|
||||||
|
defaultValue: 70
|
||||||
|
minimum: 0
|
||||||
|
maximum: 95
|
||||||
|
unit: "%"
|
||||||
|
leftIcon: "light_mode"
|
||||||
|
rightIcon: "dark_mode"
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: resetColumn.implicitHeight
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: resetColumn
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Widget layout"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: "Widget positions are saved per monitor. Reset them to the default arrangement."
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: "Reset widget positions"
|
||||||
|
iconName: "restart_alt"
|
||||||
|
onClicked: settings.saveValue("layout", {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: widget
|
||||||
|
|
||||||
|
readonly property var activeModules: ["system", "cpu", "memory", "network", "disk", "diskmounts"]
|
||||||
|
readonly property var rootMount: {
|
||||||
|
const mounts = DgopService.diskMounts || [];
|
||||||
|
return mounts.find(mount => mount.mount === "/") || mounts[0] || null;
|
||||||
|
}
|
||||||
|
readonly property real diskPercent: {
|
||||||
|
if (!rootMount || !rootMount.percent)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return parseFloat(String(rootMount.percent).replace("%", "")) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRate(bytesPerSecond) {
|
||||||
|
const value = bytesPerSecond || 0;
|
||||||
|
if (value < 1024)
|
||||||
|
return value.toFixed(0) + " B/s";
|
||||||
|
if (value < 1024 * 1024)
|
||||||
|
return (value / 1024).toFixed(0) + " K/s";
|
||||||
|
if (value < 1024 * 1024 * 1024)
|
||||||
|
return (value / (1024 * 1024)).toFixed(1) + " M/s";
|
||||||
|
return (value / (1024 * 1024 * 1024)).toFixed(1) + " G/s";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatGiB(megabytes) {
|
||||||
|
return ((megabytes || 0) / 1024).toFixed(1) + " GiB";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats only get polled while this widget exists.
|
||||||
|
Component.onCompleted: DgopService.addRef(widget.activeModules)
|
||||||
|
Component.onDestruction: DgopService.removeRef(widget.activeModules)
|
||||||
|
|
||||||
|
component StatTile: Rectangle {
|
||||||
|
id: tile
|
||||||
|
|
||||||
|
property string icon: ""
|
||||||
|
property string label: ""
|
||||||
|
property string value: ""
|
||||||
|
property string detail: ""
|
||||||
|
property real fraction: 0
|
||||||
|
property color accent: Theme.primary
|
||||||
|
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.surfaceLight, Theme.popupTransparency)
|
||||||
|
border.color: Theme.outlineLight
|
||||||
|
border.width: Theme.layerOutlineWidth
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
id: tileIcon
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
name: tile.icon
|
||||||
|
size: Theme.iconSize - 6
|
||||||
|
color: tile.accent
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.left: tileIcon.right
|
||||||
|
anchors.leftMargin: Theme.spacingXS
|
||||||
|
anchors.verticalCenter: tileIcon.verticalCenter
|
||||||
|
text: tile.label
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: tileValue
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: tileIcon.verticalCenter
|
||||||
|
text: tile.value
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: tileDetail
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.bottom: tileTrack.top
|
||||||
|
anchors.bottomMargin: Theme.spacingXS
|
||||||
|
text: tile.detail
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: tileTrack
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.bottomMargin: Theme.spacingM
|
||||||
|
height: 5
|
||||||
|
radius: height / 2
|
||||||
|
color: Theme.withAlpha(Theme.outline, 0.4)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width * Math.max(0, Math.min(1, tile.fraction))
|
||||||
|
height: parent.height
|
||||||
|
radius: parent.radius
|
||||||
|
color: tile.accent
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Grid {
|
||||||
|
anchors.fill: parent
|
||||||
|
columns: 2
|
||||||
|
rowSpacing: Theme.spacingS
|
||||||
|
columnSpacing: Theme.spacingS
|
||||||
|
|
||||||
|
readonly property real tileWidth: (width - columnSpacing) / 2
|
||||||
|
readonly property real tileHeight: (height - rowSpacing) / 2
|
||||||
|
|
||||||
|
StatTile {
|
||||||
|
width: parent.tileWidth
|
||||||
|
height: parent.tileHeight
|
||||||
|
icon: "memory"
|
||||||
|
label: "CPU"
|
||||||
|
value: Math.round(DgopService.cpuUsage || 0) + "%"
|
||||||
|
detail: DgopService.cpuTemperature > 0 ? DgopService.cpuTemperature.toFixed(0) + "°C" : (DgopService.cpuCores + " cores")
|
||||||
|
fraction: (DgopService.cpuUsage || 0) / 100
|
||||||
|
accent: DgopService.cpuUsage > 85 ? Theme.error : (DgopService.cpuUsage > 60 ? Theme.warning : Theme.primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
StatTile {
|
||||||
|
width: parent.tileWidth
|
||||||
|
height: parent.tileHeight
|
||||||
|
icon: "developer_board"
|
||||||
|
label: "Memory"
|
||||||
|
value: Math.round(DgopService.memoryUsage || 0) + "%"
|
||||||
|
detail: widget.formatGiB(DgopService.usedMemoryMB) + " / " + widget.formatGiB(DgopService.totalMemoryMB)
|
||||||
|
fraction: (DgopService.memoryUsage || 0) / 100
|
||||||
|
accent: DgopService.memoryUsage > 85 ? Theme.error : (DgopService.memoryUsage > 60 ? Theme.warning : Theme.primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
StatTile {
|
||||||
|
width: parent.tileWidth
|
||||||
|
height: parent.tileHeight
|
||||||
|
icon: "swap_vert"
|
||||||
|
label: "Network"
|
||||||
|
value: "↓ " + widget.formatRate(DgopService.networkRxRate)
|
||||||
|
detail: "↑ " + widget.formatRate(DgopService.networkTxRate)
|
||||||
|
// Scaled against 100 MiB/s so the bar stays readable on fast links.
|
||||||
|
fraction: Math.min(1, ((DgopService.networkRxRate || 0) + (DgopService.networkTxRate || 0)) / (100 * 1024 * 1024))
|
||||||
|
accent: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
StatTile {
|
||||||
|
width: parent.tileWidth
|
||||||
|
height: parent.tileHeight
|
||||||
|
icon: "storage"
|
||||||
|
label: widget.rootMount ? widget.rootMount.mount : "Disk"
|
||||||
|
value: Math.round(widget.diskPercent) + "%"
|
||||||
|
detail: "R " + widget.formatRate(DgopService.diskReadRate) + " W " + widget.formatRate(DgopService.diskWriteRate)
|
||||||
|
fraction: widget.diskPercent / 100
|
||||||
|
accent: widget.diskPercent > 90 ? Theme.error : (widget.diskPercent > 75 ? Theme.warning : Theme.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Theme.spacingL * 2
|
||||||
|
text: "dgop is not available - install it to see system stats."
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
visible: !DgopService.dgopAvailable
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: bar
|
||||||
|
|
||||||
|
property var panel: null
|
||||||
|
|
||||||
|
readonly property var widgetDefs: (panel && panel.daemon) ? panel.daemon.widgetDefs : []
|
||||||
|
|
||||||
|
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 {}
|
||||||
|
|
||||||
|
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: 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,99 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: row
|
||||||
|
|
||||||
|
property var node: null
|
||||||
|
property int maxPercent: 100
|
||||||
|
property string activeIcon: "volume_up"
|
||||||
|
property string mutedIcon: "volume_off"
|
||||||
|
|
||||||
|
readonly property var audio: (node && node.audio) ? node.audio : null
|
||||||
|
readonly property bool muted: audio ? audio.muted : false
|
||||||
|
readonly property int percent: audio ? Math.round(audio.volume * 100) : 0
|
||||||
|
|
||||||
|
readonly property int sliderMaximum: Math.max(100, maxPercent)
|
||||||
|
|
||||||
|
// DankSlider assigns its own `value` while dragging, which breaks a declarative
|
||||||
|
// binding, so the slider is synced imperatively whenever the node volume changes.
|
||||||
|
function syncSlider() {
|
||||||
|
slider.value = Math.min(row.sliderMaximum, row.percent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyVolume(newValue) {
|
||||||
|
if (!row.audio)
|
||||||
|
return;
|
||||||
|
|
||||||
|
row.audio.volume = newValue / 100;
|
||||||
|
if (newValue > 0 && row.audio.muted)
|
||||||
|
row.audio.muted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
height: 48
|
||||||
|
onPercentChanged: row.syncSlider()
|
||||||
|
onSliderMaximumChanged: row.syncSlider()
|
||||||
|
onNodeChanged: row.syncSlider()
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: muteButton
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 40
|
||||||
|
height: 40
|
||||||
|
radius: width / 2
|
||||||
|
color: muteArea.containsMouse ? Theme.primaryHover : "transparent"
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: (row.muted || row.percent === 0) ? row.mutedIcon : row.activeIcon
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: row.muted ? Theme.error : Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: muteArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: row.audio !== null
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (row.audio)
|
||||||
|
row.audio.muted = !row.audio.muted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: valueLabel
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 48
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
text: row.percent + "%"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: row.muted ? Theme.surfaceVariantText : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
id: slider
|
||||||
|
|
||||||
|
anchors.left: muteButton.right
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.right: valueLabel.left
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
enabled: row.audio !== null
|
||||||
|
minimum: 0
|
||||||
|
maximum: row.sliderMaximum
|
||||||
|
showValue: false
|
||||||
|
onSliderValueChanged: newValue => row.applyVolume(newValue)
|
||||||
|
Component.onCompleted: row.syncSlider()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
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 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positions are stored as fractions of the screen, so a layout made on a 4K
|
||||||
|
// display still lands somewhere sensible on a 1440p one (and vice versa).
|
||||||
|
function applyStoredPosition() {
|
||||||
|
if (!panel || panel.width <= 0 || panel.height <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const stored = panel.widgetPosition(win.widgetId);
|
||||||
|
win.x = win.clampX(stored.x * panel.width);
|
||||||
|
win.y = win.clampY(stored.y * panel.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
function raise() {
|
||||||
|
if (panel)
|
||||||
|
win.z = panel.nextWindowZ();
|
||||||
|
}
|
||||||
|
|
||||||
|
function storePosition() {
|
||||||
|
if (!panel || panel.width <= 0 || panel.height <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
panel.saveWidgetPosition(win.widgetId, win.x / panel.width, win.y / panel.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 no size yet while the delegate is being created, so the
|
||||||
|
// stored fraction has to be re-applied once the screen size is known.
|
||||||
|
onPanelWidthChanged: win.applyStoredPosition()
|
||||||
|
onPanelHeightChanged: 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 keule2
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Game Bar
|
||||||
|
|
||||||
|
An overlay for [DankMaterialShell](https://danklinux.com/docs/dankmaterialshell/plugins-overview).
|
||||||
|
|
||||||
|
## Widgets
|
||||||
|
|
||||||
|
| Widget | Contents |
|
||||||
|
|---|---|
|
||||||
|
| **Audio** | Master output volume + mute, output device switching, microphone volume + mute, input device switching |
|
||||||
|
| **Apps** | Per-application volume sliders with individual mute, app icon and what is playing |
|
||||||
|
| **Performance** | CPU (usage, temperature), memory (used / total), network (rx/tx), disk (root usage, read/write) |
|
||||||
|
|
||||||
|
### IPC commands
|
||||||
|
|
||||||
|
| Command | Effect |
|
||||||
|
|---|---|
|
||||||
|
| `dms ipc call gameBar toggle` | Toggle the overlay |
|
||||||
|
| `dms ipc call gameBar open` | Open the overlay |
|
||||||
|
| `dms ipc call gameBar close` | Close the overlay |
|
||||||
|
| `dms ipc call gameBar widget audio\|apps\|system` | Show/hide one widget |
|
||||||
|
| `dms ipc call gameBar resetLayout` | Forget all saved widget positions |
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
Settings → Plugins → Game Bar (gear icon):
|
||||||
|
|
||||||
|
- **Background dimming** — how dark the desktop behind the overlay gets (0–95%).
|
||||||
|
- **Reset widget positions** — back to the default arrangement on every monitor.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://github.com/AvengeMedia/DankMaterialShell/raw/refs/heads/master/PLUGINS/plugin-schema.json",
|
||||||
|
"id": "gameBar",
|
||||||
|
"name": "Game Bar",
|
||||||
|
"description": "Game Bar style overlay with quick actions and system monitoring",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "keule",
|
||||||
|
"icon": "sports_esports",
|
||||||
|
"type": "daemon",
|
||||||
|
"capabilities": [
|
||||||
|
"daemon",
|
||||||
|
"ipc"
|
||||||
|
],
|
||||||
|
"component": "./GameBarDaemon.qml",
|
||||||
|
"settings": "./GameBarSettings.qml",
|
||||||
|
"requires_dms": ">=1.5.0",
|
||||||
|
"permissions": [
|
||||||
|
"settings_read",
|
||||||
|
"settings_write"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user