Compare commits
2
Commits
d15bb4687c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0ea2baba3
|
||
|
|
018e326918
|
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
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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", {})
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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,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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user