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:
SomeElse
2026-05-28 08:24:03 +00:00
parent b9708ddfd0
commit 1b9ee3bbb3
39 changed files with 3652 additions and 267 deletions

View 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()
}
}
}

View 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) }
}
}

View 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 }
}
}