feat(quickshell): dashboard drawer that drops from the bar
A drawer lowered out of the underside of the bar, painted in the hull colour and joined to it by two concave fillets so it reads as more of the hull rather than as a floating card — caelestia's dashboard idea, in the YoRHa language. Four columns divided by hairlines: operator and clock, the month with today as an inverted square, the system meters, and the active player with a hairline timeline and square transport buttons. Opened by clicking the bar's clock (which used to pin the calendar), by SUPER+I, or by `qs ipc call shell dashboard`. Escape or moving focus away closes it through a HyprlandFocusGrab; Actions.dashboardClosedAt keeps the clock click that cleared the grab from reopening it. The clock's calendar hover is suppressed while the drawer is down, since it shows the month. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
18091a7109
commit
ecc7d2884c
6 files changed
+688
-5
No files matched your search
@@ -9,6 +9,7 @@ end
|
|||||||
shell("lock", { key = mod .. " + CTRL + L", description = "lock screen" })
|
shell("lock", { key = mod .. " + CTRL + L", description = "lock screen" })
|
||||||
shell("power", { key = mod .. " + P", description = "power menu" })
|
shell("power", { key = mod .. " + P", description = "power menu" })
|
||||||
shell("ask", { key = mod .. " + A", description = "ask claude" })
|
shell("ask", { key = mod .. " + A", description = "ask claude" })
|
||||||
|
shell("dashboard", { key = mod .. " + I", description = "dashboard" })
|
||||||
shell("toggleDnd", { key = mod .. " + SHIFT + N", description = "toggle do not disturb" })
|
shell("toggleDnd", { key = mod .. " + SHIFT + N", description = "toggle do not disturb" })
|
||||||
shell("clearNotifications", { key = mod .. " + CTRL + SHIFT + N", description = "clear notifications" })
|
shell("clearNotifications", { key = mod .. " + CTRL + SHIFT + N", description = "clear notifications" })
|
||||||
shell("reloadConfig", { key = mod .. " + SHIFT + R", description = "reload shell config" })
|
shell("reloadConfig", { key = mod .. " + SHIFT + R", description = "reload shell config" })
|
||||||
|
|||||||
+13
-2
@@ -57,7 +57,7 @@ clear composition and stronger separation from the desktop. **Cinematic**
|
|||||||
it is never used as decoration inside ordinary chrome or popouts.
|
it is never used as decoration inside ordinary chrome or popouts.
|
||||||
|
|
||||||
```
|
```
|
||||||
shell.qml entrypoint — bar, rail, power menu, toasts, OSD per screen
|
shell.qml entrypoint — hull, bar, rail, dashboard, power menu, toasts, OSD per screen
|
||||||
config/ palette, type scale, geometry, motion curves
|
config/ palette, type scale, geometry, motion curves
|
||||||
components/ the primitives: panels, sliders, list rows, display text
|
components/ the primitives: panels, sliders, list rows, display text
|
||||||
services/ data sources, all singletons
|
services/ data sources, all singletons
|
||||||
@@ -72,13 +72,24 @@ assets/ halftone and stripe tiles, generated with ImageMagick
|
|||||||
callers set implicit sizes from their own layout (`row.implicitWidth +
|
callers set implicit sizes from their own layout (`row.implicitWidth +
|
||||||
contentPad * 2`), which keeps the shape maths out of binding loops.
|
contentPad * 2`), which keeps the shape maths out of binding loops.
|
||||||
|
|
||||||
|
## Dashboard
|
||||||
|
|
||||||
|
`dashboard/Dashboard.qml` is a drawer that slides down out of the underside of
|
||||||
|
the bar — clock click, `SUPER + I`, or `qs ipc call shell dashboard`. It is
|
||||||
|
painted in the hull colour and joins the bar through two concave fillets, so it
|
||||||
|
reads as more of the hull rather than as a floating card (caelestia's idea).
|
||||||
|
Four columns divided by hairlines: operator and time, the month, the system
|
||||||
|
meters, and what is playing. Escape or focusing anything else puts it away; a
|
||||||
|
`HyprlandFocusGrab` handles the latter, and `Actions.dashboardClosedAt` stops
|
||||||
|
the clock click that cleared the grab from opening it straight back up.
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
| Module | Where | Left click | Right click | Middle | Scroll | Hover |
|
| Module | Where | Left click | Right click | Middle | Scroll | Hover |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| Sigil | bar | app menu | reload shell | | | |
|
| Sigil | bar | app menu | reload shell | | | |
|
||||||
| Workspaces | bar | switch | | | | |
|
| Workspaces | bar | switch | | | | |
|
||||||
| Clock | bar | pin calendar | | | | calendar |
|
| Clock | bar | dashboard | | | | calendar |
|
||||||
| Network | rail | pin details | | | | state + networks |
|
| Network | rail | pin details | | | | state + networks |
|
||||||
| Bluetooth | rail | pin details | | | | state + devices |
|
| Bluetooth | rail | pin details | | | | state + devices |
|
||||||
| Volume | rail | pin audio | | | volume | state + scroll hint + audio |
|
| Volume | rail | pin audio | | | volume | state + scroll hint + audio |
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import Quickshell
|
|||||||
import "root:/config"
|
import "root:/config"
|
||||||
import "root:/components"
|
import "root:/components"
|
||||||
import "root:/popouts"
|
import "root:/popouts"
|
||||||
|
import "root:/services"
|
||||||
|
|
||||||
// Centre-stage clock. Hovering drops the calendar.
|
// Centre-stage clock. Hovering drops the calendar; clicking drops the
|
||||||
|
// dashboard, which carries the calendar too.
|
||||||
BarPill {
|
BarPill {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
@@ -20,7 +22,7 @@ BarPill {
|
|||||||
precision: SystemClock.Minutes
|
precision: SystemClock.Minutes
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: calendar.pinned = !calendar.pinned
|
onClicked: Actions.toggleDashboard()
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: layout
|
id: layout
|
||||||
@@ -116,7 +118,8 @@ BarPill {
|
|||||||
CalendarPopout {
|
CalendarPopout {
|
||||||
id: calendar
|
id: calendar
|
||||||
anchorItem: root
|
anchorItem: root
|
||||||
triggerHovered: root.hovered
|
// Not while the dashboard is down: it already shows the month.
|
||||||
|
triggerHovered: root.hovered && !Actions.dashboardOpen
|
||||||
clockDate: clock.date
|
clockDate: clock.date
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,636 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Shapes
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import "root:/config"
|
||||||
|
import "root:/components"
|
||||||
|
import "root:/services"
|
||||||
|
|
||||||
|
// The dashboard: a drawer that slides down out of the underside of the bar.
|
||||||
|
//
|
||||||
|
// The idea is caelestia's — the drawer is not a floating card but more of the
|
||||||
|
// hull, so it is painted in the hull colour and joins the bar through two
|
||||||
|
// concave fillets, and the porthole's rim runs round it without a break. Read
|
||||||
|
// against the wallpaper it is a panel lowered from the ceiling of the ship.
|
||||||
|
//
|
||||||
|
// Four columns divided by hairlines: who and when, the month, the machine,
|
||||||
|
// and what is playing. The clock in the bar toggles it, as does
|
||||||
|
// `qs ipc call shell dashboard`; Escape or a click anywhere else puts it away.
|
||||||
|
PanelWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
screen: modelData
|
||||||
|
|
||||||
|
readonly property bool open: Actions.dashboardOpen
|
||||||
|
&& Actions.dashboardScreen === modelData.name
|
||||||
|
|
||||||
|
// 0 = tucked under the bar, 1 = fully down.
|
||||||
|
property real reveal: root.open ? 1 : 0
|
||||||
|
Behavior on reveal {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.durSlow
|
||||||
|
easing.type: root.open ? Easing.OutCubic : Easing.InCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: root.open || root.reveal > 0
|
||||||
|
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
WlrLayershell.namespace: "takemi-dashboard"
|
||||||
|
WlrLayershell.keyboardFocus: root.open
|
||||||
|
? WlrKeyboardFocus.OnDemand
|
||||||
|
: WlrKeyboardFocus.None
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
|
// One pixel up, so the drawer covers the rim the hull draws along the
|
||||||
|
// underside of the bar instead of sitting below it.
|
||||||
|
anchors.top: true
|
||||||
|
margins.top: Theme.barHeight - 1
|
||||||
|
|
||||||
|
// The fillets are the same radius as the porthole's corners, so the drawer
|
||||||
|
// meets the bar the way the opening meets the frame.
|
||||||
|
readonly property real fillet: Theme.frameRadius
|
||||||
|
readonly property real radius: Theme.frameRadius
|
||||||
|
readonly property real pad: Theme.padL
|
||||||
|
|
||||||
|
readonly property real bodyWidth: columns.implicitWidth + root.pad * 2
|
||||||
|
readonly property real bodyHeight: columns.implicitHeight + root.pad * 2
|
||||||
|
|
||||||
|
implicitWidth: root.bodyWidth + root.fillet * 2
|
||||||
|
implicitHeight: root.bodyHeight + 1
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
mask: Region { item: body }
|
||||||
|
|
||||||
|
HyprlandFocusGrab {
|
||||||
|
active: root.open
|
||||||
|
windows: [root]
|
||||||
|
onCleared: Actions.closeDashboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Who is at the console. Read once; it does not change under a session.
|
||||||
|
readonly property string user: Quickshell.env("USER") ?? ""
|
||||||
|
FileView {
|
||||||
|
id: hostFile
|
||||||
|
path: "/etc/hostname"
|
||||||
|
}
|
||||||
|
readonly property string host: hostFile.text().trim()
|
||||||
|
|
||||||
|
SystemClock {
|
||||||
|
id: clock
|
||||||
|
precision: SystemClock.Seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything below slides as one sheet, clipped to the window so it
|
||||||
|
// appears to come out from behind the bar rather than fading in over it.
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: sheet
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height
|
||||||
|
y: -height * (1 - root.reveal)
|
||||||
|
|
||||||
|
// ------------------------------------------------------ silhouette
|
||||||
|
Shape {
|
||||||
|
id: silhouette
|
||||||
|
anchors.fill: parent
|
||||||
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
|
||||||
|
readonly property real w: width
|
||||||
|
readonly property real h: height
|
||||||
|
readonly property real f: root.fillet
|
||||||
|
readonly property real r: root.radius
|
||||||
|
|
||||||
|
// Down the left fillet, round the bottom, up the right fillet.
|
||||||
|
readonly property string edge:
|
||||||
|
`M 0 0 A ${f} ${f} 0 0 1 ${f} ${f}`
|
||||||
|
+ ` V ${h - r} A ${r} ${r} 0 0 0 ${f + r} ${h}`
|
||||||
|
+ ` H ${w - f - r} A ${r} ${r} 0 0 0 ${w - f} ${h - r}`
|
||||||
|
+ ` V ${f} A ${f} ${f} 0 0 1 ${w} 0`
|
||||||
|
|
||||||
|
ShapePath {
|
||||||
|
fillColor: Theme.hull
|
||||||
|
strokeColor: "transparent"
|
||||||
|
PathSvg { path: silhouette.edge + " Z" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rim, carried on round the drawer from the opening.
|
||||||
|
ShapePath {
|
||||||
|
fillColor: "transparent"
|
||||||
|
strokeColor: Theme.rim
|
||||||
|
strokeWidth: 1
|
||||||
|
PathSvg { path: silhouette.edge }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------- body
|
||||||
|
FocusScope {
|
||||||
|
id: body
|
||||||
|
x: root.fillet
|
||||||
|
width: root.bodyWidth
|
||||||
|
height: parent.height
|
||||||
|
focus: root.open
|
||||||
|
|
||||||
|
Keys.onEscapePressed: Actions.closeDashboard()
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: columns
|
||||||
|
x: root.pad
|
||||||
|
y: root.pad
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
// Who and when: the house mark, the operator, a large clock.
|
||||||
|
Item {
|
||||||
|
implicitWidth: 230
|
||||||
|
implicitHeight: root.columnHeight
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: who
|
||||||
|
spacing: Theme.padM
|
||||||
|
|
||||||
|
Porthole {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
size: 30
|
||||||
|
lit: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.user.toUpperCase()
|
||||||
|
color: Theme.text
|
||||||
|
font.family: Theme.fontDisplay
|
||||||
|
font.pixelSize: Theme.fsBody
|
||||||
|
font.weight: Theme.weightDisplay
|
||||||
|
font.letterSpacing: Theme.tracking
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.host !== "" ? "@ " + root.host : ""
|
||||||
|
color: Theme.muted
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.pixelSize: Theme.fsMicro
|
||||||
|
font.letterSpacing: 1
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: time
|
||||||
|
anchors.top: who.bottom
|
||||||
|
anchors.topMargin: Theme.padL
|
||||||
|
spacing: Theme.padS
|
||||||
|
|
||||||
|
SplitText {
|
||||||
|
id: hhmm
|
||||||
|
text: Qt.formatDateTime(clock.date, "HH:mm")
|
||||||
|
pixelSize: 54
|
||||||
|
weight: Font.Light
|
||||||
|
letterSpacing: 2
|
||||||
|
color: Theme.accent
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.baseline: hhmm.bottom
|
||||||
|
anchors.baselineOffset: -12
|
||||||
|
text: Qt.formatDateTime(clock.date, "ss")
|
||||||
|
color: Theme.muted
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.pixelSize: Theme.fsBody
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.top: time.bottom
|
||||||
|
anchors.topMargin: Theme.padS
|
||||||
|
text: Qt.formatDateTime(clock.date, "dddd · dd MMMM").toUpperCase()
|
||||||
|
color: Theme.subtext
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.pixelSize: Theme.fsMicro
|
||||||
|
font.letterSpacing: 2
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
spacing: Theme.padS
|
||||||
|
|
||||||
|
Fact { label: "UPTIME"; value: Sys.uptime || "—" }
|
||||||
|
Fact {
|
||||||
|
visible: Battery.available
|
||||||
|
label: "POWER"
|
||||||
|
value: Math.round(Battery.percent) + "%"
|
||||||
|
+ (Battery.timeLabel !== "" ? " · " + Battery.timeLabel : "")
|
||||||
|
}
|
||||||
|
Fact { label: "NOTICES"; value: Notifs.count > 0 ? Notifs.count + " held" : "none" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rule { height: root.columnHeight }
|
||||||
|
|
||||||
|
// The month, Monday first. Today is the NieR cursor: an inverted square.
|
||||||
|
Item {
|
||||||
|
id: month
|
||||||
|
implicitWidth: 238
|
||||||
|
implicitHeight: root.columnHeight
|
||||||
|
|
||||||
|
// Keyed by the day, not the clock: the clock ticks every
|
||||||
|
// second, and rebuilding 42 cells that often is waste.
|
||||||
|
readonly property string dayKey: Qt.formatDateTime(clock.date, "yyyy-MM-dd")
|
||||||
|
readonly property var cells: {
|
||||||
|
const now = new Date(month.dayKey + "T12:00:00");
|
||||||
|
const y = now.getFullYear(), m = now.getMonth();
|
||||||
|
const lead = (new Date(y, m, 1).getDay() + 6) % 7;
|
||||||
|
const out = [];
|
||||||
|
for (let i = 0; i < 42; i++) {
|
||||||
|
const d = new Date(y, m, 1 - lead + i);
|
||||||
|
out.push({
|
||||||
|
day: d.getDate(),
|
||||||
|
inMonth: d.getMonth() === m,
|
||||||
|
today: d.toDateString() === now.toDateString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionRule {
|
||||||
|
id: monthHead
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
text: Qt.formatDateTime(clock.date, "MMMM yyyy")
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: weekdays
|
||||||
|
anchors.top: monthHead.bottom
|
||||||
|
anchors.topMargin: Theme.padS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
||||||
|
|
||||||
|
Text {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
width: month.width / 7
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: modelData
|
||||||
|
color: index >= 5 ? Theme.subtext : Theme.muted
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.pixelSize: Theme.fsMicro
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Grid {
|
||||||
|
anchors.top: weekdays.bottom
|
||||||
|
anchors.topMargin: Theme.padS
|
||||||
|
columns: 7
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: month.cells
|
||||||
|
|
||||||
|
Item {
|
||||||
|
required property var modelData
|
||||||
|
width: month.width / 7
|
||||||
|
height: 26
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 22
|
||||||
|
height: 20
|
||||||
|
visible: parent.modelData.today
|
||||||
|
color: Theme.treatmentSelectedFill
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: parent.modelData.day
|
||||||
|
color: parent.modelData.today ? Theme.treatmentSelectedText
|
||||||
|
: parent.modelData.inMonth ? Theme.text
|
||||||
|
: Theme.alpha(Theme.muted, 0.5)
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.pixelSize: Theme.fsSmall
|
||||||
|
font.weight: parent.modelData.today ? Font.Bold : Font.Normal
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rule { height: root.columnHeight }
|
||||||
|
|
||||||
|
// The machine: the same meters the bar's readout opens onto.
|
||||||
|
Item {
|
||||||
|
implicitWidth: 238
|
||||||
|
implicitHeight: root.columnHeight
|
||||||
|
|
||||||
|
SectionRule {
|
||||||
|
id: sysHead
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
text: "System"
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.top: sysHead.bottom
|
||||||
|
anchors.topMargin: Theme.padM
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
spacing: Theme.padM
|
||||||
|
|
||||||
|
MetricRow {
|
||||||
|
width: parent.width
|
||||||
|
value: Sys.cpuUsage
|
||||||
|
label: "CPU"
|
||||||
|
sub: Math.round(Sys.cpuTemp) + "°C"
|
||||||
|
}
|
||||||
|
|
||||||
|
MetricRow {
|
||||||
|
width: parent.width
|
||||||
|
value: Sys.memPercent
|
||||||
|
label: "MEM"
|
||||||
|
sub: Sys.gb(Sys.memUsed) + " / " + Sys.gb(Sys.memTotal)
|
||||||
|
}
|
||||||
|
|
||||||
|
MetricRow {
|
||||||
|
width: parent.width
|
||||||
|
visible: Sys.gpuAvailable
|
||||||
|
value: Sys.gpuUsage
|
||||||
|
label: "GPU"
|
||||||
|
sub: Math.round(Sys.gpuTemp) + "°C"
|
||||||
|
}
|
||||||
|
|
||||||
|
MetricRow {
|
||||||
|
width: parent.width
|
||||||
|
value: Sys.diskPercent
|
||||||
|
label: "DISK"
|
||||||
|
sub: Sys.diskFree + " free"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rule { height: root.columnHeight }
|
||||||
|
|
||||||
|
// What is playing. With no player it says so, in the voice NieR's
|
||||||
|
// interface uses for an empty channel.
|
||||||
|
Item {
|
||||||
|
implicitWidth: 280
|
||||||
|
implicitHeight: root.columnHeight
|
||||||
|
|
||||||
|
SectionRule {
|
||||||
|
id: playHead
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
text: Media.hasPlayer ? Media.identity : "Audio"
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: !Media.hasPlayer
|
||||||
|
text: "NO SIGNAL"
|
||||||
|
color: Theme.muted
|
||||||
|
font.family: Theme.fontDisplay
|
||||||
|
font.pixelSize: Theme.fsBody
|
||||||
|
font.letterSpacing: Theme.tracking * 2
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.top: playHead.bottom
|
||||||
|
anchors.topMargin: Theme.padM
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
visible: Media.hasPlayer
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: artFrame
|
||||||
|
width: 96
|
||||||
|
height: 96
|
||||||
|
color: Theme.surface
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.overlay
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: art
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 1
|
||||||
|
source: Media.artUrl
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
asynchronous: true
|
||||||
|
sourceSize.width: 192
|
||||||
|
sourceSize.height: 192
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: art.status !== Image.Ready
|
||||||
|
text: ""
|
||||||
|
color: Theme.muted
|
||||||
|
font.pixelSize: Theme.fsHuge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: artFrame.right
|
||||||
|
anchors.leftMargin: Theme.padM
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: artFrame.verticalCenter
|
||||||
|
spacing: 3
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: Media.title || "Untitled"
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 2
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
color: Theme.accent
|
||||||
|
font.family: Theme.fontDisplay
|
||||||
|
font.pixelSize: Theme.fsLarge
|
||||||
|
font.weight: Theme.weightDisplay
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: Media.artist
|
||||||
|
elide: Text.ElideRight
|
||||||
|
color: Theme.subtext
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.pixelSize: Theme.fsSmall
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position: a hairline with a square riding it.
|
||||||
|
Item {
|
||||||
|
id: timeline
|
||||||
|
anchors.top: artFrame.bottom
|
||||||
|
anchors.topMargin: Theme.padM
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
height: Theme.marker
|
||||||
|
visible: Media.hasTimeline
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Theme.overlay
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width * Media.progress
|
||||||
|
height: 1
|
||||||
|
color: Theme.accent
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
x: parent.width * Media.progress - width / 2
|
||||||
|
width: Theme.marker
|
||||||
|
height: Theme.marker
|
||||||
|
color: Theme.accent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
spacing: Theme.padS
|
||||||
|
|
||||||
|
Transport {
|
||||||
|
glyph: ""
|
||||||
|
enabled: Media.canGoPrevious
|
||||||
|
onClicked: Media.previous()
|
||||||
|
}
|
||||||
|
Transport {
|
||||||
|
glyph: Media.playing ? "" : ""
|
||||||
|
onClicked: Media.playPause()
|
||||||
|
}
|
||||||
|
Transport {
|
||||||
|
glyph: ""
|
||||||
|
enabled: Media.canGoNext
|
||||||
|
onClicked: Media.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 8
|
||||||
|
anchors.right: parent.right
|
||||||
|
visible: Media.hasTimeline
|
||||||
|
text: Media.timeString(Media.position) + " / " + Media.timeString(Media.length)
|
||||||
|
color: Theme.muted
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.pixelSize: Theme.fsMicro
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ columns
|
||||||
|
// A vertical hairline between columns, the same rule the bar divides its
|
||||||
|
// modules with, at the drawer's scale.
|
||||||
|
component Rule: Item {
|
||||||
|
implicitWidth: Theme.padL * 2 + 1
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.topMargin: Theme.padS
|
||||||
|
anchors.bottomMargin: Theme.padS
|
||||||
|
width: 1
|
||||||
|
color: Theme.alpha(Theme.outline, 0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property real columnHeight: 214
|
||||||
|
|
||||||
|
// A label and its reading on one line, label left, reading right.
|
||||||
|
component Fact: Item {
|
||||||
|
property string label: ""
|
||||||
|
property string value: ""
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: 14
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
text: parent.label
|
||||||
|
color: Theme.muted
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.pixelSize: Theme.fsMicro
|
||||||
|
font.letterSpacing: 2
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.right: parent.right
|
||||||
|
text: parent.value
|
||||||
|
color: Theme.text
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.pixelSize: Theme.fsMicro
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A square hairline button. Hover lifts it; nothing here inverts, since
|
||||||
|
// inversion is reserved for the keyboard's cursor.
|
||||||
|
component Transport: Item {
|
||||||
|
id: tb
|
||||||
|
property string glyph: ""
|
||||||
|
signal clicked()
|
||||||
|
|
||||||
|
width: 30
|
||||||
|
height: 30
|
||||||
|
opacity: tb.enabled ? 1 : 0.35
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: tbHover.hovered ? Theme.treatmentHoverFill : "transparent"
|
||||||
|
border.width: 1
|
||||||
|
border.color: tbHover.hovered ? Theme.treatmentHoverMark : Theme.overlay
|
||||||
|
|
||||||
|
Behavior on color { ColorAnimation { duration: Theme.durFast } }
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: tb.glyph
|
||||||
|
color: tbHover.hovered ? Theme.accent : Theme.text
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: tbHover
|
||||||
|
enabled: tb.enabled
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
enabled: tb.enabled
|
||||||
|
onTapped: tb.clicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ Singleton {
|
|||||||
property bool sidebarOpen: false
|
property bool sidebarOpen: false
|
||||||
property bool launcherOpen: false
|
property bool launcherOpen: false
|
||||||
property bool askOpen: false
|
property bool askOpen: false
|
||||||
|
property bool dashboardOpen: false
|
||||||
|
|
||||||
// Seed text for the next launcher open. Lets a keybind or a script hand the
|
// Seed text for the next launcher open. Lets a keybind or a script hand the
|
||||||
// launcher a query it did not have to type — `qs ipc call shell prompt
|
// launcher a query it did not have to type — `qs ipc call shell prompt
|
||||||
@@ -36,6 +37,15 @@ Singleton {
|
|||||||
// …and for the ask panel, which also takes the keyboard.
|
// …and for the ask panel, which also takes the keyboard.
|
||||||
property string askScreen: ""
|
property string askScreen: ""
|
||||||
|
|
||||||
|
// …and for the dashboard drawer.
|
||||||
|
property string dashboardScreen: ""
|
||||||
|
|
||||||
|
// When the dashboard was last closed from outside. A click on the bar's
|
||||||
|
// clock first clears the drawer's focus grab, which closes it, and then
|
||||||
|
// lands on the clock, which would toggle it straight back open. A toggle
|
||||||
|
// that arrives within a moment of a grab-close is that same click.
|
||||||
|
property real dashboardClosedAt: 0
|
||||||
|
|
||||||
// Seed text for the next ask open. A seeded open asks straight away, so
|
// Seed text for the next ask open. A seeded open asks straight away, so
|
||||||
// `qs ipc call shell askAbout "$(wl-paste)"` is a one-shot question.
|
// `qs ipc call shell askAbout "$(wl-paste)"` is a one-shot question.
|
||||||
property string askSeed: ""
|
property string askSeed: ""
|
||||||
@@ -65,6 +75,19 @@ Singleton {
|
|||||||
}
|
}
|
||||||
function closeLauncher() { root.launcherOpen = false; }
|
function closeLauncher() { root.launcherOpen = false; }
|
||||||
|
|
||||||
|
function toggleDashboard() {
|
||||||
|
if (!root.dashboardOpen && Date.now() - root.dashboardClosedAt < 250)
|
||||||
|
return;
|
||||||
|
if (!root.dashboardOpen)
|
||||||
|
root.dashboardScreen = root.focusedScreenName();
|
||||||
|
root.dashboardOpen = !root.dashboardOpen;
|
||||||
|
}
|
||||||
|
function closeDashboard() {
|
||||||
|
if (!root.dashboardOpen) return;
|
||||||
|
root.dashboardOpen = false;
|
||||||
|
root.dashboardClosedAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
function toggleAsk() {
|
function toggleAsk() {
|
||||||
if (!root.askOpen) {
|
if (!root.askOpen) {
|
||||||
root.askSeed = "";
|
root.askSeed = "";
|
||||||
@@ -131,6 +154,7 @@ Singleton {
|
|||||||
function power(): void { root.togglePowerMenu(); }
|
function power(): void { root.togglePowerMenu(); }
|
||||||
function launcher(): void { root.toggleLauncher(); }
|
function launcher(): void { root.toggleLauncher(); }
|
||||||
function ask(): void { root.toggleAsk(); }
|
function ask(): void { root.toggleAsk(); }
|
||||||
|
function dashboard(): void { root.toggleDashboard(); }
|
||||||
function askAbout(question: string): void { root.askAbout(question); }
|
function askAbout(question: string): void { root.askAbout(question); }
|
||||||
function prompt(seed: string): void { root.promptLauncher(seed); }
|
function prompt(seed: string): void { root.promptLauncher(seed); }
|
||||||
function dmenu(inPath: string, outPath: string, prompt: string): void {
|
function dmenu(inPath: string, outPath: string, prompt: string): void {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import "root:/config"
|
|||||||
import "root:/services"
|
import "root:/services"
|
||||||
import "root:/bar"
|
import "root:/bar"
|
||||||
import "root:/launcher"
|
import "root:/launcher"
|
||||||
|
import "root:/dashboard"
|
||||||
import "root:/ask"
|
import "root:/ask"
|
||||||
import "root:/overlays"
|
import "root:/overlays"
|
||||||
import "root:/lock"
|
import "root:/lock"
|
||||||
@@ -14,6 +15,7 @@ import "root:/lock"
|
|||||||
// bar/ the hull, the top bar, the rail and their modules
|
// bar/ the hull, the top bar, the rail and their modules
|
||||||
// popouts/ hover panels hanging off bar modules
|
// popouts/ hover panels hanging off bar modules
|
||||||
// launcher/ the application launcher
|
// launcher/ the application launcher
|
||||||
|
// dashboard/ the drawer that drops from the bar
|
||||||
// ask/ the question panel, dropped from the top edge
|
// ask/ the question panel, dropped from the top edge
|
||||||
// overlays/ power menu, notification toasts, OSD
|
// overlays/ power menu, notification toasts, OSD
|
||||||
// lock/ session lock
|
// lock/ session lock
|
||||||
@@ -60,6 +62,12 @@ ShellRoot {
|
|||||||
Launcher {}
|
Launcher {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
model: Quickshell.screens
|
||||||
|
|
||||||
|
Dashboard {}
|
||||||
|
}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
model: Quickshell.screens
|
model: Quickshell.screens
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user