Compare commits

..
2 Commits
27 changed files with 772 additions and 139 deletions

No files matched your search

@@ -4,7 +4,7 @@ import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Modules.Plugins import qs.Modules.Plugins
import qs.Services import qs.Services
import qs.Widgets import qs.DCommon.Widgets
import "./components" import "./components"
PluginComponent { PluginComponent {
@@ -12,7 +12,7 @@ PluginComponent {
property var popoutService: null property var popoutService: null
readonly property var daemon: (typeof PluginService !== "undefined" && PluginService.pluginInstances) ? PluginService.pluginInstances["gameBar"] : null readonly property var daemon: PluginService.pluginDaemonInstances["quickpanel"] || null
// "focused" follows whichever monitor has focus, "own" sticks to the screen // "focused" follows whichever monitor has focus, "own" sticks to the screen
// this particular bar instance lives on. // this particular bar instance lives on.
readonly property string targetMode: String(pluginData.barBrightnessTarget || "focused") readonly property string targetMode: String(pluginData.barBrightnessTarget || "focused")
@@ -76,7 +76,7 @@ PluginComponent {
DisplayService.setBrightness(next, root.deviceId, false); DisplayService.setBrightness(next, root.deviceId, false);
} }
pluginId: "gameBar" pluginId: "quickpanel"
pluginService: PluginService pluginService: PluginService
popoutWidth: 320 popoutWidth: 320
popoutHeight: 150 popoutHeight: 150
@@ -117,7 +117,7 @@ PluginComponent {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankIcon { DIcon {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
name: root.brightnessIcon name: root.brightnessIcon
size: root.iconSize size: root.iconSize
@@ -150,7 +150,7 @@ PluginComponent {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 0 spacing: 0
DankIcon { DIcon {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
name: root.brightnessIcon name: root.brightnessIcon
size: root.iconSize size: root.iconSize
+58 -19
View File
@@ -10,7 +10,7 @@ import "./overlay"
PluginComponent { PluginComponent {
id: root id: root
pluginId: "gameBar" pluginId: "quickpanel"
pluginService: PluginService pluginService: PluginService
readonly property bool overlayOpen: overlay.shouldBeVisible readonly property bool overlayOpen: overlay.shouldBeVisible
@@ -38,6 +38,16 @@ PluginComponent {
"y": 0.14, "y": 0.14,
"defaultVisible": true "defaultVisible": true
}, },
{
"id": "timer",
"title": "Timer",
"icon": "timer",
"width": 380,
"height": 320,
"x": 0.57,
"y": 0.58,
"defaultVisible": false
},
{ {
"id": "capture", "id": "capture",
"title": "Capture", "title": "Capture",
@@ -120,6 +130,22 @@ PluginComponent {
pluginService.savePluginData(pluginId, key, value); pluginService.savePluginData(pluginId, key, value);
} }
function migrateLegacySettings() {
if (!pluginService || pluginData.migratedToQuickpanel === true)
return;
// Prefer the most recent name. Keep explicit Quickpanel settings and
// retain old entries as backups; enabled state belongs to this plugin.
for (const legacyId of ["controlCenter", "gameBar"]) {
const legacy = SettingsData.getPluginSettingsForPlugin(legacyId) || {};
for (const key of Object.keys(legacy)) {
if (key !== "enabled" && key !== "migratedFromGameBar" && pluginData[key] === undefined)
root.savePluginValue(key, legacy[key]);
}
}
root.savePluginValue("migratedToQuickpanel", true);
}
function isWidgetVisible(widgetId) { function isWidgetVisible(widgetId) {
const stored = pluginData.visibleWidgets; const stored = pluginData.visibleWidgets;
if (stored && stored[widgetId] !== undefined) if (stored && stored[widgetId] !== undefined)
@@ -230,7 +256,7 @@ PluginComponent {
} }
root.brightnessMapScanning = true; root.brightnessMapScanning = true;
Proc.runCommand("gameBar.ddcDetect", ["ddcutil", "detect", "--brief"], (stdout, exitCode) => { Proc.runCommand("quickpanel.ddcDetect", ["ddcutil", "detect", "--brief"], (stdout, exitCode) => {
root.brightnessMapScanning = false; root.brightnessMapScanning = false;
if (exitCode !== 0) if (exitCode !== 0)
return; return;
@@ -331,9 +357,8 @@ PluginComponent {
// ── Overlay ────────────────────────────────────────────────────────────── // ── Overlay ──────────────────────────────────────────────────────────────
function openOverlay() { function openOverlay() {
if (overlay.shouldBeVisible) // No early return on shouldBeVisible: the flag can survive a shell
return; // restart with no window mapped, which used to make open() a no-op.
if (typeof PopoutService !== "undefined" && PopoutService) if (typeof PopoutService !== "undefined" && PopoutService)
PopoutService.closeControlCenter(); PopoutService.closeControlCenter();
@@ -357,7 +382,7 @@ PluginComponent {
} }
IpcHandler { IpcHandler {
target: "gameBar" target: "quickpanel"
enabled: true enabled: true
function toggle() : string { function toggle() : string {
@@ -416,6 +441,25 @@ PluginComponent {
return "SUCCESS"; return "SUCCESS";
} }
function timer(seconds: string, label: string) : string {
const value = parseInt(seconds, 10);
if (isNaN(value) || value <= 0)
return "USAGE: timer <seconds> [label]";
timers.resetTimer();
timers.startTimer(value * 1000, label || "");
return "STARTED " + timers.formatDuration(value * 1000);
}
function timerStop() : string {
timers.resetTimer();
return "SUCCESS";
}
function timerStatus() : string {
return "timer=" + (timers.timerRunning ? "running" : (timers.timerFinished ? "finished" : "idle")) + " remaining=" + timers.formatDuration(timers.remainingMs) + " label=" + (timers.timerLabel || "(none)") + " stopwatch=" + (timers.stopwatchRunning ? "running" : "idle") + " elapsed=" + timers.formatStopwatch(timers.stopwatchElapsedMs);
}
function layoutStatus() : string { function layoutStatus() : string {
const lines = Quickshell.screens.map(screen => { const lines = Quickshell.screens.map(screen => {
const key = root.screenKey(screen); const key = root.screenKey(screen);
@@ -459,6 +503,13 @@ PluginComponent {
readonly property alias homeService: haService readonly property alias homeService: haService
readonly property alias captureService: capture readonly property alias captureService: capture
readonly property alias timerService: timers
TimerService {
id: timers
daemon: root
}
CaptureService { CaptureService {
id: capture id: capture
@@ -491,19 +542,7 @@ PluginComponent {
} }
Component.onCompleted: { Component.onCompleted: {
root.migrateLegacySettings();
root.ensureBrightnessMap(); root.ensureBrightnessMap();
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;
}
} }
} }
+28 -7
View File
@@ -1,13 +1,13 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Modules.Plugins import qs.Modules.Plugins
import qs.Widgets import qs.DCommon.Widgets
import "./settings" import "./settings"
PluginSettings { PluginSettings {
id: settings id: settings
pluginId: "gameBar" pluginId: "quickpanel"
Rectangle { Rectangle {
width: parent.width width: parent.width
@@ -35,7 +35,7 @@ PluginSettings {
StyledText { StyledText {
width: parent.width width: parent.width
text: "Commands: dms ipc call gameBar toggle / open / close / widget <name> / resetLayout" text: "Commands: dms ipc call quickpanel toggle / open / close / widget <name> / resetLayout"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.family: Theme.monoFontFamily font.family: Theme.monoFontFamily
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
@@ -66,7 +66,7 @@ PluginSettings {
SelectionSetting { SelectionSetting {
settingKey: "barBrightnessTarget" settingKey: "barBrightnessTarget"
label: "Bar brightness widget controls" label: "Bar brightness widget controls"
description: "Which display the DankBar brightness pill adjusts" description: "Which display the bar brightness pill adjusts"
options: [ options: [
{ {
"label": "Focused monitor", "label": "Focused monitor",
@@ -80,12 +80,33 @@ PluginSettings {
defaultValue: "focused" defaultValue: "focused"
} }
ToggleSetting {
settingKey: "timerPlaySound"
label: "Play a sound when a timer ends"
defaultValue: true
}
ToggleSetting {
settingKey: "timerNotify"
label: "Send a notification when a timer ends"
description: "Shows the timer's label, so it is visible even with the overlay closed"
defaultValue: true
}
StringSetting {
settingKey: "timerSound"
label: "Timer sound"
description: "Any file paplay can play"
defaultValue: "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"
placeholder: "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"
}
StringSetting { StringSetting {
settingKey: "captureDirectory" settingKey: "captureDirectory"
label: "Capture folder" label: "Capture folder"
description: "Where recordings and replays are written" description: "Where recordings and replays are written"
defaultValue: "~/Videos/GameBar" defaultValue: "~/Videos/Quickpanel"
placeholder: "~/Videos/GameBar" placeholder: "~/Videos/Quickpanel"
} }
SliderSetting { SliderSetting {
@@ -200,7 +221,7 @@ PluginSettings {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
DankButton { DButton {
text: "Reset widget positions" text: "Reset widget positions"
iconName: "restart_alt" iconName: "restart_alt"
onClicked: settings.saveValue("layout", {}) onClicked: settings.saveValue("layout", {})
+32 -19
View File
@@ -1,6 +1,14 @@
# Game Bar # Quickpanel
An overlay for [DankMaterialShell](https://danklinux.com/docs/dankmaterialshell/plugins-overview). An overlay for [Dux Shell (DMS)](https://duxlabs.dev/docs/dms/plugin-development), requiring DMS 2.0 or newer.
Previously named Game Bar / ControlCenter. The plugin directory and ID are now `quickpanel`,
and IPC commands use `dms ipc call quickpanel …` (distinct from the shell's
built-in `control-center` target). On first enable, existing ControlCenter or Game Bar settings,
tokens, and widget layouts are copied automatically without overwriting existing
Quickpanel settings; the old settings entries are retained as backups. Update any bar widget
references from `gameBar` or `controlCenter` to `quickpanel` and any keybinds to the new IPC target.
The Git remote retains its existing repository URL until renamed on the server.
## Widgets ## Widgets
@@ -13,12 +21,13 @@ An overlay for [DankMaterialShell](https://danklinux.com/docs/dankmaterialshell/
| **Visualiser** | Bars driven by whatever is actually playing out of this machine (needs `cava`) | | **Visualiser** | Bars driven by whatever is actually playing out of this machine (needs `cava`) |
| **Bluetooth** | Paired devices with battery, connect/disconnect, adapter and scan toggles | | **Bluetooth** | Paired devices with battery, connect/disconnect, adapter and scan toggles |
| **Notes** | The shell's notepad scratch tab, editable in place; file-backed tabs are shown read-only | | **Notes** | The shell's notepad scratch tab, editable in place; file-backed tabs are shown read-only |
| **Timer** | Countdown with presets and an optional label, plus a stopwatch tab with laps; ends with a sound and a notification even when the overlay is closed |
| **Performance** | CPU (usage, temperature), memory (used / total), network (rx/tx), disk (root usage, read/write) | | **Performance** | CPU (usage, temperature), memory (used / total), network (rx/tx), disk (root usage, read/write) |
| **Home** | Home Assistant entities you pick: lights (toggle + brightness), numbers (slider over the entity's own min/max/step), switches/fans, media players (volume, mute, play/pause), covers, scenes and scripts, plus read-only sensors | | **Home** | Home Assistant entities you pick: lights (toggle + brightness), numbers (slider over the entity's own min/max/step), switches/fans, media players (volume, mute, play/pause), covers, scenes and scripts, plus read-only sensors |
## Home Assistant ## Home Assistant
Settings → Plugins → Game Bar: Settings → Plugins → Quickpanel:
1. **Home Assistant URL**, e.g. `http://homeassistant.local:8123` 1. **Home Assistant URL**, e.g. `http://homeassistant.local:8123`
2. **Long-lived access token** — HA profile page → *Security* → *Create token*. 2. **Long-lived access token** — HA profile page → *Security* → *Create token*.
@@ -29,7 +38,7 @@ Settings → Plugins → Game Bar:
State is polled (default every 5 s) only while the Home widget is open. The token is handed to State is polled (default every 5 s) only while the Home widget is open. The token is handed to
`curl` through the process environment, so it never shows up in the process list. `curl` through the process environment, so it never shows up in the process list.
If a refresh fails, `dms ipc call gameBar homeStatus` shows what the plugin actually sent and got If a refresh fails, `dms ipc call quickpanel homeStatus` shows what the plugin actually sent and got
back. A `401` means Home Assistant rejected the token; compare with back. A `401` means Home Assistant rejected the token; compare with
```bash ```bash
@@ -41,28 +50,32 @@ Note that editing `plugin_settings.json` by hand does not reach a running shell.
| Command | Effect | | Command | Effect |
|---|---| |---|---|
| `dms ipc call gameBar toggle` | Toggle the overlay | | `dms ipc call quickpanel toggle` | Toggle the overlay |
| `dms ipc call gameBar open` | Open the overlay | | `dms ipc call quickpanel open` | Open the overlay |
| `dms ipc call gameBar close` | Close the overlay | | `dms ipc call quickpanel close` | Close the overlay |
| `dms ipc call gameBar widget audio\|apps\|media\|home\|system` | Show/hide one widget | | `dms ipc call quickpanel widget audio\|apps\|media\|home\|system` | Show/hide one widget |
| `dms ipc call gameBar homeToggle <entity_id>` | Toggle an entity (activates a scene/script/button) | | `dms ipc call quickpanel homeToggle <entity_id>` | Toggle an entity (activates a scene/script/button) |
| `dms ipc call gameBar homeRefresh` | Re-poll Home Assistant now | | `dms ipc call quickpanel homeRefresh` | Re-poll Home Assistant now |
| `dms ipc call gameBar homeStatus` | Print URL, token length, HTTP status, entity/picked counts and last error | | `dms ipc call quickpanel homeStatus` | Print URL, token length, HTTP status, entity/picked counts and last error |
| `dms ipc call gameBar record` | Start/stop recording the focused monitor | | `dms ipc call quickpanel record` | Start/stop recording the focused monitor |
| `dms ipc call gameBar replay` | Arm/disarm the replay buffer | | `dms ipc call quickpanel replay` | Arm/disarm the replay buffer |
| `dms ipc call gameBar saveReplay` | Write the last N seconds to disk | | `dms ipc call quickpanel saveReplay` | Write the last N seconds to disk |
| `dms ipc call gameBar captureStatus` | Print recorder mode, monitor, elapsed time and last file | | `dms ipc call quickpanel captureStatus` | Print recorder mode, monitor, elapsed time and last file |
| `dms ipc call gameBar brightnessStatus` | Print the display→DDC map, pins and resolved brightness per screen | | `dms ipc call quickpanel brightnessStatus` | Print the display→DDC map, pins and resolved brightness per screen |
| `dms ipc call gameBar layoutStatus` | Print each screen's layout key and where every widget resolves | | `dms ipc call quickpanel layoutStatus` | Print each screen's layout key and where every widget resolves |
| `dms ipc call gameBar resetLayout` | Forget all saved widget positions | | `dms ipc call quickpanel timer <seconds> [label]` | Start a countdown |
| `dms ipc call quickpanel timerStop` | Stop and reset the countdown |
| `dms ipc call quickpanel timerStatus` | Print countdown and stopwatch state |
| `dms ipc call quickpanel resetLayout` | Forget all saved widget positions |
## Settings ## Settings
Settings → Plugins → Game Bar (gear icon): Settings → Plugins → Quickpanel (gear icon):
- **Background dimming** — how dark the desktop behind the overlay gets (0–95%). - **Background dimming** — how dark the desktop behind the overlay gets (0–95%).
- **Capture** — folder, frame rate, quality, replay buffer length and the recorded audio source - **Capture** — folder, frame rate, quality, replay buffer length and the recorded audio source
(a `gpu-screen-recorder` device name, e.g. `default_output`; `gpu-screen-recorder --list-audio-devices`). (a `gpu-screen-recorder` device name, e.g. `default_output`; `gpu-screen-recorder --list-audio-devices`).
- **Timer** — sound on/off, notification on/off and which sound file to play.
- **Home Assistant** — URL, access token, refresh interval, and the entity picker. - **Home Assistant** — URL, access token, refresh interval, and the entity picker.
- **Reset widget positions** — back to the default arrangement on every monitor. - **Reset widget positions** — back to the default arrangement on every monitor.
+4 -4
View File
@@ -2,7 +2,7 @@ import QtQuick
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.DCommon.Widgets
Item { Item {
id: control id: control
@@ -48,7 +48,7 @@ Item {
daemon.ensureBrightnessMap(); daemon.ensureBrightnessMap();
} }
DankIcon { DIcon {
id: icon id: icon
anchors.left: parent.left anchors.left: parent.left
@@ -66,7 +66,7 @@ Item {
color: control.usable ? Theme.primary : Theme.surfaceVariantText color: control.usable ? Theme.primary : Theme.surfaceVariantText
} }
DankSlider { DSlider {
id: slider id: slider
anchors.left: icon.right anchors.left: icon.right
@@ -103,7 +103,7 @@ Item {
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
DankTooltipV2 { DTooltipV2 {
id: tooltip id: tooltip
} }
+4 -4
View File
@@ -3,9 +3,9 @@ import Quickshell
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.DCommon.Widgets
DankFlickable { DFlickable {
id: deviceList id: deviceList
property bool isSink: true property bool isSink: true
@@ -70,7 +70,7 @@ DankFlickable {
border.color: isActive ? Theme.primary : Theme.outlineLight border.color: isActive ? Theme.primary : Theme.outlineLight
border.width: isActive ? 2 : Theme.layerOutlineWidth border.width: isActive ? 2 : Theme.layerOutlineWidth
DankIcon { DIcon {
id: deviceIcon id: deviceIcon
anchors.left: parent.left anchors.left: parent.left
@@ -81,7 +81,7 @@ DankFlickable {
color: deviceItem.isActive ? Theme.primary : Theme.surfaceText color: deviceItem.isActive ? Theme.primary : Theme.surfaceText
} }
DankIcon { DIcon {
id: activeCheck id: activeCheck
anchors.right: parent.right anchors.right: parent.right
+4 -4
View File
@@ -1,7 +1,7 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.DCommon.Widgets
Item { Item {
id: row id: row
@@ -17,7 +17,7 @@ Item {
readonly property int sliderMaximum: Math.max(100, maxPercent) readonly property int sliderMaximum: Math.max(100, maxPercent)
// DankSlider assigns its own `value` while dragging, which breaks a declarative // DSlider assigns its own `value` while dragging, which breaks a declarative
// binding, so the slider is synced imperatively whenever the node volume changes. // binding, so the slider is synced imperatively whenever the node volume changes.
function syncSlider() { function syncSlider() {
slider.value = Math.min(row.sliderMaximum, row.percent); slider.value = Math.min(row.sliderMaximum, row.percent);
@@ -47,7 +47,7 @@ Item {
radius: width / 2 radius: width / 2
color: muteArea.containsMouse ? Theme.primaryHover : "transparent" color: muteArea.containsMouse ? Theme.primaryHover : "transparent"
DankIcon { DIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: (row.muted || row.percent === 0) ? row.mutedIcon : row.activeIcon name: (row.muted || row.percent === 0) ? row.mutedIcon : row.activeIcon
size: Theme.iconSize size: Theme.iconSize
@@ -81,7 +81,7 @@ Item {
color: row.muted ? Theme.surfaceVariantText : Theme.surfaceText color: row.muted ? Theme.surfaceVariantText : Theme.surfaceText
} }
DankSlider { DSlider {
id: slider id: slider
anchors.left: muteButton.right anchors.left: muteButton.right
+3 -3
View File
@@ -3,7 +3,7 @@ import Quickshell
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
DankModal { DModal {
id: overlay id: overlay
property var daemon: null property var daemon: null
@@ -19,8 +19,8 @@ DankModal {
overlay.close(); overlay.close();
} }
layerNamespace: "dms:plugins:gameBar" layerNamespace: "dms:plugins:quickpanel"
// Overlay layer puts the widgets above fullscreen windows, like the Xbox Game Bar. // Overlay layer puts the controls above fullscreen windows.
useOverlayLayer: true useOverlayLayer: true
positioning: "center" positioning: "center"
shouldBeVisible: false shouldBeVisible: false
+11 -1
View File
@@ -3,7 +3,7 @@ import Quickshell
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.DCommon.Widgets
import "../widgets" import "../widgets"
FocusScope { FocusScope {
@@ -108,6 +108,8 @@ FocusScope {
return audioWidgetComponent; return audioWidgetComponent;
case "apps": case "apps":
return appsWidgetComponent; return appsWidgetComponent;
case "timer":
return timerWidgetComponent;
case "capture": case "capture":
return captureWidgetComponent; return captureWidgetComponent;
case "media": case "media":
@@ -140,6 +142,14 @@ FocusScope {
AppMixer {} AppMixer {}
} }
Component {
id: timerWidgetComponent
TimerWidget {
service: root.daemon ? root.daemon.timerService : null
}
}
Component { Component {
id: captureWidgetComponent id: captureWidgetComponent
+5 -5
View File
@@ -1,7 +1,7 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Widgets import qs.DCommon.Widgets
import "../components" import "../components"
Rectangle { Rectangle {
@@ -44,9 +44,9 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS spacing: Theme.spacingS
DankIcon { DIcon {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
name: "sports_esports" name: "tune"
size: Theme.iconSize size: Theme.iconSize
color: Theme.primary color: Theme.primary
} }
@@ -107,7 +107,7 @@ Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankIcon { DIcon {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
name: toggleButton.modelData.icon name: toggleButton.modelData.icon
size: Theme.iconSize - 6 size: Theme.iconSize - 6
@@ -141,7 +141,7 @@ Rectangle {
BarSeparator {} BarSeparator {}
DankActionButton { DActionButton {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "close" iconName: "close"
buttonSize: 34 buttonSize: 34
+3 -3
View File
@@ -1,6 +1,6 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Widgets import qs.DCommon.Widgets
Item { Item {
id: win id: win
@@ -117,7 +117,7 @@ Item {
color: parent.color color: parent.color
} }
DankIcon { DIcon {
id: titleIcon id: titleIcon
anchors.left: parent.left anchors.left: parent.left
@@ -141,7 +141,7 @@ Item {
elide: Text.ElideRight elide: Text.ElideRight
} }
DankActionButton { DActionButton {
id: closeButton id: closeButton
anchors.right: parent.right anchors.right: parent.right
+10 -10
View File
@@ -1,12 +1,12 @@
{ {
"$schema": "https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/master/quickshell/PLUGINS/plugin-schema.json", "$schema": "https://raw.githubusercontent.com/duxlabs-dev/dux-shell/master/quickshell/PLUGINS/plugin-schema.json",
"id": "gameBar", "id": "quickpanel",
"name": "Game Bar", "name": "Quickpanel",
"description": "Game Bar style overlay with movable widgets for quick settings and controls", "description": "Control center overlay with movable widgets for quick settings and controls",
"version": "2.5.0", "version": "3.0.0",
"license": "MIT", "license": "MIT",
"author": "keule2", "author": "keule2",
"icon": "sports_esports", "icon": "tune",
"type": "composite", "type": "composite",
"capabilities": [ "capabilities": [
"daemon", "daemon",
@@ -15,11 +15,11 @@
"ipc" "ipc"
], ],
"components": { "components": {
"daemon": "./GameBarDaemon.qml", "daemon": "./QuickpanelDaemon.qml",
"widget": "./GameBarBarWidget.qml" "widget": "./QuickpanelBarWidget.qml"
}, },
"settings": "./GameBarSettings.qml", "settings": "./QuickpanelSettings.qml",
"requires_dms": ">=1.5.0", "requires_dms": ">=2.0.0",
"permissions": [ "permissions": [
"settings_read", "settings_read",
"settings_write", "settings_write",
+4 -4
View File
@@ -9,7 +9,7 @@ Item {
property var daemon: null property var daemon: null
readonly property var pluginData: (daemon && daemon.pluginData) ? daemon.pluginData : ({}) readonly property var pluginData: (daemon && daemon.pluginData) ? daemon.pluginData : ({})
readonly property string saveDirectory: String(pluginData.captureDirectory || "~/Videos/GameBar").trim() readonly property string saveDirectory: String(pluginData.captureDirectory || "~/Videos/Quickpanel").trim()
readonly property int fps: Math.max(10, pluginData.captureFps || 60) readonly property int fps: Math.max(10, pluginData.captureFps || 60)
readonly property int replaySeconds: Math.max(5, pluginData.captureReplaySeconds || 30) readonly property int replaySeconds: Math.max(5, pluginData.captureReplaySeconds || 30)
readonly property string audioSource: String(pluginData.captureAudio || "default_output").trim() readonly property string audioSource: String(pluginData.captureAudio || "default_output").trim()
@@ -40,7 +40,7 @@ Item {
function timestampedFile() { function timestampedFile() {
const now = new Date(); const now = new Date();
const stamp = Qt.formatDateTime(now, "yyyy-MM-dd_HH-mm-ss"); const stamp = Qt.formatDateTime(now, "yyyy-MM-dd_HH-mm-ss");
return service.expandedDirectory() + "/GameBar_" + stamp + ".mp4"; return service.expandedDirectory() + "/Quickpanel_" + stamp + ".mp4";
} }
function baseArgs(monitor) { function baseArgs(monitor) {
@@ -54,7 +54,7 @@ Item {
service.targetMonitor = monitor || "focused"; service.targetMonitor = monitor || "focused";
service.lastFile = service.timestampedFile(); service.lastFile = service.timestampedFile();
service.lastError = ""; service.lastError = "";
recorder.command = ["sh", "-c", 'mkdir -p "$1" && exec gpu-screen-recorder "${@:2}"', "gameBar", service.expandedDirectory()].concat(service.baseArgs(service.targetMonitor)).concat(["-o", service.lastFile]); recorder.command = ["sh", "-c", 'mkdir -p "$1" && exec gpu-screen-recorder "${@:2}"', "quickpanel", service.expandedDirectory()].concat(service.baseArgs(service.targetMonitor)).concat(["-o", service.lastFile]);
recorder.running = true; recorder.running = true;
service.mode = "recording"; service.mode = "recording";
service.startedAt = Date.now(); service.startedAt = Date.now();
@@ -68,7 +68,7 @@ Item {
service.targetMonitor = monitor || "focused"; service.targetMonitor = monitor || "focused";
service.lastError = ""; service.lastError = "";
recorder.command = ["sh", "-c", 'mkdir -p "$1" && exec gpu-screen-recorder "${@:2}"', "gameBar", service.expandedDirectory()].concat(service.baseArgs(service.targetMonitor)).concat(["-c", "mp4", "-r", String(service.replaySeconds), "-o", service.expandedDirectory()]); recorder.command = ["sh", "-c", 'mkdir -p "$1" && exec gpu-screen-recorder "${@:2}"', "quickpanel", service.expandedDirectory()].concat(service.baseArgs(service.targetMonitor)).concat(["-c", "mp4", "-r", String(service.replaySeconds), "-o", service.expandedDirectory()]);
recorder.running = true; recorder.running = true;
service.mode = "replay"; service.mode = "replay";
service.startedAt = Date.now(); service.startedAt = Date.now();
+2 -2
View File
@@ -75,7 +75,7 @@ Item {
statesProc.environment = { statesProc.environment = {
"GAMEBAR_HA_TOKEN": service.token "GAMEBAR_HA_TOKEN": service.token
}; };
statesProc.command = ["sh", "-c", service.curlGetScript, "gameBar", service.baseUrl + "/api/states"]; statesProc.command = ["sh", "-c", service.curlGetScript, "quickpanel", service.baseUrl + "/api/states"];
statesProc.running = true; statesProc.running = true;
} }
@@ -100,7 +100,7 @@ Item {
callProc.environment = { callProc.environment = {
"GAMEBAR_HA_TOKEN": service.token "GAMEBAR_HA_TOKEN": service.token
}; };
callProc.command = ["sh", "-c", service.curlPostScript, "gameBar", next.url, next.body]; callProc.command = ["sh", "-c", service.curlPostScript, "quickpanel", next.url, next.body];
callProc.running = true; callProc.running = true;
} }
+200
View File
@@ -0,0 +1,200 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
Item {
id: service
property var daemon: null
readonly property var pluginData: (daemon && daemon.pluginData) ? daemon.pluginData : ({})
readonly property string alarmSound: String(pluginData.timerSound || "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga").trim()
readonly property bool notifyOnFinish: pluginData.timerNotify !== false
readonly property bool soundOnFinish: pluginData.timerPlaySound !== false
// ── Countdown ────────────────────────────────────────────────────────────
property int durationMs: 5 * 60 * 1000
property int remainingMs: durationMs
property double deadline: 0
property bool timerRunning: false
property bool timerFinished: false
property string timerLabel: ""
readonly property real timerProgress: durationMs > 0 ? Math.max(0, Math.min(1, 1 - remainingMs / durationMs)) : 0
// ── Stopwatch ────────────────────────────────────────────────────────────
property double stopwatchStartedAt: 0
property int stopwatchBaseMs: 0
property int stopwatchElapsedMs: 0
property bool stopwatchRunning: false
property var laps: []
signal finished(string label)
function formatDuration(ms) {
const total = Math.max(0, Math.round(ms / 1000));
const hours = Math.floor(total / 3600);
const mins = Math.floor((total % 3600) / 60);
const secs = total % 60;
const pad = value => (value < 10 ? "0" : "") + value;
return hours > 0 ? hours + ":" + pad(mins) + ":" + pad(secs) : pad(mins) + ":" + pad(secs);
}
function formatStopwatch(ms) {
const hundredths = Math.floor((Math.max(0, ms) % 1000) / 10);
return service.formatDuration(ms) + "." + (hundredths < 10 ? "0" : "") + hundredths;
}
function setDuration(ms) {
service.durationMs = Math.max(1000, Math.round(ms));
if (!service.timerRunning) {
service.remainingMs = service.durationMs;
service.timerFinished = false;
}
}
function addTime(ms) {
if (service.timerRunning) {
service.deadline += ms;
service.durationMs = Math.max(1000, service.durationMs + ms);
service._tick();
return;
}
service.setDuration(service.durationMs + ms);
}
function startTimer(ms, label) {
if (ms !== undefined && ms !== null)
service.setDuration(ms);
if (label !== undefined && label !== null)
service.timerLabel = String(label);
if (service.remainingMs <= 0)
service.remainingMs = service.durationMs;
service.timerFinished = false;
service.deadline = Date.now() + service.remainingMs;
service.timerRunning = true;
}
function pauseTimer() {
if (!service.timerRunning)
return;
service.remainingMs = Math.max(0, service.deadline - Date.now());
service.timerRunning = false;
}
function toggleTimer() {
if (service.timerRunning)
service.pauseTimer();
else
service.startTimer();
}
function resetTimer() {
service.timerRunning = false;
service.timerFinished = false;
service.remainingMs = service.durationMs;
}
function _tick() {
if (!service.timerRunning)
return;
const left = service.deadline - Date.now();
if (left <= 0) {
service.remainingMs = 0;
service.timerRunning = false;
service.timerFinished = true;
service._announce();
return;
}
service.remainingMs = left;
}
// Fires whether or not the overlay is open - the service lives in the daemon.
function _announce() {
const label = service.timerLabel.trim();
const body = label !== "" ? label : "Timer finished after " + service.formatDuration(service.durationMs);
if (service.soundOnFinish && service.alarmSound !== "")
Quickshell.execDetached(["sh", "-c", '[ -r "$1" ] && exec paplay "$1"', "quickpanel", service.alarmSound]);
if (service.notifyOnFinish)
Quickshell.execDetached(["notify-send", "-a", "Game Bar", "-u", "critical", "-i", "timer", "Timer finished", body]);
if (typeof ToastService !== "undefined" && ToastService)
ToastService.showInfo("Timer finished", body);
service.finished(label);
}
function dismissFinished() {
service.timerFinished = false;
service.remainingMs = service.durationMs;
}
// ── Stopwatch control ────────────────────────────────────────────────────
function startStopwatch() {
if (service.stopwatchRunning)
return;
service.stopwatchStartedAt = Date.now();
service.stopwatchRunning = true;
}
function pauseStopwatch() {
if (!service.stopwatchRunning)
return;
service.stopwatchBaseMs = service.stopwatchElapsedMs;
service.stopwatchRunning = false;
}
function toggleStopwatch() {
if (service.stopwatchRunning)
service.pauseStopwatch();
else
service.startStopwatch();
}
function resetStopwatch() {
service.stopwatchRunning = false;
service.stopwatchBaseMs = 0;
service.stopwatchElapsedMs = 0;
service.laps = [];
}
function lap() {
if (!service.stopwatchRunning && service.stopwatchElapsedMs === 0)
return;
const previous = service.laps.length > 0 ? service.laps[0].total : 0;
service.laps = [
{
"index": service.laps.length + 1,
"total": service.stopwatchElapsedMs,
"split": service.stopwatchElapsedMs - previous
}
].concat(service.laps);
}
Timer {
running: service.timerRunning
interval: 200
repeat: true
onTriggered: service._tick()
}
Timer {
running: service.stopwatchRunning
interval: 50
repeat: true
onTriggered: service.stopwatchElapsedMs = service.stopwatchBaseMs + (Date.now() - service.stopwatchStartedAt)
}
}
+6 -6
View File
@@ -2,14 +2,14 @@ import QtQuick
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.DCommon.Widgets
Item { Item {
id: haSettings id: haSettings
property var settings: null property var settings: null
readonly property var daemon: (typeof PluginService !== "undefined" && PluginService.pluginInstances) ? PluginService.pluginInstances["gameBar"] : null readonly property var daemon: PluginService.pluginDaemonInstances["quickpanel"] || null
readonly property var service: daemon ? daemon.homeService : null readonly property var service: daemon ? daemon.homeService : null
readonly property var entityList: service ? service.entityList : [] readonly property var entityList: service ? service.entityList : []
@@ -122,7 +122,7 @@ Item {
elide: Text.ElideRight elide: Text.ElideRight
} }
DankButton { DButton {
id: refreshButton id: refreshButton
text: "Refresh" text: "Refresh"
@@ -132,7 +132,7 @@ Item {
} }
} }
DankTextField { DTextField {
width: parent.width width: parent.width
placeholderText: "Filter entities (e.g. light, kitchen)" placeholderText: "Filter entities (e.g. light, kitchen)"
leftIconName: "search" leftIconName: "search"
@@ -151,7 +151,7 @@ Item {
border.width: Theme.layerOutlineWidth border.width: Theme.layerOutlineWidth
visible: haSettings.entityList.length > 0 visible: haSettings.entityList.length > 0
DankListView { DListView {
id: entityListView id: entityListView
anchors.fill: parent anchors.fill: parent
@@ -198,7 +198,7 @@ Item {
} }
} }
DankToggle { DToggle {
id: pickToggle id: pickToggle
anchors.right: parent.right anchors.right: parent.right
+3 -3
View File
@@ -3,10 +3,10 @@ import Quickshell
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.DCommon.Widgets
import "../components" import "../components"
DankFlickable { DFlickable {
id: mixer id: mixer
readonly property var streams: Pipewire.nodes.values.filter(node => node.audio && node.isSink && node.isStream) readonly property var streams: Pipewire.nodes.values.filter(node => node.audio && node.isSink && node.isStream)
@@ -138,7 +138,7 @@ DankFlickable {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS spacing: Theme.spacingS
DankIcon { DIcon {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
name: "music_off" name: "music_off"
size: Theme.iconSizeLarge size: Theme.iconSizeLarge
+1 -1
View File
@@ -1,7 +1,7 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.DCommon.Widgets
import "../components" import "../components"
Item { Item {
+8 -8
View File
@@ -3,7 +3,7 @@ import Quickshell
import Quickshell.Bluetooth import Quickshell.Bluetooth
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.DCommon.Widgets
Item { Item {
id: widget id: widget
@@ -28,7 +28,7 @@ Item {
height: 34 height: 34
spacing: Theme.spacingS spacing: Theme.spacingS
DankIcon { DIcon {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
name: widget.adapterEnabled ? "bluetooth" : "bluetooth_disabled" name: widget.adapterEnabled ? "bluetooth" : "bluetooth_disabled"
size: Theme.iconSize - 6 size: Theme.iconSize - 6
@@ -44,7 +44,7 @@ Item {
elide: Text.ElideRight elide: Text.ElideRight
} }
DankActionButton { DActionButton {
id: scanButton id: scanButton
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -59,7 +59,7 @@ Item {
} }
} }
DankToggle { DToggle {
id: toggle id: toggle
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -73,7 +73,7 @@ Item {
} }
} }
DankListView { DListView {
id: deviceList id: deviceList
anchors.top: header.bottom anchors.top: header.bottom
@@ -101,7 +101,7 @@ Item {
border.color: isConnected ? Theme.primary : Theme.outlineLight border.color: isConnected ? Theme.primary : Theme.outlineLight
border.width: isConnected ? 1 : Theme.layerOutlineWidth border.width: isConnected ? 1 : Theme.layerOutlineWidth
DankIcon { DIcon {
id: deviceIcon id: deviceIcon
anchors.left: parent.left anchors.left: parent.left
@@ -142,7 +142,7 @@ Item {
} }
} }
DankSpinner { DSpinner {
id: busyIndicator id: busyIndicator
anchors.right: parent.right anchors.right: parent.right
@@ -175,7 +175,7 @@ Item {
spacing: Theme.spacingS spacing: Theme.spacingS
visible: !deviceList.visible visible: !deviceList.visible
DankIcon { DIcon {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
name: "bluetooth_disabled" name: "bluetooth_disabled"
size: Theme.iconSizeLarge size: Theme.iconSizeLarge
+2 -2
View File
@@ -1,6 +1,6 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Widgets import qs.DCommon.Widgets
Item { Item {
id: widget id: widget
@@ -45,7 +45,7 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS spacing: Theme.spacingS
DankIcon { DIcon {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
name: button.icon name: button.icon
size: Theme.iconSize - 6 size: Theme.iconSize - 6
+11 -11
View File
@@ -1,6 +1,6 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Widgets import qs.DCommon.Widgets
Rectangle { Rectangle {
id: row id: row
@@ -30,7 +30,7 @@ Rectangle {
const parsed = parseFloat(state); const parsed = parseFloat(state);
return isNaN(parsed) ? numberMin : parsed; return isNaN(parsed) ? numberMin : parsed;
} }
// DankSlider works in whole numbers, so only drive it directly when the // DSlider works in whole numbers, so only drive it directly when the
// entity's own range is integral; otherwise map it onto 0-100. // entity's own range is integral; otherwise map it onto 0-100.
readonly property bool numberDirect: isNumber && Number.isInteger(numberMin) && Number.isInteger(numberMax) && Number.isInteger(numberStep) readonly property bool numberDirect: isNumber && Number.isInteger(numberMin) && Number.isInteger(numberMax) && Number.isInteger(numberStep)
readonly property string numberUnit: attributes.unit_of_measurement || "" readonly property string numberUnit: attributes.unit_of_measurement || ""
@@ -142,7 +142,7 @@ Rectangle {
border.width: row.isOn ? 1 : Theme.layerOutlineWidth border.width: row.isOn ? 1 : Theme.layerOutlineWidth
opacity: available ? 1 : 0.55 opacity: available ? 1 : 0.55
DankIcon { DIcon {
id: entityIcon id: entityIcon
anchors.left: parent.left anchors.left: parent.left
@@ -192,7 +192,7 @@ Rectangle {
anchors.topMargin: 6 anchors.topMargin: 6
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankActionButton { DActionButton {
visible: row.isCover visible: row.isCover
iconName: "keyboard_arrow_up" iconName: "keyboard_arrow_up"
buttonSize: 30 buttonSize: 30
@@ -201,7 +201,7 @@ Rectangle {
onClicked: row.service.cover(row.entityId, "open_cover") onClicked: row.service.cover(row.entityId, "open_cover")
} }
DankActionButton { DActionButton {
visible: row.isCover visible: row.isCover
iconName: "stop" iconName: "stop"
buttonSize: 30 buttonSize: 30
@@ -210,7 +210,7 @@ Rectangle {
onClicked: row.service.cover(row.entityId, "stop_cover") onClicked: row.service.cover(row.entityId, "stop_cover")
} }
DankActionButton { DActionButton {
visible: row.isCover visible: row.isCover
iconName: "keyboard_arrow_down" iconName: "keyboard_arrow_down"
buttonSize: 30 buttonSize: 30
@@ -219,7 +219,7 @@ Rectangle {
onClicked: row.service.cover(row.entityId, "close_cover") onClicked: row.service.cover(row.entityId, "close_cover")
} }
DankActionButton { DActionButton {
visible: row.isMediaPlayer visible: row.isMediaPlayer
iconName: row.attributes.is_volume_muted === true ? "volume_off" : "volume_up" iconName: row.attributes.is_volume_muted === true ? "volume_off" : "volume_up"
buttonSize: 30 buttonSize: 30
@@ -228,7 +228,7 @@ Rectangle {
onClicked: row.service.setMuted(row.entityId, row.attributes.is_volume_muted !== true) onClicked: row.service.setMuted(row.entityId, row.attributes.is_volume_muted !== true)
} }
DankActionButton { DActionButton {
visible: row.isMediaPlayer visible: row.isMediaPlayer
iconName: row.state === "playing" ? "pause" : "play_arrow" iconName: row.state === "playing" ? "pause" : "play_arrow"
buttonSize: 30 buttonSize: 30
@@ -239,7 +239,7 @@ Rectangle {
}) })
} }
DankToggle { DToggle {
visible: row.isToggleable visible: row.isToggleable
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
checked: row.isOn checked: row.isOn
@@ -247,7 +247,7 @@ Rectangle {
onToggled: isChecked => row.service.toggle(row.entityId) onToggled: isChecked => row.service.toggle(row.entityId)
} }
DankActionButton { DActionButton {
visible: row.isButton visible: row.isButton
iconName: "play_arrow" iconName: "play_arrow"
buttonSize: 30 buttonSize: 30
@@ -257,7 +257,7 @@ Rectangle {
} }
} }
DankSlider { DSlider {
id: slider id: slider
visible: row.hasSlider visible: row.hasSlider
+3 -3
View File
@@ -1,7 +1,7 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Widgets import qs.DCommon.Widgets
Item { Item {
id: widget id: widget
@@ -24,7 +24,7 @@ Item {
service.removeConsumer(); service.removeConsumer();
} }
DankFlickable { DFlickable {
id: list id: list
anchors.fill: parent anchors.fill: parent
@@ -59,7 +59,7 @@ Item {
spacing: Theme.spacingS spacing: Theme.spacingS
visible: !list.visible visible: !list.visible
DankIcon { DIcon {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
name: widget.configured ? "playlist_add" : "home" name: widget.configured ? "playlist_add" : "home"
size: Theme.iconSizeLarge size: Theme.iconSizeLarge
+10 -9
View File
@@ -3,6 +3,7 @@ import Quickshell.Services.Mpris
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
import qs.DCommon.Widgets
Item { Item {
id: widget id: widget
@@ -32,15 +33,14 @@ Item {
anchors.fill: parent anchors.fill: parent
visible: widget.hasPlayer visible: widget.hasPlayer
DankAlbumArt { MediaArtwork {
id: art id: art
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top anchors.top: parent.top
width: 84 width: 84
height: 84 height: 84
activePlayer: widget.activePlayer artUrl: TrackArtService.resolvedArtUrl || (widget.activePlayer ? widget.activePlayer.trackArtUrl : "") || ""
showAnimation: widget.isPlaying
} }
Column { Column {
@@ -81,7 +81,7 @@ Item {
} }
} }
DankSeekbar { DSeekbar {
id: seekbar id: seekbar
anchors.left: parent.left anchors.left: parent.left
@@ -90,6 +90,7 @@ Item {
anchors.topMargin: Theme.spacingS anchors.topMargin: Theme.spacingS
height: 20 height: 20
activePlayer: widget.activePlayer activePlayer: widget.activePlayer
stableLength: MprisController.activePlayerStableLength
} }
StyledText { StyledText {
@@ -119,7 +120,7 @@ Item {
anchors.topMargin: Theme.spacingXS anchors.topMargin: Theme.spacingXS
spacing: Theme.spacingM spacing: Theme.spacingM
DankActionButton { DActionButton {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "skip_previous" iconName: "skip_previous"
buttonSize: 38 buttonSize: 38
@@ -137,7 +138,7 @@ Item {
color: Theme.primary color: Theme.primary
opacity: (widget.hasPlayer && widget.activePlayer.canTogglePlaying) ? 1 : 0.5 opacity: (widget.hasPlayer && widget.activePlayer.canTogglePlaying) ? 1 : 0.5
DankIcon { DIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: widget.isPlaying ? "pause" : "play_arrow" name: widget.isPlaying ? "pause" : "play_arrow"
size: 26 size: 26
@@ -153,7 +154,7 @@ Item {
} }
} }
DankActionButton { DActionButton {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "skip_next" iconName: "skip_next"
buttonSize: 38 buttonSize: 38
@@ -165,7 +166,7 @@ Item {
} }
// Source switcher - one chip per MPRIS player that is currently around. // Source switcher - one chip per MPRIS player that is currently around.
DankFlickable { DFlickable {
id: sourceRow id: sourceRow
anchors.left: parent.left anchors.left: parent.left
@@ -231,7 +232,7 @@ Item {
spacing: Theme.spacingS spacing: Theme.spacingS
visible: !widget.hasPlayer visible: !widget.hasPlayer
DankIcon { DIcon {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
name: "music_off" name: "music_off"
size: Theme.iconSizeLarge size: Theme.iconSizeLarge
+3 -3
View File
@@ -3,7 +3,7 @@ import QtQuick.Controls
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.DCommon.Widgets
Item { Item {
id: widget id: widget
@@ -87,7 +87,7 @@ Item {
border.width: editor.activeFocus ? 1 : Theme.layerOutlineWidth border.width: editor.activeFocus ? 1 : Theme.layerOutlineWidth
clip: true clip: true
DankFlickable { DFlickable {
id: editorFlick id: editorFlick
anchors.fill: parent anchors.fill: parent
@@ -162,7 +162,7 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankIcon { DIcon {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
name: "open_in_new" name: "open_in_new"
size: Theme.fontSizeMedium size: Theme.fontSizeMedium
+2 -2
View File
@@ -1,7 +1,7 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.DCommon.Widgets
Item { Item {
id: widget id: widget
@@ -52,7 +52,7 @@ Item {
border.color: Theme.outlineLight border.color: Theme.outlineLight
border.width: Theme.layerOutlineWidth border.width: Theme.layerOutlineWidth
DankIcon { DIcon {
id: tileIcon id: tileIcon
anchors.left: parent.left anchors.left: parent.left
+349
View File
@@ -0,0 +1,349 @@
import QtQuick
import qs.Common
import qs.DCommon.Widgets
Item {
id: widget
property var service: null
property int tabIndex: 0
readonly property bool onTimerTab: tabIndex === 0
readonly property var presets: [1, 3, 5, 10, 15, 25, 45, 60]
component ActionButton: Rectangle {
id: button
property string icon: ""
property string label: ""
property bool accent: false
property bool enabled: true
signal triggered
width: buttonRow.width + Theme.spacingM * 2
height: 34
radius: Theme.cornerRadius
color: accent ? Theme.primary : (buttonArea.containsMouse && enabled ? Theme.surfaceHover : Theme.withAlpha(Theme.surfaceLight, Theme.popupTransparency))
border.color: accent ? Theme.primary : Theme.outlineLight
border.width: Theme.layerOutlineWidth
opacity: enabled ? 1 : 0.45
Row {
id: buttonRow
anchors.centerIn: parent
spacing: Theme.spacingXS
DIcon {
anchors.verticalCenter: parent.verticalCenter
name: button.icon
size: Theme.iconSize - 8
color: button.accent ? Theme.onPrimary : Theme.surfaceText
visible: button.icon !== ""
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: button.label
font.pixelSize: Theme.fontSizeSmall
font.weight: button.accent ? Font.Medium : Font.Normal
color: button.accent ? Theme.onPrimary : Theme.surfaceText
visible: button.label !== ""
}
}
MouseArea {
id: buttonArea
anchors.fill: parent
hoverEnabled: true
enabled: button.enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: button.triggered()
}
}
DTabBar {
id: tabs
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
tabHeight: 34
showIcons: false
currentIndex: widget.tabIndex
model: [
{
"text": "Timer"
},
{
"text": "Stopwatch"
}
]
onTabClicked: index => widget.tabIndex = index
}
// ── Timer ────────────────────────────────────────────────────────────────
Item {
anchors.top: tabs.bottom
anchors.topMargin: Theme.spacingS
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
visible: widget.onTimerTab
StyledText {
id: remaining
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
text: widget.service ? widget.service.formatDuration(widget.service.remainingMs) : "00:00"
font.pixelSize: 40
font.weight: Font.Medium
color: (widget.service && widget.service.timerFinished) ? Theme.error : Theme.surfaceText
SequentialAnimation on opacity {
running: widget.service !== null && widget.service.timerFinished
loops: Animation.Infinite
NumberAnimation {
to: 0.35
duration: 500
}
NumberAnimation {
to: 1
duration: 500
}
}
}
Rectangle {
id: progress
anchors.top: remaining.bottom
anchors.topMargin: Theme.spacingXS
anchors.left: parent.left
anchors.right: parent.right
height: 4
radius: 2
color: Theme.withAlpha(Theme.outline, 0.4)
Rectangle {
width: parent.width * (widget.service ? widget.service.timerProgress : 0)
height: parent.height
radius: parent.radius
color: (widget.service && widget.service.timerFinished) ? Theme.error : Theme.primary
Behavior on width {
NumberAnimation {
duration: 180
}
}
}
}
DTextField {
id: labelField
anchors.top: progress.bottom
anchors.topMargin: Theme.spacingS
anchors.left: parent.left
anchors.right: parent.right
height: 34
placeholderText: "What is it for? (shown when it ends)"
text: widget.service ? widget.service.timerLabel : ""
onTextChanged: {
if (widget.service)
widget.service.timerLabel = text;
}
}
Flow {
id: presetFlow
anchors.top: labelField.bottom
anchors.topMargin: Theme.spacingS
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.spacingXS
Repeater {
model: widget.presets
delegate: ActionButton {
required property var modelData
label: modelData + "m"
onTriggered: {
if (!widget.service)
return;
widget.service.resetTimer();
widget.service.startTimer(modelData * 60 * 1000);
}
}
}
}
Row {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXS
ActionButton {
icon: "remove"
label: "1m"
enabled: widget.service !== null
onTriggered: widget.service.addTime(-60 * 1000)
}
ActionButton {
icon: (widget.service && widget.service.timerRunning) ? "pause" : "play_arrow"
label: (widget.service && widget.service.timerRunning) ? "Pause" : "Start"
accent: true
enabled: widget.service !== null
onTriggered: widget.service.toggleTimer()
}
ActionButton {
icon: "add"
label: "1m"
enabled: widget.service !== null
onTriggered: widget.service.addTime(60 * 1000)
}
ActionButton {
icon: "restart_alt"
label: (widget.service && widget.service.timerFinished) ? "Dismiss" : "Reset"
enabled: widget.service !== null
onTriggered: {
if (widget.service.timerFinished)
widget.service.dismissFinished();
else
widget.service.resetTimer();
}
}
}
}
// ── Stopwatch ────────────────────────────────────────────────────────────
Item {
anchors.top: tabs.bottom
anchors.topMargin: Theme.spacingS
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
visible: !widget.onTimerTab
StyledText {
id: elapsed
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
text: widget.service ? widget.service.formatStopwatch(widget.service.stopwatchElapsedMs) : "00:00.00"
font.pixelSize: 40
font.weight: Font.Medium
color: Theme.surfaceText
}
DFlickable {
id: lapList
anchors.top: elapsed.bottom
anchors.topMargin: Theme.spacingS
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: stopwatchControls.top
anchors.bottomMargin: Theme.spacingS
clip: true
contentHeight: lapColumn.height
contentWidth: width
Column {
id: lapColumn
width: lapList.width
spacing: 2
Repeater {
model: widget.service ? widget.service.laps : []
delegate: Item {
required property var modelData
width: lapColumn.width
height: 22
StyledText {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: "Lap " + modelData.index
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
anchors.right: totalText.left
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: "+" + widget.service.formatStopwatch(modelData.split)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
id: totalText
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: widget.service.formatStopwatch(modelData.total)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
}
}
}
}
StyledText {
anchors.centerIn: lapList
text: "No laps yet"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: !widget.service || widget.service.laps.length === 0
}
Row {
id: stopwatchControls
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXS
ActionButton {
icon: (widget.service && widget.service.stopwatchRunning) ? "pause" : "play_arrow"
label: (widget.service && widget.service.stopwatchRunning) ? "Pause" : "Start"
accent: true
enabled: widget.service !== null
onTriggered: widget.service.toggleStopwatch()
}
ActionButton {
icon: "flag"
label: "Lap"
enabled: widget.service !== null && widget.service.stopwatchElapsedMs > 0
onTriggered: widget.service.lap()
}
ActionButton {
icon: "restart_alt"
label: "Reset"
enabled: widget.service !== null && widget.service.stopwatchElapsedMs > 0
onTriggered: widget.service.resetStopwatch()
}
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ import QtQuick
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.DCommon.Widgets
Item { Item {
id: widget id: widget