added two new themes, reworked Crypto.qml and Stats.qml, standardized the buttons for the modules, added a new Sidebar widget which contains new 3 modules
This commit is contained in:
208
widgets/sidebar/Sidebar.qml
Normal file
208
widgets/sidebar/Sidebar.qml
Normal file
@@ -0,0 +1,208 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
import "../../config" as Cfg
|
||||
import "sysinfo"
|
||||
import "mpris"
|
||||
import "weather"
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
color: "transparent"
|
||||
visible: false
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "main-shell-sidebar"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: _open ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||
|
||||
Region { id: noMask }
|
||||
mask: _open ? null : noMask
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
readonly property bool isTop: Cfg.Config.barPosition === "top"
|
||||
readonly property int frameTop: isTop ? Cfg.Config.barHeight : Cfg.Config.margin
|
||||
readonly property int frameBot: isTop ? Cfg.Config.margin : Cfg.Config.barHeight
|
||||
readonly property int inset: Cfg.Config.margin + Cfg.Config.frameBorderWidth + 8
|
||||
readonly property int sbWidth: 260
|
||||
readonly property bool isLeft: Cfg.Config.sideBarPosition === "left"
|
||||
readonly property int slideDist: root.sbWidth + 20
|
||||
readonly property int slideFrom: isLeft ? -slideDist : slideDist
|
||||
readonly property int panelX: isLeft ? root.inset : (width - root.sbWidth - root.inset)
|
||||
|
||||
property bool _open: false
|
||||
readonly property bool isOpen: _open
|
||||
|
||||
GlobalShortcut {
|
||||
name: "sidebar"
|
||||
onPressed: root.toggle()
|
||||
}
|
||||
|
||||
function open() {
|
||||
visible = true
|
||||
_open = true
|
||||
enterAnim.stop()
|
||||
exitAnim.stop()
|
||||
hideTimer.stop()
|
||||
enterAnim.start()
|
||||
}
|
||||
|
||||
function close() {
|
||||
_open = false
|
||||
enterAnim.stop()
|
||||
exitAnim.start()
|
||||
}
|
||||
|
||||
function toggle() { _open ? close() : open() }
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 280
|
||||
onTriggered: visible = false
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onActiveChanged() {
|
||||
if (!root.active && root._open) root.close()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.close()
|
||||
}
|
||||
|
||||
// ── Full right-side background panel ──────────────────────────────────
|
||||
Rectangle {
|
||||
id: sidebarPanel
|
||||
transform: Translate { id: panelSlide; x: root.slideFrom }
|
||||
|
||||
x: root.panelX
|
||||
y: root.frameTop + Cfg.Config.frameBorderWidth
|
||||
width: root.sbWidth
|
||||
height: parent.height - root.frameTop - root.frameBot - Cfg.Config.frameBorderWidth * 2
|
||||
radius: Cfg.Config.popupRadius
|
||||
color: "transparent"
|
||||
opacity: 0
|
||||
|
||||
clip: true
|
||||
|
||||
// ── Widget area ───────────────────────────────────────────────────
|
||||
Item {
|
||||
id: widgetArea
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: 10
|
||||
bottomMargin: 10
|
||||
leftMargin: 10
|
||||
rightMargin: 10
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: flick
|
||||
anchors.fill: parent
|
||||
contentWidth: width
|
||||
contentHeight: widgetCol.implicitHeight
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
clip: true
|
||||
flickDeceleration: 2000
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
width: 4
|
||||
contentItem: Rectangle {
|
||||
radius: 2
|
||||
color: Qt.alpha(root.t.accent, 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: widgetCol
|
||||
anchors { left: parent.left; right: parent.right }
|
||||
spacing: 10
|
||||
|
||||
Loader {
|
||||
active: Cfg.Config.sideBarSysInfo
|
||||
sourceComponent: sysInfoComp
|
||||
anchors { left: parent.left; right: parent.right }
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Cfg.Config.sideBarMpris
|
||||
sourceComponent: mprisComp
|
||||
anchors { left: parent.left; right: parent.right }
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Cfg.Config.sideBarWeather
|
||||
sourceComponent: weatherComp
|
||||
anchors { left: parent.left; right: parent.right }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: sysInfoComp
|
||||
WidgetCard {
|
||||
SysInfo {
|
||||
anchors { left: parent.left; right: parent.right }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mprisComp
|
||||
WidgetCard {
|
||||
MprisWidget {
|
||||
anchors { left: parent.left; right: parent.right }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: weatherComp
|
||||
WidgetCard {
|
||||
WeatherWidget {
|
||||
anchors { left: parent.left; right: parent.right }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Enter animation ───────────────────────────────────────────────────
|
||||
ParallelAnimation {
|
||||
id: enterAnim
|
||||
NumberAnimation {
|
||||
target: panelSlide; property: "x"
|
||||
to: 0; duration: 320; easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: sidebarPanel; property: "opacity"
|
||||
to: 1; duration: 280; easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// ── Exit animation ────────────────────────────────────────────────────
|
||||
ParallelAnimation {
|
||||
id: exitAnim
|
||||
onStopped: hideTimer.restart()
|
||||
NumberAnimation {
|
||||
target: panelSlide; property: "x"
|
||||
to: root.slideFrom; duration: 250; easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: sidebarPanel; property: "opacity"
|
||||
to: 0; duration: 200; easing.type: Easing.InBack
|
||||
}
|
||||
}
|
||||
}
|
||||
31
widgets/sidebar/WidgetCard.qml
Normal file
31
widgets/sidebar/WidgetCard.qml
Normal file
@@ -0,0 +1,31 @@
|
||||
import QtQuick
|
||||
import "../../config" as Cfg
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
|
||||
anchors { left: parent.left; right: parent.right }
|
||||
|
||||
implicitHeight: contentArea.childrenRect.height + 28
|
||||
|
||||
radius: Cfg.Config.popupRadius
|
||||
color: Cfg.Config.theme.bgPopup
|
||||
border.color: Qt.alpha(Cfg.Config.theme.accent, 0.18)
|
||||
border.width: 1
|
||||
clip: true
|
||||
|
||||
default property alias content: contentArea.data
|
||||
|
||||
Item {
|
||||
id: contentArea
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
topMargin: 14
|
||||
leftMargin: 14
|
||||
rightMargin: 14
|
||||
}
|
||||
height: childrenRect.height
|
||||
}
|
||||
}
|
||||
63
widgets/sidebar/mpris/MediaButton.qml
Normal file
63
widgets/sidebar/mpris/MediaButton.qml
Normal file
@@ -0,0 +1,63 @@
|
||||
import QtQuick
|
||||
import "../../../config" as Cfg
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string icon: ""
|
||||
property bool enabled: true
|
||||
property bool primary: false
|
||||
signal clicked()
|
||||
|
||||
property bool _hovered: false
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
width: primary ? 44 : 36
|
||||
height: width
|
||||
radius: width / 2
|
||||
|
||||
border.width: 1
|
||||
scale: _hovered && enabled ? 1.08 : 1
|
||||
|
||||
readonly property color _bgDefault: enabled
|
||||
? (primary ? t.accent : t.mprisBtnBg)
|
||||
: "transparent"
|
||||
readonly property color _bgHovered: primary
|
||||
? Qt.lighter(t.accent, 1.2)
|
||||
: t.mprisBtnBgHover
|
||||
|
||||
color: _hovered && enabled ? _bgHovered : _bgDefault
|
||||
border.color: primary ? color : (
|
||||
enabled
|
||||
? (_hovered ? t.mprisBtnBorderHover : t.mprisBtnBorder)
|
||||
: t.mprisBtnBorder
|
||||
)
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutBack } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.icon
|
||||
color: root.primary ? root.t.mprisButtonText : (
|
||||
root.enabled
|
||||
? (root._hovered ? root.t.mprisBtnIconHover : root.t.mprisBtnIcon)
|
||||
: root.t.mprisTextDim
|
||||
)
|
||||
font.pixelSize: primary ? 22 : 18
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onHoveredChanged: root._hovered = hovered
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
if (root.enabled) root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
49
widgets/sidebar/mpris/MediaSlider.qml
Normal file
49
widgets/sidebar/mpris/MediaSlider.qml
Normal file
@@ -0,0 +1,49 @@
|
||||
import QtQuick
|
||||
import "../../../config" as Cfg
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real fraction: 0
|
||||
property bool interactive: true
|
||||
signal seeked(real fraction)
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
implicitHeight: 14
|
||||
|
||||
function seek(posX) {
|
||||
seeked(Math.max(0, Math.min(1, posX / width)))
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
height: 3
|
||||
radius: 2
|
||||
color: root.t.mprisTrackColor
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * root.fraction
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: root.t.mprisAccent
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: Math.max(0, Math.min(parent.width - width, (parent.width - width) * root.fraction))
|
||||
width: 10
|
||||
height: 10
|
||||
radius: 5
|
||||
color: root.t.mprisAccent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: root.interactive ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onPressed: pos => root.seek(pos.x)
|
||||
onPositionChanged: pos => { if (pressed) root.seek(pos.x) }
|
||||
}
|
||||
}
|
||||
355
widgets/sidebar/mpris/MprisWidget.qml
Normal file
355
widgets/sidebar/mpris/MprisWidget.qml
Normal file
@@ -0,0 +1,355 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.Mpris
|
||||
import "../../../config" as Cfg
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitHeight: contentCol.implicitHeight
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
property var activePlayer: null
|
||||
|
||||
readonly property bool canChangeVolume: root.activePlayer && root.activePlayer.volumeSupported && root.activePlayer.canControl
|
||||
|
||||
readonly property var _identityMap: ({
|
||||
"firefox": "Firefox",
|
||||
"spotify": "Spotify",
|
||||
"chromium": "Chromium",
|
||||
"google-chrome": "Chrome",
|
||||
"brave": "Brave",
|
||||
"vlc": "VLC",
|
||||
"mpv": "mpv"
|
||||
})
|
||||
|
||||
function selectPlayer() {
|
||||
var players = Mpris.players.values
|
||||
var fallback = null
|
||||
for (var i = 0; i < players.length; i++) {
|
||||
var p = players[i]
|
||||
if (!p || (p.trackTitle === "" && p.identity === "")) continue
|
||||
if (p.playbackState === MprisPlaybackState.Playing) { activePlayer = p; return }
|
||||
if (!fallback) fallback = p
|
||||
}
|
||||
if (activePlayer && activePlayer.playbackState !== MprisPlaybackState.Stopped) return
|
||||
activePlayer = fallback
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: Mpris.players
|
||||
Connections {
|
||||
required property MprisPlayer modelData
|
||||
target: modelData
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.activePlayer == null || modelData.playbackState === MprisPlaybackState.Playing)
|
||||
root.selectPlayer()
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
if (root.activePlayer === modelData) {
|
||||
root.activePlayer = null
|
||||
root.selectPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
function onPlaybackStateChanged() {
|
||||
root.selectPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatDuration(s) {
|
||||
if (isNaN(s) || s < 0) return "0:00"
|
||||
var m = Math.floor(s / 60)
|
||||
var sec = Math.floor(s % 60)
|
||||
return m + ":" + (sec < 10 ? "0" : "") + sec
|
||||
}
|
||||
|
||||
function formatIdentity(name) {
|
||||
if (!name) return "Media"
|
||||
var lower = name.toLowerCase()
|
||||
for (var key in root._identityMap) {
|
||||
if (lower.indexOf(key) !== -1) return root._identityMap[key]
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
function playbackStatusText() {
|
||||
if (!root.activePlayer) return "No players"
|
||||
switch (root.activePlayer.playbackState) {
|
||||
case MprisPlaybackState.Playing: return "Now playing"
|
||||
case MprisPlaybackState.Paused: return "Paused"
|
||||
default: return "Stopped"
|
||||
}
|
||||
}
|
||||
|
||||
function volumeIcon() {
|
||||
if (!root.activePlayer) return ""
|
||||
var v = root.activePlayer.volume
|
||||
if (v === 0) return ""
|
||||
if (v < 0.33) return ""
|
||||
if (v < 0.66) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: positionTimer
|
||||
running: root.activePlayer && root.activePlayer.playbackState === MprisPlaybackState.Playing
|
||||
interval: 1000
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (root.activePlayer) root.activePlayer.positionChanged()
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentCol
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 32
|
||||
color: "transparent"
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
width: 32; height: 32
|
||||
radius: 8
|
||||
color: Qt.alpha(root.t.mprisAccent, 0.12)
|
||||
border.color: Qt.alpha(root.t.mprisAccent, 0.22)
|
||||
border.width: 1
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: root.t.mprisAccent
|
||||
font.pixelSize: 18
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Text {
|
||||
text: root.activePlayer ? root.formatIdentity(root.activePlayer.identity) : "No media"
|
||||
color: root.t.mprisTextMain
|
||||
font.pixelSize: 13
|
||||
font.weight: Font.DemiBold
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: root.playbackStatusText()
|
||||
color: root.t.mprisTextDim
|
||||
font.pixelSize: 10
|
||||
opacity: 0.75
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 12 }
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop { position: 0.0; color: Qt.alpha(root.t.mprisAccent, 0.5) }
|
||||
GradientStop { position: 1.0; color: "transparent" }
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: root.activePlayer ? 10 : 0 }
|
||||
|
||||
ColumnLayout {
|
||||
id: trackArea
|
||||
Layout.fillWidth: true
|
||||
visible: root.activePlayer !== null
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
Rectangle {
|
||||
width: 64; height: 64
|
||||
radius: 8
|
||||
color: root.t.mprisAlbumBg
|
||||
border.color: root.t.mprisAlbumBorder
|
||||
border.width: 1
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
Image {
|
||||
id: albumArt
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
source: root.activePlayer ? root.activePlayer.trackArtUrl : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: root.activePlayer && root.activePlayer.trackArtUrl !== "" && status === Image.Ready
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: root.t.mprisAccent
|
||||
font.pixelSize: 28
|
||||
visible: root.activePlayer && (root.activePlayer.trackArtUrl === "" || !albumArt.visible)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: root.activePlayer ? (root.activePlayer.trackTitle || "Unknown Title") : ""
|
||||
color: root.t.mprisTextMain
|
||||
font.pixelSize: 13
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.activePlayer ? (root.activePlayer.trackArtist || "Unknown Artist") : ""
|
||||
color: root.t.mprisTextDim
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.activePlayer ? (root.activePlayer.trackAlbum || "") : ""
|
||||
color: root.t.mprisTextDim
|
||||
font.pixelSize: 10
|
||||
opacity: 0.7
|
||||
elide: Text.ElideRight
|
||||
visible: root.activePlayer && root.activePlayer.trackAlbum !== ""
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 12 }
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
visible: root.activePlayer && root.activePlayer.positionSupported
|
||||
|
||||
MediaSlider {
|
||||
Layout.fillWidth: true
|
||||
interactive: root.activePlayer && root.activePlayer.canSeek
|
||||
fraction: {
|
||||
var p = root.activePlayer
|
||||
if (!p) return 0
|
||||
var len = p.length
|
||||
if (len > 0) return Math.max(0, Math.min(1, p.position / len))
|
||||
return 0
|
||||
}
|
||||
onSeeked: frac => {
|
||||
var p = root.activePlayer
|
||||
if (p && p.canSeek && p.length > 0) {
|
||||
p.position = frac * p.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
text: root.activePlayer ? root.formatDuration(root.activePlayer.position) : "0:00"
|
||||
color: root.t.mprisTextDim
|
||||
font.pixelSize: 9
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Text {
|
||||
text: root.activePlayer ? root.formatDuration(root.activePlayer.length) : "0:00"
|
||||
color: root.t.mprisTextDim
|
||||
font.pixelSize: 9
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 12 }
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
visible: root.canChangeVolume
|
||||
|
||||
Text {
|
||||
text: root.volumeIcon()
|
||||
color: root.activePlayer && root.activePlayer.volume > 0 ? root.t.mprisAccent : root.t.mprisTextDim
|
||||
font.pixelSize: 16
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
MediaSlider {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
interactive: root.canChangeVolume
|
||||
fraction: {
|
||||
var p = root.activePlayer
|
||||
if (!p) return 1
|
||||
return p.volume
|
||||
}
|
||||
onSeeked: frac => {
|
||||
var p = root.activePlayer
|
||||
if (p && root.canChangeVolume) {
|
||||
p.volume = frac
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: root.canChangeVolume ? 12 : 0 }
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 16
|
||||
|
||||
MediaButton {
|
||||
icon: ""
|
||||
enabled: root.activePlayer && root.activePlayer.canGoPrevious
|
||||
onClicked: root.activePlayer.previous()
|
||||
}
|
||||
|
||||
MediaButton {
|
||||
primary: true
|
||||
icon: root.activePlayer && root.activePlayer.playbackState === MprisPlaybackState.Playing ? "" : ""
|
||||
enabled: root.activePlayer && root.activePlayer.canTogglePlaying
|
||||
onClicked: root.activePlayer.togglePlaying()
|
||||
}
|
||||
|
||||
MediaButton {
|
||||
icon: ""
|
||||
enabled: root.activePlayer && root.activePlayer.canGoNext
|
||||
onClicked: root.activePlayer.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 6 }
|
||||
}
|
||||
}
|
||||
588
widgets/sidebar/sysinfo/SysInfo.qml
Normal file
588
widgets/sidebar/sysinfo/SysInfo.qml
Normal file
@@ -0,0 +1,588 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "../../../config" as Cfg
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
property string osName: "Linux"
|
||||
property string kernelVer: ""
|
||||
property real cpuPct: 0
|
||||
property real memPct: 0
|
||||
property real memUsedGb: 0
|
||||
property real swapPct: 0
|
||||
property real swapUsedGb: 0
|
||||
property bool hasSwap: false
|
||||
property real diskPct: 0
|
||||
property real diskUsedGb: 0
|
||||
property string uptimeStr: "—"
|
||||
property real battPct: -1
|
||||
property bool battCharging: false
|
||||
property real cpuTemp: -1
|
||||
property real gpuTemp: -1
|
||||
property bool netOnline: false
|
||||
property var _cpuPrev: ({ total: 0, idle: 0 })
|
||||
|
||||
Behavior on cpuPct { NumberAnimation { duration: 800; easing.type: Easing.OutCubic } }
|
||||
Behavior on memPct { NumberAnimation { duration: 800; easing.type: Easing.OutCubic } }
|
||||
Behavior on memUsedGb { NumberAnimation { duration: 800; easing.type: Easing.OutCubic } }
|
||||
Behavior on swapPct { NumberAnimation { duration: 800; easing.type: Easing.OutCubic } }
|
||||
Behavior on swapUsedGb { NumberAnimation { duration: 800; easing.type: Easing.OutCubic } }
|
||||
Behavior on diskPct { NumberAnimation { duration: 800; easing.type: Easing.OutCubic } }
|
||||
Behavior on diskUsedGb { NumberAnimation { duration: 800; easing.type: Easing.OutCubic } }
|
||||
Behavior on battPct { NumberAnimation { duration: 800; easing.type: Easing.OutCubic } }
|
||||
Behavior on cpuTemp { NumberAnimation { duration: 800; easing.type: Easing.OutCubic } }
|
||||
Behavior on gpuTemp { NumberAnimation { duration: 800; easing.type: Easing.OutCubic } }
|
||||
|
||||
Process {
|
||||
running: true
|
||||
command: ["sh", "-c",
|
||||
"grep '^PRETTY_NAME=' /etc/os-release 2>/dev/null | cut -d'\"' -f2 || echo Linux"]
|
||||
stdout: SplitParser { onRead: d => { root.osName = d.trim() } }
|
||||
}
|
||||
Process {
|
||||
running: true
|
||||
command: ["uname", "-r"]
|
||||
stdout: SplitParser { onRead: d => { root.kernelVer = d.trim() } }
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cpuTempProc
|
||||
property string _buf: ""
|
||||
command: ["sh", "-c",
|
||||
"t=-1; for d in /sys/class/hwmon/hwmon*; do " +
|
||||
"n=$(cat $d/name 2>/dev/null); " +
|
||||
"if echo \"$n\" | grep -qiE 'coretemp|k10temp'; then " +
|
||||
"t=$(( $(cat $d/temp1_input 2>/dev/null) / 1000 )); break; " +
|
||||
"fi; done; echo $t"]
|
||||
stdout: SplitParser { onRead: d => { cpuTempProc._buf = d } }
|
||||
onRunningChanged: {
|
||||
if (running || !_buf) return
|
||||
root.cpuTemp = parseInt(_buf)
|
||||
_buf = ""
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuTempProc
|
||||
property string _buf: ""
|
||||
command: ["sh", "-c",
|
||||
"t=-1; for d in /sys/class/hwmon/hwmon*; do " +
|
||||
"n=$(cat $d/name 2>/dev/null); " +
|
||||
"if echo \"$n\" | grep -qiE 'amdgpu|radeon'; then " +
|
||||
"t=$(( $(cat $d/temp1_input 2>/dev/null) / 1000 )); break; " +
|
||||
"fi; done; echo $t"]
|
||||
stdout: SplitParser { onRead: d => { gpuTempProc._buf = d } }
|
||||
onRunningChanged: {
|
||||
if (running || !_buf) return
|
||||
root.gpuTemp = parseInt(_buf)
|
||||
_buf = ""
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cpuProc
|
||||
property string _buf: ""
|
||||
command: ["sh", "-c", "grep '^cpu ' /proc/stat | head -1"]
|
||||
stdout: SplitParser { onRead: d => { cpuProc._buf = d } }
|
||||
onRunningChanged: {
|
||||
if (running || !_buf) return
|
||||
var p = _buf.trim().split(/\s+/)
|
||||
var user = +p[1], nice = +p[2], sys = +p[3]
|
||||
var idle = +p[4], iow = +p[5], irq = +p[6], sirq = +p[7]
|
||||
var total = user + nice + sys + idle + iow + irq + sirq
|
||||
var idleSum = idle + iow
|
||||
if (root._cpuPrev.total > 0) {
|
||||
var dt = total - root._cpuPrev.total
|
||||
var di = idleSum - root._cpuPrev.idle
|
||||
root.cpuPct = dt > 0 ? Math.round((dt - di) / dt * 100) : 0
|
||||
}
|
||||
root._cpuPrev = { total: total, idle: idleSum }
|
||||
_buf = ""
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: memProc
|
||||
property string _buf: ""
|
||||
command: ["sh", "-c", "free -b | awk '/^Mem:/{print $2,$3,$7}'"]
|
||||
stdout: SplitParser { onRead: d => { memProc._buf = d } }
|
||||
onRunningChanged: {
|
||||
if (running || !_buf) return
|
||||
var p = _buf.trim().split(/\s+/)
|
||||
var total = +p[0], avail = +p[2]
|
||||
var used = total - avail
|
||||
root.memUsedGb = used / 1073741824
|
||||
root.memPct = total > 0 ? (used / total * 100) : 0
|
||||
_buf = ""
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: swapProc
|
||||
property string _buf: ""
|
||||
command: ["sh", "-c", "free -b | awk '/^Swap:/{print $2,$3}'"]
|
||||
stdout: SplitParser { onRead: d => { swapProc._buf = d } }
|
||||
onRunningChanged: {
|
||||
if (running || !_buf) return
|
||||
var p = _buf.trim().split(/\s+/)
|
||||
var total = +p[0], used = +p[1]
|
||||
root.hasSwap = total > 0
|
||||
root.swapUsedGb = total > 0 ? used / 1073741824 : 0
|
||||
root.swapPct = total > 0 ? (used / total * 100) : 0
|
||||
_buf = ""
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: uptimeProc
|
||||
property string _buf: ""
|
||||
command: ["sh", "-c",
|
||||
"u=$(awk '{print int($1)}' /proc/uptime); " +
|
||||
"d=$((u/86400)); h=$(((u%86400)/3600)); m=$(((u%3600)/60)); " +
|
||||
"[ $d -gt 0 ] && echo ${d}d${h}h || [ $h -gt 0 ] && echo ${h}h${m}m || echo ${m}m"]
|
||||
stdout: SplitParser { onRead: d => { uptimeProc._buf = d } }
|
||||
onRunningChanged: {
|
||||
if (running || !_buf) return
|
||||
root.uptimeStr = _buf.trim()
|
||||
_buf = ""
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: diskProc
|
||||
property string _buf: ""
|
||||
command: ["sh", "-c", "df -B1 / | awk 'NR==2{print $3,$2}'"]
|
||||
stdout: SplitParser { onRead: d => { diskProc._buf = d } }
|
||||
onRunningChanged: {
|
||||
if (running || !_buf) return
|
||||
var p = _buf.trim().split(/\s+/)
|
||||
var used = +p[0], total = +p[1]
|
||||
root.diskUsedGb = used / 1073741824
|
||||
root.diskPct = total > 0 ? (used / total * 100) : 0
|
||||
_buf = ""
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: battProc
|
||||
property string _buf: ""
|
||||
command: ["sh", "-c",
|
||||
"p=/sys/class/power_supply/BAT0; " +
|
||||
"[ -f $p/capacity ] && echo $(cat $p/capacity) $(cat $p/status) || echo -1 Unknown"]
|
||||
stdout: SplitParser { onRead: d => { battProc._buf = d } }
|
||||
onRunningChanged: {
|
||||
if (running || !_buf) return
|
||||
var p = _buf.trim().split(/\s+/)
|
||||
root.battPct = parseInt(p[0])
|
||||
root.battCharging = (p[1] === "Charging" || p[1] === "Full")
|
||||
_buf = ""
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: netProc
|
||||
property string _buf: ""
|
||||
command: ["sh", "-c", "ip route show default 2>/dev/null | grep -c default || echo 0"]
|
||||
stdout: SplitParser { onRead: d => { netProc._buf = d } }
|
||||
onRunningChanged: {
|
||||
if (running) return
|
||||
root.netOnline = parseInt(netProc._buf.trim()) > 0
|
||||
_buf = ""
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (!cpuProc.running) cpuProc.running = true
|
||||
if (!memProc.running) memProc.running = true
|
||||
if (!swapProc.running) swapProc.running = true
|
||||
if (!diskProc.running) diskProc.running = true
|
||||
if (!uptimeProc.running) uptimeProc.running = true
|
||||
if (!battProc.running) battProc.running = true
|
||||
if (!netProc.running) netProc.running = true
|
||||
if (!cpuTempProc.running) cpuTempProc.running = true
|
||||
if (!gpuTempProc.running) gpuTempProc.running = true
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 5000
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
width: 32; height: 32
|
||||
radius: 8
|
||||
color: Qt.alpha(root.t.sysInfoAccent, 0.12)
|
||||
border.color: Qt.alpha(root.t.sysInfoAccent, 0.22)
|
||||
border.width: 1
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: root.t.sysInfoAccent
|
||||
font.pixelSize: 20
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Text {
|
||||
text: root.osName
|
||||
color: root.t.sysInfoTextMain
|
||||
font.pixelSize: 13
|
||||
font.weight: Font.DemiBold
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: root.kernelVer
|
||||
color: root.t.sysInfoTextDim
|
||||
font.pixelSize: 10
|
||||
opacity: 0.75
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 12 }
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop { position: 0.0; color: Qt.alpha(root.t.sysInfoAccent, 0.5) }
|
||||
GradientStop { position: 1.0; color: "transparent" }
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 10 }
|
||||
|
||||
SysInfoRow {
|
||||
Layout.fillWidth: true
|
||||
icon: ""
|
||||
label: "CPU"
|
||||
value: Math.round(root.cpuPct) + "%"
|
||||
iconColor: root.t.sysInfoCpuColor
|
||||
dimColor: root.t.sysInfoTextDim
|
||||
textColor: root.t.sysInfoTextMain
|
||||
showBar: true
|
||||
barPct: root.cpuPct
|
||||
barColor: root.t.sysInfoCpuColor
|
||||
trackColor: root.t.sysInfoTrackColor
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 9 }
|
||||
|
||||
SysInfoRow {
|
||||
Layout.fillWidth: true
|
||||
visible: root.cpuTemp >= 0
|
||||
icon: ""
|
||||
label: "CPU Temp"
|
||||
value: root.cpuTemp >= 0 ? Math.round(root.cpuTemp) + "°C" : "—"
|
||||
iconColor: root.cpuTemp > 80 ? root.t.statusErr
|
||||
: root.cpuTemp > 60 ? root.t.statusWarn
|
||||
: root.t.sysInfoCpuTempColor
|
||||
dimColor: root.t.sysInfoTextDim
|
||||
textColor: root.t.sysInfoTextMain
|
||||
showBar: true
|
||||
barPct: Math.min(100, Math.round(root.cpuTemp * 100 / 95))
|
||||
barColor: root.cpuTemp > 80 ? root.t.statusErr
|
||||
: root.cpuTemp > 60 ? root.t.statusWarn
|
||||
: root.t.sysInfoCpuTempColor
|
||||
trackColor: root.t.sysInfoTrackColor
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: root.gpuTemp >= 0 ? 9 : 0
|
||||
visible: root.gpuTemp >= 0
|
||||
}
|
||||
|
||||
SysInfoRow {
|
||||
Layout.fillWidth: true
|
||||
visible: root.gpuTemp >= 0
|
||||
icon: ""
|
||||
label: "GPU Temp"
|
||||
value: Math.round(root.gpuTemp) + "°C"
|
||||
iconColor: root.gpuTemp > 80 ? root.t.statusErr
|
||||
: root.gpuTemp > 65 ? root.t.statusWarn
|
||||
: root.t.sysInfoGpuColor
|
||||
dimColor: root.t.sysInfoTextDim
|
||||
textColor: root.t.sysInfoTextMain
|
||||
showBar: true
|
||||
barPct: Math.min(100, Math.round(root.gpuTemp * 100 / 95))
|
||||
barColor: root.gpuTemp > 80 ? root.t.statusErr
|
||||
: root.gpuTemp > 65 ? root.t.statusWarn
|
||||
: root.t.sysInfoGpuColor
|
||||
trackColor: root.t.sysInfoTrackColor
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 9 }
|
||||
|
||||
SysInfoRow {
|
||||
Layout.fillWidth: true
|
||||
icon: ""
|
||||
label: "Memory"
|
||||
value: root.memUsedGb.toFixed(2) + " GB"
|
||||
iconColor: root.t.sysInfoMemColor
|
||||
dimColor: root.t.sysInfoTextDim
|
||||
textColor: root.t.sysInfoTextMain
|
||||
showBar: true
|
||||
barPct: root.memPct
|
||||
barColor: root.t.sysInfoMemColor
|
||||
trackColor: root.t.sysInfoTrackColor
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 9 }
|
||||
|
||||
SysInfoRow {
|
||||
Layout.fillWidth: true
|
||||
icon: ""
|
||||
label: "Disk (/)"
|
||||
value: root.diskUsedGb.toFixed(1) + " GB"
|
||||
iconColor: root.t.sysInfoDiskColor
|
||||
dimColor: root.t.sysInfoTextDim
|
||||
textColor: root.t.sysInfoTextMain
|
||||
showBar: true
|
||||
barPct: root.diskPct
|
||||
barColor: root.t.sysInfoDiskColor
|
||||
trackColor: root.t.sysInfoTrackColor
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: root.hasSwap ? swapItem.implicitHeight + 9 : 0
|
||||
visible: root.hasSwap
|
||||
clip: true
|
||||
|
||||
ColumnLayout {
|
||||
id: swapItem
|
||||
anchors { top: parent.top; left: parent.left; right: parent.right }
|
||||
anchors.topMargin: 9
|
||||
spacing: 5
|
||||
|
||||
SysInfoRow {
|
||||
Layout.fillWidth: true
|
||||
icon: ""
|
||||
label: "Swap"
|
||||
value: root.swapUsedGb.toFixed(1) + " GB"
|
||||
iconColor: root.t.sysInfoMemColor
|
||||
dimColor: root.t.sysInfoTextDim
|
||||
textColor: root.t.sysInfoTextMain
|
||||
showBar: true
|
||||
barPct: root.swapPct
|
||||
barColor: root.t.sysInfoMemColor
|
||||
trackColor: root.t.sysInfoTrackColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 9 }
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
width: 18; height: 18
|
||||
radius: 4
|
||||
color: Qt.alpha(root.t.sysInfoAccent, 0.15)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: root.t.sysInfoAccent
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Uptime"
|
||||
color: root.t.sysInfoTextDim
|
||||
font.pixelSize: 12
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.uptimeStr
|
||||
color: root.t.sysInfoTextMain
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Medium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 9 }
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
width: 18; height: 18
|
||||
radius: 4
|
||||
color: Qt.alpha(root.t.sysInfoAccent, 0.15)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: root.t.sysInfoAccent
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Kernel"
|
||||
color: root.t.sysInfoTextDim
|
||||
font.pixelSize: 12
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.kernelVer
|
||||
color: root.t.sysInfoTextMain
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Medium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: root.battPct >= 0 ? battItem.implicitHeight + 9 : 0
|
||||
visible: root.battPct >= 0
|
||||
clip: true
|
||||
|
||||
ColumnLayout {
|
||||
id: battItem
|
||||
anchors { top: parent.top; left: parent.left; right: parent.right }
|
||||
anchors.topMargin: 9
|
||||
spacing: 5
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
width: 18; height: 18
|
||||
radius: 4
|
||||
color: Qt.alpha(battIconColor, 0.15)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
readonly property color battIconColor:
|
||||
root.battPct <= 20 ? root.t.statusErr
|
||||
: root.battPct <= 40 ? root.t.statusWarn
|
||||
: root.battCharging ? root.t.statusOk
|
||||
: root.t.sysInfoAccent
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.battCharging ? ""
|
||||
: root.battPct > 80 ? ""
|
||||
: root.battPct > 60 ? ""
|
||||
: root.battPct > 40 ? ""
|
||||
: root.battPct > 20 ? ""
|
||||
: ""
|
||||
color: parent.battIconColor
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Battery"
|
||||
color: root.t.sysInfoTextDim
|
||||
font.pixelSize: 12
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Math.round(root.battPct) + "%"
|
||||
color: root.t.sysInfoTextMain
|
||||
font.pixelSize: 12
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 3
|
||||
radius: 2
|
||||
color: root.t.sysInfoTrackColor
|
||||
|
||||
readonly property color fillColor:
|
||||
root.battPct <= 20 ? root.t.statusErr
|
||||
: root.battPct <= 40 ? root.t.statusWarn
|
||||
: root.battCharging ? root.t.statusOk
|
||||
: root.t.sysInfoAccent
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * Math.max(0, Math.min(root.battPct, 100)) / 100
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: parent.fillColor
|
||||
Behavior on width { NumberAnimation { duration: 800; easing.type: Easing.OutCubic } }
|
||||
Behavior on color { ColorAnimation { duration: 400 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 9 }
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
width: 18; height: 18
|
||||
radius: 4
|
||||
color: Qt.alpha(root.netOnline ? root.t.statusOk : root.t.sysInfoTextDim, 0.15)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.netOnline ? "" : ""
|
||||
color: root.netOnline ? root.t.statusOk : root.t.sysInfoTextDim
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Network"
|
||||
color: root.t.sysInfoTextDim
|
||||
font.pixelSize: 12
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.netOnline ? "Connected" : "Offline"
|
||||
color: root.netOnline ? root.t.statusOk : root.t.statusErr
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Medium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
90
widgets/sidebar/sysinfo/SysInfoRow.qml
Normal file
90
widgets/sidebar/sysinfo/SysInfoRow.qml
Normal file
@@ -0,0 +1,90 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
// ── SysInfoRow ────────────────────────────────────────────────────────────────
|
||||
// Metric row with icon badge, label, right-aligned value, and an optional
|
||||
// animated progress bar underneath.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string icon: ""
|
||||
property string label: ""
|
||||
property string value: "—"
|
||||
property color iconColor: "#ffffffff"
|
||||
property color dimColor: "#997a8996"
|
||||
property color textColor: "#ffe8eef2"
|
||||
property bool showBar: false
|
||||
property int barPct: 0 // 0-100
|
||||
property color barColor: iconColor
|
||||
property color trackColor: "#15ffffff"
|
||||
|
||||
implicitHeight: col.implicitHeight
|
||||
implicitWidth: col.implicitWidth
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
anchors { left: parent.left; right: parent.right }
|
||||
spacing: 5
|
||||
|
||||
// ── Label row ─────────────────────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
// Icon in a tinted pill badge
|
||||
Rectangle {
|
||||
width: 18; height: 18
|
||||
radius: 4
|
||||
color: Qt.alpha(root.iconColor, 0.15)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.icon
|
||||
color: root.iconColor
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.label
|
||||
color: root.dimColor
|
||||
font.pixelSize: 12
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.value
|
||||
color: root.textColor
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Medium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// ── Progress bar (optional) ───────────────────────────────────────────
|
||||
Rectangle {
|
||||
visible: root.showBar
|
||||
Layout.fillWidth: true
|
||||
height: 3
|
||||
radius: 2
|
||||
color: root.trackColor
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * Math.max(0, Math.min(root.barPct, 100)) / 100
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: root.barColor
|
||||
|
||||
// smooth fill transitions on data refresh
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 800; easing.type: Easing.OutCubic }
|
||||
}
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 300 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
319
widgets/sidebar/weather/WeatherWidget.qml
Normal file
319
widgets/sidebar/weather/WeatherWidget.qml
Normal file
@@ -0,0 +1,319 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import "../../../config" as Cfg
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitHeight: contentCol.implicitHeight
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
property string icon: "—"
|
||||
property string condition: "—"
|
||||
property string temp: "—"
|
||||
property string feelsLike: "—"
|
||||
property string humidity: "—"
|
||||
property string wind: "—"
|
||||
property string location: "—"
|
||||
|
||||
readonly property string _url: {
|
||||
var loc = Cfg.Config.weatherLocation.trim()
|
||||
if (!loc) return "wttr.in?format=%t|%f|%c|%C|%h|%w|%l&m"
|
||||
loc = loc.replace(/ /g, "+")
|
||||
return "wttr.in/" + loc + "?format=%t|%f|%c|%C|%h|%w|%l&m"
|
||||
}
|
||||
|
||||
function fetchWeather() {
|
||||
weatherProc.command = ["sh", "-c", "curl -s \"" + root._url + "\""]
|
||||
weatherProc.running = true
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: Cfg.Config.weatherRefreshMin * 60000
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.fetchWeather()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: weatherProc
|
||||
property string _buf: ""
|
||||
command: ["sh", "-c", ""]
|
||||
stdout: SplitParser {
|
||||
onRead: d => { weatherProc._buf = d }
|
||||
}
|
||||
onRunningChanged: {
|
||||
if (running || !_buf) return
|
||||
var parts = _buf.trim().split("|")
|
||||
if (parts.length >= 7) {
|
||||
root.temp = parts[0] || "—"
|
||||
root.feelsLike = parts[1] || "—"
|
||||
root.icon = parts[2] || "—"
|
||||
root.condition = parts[3] || "—"
|
||||
root.humidity = parts[4] || "—"
|
||||
root.wind = parts[5] || "—"
|
||||
root.location = parts[6] || "—"
|
||||
}
|
||||
_buf = ""
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentCol
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 32
|
||||
color: "transparent"
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
width: 32; height: 32
|
||||
radius: 8
|
||||
color: Qt.alpha(root.t.weatherAccent, 0.12)
|
||||
border.color: Qt.alpha(root.t.weatherAccent, 0.22)
|
||||
border.width: 1
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.icon
|
||||
color: root.t.weatherAccent
|
||||
font.pixelSize: 18
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Text {
|
||||
text: root.condition
|
||||
color: root.t.weatherTextMain
|
||||
font.pixelSize: 13
|
||||
font.weight: Font.DemiBold
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: root.location
|
||||
color: root.t.weatherTextDim
|
||||
font.pixelSize: 10
|
||||
opacity: 0.75
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 24; height: 24
|
||||
radius: 6
|
||||
color: refreshMouse.containsMouse
|
||||
? (refreshMouse.pressed ? Qt.alpha(root.t.weatherAccent, 0.25) : Qt.alpha(root.t.weatherAccent, 0.12))
|
||||
: "transparent"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
|
||||
Text {
|
||||
id: refreshIcon
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: refreshMouse.containsMouse ? root.t.weatherAccent : root.t.weatherTextDim
|
||||
font.pixelSize: 14
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
|
||||
RotationAnimator {
|
||||
id: spinAnim
|
||||
target: refreshIcon
|
||||
from: 0; to: 360
|
||||
duration: 600
|
||||
easing.type: Easing.OutCubic
|
||||
running: false
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: refreshMouse
|
||||
anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
spinAnim.restart()
|
||||
root.fetchWeather()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 12 }
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop { position: 0.0; color: Qt.alpha(root.t.weatherAccent, 0.5) }
|
||||
GradientStop { position: 1.0; color: "transparent" }
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 10 }
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
width: 18; height: 18
|
||||
radius: 4
|
||||
color: Qt.alpha(root.t.weatherTempColor, 0.15)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: root.t.weatherTempColor
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Temperature"
|
||||
color: root.t.weatherTextDim
|
||||
font.pixelSize: 12
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.temp
|
||||
color: root.t.weatherTextMain
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Medium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 9 }
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
width: 18; height: 18
|
||||
radius: 4
|
||||
color: Qt.alpha(root.t.weatherFeelsColor, 0.15)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: root.t.weatherFeelsColor
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Feels like"
|
||||
color: root.t.weatherTextDim
|
||||
font.pixelSize: 12
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.feelsLike
|
||||
color: root.t.weatherTextMain
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Medium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 9 }
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
width: 18; height: 18
|
||||
radius: 4
|
||||
color: Qt.alpha(root.t.weatherHumidColor, 0.15)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: root.t.weatherHumidColor
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Humidity"
|
||||
color: root.t.weatherTextDim
|
||||
font.pixelSize: 12
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.humidity
|
||||
color: root.t.weatherTextMain
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Medium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 9 }
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
width: 18; height: 18
|
||||
radius: 4
|
||||
color: Qt.alpha(root.t.weatherWindColor, 0.15)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: root.t.weatherWindColor
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Wind"
|
||||
color: root.t.weatherTextDim
|
||||
font.pixelSize: 12
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.wind
|
||||
color: root.t.weatherTextMain
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Medium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 6 }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user