698 lines
26 KiB
QML
698 lines
26 KiB
QML
import QtQuick
|
|
import QtQuick.Layouts
|
|
import QtQuick.Controls
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import Quickshell.Io
|
|
import "../theme"
|
|
import ".."
|
|
|
|
PanelWindow {
|
|
id: root
|
|
|
|
anchors {
|
|
top: true
|
|
bottom: true
|
|
left: true
|
|
}
|
|
|
|
margins {
|
|
left: Theme.sidebarWidth
|
|
top: Theme.topStripHeight
|
|
}
|
|
|
|
implicitWidth: Theme.hudWidth
|
|
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.namespace: "quickshell-systemhud"
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
|
|
|
visible: GlobalState.systemHudVisible
|
|
|
|
color: "transparent"
|
|
|
|
property real slideOffset: GlobalState.systemHudVisible ? 0 : -Theme.hudWidth
|
|
Behavior on slideOffset {
|
|
NumberAnimation {
|
|
duration: Theme.animSmooth
|
|
easing.type: Easing.OutQuint
|
|
}
|
|
}
|
|
|
|
Shortcut {
|
|
sequence: "Escape"
|
|
onActivated: GlobalState.systemHudVisible = false
|
|
}
|
|
|
|
component SectionHeader: Item {
|
|
property string sectionNum: "00"
|
|
property string title: "Section"
|
|
implicitWidth: parent.width
|
|
implicitHeight: 24
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
spacing: 8
|
|
Text {
|
|
text: sectionNum + " //"
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: 12
|
|
color: Theme.accentVolt
|
|
}
|
|
Text {
|
|
text: title
|
|
font.family: Theme.fontSerif
|
|
font.italic: true
|
|
font.pixelSize: 16
|
|
color: Theme.textPrimary
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
}
|
|
|
|
component ToggleCard: Rectangle {
|
|
property string title: ""
|
|
property string icon: ""
|
|
property string glyph: "" // Nerd Font glyph alternative to icon file
|
|
property bool active: false
|
|
signal clicked()
|
|
|
|
Layout.preferredWidth: 160
|
|
Layout.preferredHeight: 56
|
|
color: Theme.bgPrimary
|
|
border.color: active ? Qt.rgba(Theme.accentVolt.r, Theme.accentVolt.g, Theme.accentVolt.b, 0.3) : Theme.glassBorder
|
|
border.width: 1
|
|
radius: 4
|
|
|
|
Behavior on border.color { ColorAnimation { duration: Theme.animFast } }
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: clicked()
|
|
onEntered: if (!active) border.color = Theme.glassBorderHover
|
|
onExited: if (!active) border.color = Theme.glassBorder
|
|
}
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 12
|
|
spacing: 12
|
|
Text {
|
|
visible: parent.parent.glyph !== ""
|
|
text: parent.parent.glyph
|
|
font.family: "JetBrainsMono Nerd Font"
|
|
font.pixelSize: 20
|
|
color: parent.parent.active ? Theme.accentVolt : Theme.textMuted
|
|
}
|
|
Image {
|
|
source: parent.parent.icon && parent.parent.glyph === "" ? Qt.resolvedUrl("../" + parent.parent.icon) : ""
|
|
Layout.preferredWidth: 24
|
|
Layout.preferredHeight: 24
|
|
sourceSize: Qt.size(24, 24)
|
|
visible: parent.parent.icon !== "" && parent.parent.glyph === ""
|
|
}
|
|
Text {
|
|
text: parent.parent.title
|
|
color: Theme.textPrimary
|
|
font.family: Theme.fontSans
|
|
font.pixelSize: 13
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
|
|
component CustomSlider: Item {
|
|
property real value: 0
|
|
signal valueChangedUser(real newVal)
|
|
|
|
implicitWidth: parent.width
|
|
implicitHeight: 24
|
|
|
|
Rectangle {
|
|
id: track
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: parent.width - 40
|
|
height: 6
|
|
color: Theme.bgSecondary
|
|
radius: 3
|
|
|
|
Rectangle {
|
|
id: fill
|
|
width: track.width * (parent.parent.value / 100)
|
|
height: track.height
|
|
color: Theme.accentVolt
|
|
radius: 3
|
|
}
|
|
|
|
Rectangle {
|
|
width: 14
|
|
height: 14
|
|
radius: 7
|
|
color: Theme.textPrimary
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
x: Math.max(0, fill.width - 7)
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
anchors.margins: -10
|
|
function updateValue(mouseX) {
|
|
let p = Math.max(0, Math.min(1, mouseX / track.width))
|
|
parent.parent.valueChangedUser(p * 100)
|
|
}
|
|
onPressed: (mouse) => updateValue(mouse.x)
|
|
onPositionChanged: (mouse) => updateValue(mouse.x)
|
|
}
|
|
}
|
|
Text {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: Math.round(parent.value) + "%"
|
|
color: Theme.textPrimary
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: 12
|
|
}
|
|
}
|
|
|
|
component ProgressBarItem: ColumnLayout {
|
|
property string label: ""
|
|
property real percent: 0
|
|
property string valueText: ""
|
|
property color barColor: Theme.accentVolt
|
|
|
|
Layout.fillWidth: true
|
|
spacing: 4
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Text {
|
|
text: parent.parent.label
|
|
color: Theme.textPrimary
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: 12
|
|
}
|
|
Item { Layout.fillWidth: true }
|
|
Text {
|
|
text: parent.parent.valueText
|
|
color: Theme.textSecondary
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: 12
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 8
|
|
color: Theme.bgSecondary
|
|
radius: 4
|
|
|
|
Rectangle {
|
|
width: parent.width * (parent.parent.percent / 100)
|
|
height: parent.height
|
|
color: parent.parent.barColor
|
|
radius: 4
|
|
Behavior on width { NumberAnimation { duration: Theme.animFast; easing.type: Easing.OutCubic } }
|
|
Behavior on color { ColorAnimation { duration: Theme.animFast } }
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: panelContent
|
|
anchors.fill: parent
|
|
color: Theme.glassBg
|
|
opacity: GlobalState.systemHudVisible ? 1 : 0
|
|
|
|
transform: Translate { x: slideOffset }
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: Theme.animSmooth
|
|
easing.type: Easing.OutQuint
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
width: 1
|
|
color: Theme.glassBorder
|
|
}
|
|
|
|
Item {
|
|
id: header
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
height: 60
|
|
|
|
Rectangle {
|
|
anchors.bottom: parent.bottom
|
|
width: parent.width
|
|
height: 1
|
|
color: Theme.glassBorder
|
|
}
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 20
|
|
anchors.rightMargin: 20
|
|
|
|
Text {
|
|
text: "System HUD"
|
|
font.family: Theme.fontSerif
|
|
font.italic: true
|
|
font.pixelSize: 19
|
|
color: Theme.textPrimary
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
Text {
|
|
text: "✕"
|
|
color: Theme.textMuted
|
|
font.pixelSize: 16
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
anchors.margins: -10
|
|
hoverEnabled: true
|
|
onEntered: parent.color = Theme.textPrimary
|
|
onExited: parent.color = Theme.textMuted
|
|
onClicked: GlobalState.systemHudVisible = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ScrollView {
|
|
id: scrollView
|
|
anchors.top: header.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
contentWidth: availableWidth
|
|
clip: true
|
|
|
|
ColumnLayout {
|
|
id: contentLayout
|
|
width: parent.width - 40
|
|
x: 20
|
|
spacing: 32
|
|
|
|
Item { Layout.preferredHeight: 16 } // Top padding
|
|
|
|
// Section 1: Toggles
|
|
ColumnLayout {
|
|
id: sectionToggles
|
|
Layout.fillWidth: true
|
|
spacing: 16
|
|
SectionHeader { sectionNum: "01"; title: "Toggles" }
|
|
GridLayout {
|
|
columns: 2
|
|
columnSpacing: 12
|
|
rowSpacing: 12
|
|
Layout.fillWidth: true
|
|
|
|
ToggleCard {
|
|
title: GlobalState.wifiSsid !== "" ? GlobalState.wifiSsid : "Wi-Fi"
|
|
icon: "assets/wifi.svg"
|
|
active: GlobalState.wifiConnected
|
|
onClicked: {
|
|
Quickshell.execDetached(["sh", "-c", "nm-connection-editor || foot -e nmtui"])
|
|
}
|
|
}
|
|
|
|
ToggleCard {
|
|
title: "Bluetooth"
|
|
icon: "assets/bluetooth.svg"
|
|
active: false // Adjust if there's a bluetooth state
|
|
onClicked: {
|
|
let proc = Quickshell.createProcess("sh", ["-c", "foot -e bluetoothctl"])
|
|
proc.start()
|
|
}
|
|
}
|
|
|
|
ToggleCard {
|
|
property bool dndActive: false
|
|
title: "Do Not Disturb"
|
|
glyph: "\uF186"
|
|
active: dndActive
|
|
onClicked: dndActive = !dndActive
|
|
}
|
|
|
|
ToggleCard {
|
|
title: GlobalState.volumeMuted ? "Muted" : "Sound"
|
|
glyph: GlobalState.volumeMuted ? "\uF026" : "\uF028"
|
|
active: !GlobalState.volumeMuted
|
|
onClicked: GlobalState.toggleVolumeMute()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Section 2: Volume
|
|
ColumnLayout {
|
|
id: sectionVolume
|
|
Layout.fillWidth: true
|
|
spacing: 16
|
|
SectionHeader { sectionNum: "02"; title: "Volume" }
|
|
|
|
CustomSlider {
|
|
Layout.fillWidth: true
|
|
value: GlobalState.volume
|
|
onValueChangedUser: (val) => GlobalState.volume = val // Adjust to actual volume setter if needed
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Text {
|
|
text: GlobalState.volumeMuted ? "Unmute" : "Mute"
|
|
color: Theme.textSecondary
|
|
font.family: Theme.fontSans
|
|
font.pixelSize: 13
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: GlobalState.toggleVolumeMute()
|
|
}
|
|
}
|
|
Item { Layout.fillWidth: true }
|
|
Text {
|
|
text: "Sound Preferences..."
|
|
color: Theme.textMuted
|
|
font.family: Theme.fontSans
|
|
font.pixelSize: 13
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onEntered: parent.color = Theme.textPrimary
|
|
onExited: parent.color = Theme.textMuted
|
|
onClicked: {
|
|
let proc = Quickshell.createProcess("pavucontrol", [])
|
|
proc.start()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Section 3: Brightness
|
|
ColumnLayout {
|
|
id: sectionBrightness
|
|
Layout.fillWidth: true
|
|
spacing: 16
|
|
SectionHeader { sectionNum: "03"; title: "Brightness" }
|
|
|
|
CustomSlider {
|
|
Layout.fillWidth: true
|
|
value: GlobalState.brightness
|
|
onValueChangedUser: (val) => GlobalState.setBrightness(val)
|
|
}
|
|
}
|
|
|
|
// Section 4: Performance
|
|
ColumnLayout {
|
|
id: sectionPerformance
|
|
Layout.fillWidth: true
|
|
spacing: 16
|
|
SectionHeader { sectionNum: "04"; title: "Performance" }
|
|
|
|
ProgressBarItem {
|
|
label: "CPU"
|
|
percent: GlobalState.cpuPercent
|
|
valueText: GlobalState.cpuPercent + "%"
|
|
barColor: GlobalState.cpuPercent > 80 ? Theme.accentCoral : Theme.accentVolt
|
|
}
|
|
|
|
ProgressBarItem {
|
|
label: "RAM"
|
|
percent: (GlobalState.memUsedGb / Math.max(1, GlobalState.memTotalGb)) * 100
|
|
valueText: GlobalState.memUsedGb + " / " + GlobalState.memTotalGb + " GB"
|
|
}
|
|
|
|
ProgressBarItem {
|
|
label: "DISK"
|
|
percent: GlobalState.diskPercent
|
|
valueText: GlobalState.diskUsedGb + " / " + GlobalState.diskTotalGb + " GB"
|
|
barColor: GlobalState.diskPercent > 90 ? Theme.accentCoral : Theme.accentVolt
|
|
}
|
|
|
|
ProgressBarItem {
|
|
label: "VRAM"
|
|
percent: (GlobalState.gpuVramUsed / Math.max(1, GlobalState.gpuVramTotal)) * 100
|
|
valueText: GlobalState.gpuVramUsed + " / " + GlobalState.gpuVramTotal + " GB"
|
|
}
|
|
}
|
|
|
|
// Section 5: Network
|
|
ColumnLayout {
|
|
id: sectionNetwork
|
|
Layout.fillWidth: true
|
|
spacing: 16
|
|
SectionHeader { sectionNum: "05"; title: "Network" }
|
|
|
|
RowLayout {
|
|
spacing: 8
|
|
Rectangle {
|
|
width: 8
|
|
height: 8
|
|
radius: 4
|
|
color: GlobalState.wifiConnected ? Theme.accentVolt : Theme.accentCoral
|
|
}
|
|
Text {
|
|
text: {
|
|
if (!GlobalState.wifiConnected) return "Disconnected";
|
|
if (GlobalState.connectionType === "wifi") return "Wi-Fi — " + (GlobalState.wifiSsid !== "" ? GlobalState.wifiSsid : "unknown");
|
|
if (GlobalState.connectionType === "eth") return "Ethernet";
|
|
return "Connected";
|
|
}
|
|
color: Theme.textPrimary
|
|
font.family: Theme.fontSans
|
|
font.pixelSize: 14
|
|
}
|
|
}
|
|
|
|
Text {
|
|
visible: GlobalState.wifiConnected
|
|
text: "IP: " + (GlobalState.ipAddress !== "" ? GlobalState.ipAddress : "—")
|
|
color: Theme.textSecondary
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: 13
|
|
}
|
|
|
|
Text {
|
|
text: "Network Preferences..."
|
|
color: Theme.textMuted
|
|
font.family: Theme.fontSans
|
|
font.pixelSize: 13
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onEntered: parent.color = Theme.textPrimary
|
|
onExited: parent.color = Theme.textMuted
|
|
onClicked: {
|
|
Quickshell.execDetached(["sh", "-c", "nm-connection-editor || foot -e nmtui"])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Section 6: Battery
|
|
ColumnLayout {
|
|
id: sectionBattery
|
|
Layout.fillWidth: true
|
|
spacing: 16
|
|
visible: GlobalState.batteryPresent
|
|
SectionHeader { sectionNum: "06"; title: "Battery" }
|
|
|
|
RowLayout {
|
|
spacing: 16
|
|
Text {
|
|
text: GlobalState.batteryPercent + "%"
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: 24
|
|
color: GlobalState.batteryStatus === "Charging" ? Theme.accentVolt :
|
|
(GlobalState.batteryPercent < 20 ? Theme.accentCoral : Theme.textPrimary)
|
|
}
|
|
Text {
|
|
text: GlobalState.batteryStatus
|
|
color: Theme.textSecondary
|
|
font.family: Theme.fontSans
|
|
font.pixelSize: 14
|
|
}
|
|
}
|
|
}
|
|
|
|
// Section 7: Now Playing
|
|
ColumnLayout {
|
|
id: sectionNowPlaying
|
|
Layout.fillWidth: true
|
|
spacing: 16
|
|
visible: GlobalState.mediaTitle !== ""
|
|
SectionHeader { sectionNum: "07"; title: "Now Playing" }
|
|
|
|
ColumnLayout {
|
|
spacing: 4
|
|
Text {
|
|
text: GlobalState.mediaTitle
|
|
color: Theme.textPrimary
|
|
font.family: Theme.fontSans
|
|
font.bold: true
|
|
font.pixelSize: 14
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
}
|
|
Text {
|
|
text: GlobalState.mediaArtist
|
|
color: Theme.textSecondary
|
|
font.family: Theme.fontSans
|
|
font.pixelSize: 12
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
spacing: 16
|
|
|
|
Text {
|
|
text: "⏮"
|
|
font.pixelSize: 20
|
|
color: Theme.textSecondary
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
anchors.margins: -10
|
|
hoverEnabled: true
|
|
onEntered: parent.color = Theme.accentVolt
|
|
onExited: parent.color = Theme.textSecondary
|
|
onClicked: GlobalState.mediaPrevious()
|
|
}
|
|
}
|
|
Text {
|
|
text: GlobalState.mediaPlaying ? "⏸" : "▶"
|
|
font.pixelSize: 24
|
|
color: Theme.textSecondary
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
anchors.margins: -10
|
|
hoverEnabled: true
|
|
onEntered: parent.color = Theme.accentVolt
|
|
onExited: parent.color = Theme.textSecondary
|
|
onClicked: GlobalState.mediaPlayPause()
|
|
}
|
|
}
|
|
Text {
|
|
text: "⏭"
|
|
font.pixelSize: 20
|
|
color: Theme.textSecondary
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
anchors.margins: -10
|
|
hoverEnabled: true
|
|
onEntered: parent.color = Theme.accentVolt
|
|
onExited: parent.color = Theme.textSecondary
|
|
onClicked: GlobalState.mediaNext()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Section 8: Calendar
|
|
ColumnLayout {
|
|
id: sectionCalendar
|
|
Layout.fillWidth: true
|
|
spacing: 16
|
|
SectionHeader { sectionNum: "08"; title: "Calendar" }
|
|
|
|
RowLayout {
|
|
spacing: 12
|
|
Text {
|
|
text: GlobalState.timeString
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: 22
|
|
color: Theme.accentVolt
|
|
}
|
|
Text {
|
|
text: GlobalState.fullDateString
|
|
font.family: Theme.fontSans
|
|
font.pixelSize: 13
|
|
color: Theme.textSecondary
|
|
}
|
|
}
|
|
|
|
GridLayout {
|
|
columns: 7
|
|
columnSpacing: 4
|
|
rowSpacing: 4
|
|
Layout.alignment: Qt.AlignHCenter
|
|
|
|
Repeater {
|
|
model: ["S", "M", "T", "W", "T", "F", "S"]
|
|
Text {
|
|
text: modelData
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: 11
|
|
color: Theme.textMuted
|
|
horizontalAlignment: Text.AlignHCenter
|
|
Layout.preferredWidth: 32
|
|
}
|
|
}
|
|
|
|
Repeater {
|
|
model: 31
|
|
Rectangle {
|
|
property bool isToday: (index + 1) === new Date().getDate()
|
|
Layout.preferredWidth: 32
|
|
Layout.preferredHeight: 32
|
|
color: isToday ? Theme.accentVolt : "transparent"
|
|
radius: isToday ? 0 : 4
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: index + 1
|
|
color: parent.isToday ? "#ffffff" : Theme.textSecondary
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: 12
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onEntered: if (!parent.isToday) parent.color = Qt.rgba(1, 1, 1, 0.04)
|
|
onExited: if (!parent.isToday) parent.color = "transparent"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item { Layout.preferredHeight: 20 } // Bottom padding
|
|
}
|
|
}
|
|
}
|
|
|
|
// Auto-scroll to active section when changed
|
|
Connections {
|
|
target: GlobalState
|
|
function onSystemHudActiveSectionChanged() {
|
|
if (!GlobalState.systemHudVisible) return;
|
|
|
|
let targetY = 0;
|
|
if (GlobalState.systemHudActiveSection === "volume") targetY = sectionVolume.y;
|
|
else if (GlobalState.systemHudActiveSection === "brightness") targetY = sectionBrightness.y;
|
|
else if (GlobalState.systemHudActiveSection === "performance") targetY = sectionPerformance.y;
|
|
else if (GlobalState.systemHudActiveSection === "network") targetY = sectionNetwork.y;
|
|
else if (GlobalState.systemHudActiveSection === "battery") targetY = sectionBattery.y;
|
|
else if (GlobalState.systemHudActiveSection === "nowplaying") targetY = sectionNowPlaying.y;
|
|
else if (GlobalState.systemHudActiveSection === "calendar") targetY = sectionCalendar.y;
|
|
|
|
if (targetY > 0) {
|
|
// Ensure targetY doesn't scroll past max possible scroll
|
|
let maxY = Math.max(0, contentLayout.height - scrollView.height);
|
|
scrollView.ScrollBar.vertical.position = Math.min(targetY, maxY) / contentLayout.height;
|
|
}
|
|
}
|
|
}
|
|
}
|