added three new modules (Crypto.qml, Battery.qml, Network.qml), improved Stats.qml, improved Tray.qml, optimized code in general, added a polkit agent, configurations change (check configs)
This commit is contained in:
589
bar/modules/Battery.qml
Normal file
589
bar/modules/Battery.qml
Normal file
@@ -0,0 +1,589 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.UPower
|
||||
|
||||
import "../../config" as Cfg
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
|
||||
property bool isTop: true
|
||||
property var targetScreen: null
|
||||
property int barHeight: Cfg.Config.barHeight
|
||||
property int barMargin: Cfg.Config.margin
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
readonly property var bat: UPower.displayDevice
|
||||
readonly property bool batReady: bat && bat.ready && bat.isLaptopBattery
|
||||
|
||||
readonly property bool moduleEnabled: batready
|
||||
|
||||
readonly property real pct: batReady ? bat.percentage * 100 : 72
|
||||
readonly property int state: batReady ? bat.state : UPowerDeviceState.Unknown
|
||||
readonly property bool charging: state === UPowerDeviceState.Charging
|
||||
readonly property bool fullCharge: state === UPowerDeviceState.FullyCharged
|
||||
|
||||
readonly property string batIcon: {
|
||||
if (!batReady) return ""
|
||||
if (fullCharge) return ""
|
||||
if (charging) {
|
||||
if (pct >= 90) return ""
|
||||
if (pct >= 80) return ""
|
||||
if (pct >= 70) return ""
|
||||
if (pct >= 60) return ""
|
||||
if (pct >= 50) return ""
|
||||
if (pct >= 40) return ""
|
||||
if (pct >= 30) return ""
|
||||
if (pct >= 20) return ""
|
||||
if (pct >= 10) return ""
|
||||
return ""
|
||||
}
|
||||
if (pct >= 90) return ""
|
||||
if (pct >= 80) return ""
|
||||
if (pct >= 70) return ""
|
||||
if (pct >= 60) return ""
|
||||
if (pct >= 50) return ""
|
||||
if (pct >= 40) return ""
|
||||
if (pct >= 30) return ""
|
||||
if (pct >= 20) return ""
|
||||
if (pct >= 10) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
readonly property bool isCritical: batReady && !charging && pct <= 15
|
||||
readonly property bool isLow: batReady && !charging && pct <= 30
|
||||
|
||||
readonly property color iconColor: {
|
||||
if (fullCharge) return t.batteryFull ?? t.accent
|
||||
if (charging) return t.batteryCharging ?? t.accent
|
||||
if (isCritical) return t.batteryCritical ?? "#ff5555"
|
||||
if (isLow) return t.batteryLow ?? "#ffb86c"
|
||||
return t.batteryIcon ?? t.clockIcon
|
||||
}
|
||||
|
||||
readonly property var up: UPower
|
||||
readonly property var pp: PowerProfiles
|
||||
|
||||
readonly property string typeText: {
|
||||
if (!batReady) return "—"
|
||||
switch (bat.type) {
|
||||
case UPowerDeviceType.Battery: return "Battery"
|
||||
case UPowerDeviceType.Ups: return "UPS"
|
||||
case UPowerDeviceType.Mouse: return "Mouse"
|
||||
case UPowerDeviceType.Keyboard: return "Keyboard"
|
||||
case UPowerDeviceType.Phone: return "Phone"
|
||||
case UPowerDeviceType.Tablet: return "Tablet"
|
||||
default: return "Other"
|
||||
}
|
||||
}
|
||||
|
||||
readonly property string degradationText: {
|
||||
if (pp.degradationReason === PerformanceDegradationReason.None) return ""
|
||||
if (pp.degradationReason === PerformanceDegradationReason.LapDetected) return "Lap detected"
|
||||
if (pp.degradationReason === PerformanceDegradationReason.HighTemperature) return "High temp"
|
||||
return ""
|
||||
}
|
||||
|
||||
readonly property var profileHolds: {
|
||||
let names = []
|
||||
for (let i = 0; i < pp.holds.length; i++) {
|
||||
let h = pp.holds[i]
|
||||
let label = PowerProfile.toString(h.profile)
|
||||
if (h.applicationId) label += " (" + h.applicationId + ")"
|
||||
names.push(label)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
width: batLayout.implicitWidth + 24
|
||||
height: Cfg.Config.moduleHeight
|
||||
radius: panelRadius
|
||||
|
||||
color: hoverHandler.hovered ? t.batBgHover : t.batBg
|
||||
border.color: hoverHandler.hovered ? t.batBorderHover : t.batBorder
|
||||
border.width: borderWidth
|
||||
|
||||
Behavior on width { NumberAnimation { duration: 250; easing.type: Easing.OutCubic } }
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
|
||||
HoverHandler { id: hoverHandler; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: batteryPopup.toggle() }
|
||||
|
||||
RowLayout {
|
||||
id: batLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
text: root.batIcon
|
||||
color: root.iconColor
|
||||
font.pixelSize: 16
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.rightMargin: 7
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: root.isCritical
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { to: 0.3; duration: 700; easing.type: Easing.InOutSine }
|
||||
NumberAnimation { to: 1.0; duration: 700; easing.type: Easing.InOutSine }
|
||||
}
|
||||
opacity: 1.0
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.batReady ? Math.round(root.pct) + "%" : "—"
|
||||
color: root.t.batText
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
id: chargingLabel
|
||||
text: root.charging ? " +" : (root.fullCharge ? " ✓" : "")
|
||||
color: root.iconColor
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
opacity: (hoverHandler.hovered || batteryPopup.popupOpen) ? 1.0 : 0.0
|
||||
Layout.preferredWidth: (hoverHandler.hovered || batteryPopup.popupOpen)
|
||||
? implicitWidth : 0
|
||||
clip: true
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||
Behavior on Layout.preferredWidth { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: batteryPopup
|
||||
screen: root.targetScreen
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function open() { hideTimer.stop(); visible = true; popupOpen = true }
|
||||
function close() { popupOpen = false; hideTimer.restart() }
|
||||
function toggle() { popupOpen ? close() : open() }
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: Cfg.Config.popupAnimDuration + 70
|
||||
onTriggered: batteryPopup.visible = false
|
||||
}
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "main-shell-battery"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
mask: popupOpen ? null : _noInput
|
||||
Region { id: _noInput }
|
||||
|
||||
TapHandler { onTapped: batteryPopup.close() }
|
||||
|
||||
Item {
|
||||
id: popupClipContainer
|
||||
readonly property int cardW: 260
|
||||
readonly property int screenPad: root.barMargin + 10
|
||||
|
||||
x: {
|
||||
let centerX = root.mapToItem(null, 0, 0).x + root.width / 2
|
||||
let desiredX = centerX - cardW / 2
|
||||
return Math.max(screenPad,
|
||||
Math.min(desiredX, parent.width - cardW - screenPad))
|
||||
}
|
||||
width: cardW
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
|
||||
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
|
||||
height: parent.height - root.barHeight - 10
|
||||
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
id: cardShadow
|
||||
anchors.fill: card
|
||||
anchors.margins: -3
|
||||
radius: card.radius + 3
|
||||
color: "transparent"
|
||||
border.color: Qt.rgba(0, 0, 0, 0.35)
|
||||
border.width: 6
|
||||
visible: batteryPopup.popupOpen
|
||||
opacity: batteryPopup.popupOpen ? 0.6 : 0
|
||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
width: parent.width
|
||||
height: cardCol.implicitHeight + 28
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
|
||||
transform: Translate {
|
||||
y: batteryPopup.popupOpen ? 0
|
||||
: (root.isTop ? -card.height : card.height)
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Cfg.Config.popupAnimDuration
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
enabled: batteryPopup.visible
|
||||
}
|
||||
}
|
||||
|
||||
opacity: batteryPopup.popupOpen ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 220 } }
|
||||
|
||||
color: root.t.batPopupBg
|
||||
radius: Cfg.Config.popupRadius
|
||||
border.color: root.t.batPopupBorder
|
||||
border.width: Cfg.Config.popupBorderWidth
|
||||
layer.enabled: true
|
||||
|
||||
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
|
||||
|
||||
ColumnLayout {
|
||||
id: cardCol
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
margins: 18
|
||||
topMargin: 20
|
||||
}
|
||||
spacing: 12
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 14
|
||||
|
||||
Item {
|
||||
width: 52
|
||||
height: 52
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 46
|
||||
height: 46
|
||||
radius: 23
|
||||
color: Qt.rgba(
|
||||
Qt.darker(root.iconColor, 1.0).r,
|
||||
Qt.darker(root.iconColor, 1.0).g,
|
||||
Qt.darker(root.iconColor, 1.0).b,
|
||||
0.15
|
||||
)
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.batIcon
|
||||
color: root.iconColor
|
||||
font.pixelSize: 26
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: root.isCritical
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { to: 0.3; duration: 700; easing.type: Easing.InOutSine }
|
||||
NumberAnimation { to: 1.0; duration: 700; easing.type: Easing.InOutSine }
|
||||
}
|
||||
opacity: 1.0
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
Layout.fillWidth: true
|
||||
|
||||
Text {
|
||||
text: root.batReady ? Math.round(root.pct) + "%" : "—"
|
||||
color: root.t.batPopupHeader
|
||||
font.pixelSize: 30
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (!root.batReady) return "No battery"
|
||||
if (root.fullCharge) return "Fully charged"
|
||||
if (root.charging) return "Charging"
|
||||
if (root.isCritical) return "Critical"
|
||||
if (root.isLow) return "Low"
|
||||
return "Discharging"
|
||||
}
|
||||
color: root.iconColor
|
||||
font.pixelSize: 11
|
||||
font.capitalization: Font.AllUppercase
|
||||
font.letterSpacing: 0.8
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
height: 10
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 5
|
||||
color: root.t.batProgressTrack
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(height, parent.width * (root.pct / 100))
|
||||
height: parent.height
|
||||
radius: 5
|
||||
clip: false
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: Qt.darker(root.iconColor, 1.25)
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: root.iconColor
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width { NumberAnimation { duration: 450; easing.type: Easing.OutCubic } }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 5
|
||||
color: "transparent"
|
||||
border.color: Qt.rgba(root.iconColor.r,
|
||||
root.iconColor.g,
|
||||
root.iconColor.b, 0.2)
|
||||
border.width: 1
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: root.t.batPopupSeparator
|
||||
opacity: 0.6
|
||||
}
|
||||
|
||||
component DetailRow: RowLayout {
|
||||
required property string label
|
||||
required property string value
|
||||
Layout.fillWidth: true
|
||||
|
||||
Text {
|
||||
text: label
|
||||
color: root.t.batPopupDim
|
||||
font.pixelSize: 12
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: value
|
||||
color: root.t.batPopupText
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
font.family: "monospace"
|
||||
}
|
||||
}
|
||||
|
||||
DetailRow {
|
||||
label: root.charging ? "Time to full" : "Time left"
|
||||
value: {
|
||||
if (!root.batReady || root.fullCharge) return "—"
|
||||
let secs = root.charging ? bat.timeToFull : bat.timeToEmpty
|
||||
if (secs <= 0) return "—"
|
||||
let h = Math.floor(secs / 3600)
|
||||
let m = Math.floor((secs % 3600) / 60)
|
||||
return h > 0 ? h + "h " + m + "m" : m + "m"
|
||||
}
|
||||
}
|
||||
|
||||
DetailRow {
|
||||
label: "Rate"
|
||||
value: {
|
||||
if (!root.batReady) return "—"
|
||||
let r = bat.changeRate
|
||||
return (r >= 0 ? "+" : "") + r.toFixed(1) + " W"
|
||||
}
|
||||
}
|
||||
|
||||
DetailRow {
|
||||
label: "Health"
|
||||
value: {
|
||||
if (!root.batReady) return "—"
|
||||
if (!bat.healthSupported) return "Undetected"
|
||||
return Math.round(bat.healthPercentage) + "%"
|
||||
}
|
||||
}
|
||||
|
||||
DetailRow {
|
||||
label: "Energy"
|
||||
value: {
|
||||
if (!root.batReady) return "—"
|
||||
return bat.energy.toFixed(1) + " / "
|
||||
+ bat.energyCapacity.toFixed(1) + " Wh"
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
height: 16
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: root.t.batPopupSeparator
|
||||
opacity: 0.6
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: sectionLabel.implicitWidth + 14
|
||||
height: sectionLabel.implicitHeight + 4
|
||||
radius: 4
|
||||
color: root.t.batPopupBg
|
||||
|
||||
Text {
|
||||
id: sectionLabel
|
||||
anchors.centerIn: parent
|
||||
text: "Power Profile"
|
||||
color: root.t.batPopupDim
|
||||
font.pixelSize: 10
|
||||
font.capitalization: Font.AllUppercase
|
||||
font.letterSpacing: 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component ProfileButton: Rectangle {
|
||||
required property string text
|
||||
required property var profileValue
|
||||
property bool interactive: true
|
||||
|
||||
property bool isActive: interactive && (root.pp.profile === profileValue)
|
||||
|
||||
height: 38
|
||||
radius: 10
|
||||
color: isActive ? root.iconColor
|
||||
: Qt.rgba(root.iconColor.r, root.iconColor.g, root.iconColor.b,
|
||||
(profileHover.containsMouse && interactive) ? 0.14 : 0.06)
|
||||
border.color: isActive
|
||||
? Qt.lighter(root.iconColor, 1.2)
|
||||
: Qt.rgba(root.iconColor.r, root.iconColor.g, root.iconColor.b, 0.12)
|
||||
border.width: 1
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: parent.text
|
||||
font.pixelSize: 10
|
||||
font.bold: parent.isActive
|
||||
font.letterSpacing: 0.4
|
||||
color: parent.isActive ? root.t.batProfileActiveText : root.t.batPopupText
|
||||
}
|
||||
|
||||
HoverHandler { id: profileHover; enabled: parent.interactive }
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: parent.interactive ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: {
|
||||
if (parent.interactive) root.pp.profile = parent.profileValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
ProfileButton {
|
||||
text: "Power Saver"
|
||||
profileValue: PowerProfile.PowerSaver
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
ProfileButton {
|
||||
text: "Balanced"
|
||||
profileValue: PowerProfile.Balanced
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
ProfileButton {
|
||||
text: "Performance"
|
||||
profileValue: PowerProfile.Performance
|
||||
interactive: root.pp.hasPerformanceProfile
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
DetailRow {
|
||||
label: "Degradation"
|
||||
value: root.degradationText || "None"
|
||||
}
|
||||
|
||||
DetailRow {
|
||||
label: "Holds"
|
||||
value: root.profileHolds.length > 0 ? root.profileHolds[0] : "—"
|
||||
visible: root.profileHolds.length > 0
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
height: 16
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: root.t.batPopupSeparator
|
||||
opacity: 0.6
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: devLabel.implicitWidth + 14
|
||||
height: devLabel.implicitHeight + 4
|
||||
radius: 4
|
||||
color: root.t.batPopupBg
|
||||
|
||||
Text {
|
||||
id: devLabel
|
||||
anchors.centerIn: parent
|
||||
text: "Device"
|
||||
color: root.t.batPopupDim
|
||||
font.pixelSize: 10
|
||||
font.capitalization: Font.AllUppercase
|
||||
font.letterSpacing: 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DetailRow { label: "Type"; value: root.typeText }
|
||||
DetailRow { label: "Model"; value: root.batReady ? (bat.model || "Undetected") : "—" }
|
||||
DetailRow { label: "Power supply"; value: root.batReady ? (bat.powerSupply ? "Yes" : "No") : "—" }
|
||||
DetailRow { label: "On battery"; value: root.up.onBattery ? "Yes" : "No" }
|
||||
DetailRow { label: "Devices"; value: root.up.devices.values.length + " total" }
|
||||
|
||||
Item { height: 2 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
|
||||
import "../../config" as Cfg
|
||||
|
||||
@@ -17,8 +18,44 @@ Rectangle {
|
||||
property int barMargin: Cfg.Config.margin
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
readonly property string tz: Cfg.Config.timezone || ""
|
||||
|
||||
readonly property int tzOffsetHours: -timeManager.now.getTimezoneOffset() / 60
|
||||
|
||||
property int tzOffsetMin: 0
|
||||
|
||||
Process {
|
||||
id: tzProc
|
||||
command: ["bash", "-c", "TZ=" + (root.tz || "UTC") + " date +%z"]
|
||||
running: root.tz !== ""
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
var s = data.trim()
|
||||
if (s.length < 5) return
|
||||
var sign = (s[0] === "-") ? -1 : 1
|
||||
var h = parseInt(s.substring(1, 3), 10)
|
||||
var m = parseInt(s.substring(3, 5), 10)
|
||||
root.tzOffsetMin = sign * (h * 60 + m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1800000; running: root.tz !== ""; repeat: true
|
||||
onTriggered: tzProc.running = true
|
||||
}
|
||||
|
||||
function toTz(date) {
|
||||
if (!tz) return date
|
||||
|
||||
return new Date(date.getTime()
|
||||
+ date.getTimezoneOffset() * 60000
|
||||
+ tzOffsetMin * 60000)
|
||||
}
|
||||
|
||||
function tzOffsetHours() {
|
||||
if (!tz) return -timeManager.now.getTimezoneOffset() / 60
|
||||
return tzOffsetMin / 60
|
||||
}
|
||||
|
||||
width: clockLayout.implicitWidth + 24
|
||||
height: Cfg.Config.moduleHeight
|
||||
@@ -66,7 +103,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
Text {
|
||||
text: timeManager.now.toLocaleTimeString(Qt.locale(), "HH:mm")
|
||||
text: root.toTz(timeManager.now).toLocaleTimeString(Qt.locale(), "HH:mm")
|
||||
color: root.t.clockTime
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
@@ -75,7 +112,7 @@ Rectangle {
|
||||
|
||||
Text {
|
||||
id: secondsText
|
||||
text: ":" + timeManager.now.toLocaleTimeString(Qt.locale(), "ss")
|
||||
text: ":" + root.toTz(timeManager.now).toLocaleTimeString(Qt.locale(), "ss")
|
||||
color: root.t.clockSeconds
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
@@ -230,7 +267,7 @@ Rectangle {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Text {
|
||||
text: timeManager.now.toLocaleDateString(Qt.locale(), "dddd")
|
||||
text: root.toTz(timeManager.now).toLocaleDateString(Qt.locale(), "dddd")
|
||||
color: root.t.clockPopupDim
|
||||
font.pixelSize: 12
|
||||
font.capitalization: Font.AllUppercase
|
||||
@@ -239,7 +276,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
Text {
|
||||
text: timeManager.now.toLocaleDateString(Qt.locale(), "d MMMM yyyy")
|
||||
text: root.toTz(timeManager.now).toLocaleDateString(Qt.locale(), "d MMMM yyyy")
|
||||
color: root.t.clockPopupHeader
|
||||
font.pixelSize: 22
|
||||
font.bold: true
|
||||
@@ -269,8 +306,8 @@ Rectangle {
|
||||
id: cal
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: gridCol.implicitHeight
|
||||
property int displayYear: timeManager.now.getFullYear()
|
||||
property int displayMonth: timeManager.now.getMonth()
|
||||
property int displayYear: root.toTz(timeManager.now).getFullYear()
|
||||
property int displayMonth: root.toTz(timeManager.now).getMonth()
|
||||
property int pendingYear: displayYear
|
||||
property int pendingMonth: displayMonth
|
||||
property int slideDir: 1
|
||||
@@ -279,8 +316,9 @@ Rectangle {
|
||||
target: calendarPopup
|
||||
function onPopupOpenChanged() {
|
||||
if (calendarPopup.popupOpen) {
|
||||
cal.displayYear = timeManager.now.getFullYear()
|
||||
cal.displayMonth = timeManager.now.getMonth()
|
||||
let tzd = root.toTz(timeManager.now)
|
||||
cal.displayYear = tzd.getFullYear()
|
||||
cal.displayMonth = tzd.getMonth()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,7 +377,12 @@ Rectangle {
|
||||
width: (card.width - 32) / 7; height: 26
|
||||
readonly property int dayNum: (weekRow.weekIndex * 7 + index) - ((new Date(cal.displayYear, cal.displayMonth, 1).getDay() + 6) % 7) + 1
|
||||
readonly property bool inMonth: dayNum >= 1 && dayNum <= new Date(cal.displayYear, cal.displayMonth + 1, 0).getDate()
|
||||
readonly property bool isToday: inMonth && dayNum === timeManager.now.getDate() && cal.displayYear === timeManager.now.getFullYear() && cal.displayMonth === timeManager.now.getMonth()
|
||||
readonly property bool isToday: {
|
||||
let tzd = root.toTz(timeManager.now)
|
||||
return inMonth && dayNum === tzd.getDate()
|
||||
&& cal.displayYear === tzd.getFullYear()
|
||||
&& cal.displayMonth === tzd.getMonth()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent; width: 22; height: 22; radius: 11
|
||||
@@ -371,7 +414,7 @@ Rectangle {
|
||||
Layout.rightMargin: 6
|
||||
|
||||
Text {
|
||||
text: "UTC " + (root.tzOffsetHours >= 0 ? "+" : "") + root.tzOffsetHours
|
||||
text: "UTC " + (root.tzOffsetHours() >= 0 ? "+" : "") + root.tzOffsetHours()
|
||||
color: root.t.clockPopupUtc
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
@@ -382,11 +425,11 @@ Rectangle {
|
||||
|
||||
Text {
|
||||
text: {
|
||||
let d = timeManager.now
|
||||
let d = root.toTz(timeManager.now)
|
||||
let month = d.getMonth() + 1
|
||||
let day = d.getDate()
|
||||
let year = d.getFullYear()
|
||||
if (Cfg.Config.clockDateFormat === "DMY")
|
||||
if (Cfg.Config.dateFormat === "DMY")
|
||||
return day + "/" + month + "/" + year
|
||||
return month + "/" + day + "/" + year
|
||||
}
|
||||
|
||||
1200
bar/modules/Crypto.qml
Normal file
1200
bar/modules/Crypto.qml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,4 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
import "../../config" as Cfg
|
||||
|
||||
@@ -37,10 +35,10 @@ Rectangle {
|
||||
width: Cfg.Config.logoIconSize * 2 + 4
|
||||
height: parent.height
|
||||
|
||||
color: isHovered ? t.archLogoBgHover : t.archLogoBg
|
||||
color: isHovered ? t.logoBgHover : t.logoBg
|
||||
radius: panelRadius
|
||||
|
||||
border.color: launcherActive ? t.archLogoBorderActive : t.archLogoBorder
|
||||
border.color: launcherActive ? t.logoBorderActive : t.logoBorder
|
||||
border.width: borderWidth
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 250 } }
|
||||
@@ -69,7 +67,7 @@ Rectangle {
|
||||
anchors.centerIn: parent
|
||||
text: Cfg.Config.logoIcon
|
||||
|
||||
color: launcherActive ? t.archLogoIconActive : t.archLogoIcon
|
||||
color: launcherActive ? t.logoIconActive : t.logoIcon
|
||||
font.pixelSize: Cfg.Config.logoIconSize
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 250 } }
|
||||
728
bar/modules/Network.qml
Normal file
728
bar/modules/Network.qml
Normal file
@@ -0,0 +1,728 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Networking
|
||||
import Quickshell.Services.Polkit
|
||||
|
||||
import "../../config" as Cfg
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
property bool isTop: true
|
||||
property var targetScreen: null
|
||||
property int barHeight: Cfg.Config.barHeight
|
||||
property int barMargin: Cfg.Config.margin
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
readonly property var wifiDevice: {
|
||||
for (const dev of (Networking.devices?.values ?? []))
|
||||
if (dev.type === DeviceType.Wifi) return dev
|
||||
return null
|
||||
}
|
||||
|
||||
readonly property var wiredDevice: {
|
||||
for (const dev of (Networking.devices?.values ?? []))
|
||||
if (dev.type === DeviceType.Wired) return dev
|
||||
return null
|
||||
}
|
||||
|
||||
readonly property bool hasWifi: wifiDevice !== null
|
||||
readonly property bool hasWired: wiredDevice !== null
|
||||
readonly property bool wifiUp: hasWifi && wifiDevice.connected
|
||||
readonly property bool wiredUp: hasWired && wiredDevice.connected
|
||||
|
||||
readonly property string connectivityLabel: {
|
||||
switch (Networking.connectivity) {
|
||||
case NetworkConnectivity.Portal: return "Captive Portal"
|
||||
case NetworkConnectivity.Limited: return "Limited"
|
||||
case NetworkConnectivity.None: return "No Internet"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
|
||||
readonly property string wifiModeLabel: {
|
||||
if (!wifiUp || !wifiDevice) return ""
|
||||
switch (wifiDevice.mode) {
|
||||
case WifiDeviceMode.Infra: return "Infra"
|
||||
case WifiDeviceMode.Ap: return "AP"
|
||||
case WifiDeviceMode.Adhoc: return "Ad-Hoc"
|
||||
case WifiDeviceMode.Mesh: return "Mesh"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
readonly property var activeAP: {
|
||||
for (const n of (wifiDevice?.networks?.values ?? []))
|
||||
if (n.connected) return n
|
||||
return null
|
||||
}
|
||||
readonly property string ssid: activeAP ? (activeAP.name || "") : ""
|
||||
readonly property int strength: activeAP ? Math.round((activeAP.signalStrength || 0) * 100) : 0
|
||||
|
||||
readonly property string wifiIcon: {
|
||||
if (!wifiUp) return ""
|
||||
if (strength >= 80) return ""
|
||||
if (strength >= 60) return ""
|
||||
if (strength >= 40) return ""
|
||||
if (strength >= 20) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
readonly property string displayIcon: hasWifi ? wifiIcon : ""
|
||||
|
||||
readonly property color iconColor: {
|
||||
if (hasWifi) return wifiUp ? t.statusOk : t.statusErr
|
||||
if (hasWired) return wiredUp ? t.statusOk : t.statusErr
|
||||
return t.textDim
|
||||
}
|
||||
|
||||
readonly property string chipLabel: {
|
||||
if (hasWifi) return (wifiUp && ssid !== "") ? ssid : "Wi-Fi"
|
||||
if (hasWired) return wiredUp ? "Ethernet" : "Offline"
|
||||
return "No NIC"
|
||||
}
|
||||
|
||||
property var pendingAP: null
|
||||
property string pendingSSID: ""
|
||||
property bool showPwdPrompt: false
|
||||
property string enteredPwd: ""
|
||||
|
||||
function attemptConnect() {
|
||||
if (!pendingAP) return
|
||||
if (enteredPwd.length > 0)
|
||||
pendingAP.connectWithPsk(enteredPwd)
|
||||
else
|
||||
pendingAP.connect()
|
||||
pendingAP = null
|
||||
pendingSSID = ""
|
||||
enteredPwd = ""
|
||||
showPwdPrompt = false
|
||||
}
|
||||
|
||||
function cancelConnect() {
|
||||
pendingAP = null
|
||||
pendingSSID = ""
|
||||
enteredPwd = ""
|
||||
showPwdPrompt = false
|
||||
}
|
||||
|
||||
implicitWidth: chipRow.implicitWidth + 24
|
||||
height: Cfg.Config.moduleHeight
|
||||
radius: panelRadius
|
||||
color: chipHover.hovered ? t.netBgHover : t.netBg
|
||||
border.color: chipHover.hovered ? t.netBorderHover : t.netBorder
|
||||
border.width: borderWidth
|
||||
|
||||
Behavior on implicitWidth { NumberAnimation { duration: 250; easing.type: Easing.OutCubic } }
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
|
||||
HoverHandler { id: chipHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: netPopup.toggle() }
|
||||
|
||||
RowLayout {
|
||||
id: chipRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
text: root.displayIcon
|
||||
color: root.iconColor
|
||||
font.pixelSize: 16
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: root.hasWifi && root.wifiDevice
|
||||
&& (root.wifiDevice.scannerEnabled || false)
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { to: 0.4; duration: 600; easing.type: Easing.InOutSine }
|
||||
NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutSine }
|
||||
}
|
||||
opacity: 1.0
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.chipLabel
|
||||
color: root.t.netText
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
Layout.maximumWidth: 110
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: netPopup
|
||||
screen: root.targetScreen
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function open() {
|
||||
root.cancelConnect()
|
||||
hideTimer.stop()
|
||||
visible = true
|
||||
popupOpen = true
|
||||
if (root.hasWifi && root.wifiDevice)
|
||||
root.wifiDevice.scannerEnabled = true
|
||||
}
|
||||
function close() { popupOpen = false; hideTimer.restart() }
|
||||
function toggle() { popupOpen ? close() : open() }
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: Cfg.Config.popupAnimDuration + 70
|
||||
onTriggered: netPopup.visible = false
|
||||
}
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "main-shell-network"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
mask: netPopup.popupOpen ? null : _noInput
|
||||
Region { id: _noInput }
|
||||
|
||||
TapHandler { onTapped: netPopup.close() }
|
||||
|
||||
Item {
|
||||
id: popupClip
|
||||
readonly property int cardW: 310
|
||||
readonly property int screenPad: root.barMargin + 10
|
||||
|
||||
x: {
|
||||
var cx = root.mapToItem(null, 0, 0).x + root.width / 2
|
||||
var dx = cx - cardW / 2
|
||||
return Math.max(screenPad, Math.min(dx, parent.width - cardW - screenPad))
|
||||
}
|
||||
width: cardW
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
|
||||
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
|
||||
height: parent.height - root.barHeight - 10
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
width: parent.width
|
||||
height: cardBody.implicitHeight + 24
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
|
||||
transform: Translate {
|
||||
y: netPopup.popupOpen ? 0 : (root.isTop ? -card.height : card.height)
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Cfg.Config.popupAnimDuration
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
enabled: netPopup.visible
|
||||
}
|
||||
}
|
||||
|
||||
opacity: netPopup.popupOpen ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||
|
||||
color: root.t.netPopupBg
|
||||
radius: Cfg.Config.popupRadius
|
||||
border.color: root.t.netPopupBorder
|
||||
border.width: Cfg.Config.popupBorderWidth
|
||||
layer.enabled: true
|
||||
|
||||
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
|
||||
|
||||
ColumnLayout {
|
||||
id: cardBody
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
margins: 16
|
||||
topMargin: 18
|
||||
}
|
||||
spacing: 12
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: root.displayIcon
|
||||
color: root.iconColor
|
||||
font.pixelSize: 36
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
Layout.fillWidth: true
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (root.hasWifi)
|
||||
return root.wifiUp
|
||||
? (root.ssid !== "" ? root.ssid : "Connected")
|
||||
: "Wi-Fi"
|
||||
if (root.hasWired)
|
||||
return root.wiredUp ? "Ethernet" : "Disconnected"
|
||||
return "No Network"
|
||||
}
|
||||
color: root.t.netPopupHeader
|
||||
font.pixelSize: 17
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (root.hasWifi && root.wifiDevice) {
|
||||
var st = root.wifiDevice.state
|
||||
if (st === ConnectionState.Activating) return "Connecting..."
|
||||
if (st === ConnectionState.Deactivating) return "Disconnecting..."
|
||||
if (root.wifiUp && root.activeAP) {
|
||||
var s = root.strength + "% signal"
|
||||
if (root.wifiModeLabel !== "") s += " · " + root.wifiModeLabel
|
||||
return s
|
||||
}
|
||||
if (st === ConnectionState.Deactivated)
|
||||
return "Disconnected"
|
||||
return "Not connected"
|
||||
}
|
||||
if (root.hasWired && root.wiredDevice)
|
||||
return root.wiredDevice.name || "—"
|
||||
return "No hardware found"
|
||||
}
|
||||
color: root.t.netPopupDim
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.connectivityLabel !== ""
|
||||
text: root.connectivityLabel
|
||||
color: root.t.statusErr
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.hasWifi
|
||||
width: 28; height: 28
|
||||
radius: 8
|
||||
color: scanHover.hovered ? root.t.netApHoverBg : "transparent"
|
||||
border.color: root.t.netPopupBorder
|
||||
border.width: 1
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "\uf0450"
|
||||
font.pixelSize: 15
|
||||
color: scanHover.hovered ? root.t.accent : root.t.netPopupDim
|
||||
|
||||
RotationAnimator on rotation {
|
||||
id: scanSpin; running: false
|
||||
from: 0; to: 360; duration: 700; loops: 2
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler { id: scanHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
scanSpin.restart()
|
||||
if (root.wifiDevice) root.wifiDevice.scannerEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.netPopupSeparator }
|
||||
|
||||
ColumnLayout {
|
||||
visible: root.hasWifi
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: "AVAILABLE NETWORKS"
|
||||
color: root.t.netPopupDim
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 1.2
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.hasWifi && root.wifiDevice
|
||||
&& root.wifiDevice.networks.values.length === 0
|
||||
text: (root.wifiDevice && (root.wifiDevice.scannerEnabled || false))
|
||||
? "Scanning..." : "No networks found"
|
||||
color: root.t.netPopupDim
|
||||
font.pixelSize: 12
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 4
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: (root.hasWifi && root.wifiDevice) ? root.wifiDevice.networks.values : []
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
height: 50
|
||||
radius: 10
|
||||
color: {
|
||||
var active = modelData.connected || false
|
||||
if (active) return root.t.netApActiveBg
|
||||
if (apHover.hovered) return root.t.netApHoverBg
|
||||
return "transparent"
|
||||
}
|
||||
border.color: (modelData.connected || false) ? root.t.accent : "transparent"
|
||||
border.width: 1
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 120 } }
|
||||
|
||||
HoverHandler { id: apHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
if (modelData.connected || false) return
|
||||
if (modelData.security !== WifiSecurityType.None) {
|
||||
root.pendingAP = modelData
|
||||
root.pendingSSID = modelData.name || ""
|
||||
root.enteredPwd = ""
|
||||
root.showPwdPrompt = true
|
||||
} else {
|
||||
modelData.connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors { fill: parent; leftMargin: 12; rightMargin: 12 }
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
text: {
|
||||
var s = Math.round((modelData.signalStrength || 0) * 100)
|
||||
if (s >= 80) return "\uf0ec8"
|
||||
if (s >= 60) return "\uf0ec5"
|
||||
if (s >= 40) return "\uf0ec2"
|
||||
if (s >= 20) return "\uf0ebf"
|
||||
return "\uf0eef"
|
||||
}
|
||||
color: (modelData.connected || false)
|
||||
? root.t.accent : root.t.netPopupDim
|
||||
font.pixelSize: 18
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
Layout.fillWidth: true
|
||||
|
||||
Text {
|
||||
text: modelData.name || "(hidden)"
|
||||
color: (modelData.connected || false)
|
||||
? root.t.netPopupHeader
|
||||
: root.t.netPopupText
|
||||
font.pixelSize: 13
|
||||
font.bold: (modelData.connected || false)
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (modelData.stateChanging)
|
||||
return modelData.state === ConnectionState.Activating
|
||||
? "Connecting..." : "Disconnecting..."
|
||||
var parts = []
|
||||
if (modelData.known) parts.push("Saved")
|
||||
if (modelData.security !== WifiSecurityType.None) parts.push("Secured")
|
||||
else parts.push("Open")
|
||||
parts.push(Math.round((modelData.signalStrength || 0) * 100) + "%")
|
||||
return parts.join(" · ")
|
||||
}
|
||||
color: root.t.netPopupDim
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: (modelData.security !== WifiSecurityType.None) && !(modelData.connected || false)
|
||||
text: "\uf033e"
|
||||
color: root.t.netPopupDim
|
||||
font.pixelSize: 13
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: (modelData.connected || false)
|
||||
text: "\uf012c"
|
||||
color: root.t.statusOk
|
||||
font.pixelSize: 15
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.wifiUp
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 4
|
||||
height: 36; radius: 10
|
||||
color: discoHover.hovered
|
||||
? root.t.netDisconnectBg : "transparent"
|
||||
border.color: root.t.netDisconnectBorder
|
||||
border.width: 1
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
HoverHandler { id: discoHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
if (root.activeAP)
|
||||
root.activeAP.disconnect()
|
||||
}
|
||||
}
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Disconnect"
|
||||
color: root.t.statusErr
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: !root.hasWifi && root.hasWired
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
text: "CONNECTION DETAILS"
|
||||
color: root.t.netPopupDim
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 1.2
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 38; radius: 10
|
||||
color: root.wiredUp
|
||||
? root.t.netWiredOkBg
|
||||
: root.t.netWiredErrBg
|
||||
border.color: root.wiredUp
|
||||
? root.t.netWiredOkBorder
|
||||
: root.t.netWiredErrBorder
|
||||
border.width: 1
|
||||
|
||||
RowLayout {
|
||||
anchors { fill: parent; leftMargin: 14; rightMargin: 14 }
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
text: root.wiredUp ? "\uf0601" : "\uf0602"
|
||||
color: root.wiredUp ? root.t.statusOk : root.t.statusErr
|
||||
font.pixelSize: 18
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
Text {
|
||||
text: root.wiredUp ? "Connected" : "Cable unplugged"
|
||||
color: root.wiredUp ? root.t.statusOk : root.t.statusErr
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{
|
||||
label: "Interface",
|
||||
value: (root.wiredDevice && root.wiredDevice.name)
|
||||
? root.wiredDevice.name : "—"
|
||||
},
|
||||
{
|
||||
label: "MAC",
|
||||
value: (root.wiredDevice && root.wiredDevice.address)
|
||||
? root.wiredDevice.address : "—"
|
||||
},
|
||||
{
|
||||
label: "Link",
|
||||
value: (root.wiredDevice)
|
||||
? (root.wiredDevice.hasLink ? "Up" : "Down") : "—"
|
||||
},
|
||||
{
|
||||
label: "Speed",
|
||||
value: (root.wiredDevice && root.wiredUp &&
|
||||
root.wiredDevice.linkSpeed > 0)
|
||||
? (root.wiredDevice.linkSpeed + " Mbps") : "—"
|
||||
}
|
||||
]
|
||||
|
||||
delegate: RowLayout {
|
||||
required property var modelData
|
||||
Layout.fillWidth: true
|
||||
|
||||
Text {
|
||||
text: modelData.label
|
||||
color: root.t.netPopupDim
|
||||
font.pixelSize: 12
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: modelData.value
|
||||
color: root.t.netPopupText
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
font.family: "monospace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: !root.hasWifi && !root.hasWired
|
||||
text: "No network hardware detected."
|
||||
color: root.t.netPopupDim
|
||||
font.pixelSize: 12
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 4
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: root.showPwdPrompt
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.netPopupSeparator }
|
||||
|
||||
Text {
|
||||
text: "Connect to \"" + root.pendingSSID + "\""
|
||||
color: root.t.netPopupHeader
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 36; radius: 8
|
||||
color: root.t.netFieldBg
|
||||
border.color: pwdField.activeFocus ? root.t.accent : root.t.netPopupBorder
|
||||
border.width: 1
|
||||
Behavior on border.color { ColorAnimation { duration: 120 } }
|
||||
|
||||
RowLayout {
|
||||
anchors { fill: parent; leftMargin: 12; rightMargin: 10 }
|
||||
spacing: 6
|
||||
|
||||
TextInput {
|
||||
id: pwdField
|
||||
Layout.fillWidth: true
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
echoMode: eyeToggle.showClear
|
||||
? TextInput.Normal : TextInput.Password
|
||||
color: root.t.netPopupText
|
||||
font.pixelSize: 13
|
||||
selectionColor: Qt.alpha(root.t.accent, 0.4)
|
||||
clip: true
|
||||
text: root.enteredPwd
|
||||
onTextChanged: root.enteredPwd = text
|
||||
|
||||
Keys.onReturnPressed: root.attemptConnect()
|
||||
Keys.onEscapePressed: root.cancelConnect()
|
||||
onVisibleChanged: if (visible) forceActiveFocus()
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: "Password"
|
||||
color: root.t.netPopupDim
|
||||
font.pixelSize: 13
|
||||
visible: pwdField.text === "" && !pwdField.activeFocus
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: eyeToggle
|
||||
property bool showClear: false
|
||||
text: showClear ? "\uf0349" : "\uf0354"
|
||||
color: eyeHover.hovered ? root.t.accent : root.t.netPopupDim
|
||||
font.pixelSize: 15
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
HoverHandler { id: eyeHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: eyeToggle.showClear = !eyeToggle.showClear }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 34; radius: 8
|
||||
color: cancelHover.hovered ? root.t.netCancelBgHover : "transparent"
|
||||
border.color: root.t.netPopupBorder
|
||||
border.width: 1
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
HoverHandler { id: cancelHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: root.cancelConnect() }
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Cancel"
|
||||
color: root.t.netPopupDim
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 34; radius: 8
|
||||
color: connHover.hovered ? root.t.accent : Qt.alpha(root.t.accent, 0.75)
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
HoverHandler { id: connHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: root.attemptConnect() }
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Connect"
|
||||
color: root.t.netConnectText
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { height: 4 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
import "../../config" as Cfg
|
||||
@@ -25,7 +24,19 @@ Rectangle {
|
||||
NumberAnimation { duration: 250; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
HoverHandler { cursorShape: Qt.PointingHandCursor }
|
||||
property bool expanded: Cfg.Config.statsStartExpanded
|
||||
property bool hovered: false
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: root.hovered = hovered
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
root.expanded = !root.expanded
|
||||
if (!root.expanded) root.hovered = false
|
||||
}
|
||||
}
|
||||
|
||||
property real cpuVal: 0
|
||||
property real memVal: 0
|
||||
@@ -36,166 +47,162 @@ Rectangle {
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
cpuProc.running = true
|
||||
memProc.running = true
|
||||
diskProc.running = true
|
||||
}
|
||||
onTriggered: { statsProc.running = true }
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cpuProc
|
||||
command: ["sh", "-c", "top -bn1 | awk '/^%Cpu/ {print 100-$8}'"]
|
||||
id: statsProc
|
||||
command: ["sh", "-c", "top -bn1 | awk '/^%Cpu/ {print 100-$8}'; free | awk '/Mem:/ {print ($2-$7)/$2 * 100}'; df / --output=pcent | tail -1 | tr -dc '0-9'"]
|
||||
property string rawOutput: ""
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
let val = parseFloat(data.trim())
|
||||
root.cpuVal = isNaN(val) ? 0 : val
|
||||
onRead: data => { statsProc.rawOutput += data + "\n" }
|
||||
}
|
||||
onRunningChanged: {
|
||||
if (!statsProc.running && statsProc.rawOutput) {
|
||||
const lines = statsProc.rawOutput.trim().split("\n")
|
||||
if (lines.length >= 3) {
|
||||
let v = parseFloat(lines[0]); root.cpuVal = isNaN(v) ? 0 : v
|
||||
v = parseFloat(lines[1]); root.memVal = isNaN(v) ? 0 : v
|
||||
v = parseFloat(lines[2]); root.diskVal = isNaN(v) ? 0 : v
|
||||
}
|
||||
statsProc.rawOutput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: memProc
|
||||
command: ["sh", "-c", "free | awk '/Mem:/ {print ($2-$7)/$2 * 100}'"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
let val = parseFloat(data.trim())
|
||||
root.memVal = isNaN(val) ? 0 : val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: diskProc
|
||||
command: ["sh", "-c", "df / --output=pcent | tail -1 | tr -dc '0-9'"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
let val = parseFloat(data.trim())
|
||||
root.diskVal = isNaN(val) ? 0 : val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component StatRing: Item {
|
||||
component StatRing: RowLayout {
|
||||
id: ring
|
||||
spacing: 5
|
||||
|
||||
property real value: 0.0
|
||||
property string label: ""
|
||||
property string icon: ""
|
||||
property real iconXOff: 0
|
||||
property color ringColor: Cfg.Config.theme.accent
|
||||
property color trackColor: Cfg.Config.theme.statsTrackColor
|
||||
property color labelColor: Cfg.Config.theme.textMain
|
||||
|
||||
Behavior on value {
|
||||
NumberAnimation {
|
||||
duration: 800
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation { duration: 800; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
readonly property real fraction: Math.max(0, Math.min(value, 100)) / 100
|
||||
|
||||
width: Cfg.Config.statsRingSize
|
||||
height: Cfg.Config.statsRingSize
|
||||
Text {
|
||||
id: leftPct
|
||||
text: Math.round(ring.value) + "%"
|
||||
color: ring.ringColor
|
||||
font.pixelSize: Cfg.Config.statsRingFontSize
|
||||
font.bold: true
|
||||
font.family: "monospace"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
clip: true
|
||||
|
||||
onFractionChanged: arc.requestPaint()
|
||||
onRingColorChanged: arc.requestPaint()
|
||||
readonly property bool active: Cfg.Config.statsLabelPosition === "left" && (root.expanded || root.hovered)
|
||||
opacity: active ? 1 : 0
|
||||
Layout.preferredWidth: active ? implicitWidth : 0
|
||||
|
||||
Canvas {
|
||||
id: arc
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
Behavior on Layout.preferredWidth { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.reset()
|
||||
Item {
|
||||
id: ringCore
|
||||
Layout.preferredWidth: Cfg.Config.statsRingSize
|
||||
Layout.preferredHeight: Cfg.Config.statsRingSize
|
||||
|
||||
var cx = width / 2
|
||||
var cy = height / 2
|
||||
var lw = 2.8
|
||||
var r = Math.min(cx, cy) - lw / 2 - 0.5
|
||||
onVisibleChanged: if (visible) arc.requestPaint()
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, r, 0, Math.PI * 2)
|
||||
ctx.strokeStyle = ring.trackColor
|
||||
ctx.lineWidth = lw
|
||||
ctx.stroke()
|
||||
Canvas {
|
||||
id: arc
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
|
||||
Connections {
|
||||
target: ring
|
||||
function onFractionChanged() { arc.requestPaint() }
|
||||
function onRingColorChanged() { arc.requestPaint() }
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.reset()
|
||||
|
||||
var cx = width / 2
|
||||
var cy = height / 2
|
||||
var lw = 2.8
|
||||
var r = Math.min(cx, cy) - lw / 2 - 0.5
|
||||
|
||||
if (ring.fraction > 0) {
|
||||
var start = -Math.PI / 2
|
||||
var end = start + (ring.fraction * Math.PI * 2)
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, r, start, end)
|
||||
ctx.strokeStyle = ring.ringColor
|
||||
ctx.arc(cx, cy, r, 0, Math.PI * 2)
|
||||
ctx.strokeStyle = ring.trackColor
|
||||
ctx.lineWidth = lw
|
||||
ctx.lineCap = "round"
|
||||
ctx.stroke()
|
||||
|
||||
if (ring.fraction > 0) {
|
||||
var start = -Math.PI / 2
|
||||
var end = start + (ring.fraction * Math.PI * 2)
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, r, start, end)
|
||||
ctx.strokeStyle = ring.ringColor
|
||||
ctx.lineWidth = lw
|
||||
ctx.lineCap = "round"
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hov
|
||||
onHoveredChanged: if (!hovered) ring.showAlt = false
|
||||
}
|
||||
|
||||
property bool showAlt: false
|
||||
onShowAltChanged: arc.requestPaint()
|
||||
|
||||
TapHandler {
|
||||
enabled: !Cfg.Config.statsHideTextUntilHover || hov.hovered
|
||||
onTapped: ring.showAlt = !ring.showAlt
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: ring.iconXOff
|
||||
text: ring.icon
|
||||
color: ring.ringColor
|
||||
font.pixelSize: Cfg.Config.statsIconSize
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
id: rightPct
|
||||
text: Math.round(ring.value) + "%"
|
||||
color: ring.ringColor
|
||||
font.pixelSize: Cfg.Config.statsRingFontSize
|
||||
font.bold: true
|
||||
font.family: "monospace"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
clip: true
|
||||
|
||||
text: {
|
||||
const hide = Cfg.Config.statsHideTextUntilHover
|
||||
const byDefault = Cfg.Config.statsShowPercentByDefault
|
||||
if (hide && !hov.hovered) return ""
|
||||
const showPct = ring.showAlt ? !byDefault : byDefault
|
||||
return showPct ? Math.round(ring.value) + "%" : ring.label
|
||||
}
|
||||
readonly property bool active: Cfg.Config.statsLabelPosition === "right" && (root.expanded || root.hovered)
|
||||
opacity: active ? 1 : 0
|
||||
Layout.preferredWidth: active ? implicitWidth : 0
|
||||
|
||||
color: ring.labelColor
|
||||
font.pixelSize: Cfg.Config.statsRingFontSize
|
||||
font.bold: true
|
||||
font.family: "monospace"
|
||||
|
||||
opacity: (Cfg.Config.statsHideTextUntilHover && !hov.hovered) ? 0 : 1
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 180; easing.type: Easing.OutCubic }
|
||||
}
|
||||
Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
Behavior on Layout.preferredWidth { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: statsLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 10
|
||||
spacing: 8
|
||||
|
||||
StatRing {
|
||||
value: root.cpuVal
|
||||
label: "cpu"
|
||||
icon: ""
|
||||
iconXOff: 0.3
|
||||
ringColor: root.t.statsCpuColor
|
||||
trackColor: root.t.statsTrackColor
|
||||
labelColor: root.t.statsText
|
||||
}
|
||||
|
||||
StatRing {
|
||||
value: root.memVal
|
||||
label: "ram"
|
||||
icon: ""
|
||||
ringColor: root.t.statsMemColor
|
||||
trackColor: root.t.statsTrackColor
|
||||
labelColor: root.t.statsText
|
||||
}
|
||||
|
||||
StatRing {
|
||||
value: root.diskVal
|
||||
label: "disk"
|
||||
icon: ""
|
||||
ringColor: root.t.statsDiskColor
|
||||
trackColor: root.t.statsTrackColor
|
||||
labelColor: root.t.statsText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ Item {
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
|
||||
property bool isTop: true
|
||||
property var targetScreen: null
|
||||
property string barSide: "right"
|
||||
property bool isTop: true
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
@@ -23,14 +23,24 @@ Item {
|
||||
property bool manuallyExpanded: false
|
||||
property var menuWindow: null
|
||||
|
||||
readonly property bool effectiveExpanded: expanded || trayMenuPopup.popupOpen
|
||||
|
||||
implicitWidth: bgRect.width
|
||||
implicitHeight: bgRect.height
|
||||
|
||||
readonly property color tmBg: t.trayMenuBg
|
||||
readonly property color tmBorder: t.trayMenuBorder
|
||||
readonly property color tmText: t.trayMenuText
|
||||
readonly property color tmDim: t.trayMenuDim
|
||||
readonly property color tmHover: t.trayMenuHover
|
||||
readonly property color tmCheck: t.trayMenuCheck
|
||||
readonly property color tmSeparator: t.trayMenuSeparator
|
||||
|
||||
Rectangle {
|
||||
id: bgRect
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
height: Cfg.Config.trayHeight
|
||||
height: parent.height
|
||||
width: mainRow.width
|
||||
radius: root.panelRadius
|
||||
|
||||
@@ -41,8 +51,8 @@ Item {
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
|
||||
HoverHandler {
|
||||
id: bgHover
|
||||
HoverHandler {
|
||||
id: bgHover
|
||||
onHoveredChanged: {
|
||||
if (!root.manuallyExpanded) {
|
||||
root.expanded = hovered;
|
||||
@@ -54,21 +64,21 @@ Item {
|
||||
id: mainRow
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
leftPadding: root.expanded && root.barSide === "right" ? 8 : 0
|
||||
rightPadding: root.expanded && root.barSide === "left" ? 8 : 0
|
||||
leftPadding: root.effectiveExpanded && root.barSide === "right" ? 8 : 0
|
||||
rightPadding: root.effectiveExpanded && root.barSide === "left" ? 8 : 0
|
||||
|
||||
Behavior on leftPadding { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
Behavior on rightPadding { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
|
||||
layoutDirection: root.barSide === "left" ? Qt.LeftToRight : Qt.RightToLeft
|
||||
spacing: root.expanded ? 4 : 0
|
||||
spacing: root.effectiveExpanded ? 4 : 0
|
||||
|
||||
Behavior on spacing { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } }
|
||||
|
||||
Item {
|
||||
id: toggleArea
|
||||
width: Cfg.Config.trayHeight
|
||||
height: Cfg.Config.trayHeight
|
||||
width: Cfg.Config.trayToggleSize
|
||||
height: Cfg.Config.trayToggleSize
|
||||
z: 10
|
||||
|
||||
TapHandler {
|
||||
@@ -80,11 +90,11 @@ Item {
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.expanded
|
||||
text: root.effectiveExpanded
|
||||
? (root.barSide === "left" ? "" : "")
|
||||
: (root.barSide === "left" ? "" : "")
|
||||
color: root.t.trayIcon
|
||||
font.pixelSize: Cfg.Config.trayHeight * 0.55
|
||||
font.pixelSize: Cfg.Config.trayToggleSize * 0.55
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,9 +104,9 @@ Item {
|
||||
clip: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
width: root.expanded ? implicitWidth : 0
|
||||
opacity: root.expanded ? 1 : 0
|
||||
|
||||
width: root.effectiveExpanded ? implicitWidth : 0
|
||||
opacity: root.effectiveExpanded ? 1 : 0
|
||||
|
||||
Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
|
||||
@@ -122,7 +132,9 @@ Item {
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
item.secondaryActivate()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
menuAnchor.open()
|
||||
if (item.hasMenu) {
|
||||
trayMenuPopup.open(item, iconDelegate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,22 +147,462 @@ Item {
|
||||
anchors.fill: parent
|
||||
source: item.icon
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QsMenuAnchor {
|
||||
id: menuAnchor
|
||||
menu: item.menu
|
||||
anchor.window: root.menuWindow
|
||||
anchor.adjustment: PopupAdjustment.Flip
|
||||
anchor.onAnchoring: {
|
||||
if (!root.menuWindow) return
|
||||
const global = iconDelegate.mapToGlobal(0, iconDelegate.height)
|
||||
const local = root.menuWindow.contentItem.mapFromGlobal(
|
||||
global.x, global.y
|
||||
)
|
||||
menuAnchor.anchor.rect = Qt.rect(
|
||||
local.x, local.y,
|
||||
iconDelegate.width, 0
|
||||
)
|
||||
|
||||
|
||||
|
||||
PanelWindow {
|
||||
id: trayMenuPopup
|
||||
screen: root.targetScreen
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
property bool popupOpen: false
|
||||
property bool _pendingOpen: false
|
||||
property var activeTrayItem: null
|
||||
property var activeDelegate: null
|
||||
|
||||
function open(item, delegate) {
|
||||
if (popupOpen) close()
|
||||
hideTimer.stop()
|
||||
_pendingOpen = false
|
||||
activeTrayItem = item
|
||||
activeDelegate = delegate
|
||||
rootMenuOpener.menu = item.menu
|
||||
visible = true
|
||||
|
||||
|
||||
if (menuCol.height > 0) {
|
||||
popupOpen = true
|
||||
} else {
|
||||
_pendingOpen = true
|
||||
openFallbackTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
_pendingOpen = false
|
||||
openFallbackTimer.stop()
|
||||
popupOpen = false
|
||||
hideTimer.restart()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Connections {
|
||||
target: menuCol
|
||||
function onHeightChanged() {
|
||||
if (trayMenuPopup._pendingOpen && menuCol.height > 0) {
|
||||
trayMenuPopup._pendingOpen = false
|
||||
openFallbackTimer.stop()
|
||||
trayMenuPopup.popupOpen = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Timer {
|
||||
id: openFallbackTimer
|
||||
interval: 400
|
||||
onTriggered: {
|
||||
if (trayMenuPopup._pendingOpen) {
|
||||
trayMenuPopup._pendingOpen = false
|
||||
trayMenuPopup.popupOpen = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: Cfg.Config.popupAnimDuration + 70
|
||||
onTriggered: {
|
||||
trayMenuPopup.visible = false
|
||||
rootMenuOpener.menu = null
|
||||
trayMenuPopup.activeTrayItem = null
|
||||
trayMenuPopup.activeDelegate = null
|
||||
}
|
||||
}
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "main-shell-tray-menu"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
mask: popupOpen ? null : _trayMenuNoInput
|
||||
Region { id: _trayMenuNoInput }
|
||||
|
||||
TapHandler { onTapped: trayMenuPopup.close() }
|
||||
|
||||
QsMenuOpener { id: rootMenuOpener }
|
||||
|
||||
|
||||
Item {
|
||||
id: menuCardWrapper
|
||||
readonly property int cardW: 220
|
||||
readonly property int pad: 10
|
||||
|
||||
|
||||
x: {
|
||||
if (!trayMenuPopup.activeDelegate)
|
||||
return (parent.width - cardW) / 2
|
||||
var gp = trayMenuPopup.activeDelegate.mapToGlobal(0, 0)
|
||||
var cx = gp.x + trayMenuPopup.activeDelegate.width / 2
|
||||
return Math.max(pad,
|
||||
Math.min(cx - cardW / 2, parent.width - cardW - pad))
|
||||
}
|
||||
|
||||
width: cardW
|
||||
clip: true
|
||||
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
anchors.topMargin: root.isTop ? (Cfg.Config.barHeight + 8) : 0
|
||||
anchors.bottomMargin: root.isTop ? 0 : (Cfg.Config.barHeight + 8)
|
||||
height: parent.height - Cfg.Config.barHeight - 8
|
||||
|
||||
Rectangle {
|
||||
id: menuCard
|
||||
width: parent.width
|
||||
height: menuCol.height + 12
|
||||
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
|
||||
transform: Translate {
|
||||
y: trayMenuPopup.popupOpen ? 0
|
||||
: (root.isTop ? -menuCard.height : menuCard.height)
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Cfg.Config.popupAnimDuration
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
opacity: trayMenuPopup.popupOpen ? 1 : 0
|
||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||
|
||||
color: root.tmBg
|
||||
radius: Cfg.Config.popupRadius
|
||||
border.color: root.tmBorder
|
||||
border.width: Cfg.Config.popupBorderWidth
|
||||
|
||||
Column {
|
||||
id: menuCol
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
anchors.margins: 6
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: rootMenuOpener.children
|
||||
|
||||
delegate: Item {
|
||||
id: menuDelegate
|
||||
required property var modelData
|
||||
property bool isSep: modelData.isSeparator
|
||||
|
||||
width: menuCol.width
|
||||
height: isSep ? 5 : 32
|
||||
|
||||
Rectangle {
|
||||
visible: isSep
|
||||
anchors {
|
||||
left: parent.left; right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
height: 1
|
||||
color: root.tmSeparator
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: itemBg
|
||||
visible: !isSep
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
radius: 10
|
||||
property bool active: itemMouse.containsMouse
|
||||
|| (submenuCard.visible
|
||||
&& trayMenuPopup._submenuParentRef === menuDelegate)
|
||||
color: active ? root.tmHover : "transparent"
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
spacing: 8
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 16
|
||||
Layout.preferredHeight: 16
|
||||
visible: !isSep
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: modelData.buttonType === QsMenuButtonType.CheckBox
|
||||
&& modelData.checkState === Qt.Checked
|
||||
text: "✓"
|
||||
color: root.tmCheck
|
||||
font.pixelSize: 14
|
||||
}
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: modelData.buttonType === QsMenuButtonType.RadioButton
|
||||
&& modelData.checkState === Qt.Checked
|
||||
text: "●"
|
||||
color: root.tmCheck
|
||||
font.pixelSize: 9
|
||||
}
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: modelData.buttonType === QsMenuButtonType.RadioButton
|
||||
&& modelData.checkState !== Qt.Checked
|
||||
text: "○"
|
||||
color: root.tmDim
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
visible: !isSep && modelData.icon !== ""
|
||||
source: modelData.icon
|
||||
width: 16; height: 16
|
||||
sourceSize: Qt.size(16, 16)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.text
|
||||
color: modelData.enabled ? root.tmText : root.tmDim
|
||||
font.pixelSize: 13
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: !isSep && modelData.hasChildren
|
||||
text: "▶"
|
||||
color: root.tmDim
|
||||
font.pixelSize: 9
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: itemMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: !isSep && modelData.enabled
|
||||
|
||||
onClicked: {
|
||||
if (modelData.hasChildren) {
|
||||
closeSubmenuTimer.stop()
|
||||
submenuDelayTimer.itemRef = menuDelegate
|
||||
submenuDelayTimer.restart()
|
||||
} else {
|
||||
modelData.triggered()
|
||||
trayMenuPopup.close()
|
||||
}
|
||||
}
|
||||
onEntered: {
|
||||
if (modelData.hasChildren) {
|
||||
closeSubmenuTimer.stop()
|
||||
submenuDelayTimer.itemRef = menuDelegate
|
||||
submenuDelayTimer.restart()
|
||||
} else {
|
||||
closeSubmenuTimer.stop()
|
||||
submenuCard.visible = false
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (modelData.hasChildren)
|
||||
closeSubmenuTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
property var _submenuParentRef: null
|
||||
|
||||
QsMenuOpener { id: subMenuOpener }
|
||||
|
||||
Timer {
|
||||
id: submenuDelayTimer
|
||||
interval: 250
|
||||
property var itemRef: null
|
||||
onTriggered: {
|
||||
if (!itemRef) return
|
||||
var entry = itemRef.modelData
|
||||
if (!entry || !entry.hasChildren) return
|
||||
subMenuOpener.menu = entry
|
||||
trayMenuPopup._submenuParentRef = itemRef
|
||||
submenuCard.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeSubmenuTimer
|
||||
interval: 300
|
||||
onTriggered: {
|
||||
if (!submenuCard.containsMouse) {
|
||||
submenuCard.visible = false
|
||||
subMenuOpener.menu = null
|
||||
trayMenuPopup._submenuParentRef = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Rectangle {
|
||||
id: submenuCard
|
||||
visible: false
|
||||
property bool containsMouse: false
|
||||
|
||||
property int subW: Math.max(submenuCol.width, 180)
|
||||
|
||||
x: {
|
||||
if (!trayMenuPopup._submenuParentRef) return 0
|
||||
var pItem = trayMenuPopup._submenuParentRef
|
||||
var gp = pItem.mapToGlobal(0, 0)
|
||||
var lp = trayMenuPopup.contentItem.mapFromGlobal(gp.x, gp.y)
|
||||
var fromLeft = lp.x + pItem.width + 4
|
||||
if (fromLeft + subW + 10 > trayMenuPopup.contentItem.width)
|
||||
return Math.max(2, lp.x - subW - 4)
|
||||
return fromLeft
|
||||
}
|
||||
|
||||
y: {
|
||||
if (!trayMenuPopup._submenuParentRef) return 0
|
||||
var pItem = trayMenuPopup._submenuParentRef
|
||||
var gp = pItem.mapToGlobal(0, 0)
|
||||
var lp = trayMenuPopup.contentItem.mapFromGlobal(gp.x, gp.y)
|
||||
return Math.max(2, lp.y - 2)
|
||||
}
|
||||
|
||||
width: subW
|
||||
height: submenuCol.height + 12
|
||||
|
||||
color: root.tmBg
|
||||
radius: Cfg.Config.popupRadius
|
||||
border.color: root.tmBorder
|
||||
border.width: Cfg.Config.popupBorderWidth
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: {
|
||||
submenuCard.containsMouse = hovered
|
||||
if (!hovered)
|
||||
closeSubmenuTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: submenuCol
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
anchors.margins: 6
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: subMenuOpener.children
|
||||
|
||||
delegate: Item {
|
||||
required property var modelData
|
||||
property bool isSep: modelData.isSeparator
|
||||
|
||||
width: submenuCol.width
|
||||
height: isSep ? 5 : 32
|
||||
|
||||
Rectangle {
|
||||
visible: isSep
|
||||
anchors {
|
||||
left: parent.left; right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
anchors.leftMargin: 8; anchors.rightMargin: 8
|
||||
height: 1
|
||||
color: root.tmSeparator
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: subBg
|
||||
visible: !isSep
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
radius: 10
|
||||
color: subMouse.containsMouse ? root.tmHover : "transparent"
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8; anchors.rightMargin: 8
|
||||
spacing: 8
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 16
|
||||
Layout.preferredHeight: 16
|
||||
visible: !isSep
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: modelData.buttonType === QsMenuButtonType.CheckBox
|
||||
&& modelData.checkState === Qt.Checked
|
||||
text: "✓"; color: root.tmCheck
|
||||
font.pixelSize: 14
|
||||
}
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: modelData.buttonType === QsMenuButtonType.RadioButton
|
||||
&& modelData.checkState === Qt.Checked
|
||||
text: "●"; color: root.tmCheck
|
||||
font.pixelSize: 9
|
||||
}
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: modelData.buttonType === QsMenuButtonType.RadioButton
|
||||
&& modelData.checkState !== Qt.Checked
|
||||
text: "○"; color: root.tmDim
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
visible: !isSep && modelData.icon !== ""
|
||||
source: modelData.icon
|
||||
width: 16; height: 16
|
||||
sourceSize: Qt.size(16, 16)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.text
|
||||
color: modelData.enabled ? root.tmText : root.tmDim
|
||||
font.pixelSize: 13
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: subMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: !isSep && modelData.enabled
|
||||
onClicked: {
|
||||
modelData.triggered()
|
||||
trayMenuPopup.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,42 @@ Rectangle {
|
||||
property int outputMaxVolume: Cfg.Config.volumeOutputMax
|
||||
property int inputMaxVolume: Cfg.Config.volumeInputMax
|
||||
|
||||
// Path to config.qml — using HOME to avoid Qt.resolvedUrl ambiguity
|
||||
readonly property string configPath:
|
||||
Quickshell.env("HOME") + "/.config/quickshell/config/config.qml"
|
||||
|
||||
// Process that patches volumeOutputMax / volumeInputMax in config.qml
|
||||
Process {
|
||||
id: volConfigWriter
|
||||
running: false
|
||||
onExited: (code) => {
|
||||
if (code !== 0)
|
||||
console.warn("[Volume] Failed to patch config.qml (exit " + code + ")")
|
||||
}
|
||||
}
|
||||
|
||||
// Debounce: write to disk 800 ms after the last limit change
|
||||
Timer {
|
||||
id: saveDebounce
|
||||
interval: 800
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
const outMax = Math.round(root.outputMaxVolume).toString()
|
||||
const inMax = Math.round(root.inputMaxVolume).toString()
|
||||
const cfgPath = root.configPath
|
||||
const sh =
|
||||
"sed -i" +
|
||||
" -e 's/\\(property int[ ]\\+volumeOutputMax:[ ]\\+\\)[0-9.]\\+/\\1" + outMax + "/'" +
|
||||
" -e 's/\\(property int[ ]\\+volumeInputMax:[ ]\\+\\)[0-9.]\\+/\\1" + inMax + "/'" +
|
||||
" " + JSON.stringify(cfgPath)
|
||||
volConfigWriter.command = ["/bin/sh", "-c", sh]
|
||||
volConfigWriter.running = true
|
||||
}
|
||||
}
|
||||
|
||||
onOutputMaxVolumeChanged: { if (root.outputMaxVolume >= 10) saveDebounce.restart() }
|
||||
onInputMaxVolumeChanged: { if (root.inputMaxVolume >= 10) saveDebounce.restart() }
|
||||
|
||||
readonly property var sink: Pipewire.defaultAudioSink
|
||||
readonly property var source: Pipewire.defaultAudioSource
|
||||
|
||||
|
||||
Reference in New Issue
Block a user