added quickshell
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// Output and input levels, the sink picker, and a per-app mixer.
|
||||
Popout {
|
||||
id: root
|
||||
|
||||
contentWidth: 360
|
||||
contentHeight: surface.implicitContentHeight + Theme.padM * 2
|
||||
|
||||
PopoutSurface {
|
||||
id: surface
|
||||
anchors.fill: parent
|
||||
tone: Theme.primary
|
||||
|
||||
readonly property real implicitContentHeight: header.height + column.implicitHeight + Theme.padM * 2
|
||||
|
||||
PopoutHeader {
|
||||
id: header
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
title: "Audio"
|
||||
icon: Audio.icon
|
||||
accent: Theme.primary
|
||||
subtitle: Audio.sinkName
|
||||
}
|
||||
|
||||
Column {
|
||||
id: column
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: Theme.padM
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.padM
|
||||
|
||||
// ------------------------------------------------------- output
|
||||
LevelRow {
|
||||
width: parent.width
|
||||
icon: Audio.icon
|
||||
label: "Output"
|
||||
readout: Audio.muted ? "MUTED" : Audio.volumePercent + "%"
|
||||
value: Audio.volume
|
||||
muted: Audio.muted
|
||||
accent: Theme.primary
|
||||
onMoved: v => Audio.setVolume(v)
|
||||
onToggled: Audio.toggleMute()
|
||||
}
|
||||
|
||||
// -------------------------------------------------------- input
|
||||
LevelRow {
|
||||
width: parent.width
|
||||
icon: Audio.micIcon
|
||||
label: "Input"
|
||||
readout: Audio.micMuted ? "MUTED" : Audio.micPercent + "%"
|
||||
value: Audio.micVolume
|
||||
muted: Audio.micMuted
|
||||
accent: Theme.blue
|
||||
onMoved: v => Audio.setMicVolume(v)
|
||||
onToggled: Audio.toggleMicMute()
|
||||
}
|
||||
|
||||
SectionRule {
|
||||
width: parent.width
|
||||
text: "Outputs"
|
||||
visible: Audio.sinks.length > 1
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Audio.sinks.length > 1 ? Audio.sinks : []
|
||||
|
||||
PickRow {
|
||||
required property var modelData
|
||||
width: column.width
|
||||
label: modelData.nickname || modelData.description || modelData.name
|
||||
icon: ""
|
||||
selected: Audio.sink === modelData
|
||||
onClicked: Audio.setSink(modelData)
|
||||
}
|
||||
}
|
||||
|
||||
SectionRule {
|
||||
width: parent.width
|
||||
text: "Applications"
|
||||
visible: Audio.streams.length > 0
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Audio.streams
|
||||
|
||||
LevelRow {
|
||||
required property var modelData
|
||||
width: column.width
|
||||
icon: ""
|
||||
label: Audio.streamName(modelData)
|
||||
readout: modelData.audio ? Math.round(modelData.audio.volume * 100) + "%" : ""
|
||||
value: modelData.audio?.volume ?? 0
|
||||
muted: modelData.audio?.muted ?? false
|
||||
accent: Theme.glow
|
||||
onMoved: v => { if (modelData.audio) modelData.audio.volume = v; }
|
||||
onToggled: { if (modelData.audio) modelData.audio.muted = !modelData.audio.muted; }
|
||||
}
|
||||
}
|
||||
|
||||
P5Button {
|
||||
text: "Mixer"
|
||||
icon: ""
|
||||
onClicked: Actions.openAudioSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- pieces
|
||||
component LevelRow: Item {
|
||||
id: levelRow
|
||||
|
||||
property string icon: ""
|
||||
property string label: ""
|
||||
property string readout: ""
|
||||
property real value: 0
|
||||
property bool muted: false
|
||||
property color accent: Theme.primary
|
||||
|
||||
signal moved(real value)
|
||||
signal toggled()
|
||||
|
||||
implicitHeight: 42
|
||||
|
||||
Row {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.padS
|
||||
|
||||
Icon {
|
||||
id: levelIcon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: levelRow.icon
|
||||
color: levelRow.muted ? Theme.accent : levelRow.accent
|
||||
font.pixelSize: Theme.fsLarge
|
||||
|
||||
TapHandler { onTapped: levelRow.toggled() }
|
||||
HoverHandler { cursorShape: Qt.PointingHandCursor }
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: levelRow.width - levelIcon.width - readoutText.width - Theme.padS * 3
|
||||
text: levelRow.label
|
||||
elide: Text.ElideRight
|
||||
color: Theme.text
|
||||
font.family: Theme.fontDisplay
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.weight: Font.DemiBold
|
||||
font.italic: true
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Text {
|
||||
id: readoutText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: levelRow.readout
|
||||
color: levelRow.muted ? Theme.accent : Theme.subtext
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.weight: Font.Bold
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
|
||||
P5Slider {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
value: levelRow.value
|
||||
accent: levelRow.accent
|
||||
enabledControl: !levelRow.muted
|
||||
onMoved: v => levelRow.moved(v)
|
||||
}
|
||||
}
|
||||
|
||||
component SectionRule: Item {
|
||||
property string text: ""
|
||||
implicitHeight: visible ? 18 : 0
|
||||
|
||||
Text {
|
||||
id: sectionLabel
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: parent.text.toUpperCase()
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.letterSpacing: 2
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: sectionLabel.right
|
||||
anchors.leftMargin: Theme.padS
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: 1
|
||||
color: Theme.alpha(Theme.outline, 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
component PickRow: Item {
|
||||
id: pickRow
|
||||
|
||||
property string label: ""
|
||||
property string icon: ""
|
||||
property bool selected: false
|
||||
|
||||
signal clicked()
|
||||
|
||||
implicitHeight: 26
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: pickHover.hovered
|
||||
? Theme.alpha(Theme.primary, 0.14)
|
||||
: (pickRow.selected ? Theme.alpha(Theme.primary, 0.07) : "transparent")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 3
|
||||
height: pickRow.selected ? 18 : 0
|
||||
color: Theme.accent
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.padM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.padS
|
||||
|
||||
Icon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: pickRow.icon
|
||||
color: pickRow.selected ? Theme.primary : Theme.muted
|
||||
font.pixelSize: Theme.fsBody
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: pickRow.label
|
||||
color: pickRow.selected ? Theme.text : Theme.subtext
|
||||
font.family: Theme.fontDisplay
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.italic: true
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: pickHover
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: pickRow.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// Charge level, time estimate, draw rate and health.
|
||||
Popout {
|
||||
id: root
|
||||
|
||||
contentWidth: 310
|
||||
contentHeight: 296
|
||||
|
||||
PopoutSurface {
|
||||
anchors.fill: parent
|
||||
tone: Battery.tint
|
||||
|
||||
PopoutHeader {
|
||||
id: header
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
title: "Power"
|
||||
icon: Battery.icon
|
||||
accent: Battery.tint
|
||||
subtitle: Battery.charging ? "CHARGING" : (Battery.onBattery ? "ON BATTERY" : "AC")
|
||||
}
|
||||
|
||||
Row {
|
||||
id: readout
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: Theme.padM
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.padM
|
||||
|
||||
Gauge {
|
||||
width: 92; height: 92
|
||||
value: Battery.percent
|
||||
label: "CHARGE"
|
||||
arcColor: Battery.tint
|
||||
readout: Math.round(Battery.percent) + "%"
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.padS
|
||||
|
||||
SplitText {
|
||||
text: Battery.timeLabel
|
||||
pixelSize: Theme.fsBody
|
||||
split: 1.1
|
||||
splitOpacity: 0.55
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Battery.changeRate > 0
|
||||
? Battery.changeRate.toFixed(1) + " W " + (Battery.charging ? "in" : "draw")
|
||||
: "idle"
|
||||
color: Theme.subtext
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsSmall
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: Battery.health > 0
|
||||
text: "Health " + Math.round(Battery.health) + "%"
|
||||
color: Battery.health < 70 ? Theme.warn : Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MeterBar {
|
||||
id: bar
|
||||
anchors.top: readout.bottom
|
||||
anchors.topMargin: Theme.padM
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 12
|
||||
segments: 20
|
||||
segmentWidth: (width - 19 * 3) / 20
|
||||
spacing: 3
|
||||
value: Battery.percent
|
||||
activeColor: Battery.tint
|
||||
}
|
||||
|
||||
P5Button {
|
||||
id: suspendButton
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
text: "Suspend"
|
||||
icon: ""
|
||||
onClicked: {
|
||||
root.pinned = false;
|
||||
Actions.suspend();
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.top: bar.bottom
|
||||
anchors.topMargin: Theme.padS
|
||||
anchors.bottom: suspendButton.top
|
||||
anchors.bottomMargin: Theme.padS
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
text: Battery.detail
|
||||
wrapMode: Text.WordWrap
|
||||
verticalAlignment: Text.AlignTop
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// Adapter toggle, scan, and the device list with battery levels where BlueZ
|
||||
// reports them.
|
||||
Popout {
|
||||
id: root
|
||||
|
||||
contentWidth: 340
|
||||
contentHeight: 280
|
||||
|
||||
// Discovery is expensive; run it only while the panel is open.
|
||||
onShownChanged: {
|
||||
if (!shown && Bt.discovering && Bt.adapter) Bt.adapter.discovering = false;
|
||||
}
|
||||
|
||||
PopoutSurface {
|
||||
anchors.fill: parent
|
||||
tone: Bt.enabled ? Theme.blue : Theme.muted
|
||||
|
||||
PopoutHeader {
|
||||
id: header
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
title: "Bluetooth"
|
||||
icon: Bt.icon
|
||||
accent: Theme.blue
|
||||
subtitle: Bt.adapter ? Bt.adapter.name : "NO ADAPTER"
|
||||
}
|
||||
|
||||
Row {
|
||||
id: controls
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: Theme.padM
|
||||
anchors.left: parent.left
|
||||
spacing: Theme.padS
|
||||
|
||||
P5Button {
|
||||
text: Bt.enabled ? "On" : "Off"
|
||||
icon: Bt.enabled ? "" : ""
|
||||
accent: Bt.enabled ? Theme.blue : Theme.muted
|
||||
onClicked: Bt.toggle()
|
||||
}
|
||||
|
||||
P5Button {
|
||||
text: Bt.discovering ? "Scanning" : "Scan"
|
||||
icon: ""
|
||||
accent: Bt.discovering ? Theme.accent : Theme.primary
|
||||
onClicked: Bt.toggleScan()
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: listLabel
|
||||
anchors.top: controls.bottom
|
||||
anchors.topMargin: Theme.padM
|
||||
anchors.left: parent.left
|
||||
text: Bt.devices.length > 0 ? "DEVICES" : "NO DEVICES"
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.letterSpacing: 2
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
ListView {
|
||||
anchors.top: listLabel.bottom
|
||||
anchors.topMargin: Theme.padS
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
clip: true
|
||||
spacing: 2
|
||||
model: Bt.devices
|
||||
|
||||
delegate: Item {
|
||||
id: devRow
|
||||
|
||||
required property BluetoothDevice modelData
|
||||
|
||||
width: ListView.view.width
|
||||
height: 34
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: devHover.hovered
|
||||
? Theme.alpha(Theme.blue, 0.16)
|
||||
: (devRow.modelData.connected ? Theme.alpha(Theme.blue, 0.08) : "transparent")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 3
|
||||
height: devRow.modelData.connected ? 22 : 0
|
||||
color: Theme.accent
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.padM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.padS
|
||||
|
||||
Icon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Bt.deviceIcon(devRow.modelData)
|
||||
color: devRow.modelData.connected ? Theme.blue : Theme.subtext
|
||||
font.pixelSize: Theme.fsLarge
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: -2
|
||||
|
||||
Text {
|
||||
text: Bt.deviceLabel(devRow.modelData)
|
||||
color: devRow.modelData.connected ? Theme.text : Theme.subtext
|
||||
font.family: Theme.fontDisplay
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.italic: true
|
||||
font.weight: devRow.modelData.connected ? Font.Black : Font.Medium
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (devRow.modelData.pairing) return "pairing…";
|
||||
if (devRow.modelData.connected) return "connected";
|
||||
if (devRow.modelData.paired || devRow.modelData.bonded) return "paired";
|
||||
return devRow.modelData.address;
|
||||
}
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: 8
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Device battery, where BlueZ exposes it.
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.padS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 3
|
||||
visible: devRow.modelData.batteryAvailable
|
||||
|
||||
Icon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: ""
|
||||
color: devRow.modelData.battery > 0.25 ? Theme.ok : Theme.warn
|
||||
font.pixelSize: Theme.fsSmall
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Math.round(devRow.modelData.battery * 100) + "%"
|
||||
color: Theme.subtext
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: devHover
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: Bt.toggleDevice(devRow.modelData)
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: if (devRow.modelData.paired) devRow.modelData.forget()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// Month grid with the current day slashed in crimson, plus uptime and a couple
|
||||
// of world clocks.
|
||||
Popout {
|
||||
id: root
|
||||
|
||||
property date clockDate: new Date()
|
||||
|
||||
contentWidth: 320
|
||||
contentHeight: 352
|
||||
|
||||
// Month currently on display, as an offset from the real one.
|
||||
property int monthOffset: 0
|
||||
|
||||
onShownChanged: if (!shown) monthOffset = 0
|
||||
|
||||
readonly property date viewDate: {
|
||||
const d = new Date(root.clockDate);
|
||||
d.setDate(1);
|
||||
d.setMonth(d.getMonth() + root.monthOffset);
|
||||
return d;
|
||||
}
|
||||
|
||||
readonly property int viewYear: viewDate.getFullYear()
|
||||
readonly property int viewMonth: viewDate.getMonth()
|
||||
|
||||
// Monday-first grid, 6 rows of 7.
|
||||
readonly property var cells: {
|
||||
const first = new Date(root.viewYear, root.viewMonth, 1);
|
||||
const lead = (first.getDay() + 6) % 7;
|
||||
const start = new Date(root.viewYear, root.viewMonth, 1 - lead);
|
||||
const out = [];
|
||||
for (let i = 0; i < 42; i++) {
|
||||
const d = new Date(start);
|
||||
d.setDate(start.getDate() + i);
|
||||
out.push({
|
||||
day: d.getDate(),
|
||||
inMonth: d.getMonth() === root.viewMonth,
|
||||
today: d.toDateString() === root.clockDate.toDateString()
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
PopoutSurface {
|
||||
anchors.fill: parent
|
||||
tone: Theme.accent
|
||||
|
||||
PopoutHeader {
|
||||
id: header
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
title: Qt.formatDateTime(root.viewDate, "MMMM")
|
||||
icon: ""
|
||||
accent: Theme.accent
|
||||
subtitle: Qt.formatDateTime(root.viewDate, "yyyy")
|
||||
}
|
||||
|
||||
// Month stepper.
|
||||
Row {
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: Theme.padS
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.padS
|
||||
z: 2
|
||||
|
||||
StepButton {
|
||||
glyph: ""
|
||||
onClicked: root.monthOffset--
|
||||
}
|
||||
|
||||
StepButton {
|
||||
glyph: ""
|
||||
onClicked: root.monthOffset++
|
||||
}
|
||||
}
|
||||
|
||||
// Weekday header, clear of the month stepper above it.
|
||||
Row {
|
||||
id: dayNames
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: 36
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
Repeater {
|
||||
model: ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
||||
|
||||
Text {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: dayNames.width / 7
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: modelData
|
||||
color: index >= 5 ? Theme.alpha(Theme.accent, 0.8) : Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: 8
|
||||
font.letterSpacing: 1
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Grid {
|
||||
id: grid
|
||||
anchors.top: dayNames.bottom
|
||||
anchors.topMargin: Theme.padS
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
columns: 7
|
||||
|
||||
Repeater {
|
||||
model: root.cells
|
||||
|
||||
Item {
|
||||
id: cell
|
||||
required property var modelData
|
||||
|
||||
width: grid.width / 7
|
||||
height: 30
|
||||
|
||||
// Today gets a leaning crimson slab.
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 26
|
||||
height: 24
|
||||
visible: cell.modelData.today
|
||||
color: Theme.accent
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 24 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: cell.modelData.day
|
||||
color: {
|
||||
if (cell.modelData.today) return Theme.text;
|
||||
if (!cell.modelData.inMonth) return Theme.alpha(Theme.muted, 0.45);
|
||||
return Theme.subtext;
|
||||
}
|
||||
font.family: Theme.fontDisplay
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.italic: true
|
||||
font.weight: cell.modelData.today ? Font.Black : Font.Medium
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Footer: uptime and a second time zone.
|
||||
Row {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
Column {
|
||||
width: parent.width / 2
|
||||
spacing: -2
|
||||
|
||||
Text {
|
||||
text: "UPTIME"
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: 8
|
||||
font.letterSpacing: 1.5
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Sys.uptime
|
||||
color: Theme.text
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.weight: Font.DemiBold
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width / 2
|
||||
spacing: -2
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
text: "TOKYO"
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: 8
|
||||
font.letterSpacing: 1.5
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
text: {
|
||||
// Tokyo is UTC+9 year-round.
|
||||
const utc = root.clockDate.getTime()
|
||||
+ root.clockDate.getTimezoneOffset() * 60000;
|
||||
const tokyo = new Date(utc + 9 * 3600000);
|
||||
return Qt.formatDateTime(tokyo, "HH:mm");
|
||||
}
|
||||
color: Theme.primary
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.weight: Font.DemiBold
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component StepButton: Item {
|
||||
id: step
|
||||
|
||||
property string glyph: ""
|
||||
signal clicked()
|
||||
|
||||
implicitWidth: 22
|
||||
implicitHeight: 22
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: stepHover.hovered ? Theme.alpha(Theme.accent, 0.25) : "transparent"
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.outline, 0.7)
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
Icon {
|
||||
anchors.centerIn: parent
|
||||
text: step.glyph
|
||||
color: stepHover.hovered ? Theme.text : Theme.subtext
|
||||
font.pixelSize: Theme.fsBody
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: stepHover
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: step.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// Full player: cover art, transport, seek bar, and a picker when more than one
|
||||
// player is running.
|
||||
Popout {
|
||||
id: root
|
||||
|
||||
contentWidth: 400
|
||||
contentHeight: 200 + (Media.players.length > 1 ? 34 : 0)
|
||||
|
||||
PopoutSurface {
|
||||
id: surface
|
||||
anchors.fill: parent
|
||||
tone: Theme.primary
|
||||
|
||||
PopoutHeader {
|
||||
id: header
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
title: "Now Playing"
|
||||
icon: Media.sourceIcon
|
||||
subtitle: Media.identity
|
||||
}
|
||||
|
||||
// Cover art, chopped.
|
||||
Item {
|
||||
id: artFrame
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: Theme.padM
|
||||
anchors.left: parent.left
|
||||
width: 108
|
||||
height: 108
|
||||
|
||||
Shape {
|
||||
id: artMask
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
ShapePath {
|
||||
fillColor: "white"
|
||||
strokeColor: "transparent"
|
||||
PathPolyline {
|
||||
path: Geom.chamfer(Geom.parallelogram(108, 108, 0.1), 18, [1, 3])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: art
|
||||
anchors.fill: parent
|
||||
source: Media.artUrl
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
cache: true
|
||||
visible: status === Image.Ready
|
||||
layer.enabled: true
|
||||
layer.effect: MaskedArt { maskItem: artMask }
|
||||
}
|
||||
|
||||
// Placeholder when the player exposes no artwork.
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
visible: art.status !== Image.Ready
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
ShapePath {
|
||||
fillColor: Theme.surface
|
||||
strokeColor: Theme.alpha(Theme.primary, 0.5)
|
||||
strokeWidth: 2
|
||||
PathPolyline {
|
||||
path: Geom.chamfer(Geom.parallelogram(108, 108, 0.1), 18, [1, 3])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Icon {
|
||||
anchors.centerIn: parent
|
||||
visible: art.status !== Image.Ready
|
||||
text: Media.sourceIcon
|
||||
font.pixelSize: 42
|
||||
color: Theme.alpha(Theme.primary, 0.7)
|
||||
}
|
||||
|
||||
// Spinning rule around the art while playing.
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
width: 30
|
||||
height: 3
|
||||
color: Theme.accent
|
||||
visible: Media.playing
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: info
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: Theme.padM
|
||||
anchors.left: artFrame.right
|
||||
anchors.leftMargin: Theme.padM
|
||||
anchors.right: parent.right
|
||||
spacing: 2
|
||||
|
||||
Marquee {
|
||||
width: parent.width
|
||||
height: 24
|
||||
text: Media.title || "Nothing playing"
|
||||
color: Theme.text
|
||||
pixelSize: Theme.fsLarge
|
||||
family: Theme.fontDisplay
|
||||
weight: Font.Black
|
||||
italic: true
|
||||
}
|
||||
|
||||
Marquee {
|
||||
width: parent.width
|
||||
height: 16
|
||||
text: Media.artist
|
||||
color: Theme.primary
|
||||
pixelSize: Theme.fsSmall
|
||||
family: Theme.fontDisplay
|
||||
italic: true
|
||||
weight: Font.DemiBold
|
||||
}
|
||||
|
||||
Marquee {
|
||||
width: parent.width
|
||||
height: 14
|
||||
text: Media.album
|
||||
color: Theme.muted
|
||||
pixelSize: Theme.fsMicro
|
||||
family: Theme.fontMono
|
||||
}
|
||||
|
||||
Item { width: 1; height: Theme.padS }
|
||||
|
||||
// Transport
|
||||
Row {
|
||||
spacing: Theme.padM
|
||||
|
||||
TransportButton {
|
||||
glyph: ""
|
||||
enabledControl: Media.canGoPrevious
|
||||
onClicked: Media.previous()
|
||||
}
|
||||
|
||||
TransportButton {
|
||||
glyph: Media.playing ? "" : ""
|
||||
primary: true
|
||||
enabledControl: Media.hasPlayer
|
||||
onClicked: Media.playPause()
|
||||
}
|
||||
|
||||
TransportButton {
|
||||
glyph: ""
|
||||
enabledControl: Media.canGoNext
|
||||
onClicked: Media.next()
|
||||
}
|
||||
|
||||
TransportButton {
|
||||
glyph: ""
|
||||
enabledControl: Media.hasPlayer
|
||||
onClicked: Media.raise()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Seek bar across the bottom.
|
||||
Item {
|
||||
id: seekArea
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: picker.visible ? picker.top : parent.bottom
|
||||
anchors.bottomMargin: Theme.padS
|
||||
height: 26
|
||||
visible: Media.hasPlayer
|
||||
|
||||
Text {
|
||||
id: elapsed
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Media.timeString(Media.position)
|
||||
color: Theme.subtext
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Text {
|
||||
id: total
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Media.timeString(Media.length)
|
||||
color: Theme.subtext
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
P5Slider {
|
||||
anchors.left: elapsed.right
|
||||
anchors.right: total.left
|
||||
anchors.leftMargin: Theme.padS
|
||||
anchors.rightMargin: Theme.padS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
value: Media.progress
|
||||
accent: Theme.accent
|
||||
enabledControl: Media.canSeek
|
||||
onMoved: v => Media.seekTo(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Player picker, only when it matters.
|
||||
Row {
|
||||
id: picker
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: Theme.padS
|
||||
visible: Media.players.length > 1
|
||||
|
||||
Repeater {
|
||||
model: Media.players
|
||||
|
||||
Item {
|
||||
id: chip
|
||||
required property var modelData
|
||||
|
||||
readonly property bool current: Media.active === modelData
|
||||
|
||||
implicitWidth: chipLabel.implicitWidth + Theme.padM
|
||||
implicitHeight: 22
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: chip.current
|
||||
? Theme.alpha(Theme.primary, 0.22)
|
||||
: (chipHover.hovered ? Theme.alpha(Theme.primary, 0.1) : "transparent")
|
||||
border.width: 1
|
||||
border.color: chip.current ? Theme.primary : Theme.alpha(Theme.outline, 0.7)
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: chipLabel
|
||||
anchors.centerIn: parent
|
||||
text: chip.modelData.identity
|
||||
color: chip.current ? Theme.text : Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: chipHover
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: Media.select(chip.modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component TransportButton: Item {
|
||||
id: tb
|
||||
|
||||
property string glyph: ""
|
||||
property bool primary: false
|
||||
property bool enabledControl: true
|
||||
|
||||
signal clicked()
|
||||
|
||||
implicitWidth: primary ? 38 : 30
|
||||
implicitHeight: primary ? 38 : 30
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: {
|
||||
if (!tb.enabledControl) return "transparent";
|
||||
if (tbHover.hovered) return tb.primary ? Theme.accent : Theme.alpha(Theme.primary, 0.22);
|
||||
return tb.primary ? Theme.alpha(Theme.primary, 0.18) : "transparent";
|
||||
}
|
||||
border.width: tb.primary ? 2 : 1
|
||||
border.color: tb.enabledControl
|
||||
? (tbHover.hovered ? Theme.text : Theme.alpha(Theme.primary, 0.6))
|
||||
: Theme.alpha(Theme.outline, 0.4)
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, tb.height * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.durFast }
|
||||
}
|
||||
}
|
||||
|
||||
Icon {
|
||||
anchors.centerIn: parent
|
||||
text: tb.glyph
|
||||
font.pixelSize: tb.primary ? Theme.fsTitle : Theme.fsLarge
|
||||
color: {
|
||||
if (!tb.enabledControl) return Theme.alpha(Theme.muted, 0.5);
|
||||
if (tbHover.hovered && tb.primary) return Theme.text;
|
||||
return Theme.primary;
|
||||
}
|
||||
}
|
||||
|
||||
scale: tbHover.hovered && tb.enabledControl ? 1.12 : 1.0
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: tbHover
|
||||
enabled: tb.enabledControl
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
enabled: tb.enabledControl
|
||||
onTapped: tb.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Networking
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// Link state, throughput, and the Wi-Fi picker.
|
||||
Popout {
|
||||
id: root
|
||||
|
||||
contentWidth: 360
|
||||
contentHeight: 300
|
||||
|
||||
// Scan only while the popout is actually on screen.
|
||||
onShownChanged: Net.setScanning(shown)
|
||||
|
||||
PopoutSurface {
|
||||
anchors.fill: parent
|
||||
tone: Sys.netUp ? Theme.primary : Theme.muted
|
||||
|
||||
PopoutHeader {
|
||||
id: header
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
title: "Network"
|
||||
icon: Net.icon
|
||||
subtitle: Sys.netUp ? Sys.netIp : "OFFLINE"
|
||||
}
|
||||
|
||||
// Throughput
|
||||
Row {
|
||||
id: rates
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: Theme.padM
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.padM
|
||||
|
||||
RateBlock {
|
||||
width: (parent.width - Theme.padM) / 2
|
||||
glyph: ""
|
||||
label: "Down"
|
||||
value: Sys.rate(Sys.netRx)
|
||||
tone: Theme.primary
|
||||
}
|
||||
|
||||
RateBlock {
|
||||
width: (parent.width - Theme.padM) / 2
|
||||
glyph: ""
|
||||
label: "Up"
|
||||
value: Sys.rate(Sys.netTx)
|
||||
tone: Theme.accent
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: controls
|
||||
anchors.top: rates.bottom
|
||||
anchors.topMargin: Theme.padM
|
||||
anchors.left: parent.left
|
||||
spacing: Theme.padS
|
||||
|
||||
P5Button {
|
||||
text: Net.wifiEnabled ? "Wi-Fi On" : "Wi-Fi Off"
|
||||
icon: Net.wifiEnabled ? "" : ""
|
||||
accent: Net.wifiEnabled ? Theme.primary : Theme.muted
|
||||
onClicked: Net.toggleWifi()
|
||||
}
|
||||
|
||||
P5Button {
|
||||
text: "Settings"
|
||||
icon: ""
|
||||
onClicked: Actions.openNetworkSettings()
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: apsLabel
|
||||
anchors.top: controls.bottom
|
||||
anchors.topMargin: Theme.padM
|
||||
anchors.left: parent.left
|
||||
text: Net.scanning ? "SCANNING…" : "NETWORKS"
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.letterSpacing: 2
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
ListView {
|
||||
anchors.top: apsLabel.bottom
|
||||
anchors.topMargin: Theme.padS
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
clip: true
|
||||
spacing: 2
|
||||
model: Net.wifiNetworks
|
||||
|
||||
delegate: Item {
|
||||
id: apRow
|
||||
|
||||
required property var modelData
|
||||
|
||||
width: ListView.view.width
|
||||
height: 30
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: apHover.hovered
|
||||
? Theme.alpha(Theme.primary, 0.14)
|
||||
: (apRow.modelData.connected ? Theme.alpha(Theme.primary, 0.07) : "transparent")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 3
|
||||
height: apRow.modelData.connected ? 20 : 0
|
||||
color: Theme.accent
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.padM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.padS
|
||||
|
||||
Icon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Net.signalIcon(apRow.modelData.signalStrength ?? 0)
|
||||
color: apRow.modelData.connected ? Theme.primary : Theme.subtext
|
||||
font.pixelSize: Theme.fsBody
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: apRow.modelData.name
|
||||
color: apRow.modelData.connected ? Theme.text : Theme.subtext
|
||||
font.family: Theme.fontDisplay
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.italic: true
|
||||
font.weight: apRow.modelData.connected ? Font.Black : Font.Medium
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Icon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: Net.secured(apRow.modelData)
|
||||
text: ""
|
||||
color: Theme.muted
|
||||
font.pixelSize: Theme.fsMicro
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.padS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: apRow.modelData.known ? "SAVED" : ""
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: 8
|
||||
font.letterSpacing: 1
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: apHover
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
if (apRow.modelData.connected) {
|
||||
apRow.modelData.disconnect();
|
||||
} else {
|
||||
// Saved networks connect directly; new secured ones
|
||||
// need a passphrase, which NetworkManager's own
|
||||
// agent will prompt for.
|
||||
apRow.modelData.connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component RateBlock: Item {
|
||||
id: rateBlock
|
||||
|
||||
property string glyph: ""
|
||||
property string label: ""
|
||||
property string value: ""
|
||||
property color tone: Theme.primary
|
||||
|
||||
implicitHeight: 40
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.alpha(rateBlock.tone, 0.08)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(rateBlock.tone, 0.35)
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, rateBlock.height * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.padS
|
||||
|
||||
Icon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: rateBlock.glyph
|
||||
color: rateBlock.tone
|
||||
font.pixelSize: Theme.fsLarge
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: -1
|
||||
|
||||
Text {
|
||||
text: rateBlock.label.toUpperCase()
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: 8
|
||||
font.letterSpacing: 1.5
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Text {
|
||||
text: rateBlock.value
|
||||
color: Theme.text
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.weight: Font.DemiBold
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// Notification history and the do-not-disturb switch.
|
||||
Popout {
|
||||
id: root
|
||||
|
||||
contentWidth: 400
|
||||
contentHeight: 380
|
||||
|
||||
PopoutSurface {
|
||||
anchors.fill: parent
|
||||
tone: Notifs.dnd ? Theme.muted : Theme.primary
|
||||
|
||||
PopoutHeader {
|
||||
id: header
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
title: "Notifications"
|
||||
icon: Notifs.icon
|
||||
accent: Notifs.dnd ? Theme.muted : Theme.primary
|
||||
subtitle: Notifs.count + " HELD"
|
||||
}
|
||||
|
||||
Row {
|
||||
id: controls
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: Theme.padM
|
||||
anchors.left: parent.left
|
||||
spacing: Theme.padS
|
||||
|
||||
P5Button {
|
||||
text: Notifs.dnd ? "Silenced" : "Silence"
|
||||
icon: Notifs.dnd ? "" : ""
|
||||
accent: Notifs.dnd ? Theme.accent : Theme.primary
|
||||
onClicked: Notifs.toggleDnd()
|
||||
}
|
||||
|
||||
P5Button {
|
||||
text: "Clear"
|
||||
icon: ""
|
||||
destructive: true
|
||||
onClicked: Notifs.clearAll()
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: Notifs.count === 0
|
||||
text: "NOTHING TO REPORT"
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontDisplay
|
||||
font.pixelSize: Theme.fsBody
|
||||
font.italic: true
|
||||
font.weight: Font.Black
|
||||
font.letterSpacing: 3
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
ListView {
|
||||
anchors.top: controls.bottom
|
||||
anchors.topMargin: Theme.padM
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
clip: true
|
||||
spacing: Theme.padS
|
||||
model: Notifs.history
|
||||
|
||||
delegate: NotifCard {
|
||||
required property var modelData
|
||||
width: ListView.view.width
|
||||
notif: modelData
|
||||
onDismissed: Notifs.dismiss(modelData)
|
||||
}
|
||||
|
||||
add: Transition {
|
||||
NumberAnimation {
|
||||
properties: "x"
|
||||
from: 40
|
||||
duration: Theme.durSlow
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
NumberAnimation {
|
||||
property: "opacity"
|
||||
from: 0; to: 1
|
||||
duration: Theme.durBase
|
||||
}
|
||||
}
|
||||
|
||||
remove: Transition {
|
||||
NumberAnimation {
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: Theme.durFast
|
||||
}
|
||||
}
|
||||
|
||||
displaced: Transition {
|
||||
NumberAnimation {
|
||||
properties: "y"
|
||||
duration: Theme.durBase
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import QtQuick
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
|
||||
// Title bar for a popout: glyph, split-ink heading, right-aligned status text,
|
||||
// and a crimson rule with a chopped tail.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string title: ""
|
||||
property string subtitle: ""
|
||||
property string icon: ""
|
||||
property color accent: Theme.primary
|
||||
|
||||
implicitHeight: 34
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
spacing: Theme.padS
|
||||
|
||||
Icon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.icon !== ""
|
||||
text: root.icon
|
||||
color: root.accent
|
||||
font.pixelSize: Theme.fsTitle
|
||||
}
|
||||
|
||||
SplitText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.title.toUpperCase()
|
||||
pixelSize: Theme.fsLarge
|
||||
letterSpacing: 1.5
|
||||
split: 1.3
|
||||
splitOpacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 6
|
||||
visible: root.subtitle !== ""
|
||||
text: root.subtitle
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.letterSpacing: 1
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 22
|
||||
height: 2
|
||||
color: Theme.alpha(root.accent, 0.8)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
width: 16
|
||||
height: 2
|
||||
color: Theme.accent
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -1.4, 0, 2.8,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
|
||||
// The slab every popout sits on. Pure preset — it declares no children of its
|
||||
// own, so P5Panel's default content property stays usable by callers.
|
||||
P5Panel {
|
||||
property color tone: Theme.primary
|
||||
|
||||
fill: Theme.mantle
|
||||
fillOpacity: 0.97
|
||||
border: Theme.alpha(tone, 0.75)
|
||||
borderWidth: Theme.stroke
|
||||
halftone: true
|
||||
halftoneColor: tone
|
||||
halftoneOpacity: 0.06
|
||||
sheen: true
|
||||
slash: true
|
||||
slashColor: tone
|
||||
slashWidth: 5
|
||||
lean: 0.06
|
||||
chop: 16
|
||||
padding: Theme.padM
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// The resource dashboard: four gauges, then the details that do not fit on one.
|
||||
Popout {
|
||||
id: root
|
||||
|
||||
contentWidth: 420
|
||||
contentHeight: 268
|
||||
|
||||
PopoutSurface {
|
||||
anchors.fill: parent
|
||||
tone: Theme.heat(Math.max(Sys.cpuUsage, Sys.memPercent))
|
||||
|
||||
PopoutHeader {
|
||||
id: header
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
title: "System"
|
||||
icon: ""
|
||||
accent: Theme.primary
|
||||
subtitle: "UP " + Sys.uptime + " · " + Sys.procs + " PROCS"
|
||||
}
|
||||
|
||||
Row {
|
||||
id: gauges
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: Theme.padM
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.padM
|
||||
|
||||
Gauge {
|
||||
width: 88; height: 88
|
||||
value: Sys.cpuUsage
|
||||
label: "CPU"
|
||||
sub: Math.round(Sys.cpuTemp) + "°C"
|
||||
}
|
||||
|
||||
Gauge {
|
||||
width: 88; height: 88
|
||||
value: Sys.memPercent
|
||||
label: "MEM"
|
||||
sub: Sys.gb(Sys.memUsed) + "/" + Sys.gb(Sys.memTotal)
|
||||
}
|
||||
|
||||
Gauge {
|
||||
width: 88; height: 88
|
||||
visible: Sys.gpuAvailable
|
||||
value: Sys.gpuUsage
|
||||
label: "GPU"
|
||||
sub: Math.round(Sys.gpuTemp) + "°C"
|
||||
}
|
||||
|
||||
Gauge {
|
||||
width: 88; height: 88
|
||||
value: Sys.diskPercent
|
||||
label: "DISK"
|
||||
sub: Sys.diskFree + " free"
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.top: gauges.bottom
|
||||
anchors.topMargin: Theme.padM
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: 3
|
||||
|
||||
InfoRow {
|
||||
width: parent.width
|
||||
key: "Processor"
|
||||
value: Sys.cpuModel
|
||||
}
|
||||
|
||||
InfoRow {
|
||||
width: parent.width
|
||||
key: "Load"
|
||||
value: Sys.load1.toFixed(2) + " " + Sys.load5.toFixed(2) + " " + Sys.load15.toFixed(2)
|
||||
}
|
||||
|
||||
InfoRow {
|
||||
width: parent.width
|
||||
visible: Sys.gpuAvailable
|
||||
key: "VRAM"
|
||||
value: Sys.gb(Sys.gpuVramUsed) + " / " + Sys.gb(Sys.gpuVramTotal)
|
||||
}
|
||||
|
||||
InfoRow {
|
||||
width: parent.width
|
||||
key: "Root"
|
||||
value: Sys.gb(Sys.diskUsed) + " of " + Sys.gb(Sys.diskTotal) + " used"
|
||||
}
|
||||
|
||||
InfoRow {
|
||||
width: parent.width
|
||||
key: "Network"
|
||||
value: Sys.netUp
|
||||
? Sys.netInterface + " ↓ " + Sys.rate(Sys.netRx) + " ↑ " + Sys.rate(Sys.netTx)
|
||||
: "offline"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component InfoRow: Item {
|
||||
id: infoRow
|
||||
|
||||
property string key: ""
|
||||
property string value: ""
|
||||
|
||||
implicitHeight: visible ? 17 : 0
|
||||
|
||||
Text {
|
||||
id: keyLabel
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: infoRow.key.toUpperCase()
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.letterSpacing: 1.5
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.left: keyLabel.right
|
||||
anchors.leftMargin: Theme.padM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: infoRow.value
|
||||
horizontalAlignment: Text.AlignRight
|
||||
elide: Text.ElideRight
|
||||
color: Theme.subtext
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user