quickshell-rice/quickshell/modules/topbar/TopCenterBar.qml

512 lines
18 KiB
QML

import QtQuick
import QtQuick.Layouts
import Quickshell
import "../../components"
import "../../services"
import "../../theme"
GlassPanel {
id: root
implicitWidth: mainLayout.implicitWidth + 24
implicitHeight: 36
RowLayout {
id: mainLayout
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 8
// Time & Date Section with Bell / DND Icon on the left
Rectangle {
id: timeBtn
Layout.preferredWidth: timeRow.implicitWidth + 12
Layout.preferredHeight: 26
radius: 6
color: timeMouse.containsMouse ? Theme.currentLine : "transparent"
Behavior on color { ColorAnimation { duration: 120 } }
RowLayout {
id: timeRow
anchors.centerIn: parent
spacing: 6
// Bell / DND Icon (Left side of Time & Date)
BellIcon {
isDnd: NotificationService.isDnd
implicitWidth: 14
implicitHeight: 14
Layout.alignment: Qt.AlignVCenter
}
Text {
text: DateTimeService.timeStr
color: Theme.fg
font.pixelSize: 11
font.weight: Font.Medium
}
Text {
text: "•"
color: Theme.comment
font.pixelSize: 10
}
Text {
text: DateTimeService.dateStr
color: Theme.comment
font.pixelSize: 11
}
}
MouseArea {
id: timeMouse
anchors.fill: parent
hoverEnabled: true
onClicked: PopupService.toggleCalendar()
}
}
// Minimal Divider Line
Rectangle {
Layout.preferredWidth: 1
Layout.preferredHeight: 14
color: Theme.separator
}
// MPRIS Media Player Snippet
Rectangle {
id: mediaBtn
Layout.preferredWidth: mediaRow.implicitWidth + 12
Layout.preferredHeight: 26
radius: 6
color: mediaMouse.containsMouse ? Theme.currentLine : "transparent"
Behavior on color { ColorAnimation { duration: 120 } }
RowLayout {
id: mediaRow
anchors.centerIn: parent
spacing: 6
// Static 1:1 Album Cover Disc (Top Bar Thumbnail)
AlbumCover {
Layout.preferredWidth: 20
Layout.preferredHeight: 20
implicitWidth: 20
implicitHeight: 20
artUrl: MediaService.artUrl
}
// Active Source Badge Pill (Firefox, Spotify, VLC, etc.)
Rectangle {
visible: MediaService.hasPlayer && MediaService.playerDisplayName !== ""
implicitWidth: sourceText.implicitWidth + 10
implicitHeight: 16
radius: 4
color: Qt.rgba(Theme.accent.r, Theme.accent.g, Theme.accent.b, 0.15)
border.color: Qt.rgba(Theme.accent.r, Theme.accent.g, Theme.accent.b, 0.4)
border.width: 1
Text {
id: sourceText
anchors.centerIn: parent
text: MediaService.playerDisplayName
color: Theme.accent
font.pixelSize: 9
font.weight: Font.DemiBold
}
}
// Scrolling Marquee Track Title
MarqueeText {
id: marqueeTitle
implicitWidth: 140
implicitHeight: 16
text: MediaService.hasPlayer && MediaService.title !== "" ? MediaService.title + (MediaService.artist ? " - " + MediaService.artist : "") : "No Media Playing"
color: MediaService.hasPlayer ? Theme.fg : Theme.fg
font.pixelSize: 11
font.weight: Font.Normal
}
}
MouseArea {
id: mediaMouse
anchors.fill: parent
hoverEnabled: true
onClicked: PopupService.toggleMedia()
}
}
}
// Detached Full Monthly Calendar Popup Card
PopupWindow {
id: timeMenu
anchor.window: window
anchor.rect.x: root.x + (root.width / 2) - (implicitWidth / 2)
anchor.rect.y: 42
anchor.edges: Edges.Bottom
visible: false
color: "transparent"
implicitWidth: calendarGlass.implicitWidth
implicitHeight: calendarGlass.implicitHeight
property real animProgress: 0.0
NumberAnimation on animProgress {
id: calPopIn
running: false
to: 1.0
duration: 220
easing.type: Easing.OutBack
easing.overshoot: 1.15
}
NumberAnimation on animProgress {
id: calPopOut
running: false
to: 0.0
duration: 160
easing.type: Easing.InQuad
onFinished: timeMenu.visible = false
}
Connections {
target: PopupService
function onCalendarMenuOpenChanged() {
if (PopupService.calendarMenuOpen) {
calPopOut.running = false
timeMenu.visible = true
calPopIn.restart()
} else if (timeMenu.visible) {
calPopIn.running = false
calPopOut.restart()
}
}
}
GlassPanel {
id: calendarGlass
implicitWidth: calWidget.implicitWidth + 24
implicitHeight: calWidget.implicitHeight + 20
anchors.fill: parent
opacity: timeMenu.animProgress
scale: 0.90 + 0.10 * timeMenu.animProgress
transformOrigin: Item.Top
CalendarWidget {
id: calWidget
anchors.centerIn: parent
}
}
}
// Detached MPRIS Player Card Popup Window
PopupWindow {
id: mediaMenu
anchor.window: window
anchor.rect.x: root.x + (root.width / 2) - (implicitWidth / 2)
anchor.rect.y: 42
anchor.edges: Edges.Bottom
visible: false
color: "transparent"
implicitWidth: playerGlass.implicitWidth
implicitHeight: playerGlass.implicitHeight
property real animProgress: 0.0
NumberAnimation on animProgress {
id: mediaPopIn
running: false
to: 1.0
duration: 220
easing.type: Easing.OutBack
easing.overshoot: 1.15
}
NumberAnimation on animProgress {
id: mediaPopOut
running: false
to: 0.0
duration: 160
easing.type: Easing.InQuad
onFinished: mediaMenu.visible = false
}
Connections {
target: PopupService
function onMediaMenuOpenChanged() {
if (PopupService.mediaMenuOpen) {
mediaPopOut.running = false
mediaMenu.visible = true
mediaPopIn.restart()
} else if (mediaMenu.visible) {
mediaPopIn.running = false
mediaPopOut.restart()
}
}
}
GlassPanel {
id: playerGlass
implicitWidth: 240
implicitHeight: playerLayout.implicitHeight + 24
anchors.fill: parent
opacity: mediaMenu.animProgress
scale: 0.90 + 0.10 * mediaMenu.animProgress
transformOrigin: Item.Top
ColumnLayout {
id: playerLayout
anchors.fill: parent
anchors.margins: 12
spacing: 10
// Source Header Badge
Rectangle {
visible: MediaService.hasPlayer && MediaService.playerDisplayName !== ""
Layout.alignment: Qt.AlignHCenter
implicitWidth: popupSourceRow.implicitWidth + 12
implicitHeight: 18
radius: 5
color: Theme.currentLine
border.color: Theme.currentLine
border.width: 1
RowLayout {
id: popupSourceRow
anchors.centerIn: parent
spacing: 5
Rectangle {
width: 5; height: 5; radius: 2.5
color: MediaService.status === "Playing" ? Theme.green : Theme.yellow
}
Text {
text: MediaService.playerDisplayName.toUpperCase()
color: Theme.fg
font.pixelSize: 9
font.weight: Font.Bold
}
}
}
// Static Album Cover Disc (64x64)
AlbumCover {
Layout.preferredWidth: 64
Layout.preferredHeight: 64
implicitWidth: 64
implicitHeight: 64
artUrl: MediaService.artUrl
Layout.alignment: Qt.AlignHCenter
}
// Track Title
Text {
text: MediaService.title !== "" ? MediaService.title : "No Media Playing"
color: Theme.fg
font.pixelSize: 12
font.weight: Font.Bold
elide: Text.ElideRight
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
}
// Artist
Text {
text: MediaService.artist !== "" ? MediaService.artist : "Unknown Artist"
color: Theme.comment
font.pixelSize: 11
elide: Text.ElideRight
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
}
// Smooth Progress Bar Section with Circle Scrub Handle
ColumnLayout {
Layout.fillWidth: true
spacing: 4
visible: MediaService.hasPlayer
Item {
id: progressTrackContainer
Layout.fillWidth: true
Layout.preferredHeight: 14
property bool isDragging: seekMouse.pressed
property real dragRatio: 0.0
// Track Background
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 4
radius: 2
color: Theme.currentLine
// Active Fill
Rectangle {
width: Math.max(4, parent.width * (progressTrackContainer.isDragging ? progressTrackContainer.dragRatio : MediaService.progress))
height: parent.height
radius: 2
color: Theme.pink
Behavior on width {
enabled: !progressTrackContainer.isDragging
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
}
}
// Draggable Circle Knob Handle
Rectangle {
id: knobHandle
width: seekMouse.containsMouse || progressTrackContainer.isDragging ? 12 : 8
height: width
radius: width / 2
color: Theme.pink
border.color: Theme.bg
border.width: 1
anchors.verticalCenter: parent.verticalCenter
x: Math.max(0, Math.min(progressTrackContainer.width - width, (progressTrackContainer.width * (progressTrackContainer.isDragging ? progressTrackContainer.dragRatio : MediaService.progress)) - (width / 2)))
Behavior on width { NumberAnimation { duration: 100 } }
Behavior on x {
enabled: !progressTrackContainer.isDragging
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
}
// Mouse Scrub & Seek Handler
MouseArea {
id: seekMouse
anchors.fill: parent
hoverEnabled: true
onPositionChanged: (mouse) => {
if (pressed) {
progressTrackContainer.dragRatio = Math.max(0.0, Math.min(1.0, mouse.x / width))
}
}
onPressed: (mouse) => {
progressTrackContainer.dragRatio = Math.max(0.0, Math.min(1.0, mouse.x / width))
}
onReleased: (mouse) => {
var ratio = Math.max(0.0, Math.min(1.0, mouse.x / width))
progressTrackContainer.dragRatio = ratio
MediaService.seek(ratio * MediaService.length)
}
}
}
// Timestamps Row
RowLayout {
Layout.fillWidth: true
Text {
text: MediaService.positionStr
color: Theme.comment
font.pixelSize: 10
}
Item { Layout.fillWidth: true }
Text {
text: MediaService.lengthStr
color: Theme.comment
font.pixelSize: 10
}
}
}
// Playback Controls Row
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 14
// Previous Button
Rectangle {
width: 30
height: 30
radius: 6
color: prevMouse.containsMouse ? Theme.currentLine : "transparent"
Behavior on color { ColorAnimation { duration: 100 } }
MediaIcon {
iconType: "prev"
color: prevMouse.containsMouse ? Theme.fg : Theme.comment
anchors.centerIn: parent
implicitWidth: 12
implicitHeight: 12
}
MouseArea {
id: prevMouse
anchors.fill: parent
hoverEnabled: true
onClicked: MediaService.previous()
}
}
// Play/Pause Button
Rectangle {
width: 34
height: 34
radius: 8
color: playMouse.containsMouse ? Theme.subAccent : Theme.accent
Behavior on color { ColorAnimation { duration: 100 } }
MediaIcon {
iconType: MediaService.status === "Playing" ? "pause" : "play"
color: Theme.bg
anchors.centerIn: parent
implicitWidth: 14
implicitHeight: 14
}
MouseArea {
id: playMouse
anchors.fill: parent
hoverEnabled: true
onClicked: MediaService.playPause()
}
}
// Next Button
Rectangle {
width: 30
height: 30
radius: 6
color: nextMouse.containsMouse ? Theme.currentLine : "transparent"
Behavior on color { ColorAnimation { duration: 100 } }
MediaIcon {
iconType: "next"
color: nextMouse.containsMouse ? Theme.fg : Theme.comment
anchors.centerIn: parent
implicitWidth: 12
implicitHeight: 12
}
MouseArea {
id: nextMouse
anchors.fill: parent
hoverEnabled: true
onClicked: MediaService.next()
}
}
}
}
}
}
}