Compare commits

...

3 commits

13 changed files with 717 additions and 887 deletions

View file

@ -1,7 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland
import "." as M import "." as M
M.BarSection { M.BarSection {
@ -12,9 +11,8 @@ M.BarSection {
tooltip: "" tooltip: ""
property int percent: 0 property int percent: 0
property bool _panelHovered: false
property bool _osdActive: false property bool _osdActive: false
readonly property bool _showPanel: root._hovered || _panelHovered || _osdActive readonly property bool _showPanel: root._hovered || hoverPanel.panelHovered || _osdActive
onPercentChanged: if (percent > 0) onPercentChanged: if (percent > 0)
_flashPanel() _flashPanel()
@ -97,174 +95,83 @@ M.BarSection {
onWheel: event => root.adjust(event.angleDelta.y) onWheel: event => root.adjust(event.angleDelta.y)
} }
PanelWindow { M.HoverPanel {
id: panel id: hoverPanel
showPanel: root._showPanel
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
accentColor: root.accentColor
panelNamespace: "nova-backlight"
contentWidth: 200
screen: QsWindow.window?.screen ?? null Item {
visible: _winVisible width: parent.width
color: "transparent" height: 36
property bool _winVisible: false Text {
id: blIcon
WlrLayershell.layer: WlrLayer.Overlay anchors.left: parent.left
WlrLayershell.exclusiveZone: 0 anchors.leftMargin: 12
WlrLayershell.namespace: "nova-backlight" anchors.verticalCenter: parent.verticalCenter
text: "\uF185"
anchors.top: true color: root.accentColor
anchors.left: true font.pixelSize: M.Theme.fontSize + 2
font.family: M.Theme.iconFontFamily
margins.top: 0
margins.left: Math.max(0, Math.min(Math.round(root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) - implicitWidth / 2), (panel.screen?.width ?? 1920) - implicitWidth))
implicitWidth: panelContent.width
implicitHeight: panelContent.height
Connections {
target: root
function on_ShowPanelChanged() {
if (root._showPanel) {
panel._winVisible = true;
hideAnim.stop();
showAnim.start();
} else {
showAnim.stop();
hideAnim.start();
}
} }
}
ParallelAnimation {
id: showAnim
NumberAnimation {
target: panelContent
property: "opacity"
to: 1
duration: 120
easing.type: Easing.OutCubic
}
NumberAnimation {
target: panelContent
property: "y"
to: 0
duration: 150
easing.type: Easing.OutCubic
}
}
ParallelAnimation {
id: hideAnim
NumberAnimation {
target: panelContent
property: "opacity"
to: 0
duration: 150
easing.type: Easing.InCubic
}
NumberAnimation {
target: panelContent
property: "y"
to: -panelContent.height
duration: 150
easing.type: Easing.InCubic
}
onFinished: panel._winVisible = false
}
HoverHandler {
onHoveredChanged: root._panelHovered = hovered
}
Rectangle {
x: panelContent.x
y: panelContent.y
width: panelContent.width
height: panelContent.height
color: M.Theme.base01
opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85)
topLeftRadius: 0
topRightRadius: 0
bottomLeftRadius: M.Theme.radius
bottomRightRadius: M.Theme.radius
border.color: root.accentColor
border.width: 1
}
Column {
id: panelContent
width: 200
opacity: 0
y: -height
Item { Item {
width: parent.width id: slider
height: 36 anchors.left: blIcon.right
anchors.leftMargin: 8
anchors.right: blLabel.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
height: 6
Text { Rectangle {
id: blIcon anchors.fill: parent
anchors.left: parent.left color: M.Theme.base02
anchors.leftMargin: 12 radius: 3
anchors.verticalCenter: parent.verticalCenter }
text: "\uF185"
Rectangle {
width: parent.width * root.percent / 100
height: parent.height
color: root.accentColor color: root.accentColor
font.pixelSize: M.Theme.fontSize + 2 radius: 3
font.family: M.Theme.iconFontFamily
}
Item { Behavior on width {
id: slider NumberAnimation {
anchors.left: blIcon.right duration: 80
anchors.leftMargin: 8
anchors.right: blLabel.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
height: 6
Rectangle {
anchors.fill: parent
color: M.Theme.base02
radius: 3
}
Rectangle {
width: parent.width * root.percent / 100
height: parent.height
color: root.accentColor
radius: 3
Behavior on width {
NumberAnimation {
duration: 80
}
}
}
MouseArea {
anchors.fill: parent
anchors.margins: -6
cursorShape: Qt.PointingHandCursor
onPressed: mouse => _set(mouse)
onPositionChanged: mouse => {
if (pressed)
_set(mouse);
}
function _set(mouse) {
root.setPercent(mouse.x / slider.width * 100);
} }
} }
} }
Text { MouseArea {
id: blLabel anchors.fill: parent
anchors.right: parent.right anchors.margins: -6
anchors.rightMargin: 12 cursorShape: Qt.PointingHandCursor
anchors.verticalCenter: parent.verticalCenter onPressed: mouse => _set(mouse)
text: root.percent + "%" onPositionChanged: mouse => {
color: M.Theme.base05 if (pressed)
font.pixelSize: M.Theme.fontSize _set(mouse);
font.family: M.Theme.fontFamily }
width: 30 function _set(mouse) {
root.setPercent(mouse.x / slider.width * 100);
}
} }
} }
Text {
id: blLabel
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root.percent + "%"
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
width: 30
}
} }
} }
} }

View file

@ -101,10 +101,10 @@ M.BarSection {
} }
} }
Loader { LazyLoader {
id: menuLoader id: menuLoader
active: false active: false
sourceComponent: M.BluetoothMenu { M.BluetoothMenu {
accentColor: root.accentColor accentColor: root.accentColor
screen: root.bar.screen screen: root.bar.screen
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)

View file

@ -83,16 +83,9 @@ PanelWindow {
opacity: 0 opacity: 0
y: -height y: -height
Rectangle { M.PopupBackground {
anchors.fill: parent anchors.fill: parent
color: M.Theme.base01 accentColor: M.FlyoutState.accentColor
opacity: Math.max(M.Theme.barOpacity, 0.85)
topLeftRadius: 0
topRightRadius: 0
bottomLeftRadius: M.Theme.radius
bottomRightRadius: M.Theme.radius
border.color: M.FlyoutState.accentColor
border.width: 1
} }
Text { Text {

115
modules/HoverPanel.qml Normal file
View file

@ -0,0 +1,115 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "." as M
// Shared hover/OSD panel PanelWindow slides down from the bar on hover or
// external trigger. Parent module computes showPanel and reads panelHovered.
PanelWindow {
id: root
required property bool showPanel
required property real anchorX
required property color accentColor
property string panelNamespace: "nova-panel"
property real contentWidth: 220
property bool animateHeight: false
property bool panelHovered: false
default property alias content: panelContent.children
screen: QsWindow.window?.screen ?? null
visible: _winVisible
color: "transparent"
property bool _winVisible: false
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: 0
WlrLayershell.namespace: root.panelNamespace
anchors.top: true
anchors.left: true
margins.top: 0
margins.left: Math.max(0, Math.min(Math.round(anchorX - implicitWidth / 2), (screen?.width ?? 1920) - implicitWidth))
implicitWidth: panelContent.width
implicitHeight: panelContent.height
Behavior on implicitHeight {
enabled: root.animateHeight
NumberAnimation {
duration: 100
easing.type: Easing.OutCubic
}
}
onShowPanelChanged: {
if (showPanel) {
_winVisible = true;
hideAnim.stop();
showAnim.start();
} else {
showAnim.stop();
hideAnim.start();
}
}
ParallelAnimation {
id: showAnim
NumberAnimation {
target: panelContent
property: "opacity"
to: 1
duration: 120
easing.type: Easing.OutCubic
}
NumberAnimation {
target: panelContent
property: "y"
to: 0
duration: 150
easing.type: Easing.OutCubic
}
}
ParallelAnimation {
id: hideAnim
NumberAnimation {
target: panelContent
property: "opacity"
to: 0
duration: 150
easing.type: Easing.InCubic
}
NumberAnimation {
target: panelContent
property: "y"
to: -panelContent.height
duration: 150
easing.type: Easing.InCubic
}
onFinished: root._winVisible = false
}
HoverHandler {
onHoveredChanged: root.panelHovered = hovered
}
M.PopupBackground {
x: panelContent.x
y: panelContent.y
width: panelContent.width
height: panelContent.height
opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85)
accentColor: root.accentColor
}
Column {
id: panelContent
width: root.contentWidth
opacity: 0
y: -height
}
}

View file

@ -1,7 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import "." as M import "." as M
@ -58,9 +57,8 @@ M.BarSection {
required property var bar required property var bar
property bool _panelHovered: false
property bool _pinned: false property bool _pinned: false
readonly property bool _anyHover: root._hovered || _panelHovered readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
readonly property bool _showPanel: _anyHover || _pinned readonly property bool _showPanel: _anyHover || _pinned
on_AnyHoverChanged: { on_AnyHoverChanged: {
@ -97,389 +95,290 @@ M.BarSection {
} }
} }
PanelWindow { M.HoverPanel {
id: panel id: hoverPanel
showPanel: root._showPanel
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
accentColor: root.accentColor
panelNamespace: "nova-mpris"
contentWidth: 280
animateHeight: true
screen: QsWindow.window?.screen ?? null // Album art
visible: _winVisible Item {
color: "transparent" width: parent.width
height: _artImg._hasArt ? 140 : 60
clip: true
property bool _winVisible: false Rectangle {
anchors.fill: parent
WlrLayershell.layer: WlrLayer.Overlay color: M.Theme.base02
WlrLayershell.exclusiveZone: 0
WlrLayershell.namespace: "nova-mpris"
anchors.top: true
anchors.left: true
margins.top: 0
margins.left: Math.max(0, Math.min(Math.round(root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) - implicitWidth / 2), (panel.screen?.width ?? 1920) - implicitWidth))
implicitWidth: panelContent.width
implicitHeight: panelContent.height
Behavior on implicitHeight {
NumberAnimation {
duration: 100
easing.type: Easing.OutCubic
} }
}
Connections { Image {
target: root id: _artImg
function on_ShowPanelChanged() { anchors.fill: parent
if (root._showPanel) { fillMode: Image.PreserveAspectCrop
panel._winVisible = true; visible: _hasArt
hideAnim.stop(); asynchronous: true
showAnim.start(); source: root._cachedArt
} else {
showAnim.stop();
hideAnim.start();
}
}
}
ParallelAnimation { property bool _hasArt: false
id: showAnim onStatusChanged: if (status === Image.Ready)
NumberAnimation { _hasArt = true
target: panelContent Connections {
property: "opacity" target: root
to: 1 function on_CachedArtChanged() {
duration: 120 if (!root._cachedArt)
easing.type: Easing.OutCubic _artImg._hasArt = false;
}
NumberAnimation {
target: panelContent
property: "y"
to: 0
duration: 150
easing.type: Easing.OutCubic
}
}
ParallelAnimation {
id: hideAnim
NumberAnimation {
target: panelContent
property: "opacity"
to: 0
duration: 150
easing.type: Easing.InCubic
}
NumberAnimation {
target: panelContent
property: "y"
to: -panelContent.height
duration: 150
easing.type: Easing.InCubic
}
onFinished: panel._winVisible = false
}
HoverHandler {
onHoveredChanged: root._panelHovered = hovered
}
Rectangle {
x: panelContent.x
y: panelContent.y
width: panelContent.width
height: panelContent.height
color: M.Theme.base01
opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85)
topLeftRadius: 0
topRightRadius: 0
bottomLeftRadius: M.Theme.radius
bottomRightRadius: M.Theme.radius
border.color: root.accentColor
border.width: 1
}
Column {
id: panelContent
width: 280
opacity: 0
y: -height
topPadding: 4
bottomPadding: 4
spacing: 2
// Album art
Item {
width: parent.width
height: _artImg._hasArt ? 140 : 60
clip: true
Rectangle {
anchors.fill: parent
color: M.Theme.base02
}
Image {
id: _artImg
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
visible: _hasArt
asynchronous: true
source: root._cachedArt
property bool _hasArt: false
onStatusChanged: if (status === Image.Ready)
_hasArt = true
Connections {
target: root
function on_CachedArtChanged() {
if (!root._cachedArt)
_artImg._hasArt = false;
}
} }
} }
}
// Visualizer bars // Visualizer bars
Row { Row {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
height: parent.height * 0.6 height: parent.height * 0.6
spacing: 2 spacing: 2
visible: root.playing visible: root.playing
opacity: 0.5 opacity: 0.5
Repeater { Repeater {
model: 16 model: 16
Rectangle { Rectangle {
required property int index required property int index
width: (parent.width - 15 * parent.spacing) / 16 width: (parent.width - 15 * parent.spacing) / 16
height: parent.height * (root._cavaBars[index] ?? 0) height: parent.height * (root._cavaBars[index] ?? 0)
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
color: root.accentColor color: root.accentColor
radius: 1 radius: 1
Behavior on height { Behavior on height {
NumberAnimation { NumberAnimation {
duration: 50 duration: 50
}
} }
} }
} }
} }
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 40
visible: _artImg.visible
gradient: Gradient {
GradientStop {
position: 0
color: "transparent"
}
GradientStop {
position: 1
color: M.Theme.base01
}
}
}
Text {
anchors.centerIn: parent
text: "\uF001"
color: M.Theme.base04
font.pixelSize: 28
font.family: M.Theme.iconFontFamily
visible: _artImg.status !== Image.Ready
}
}
// Track info
Item {
width: parent.width
height: titleCol.implicitHeight + 8
Column {
id: titleCol
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 2
Text {
width: parent.width
text: root.player?.trackTitle || "No track"
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize + 1
font.family: M.Theme.fontFamily
font.bold: true
elide: Text.ElideRight
}
Text {
width: parent.width
text: {
const p = root.player;
if (!p)
return "";
const artist = Array.isArray(p.trackArtists) ? p.trackArtists.join(", ") : (p.trackArtists || "");
return [artist, p.trackAlbum].filter(s => s).join(" \u2014 ");
}
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 1
font.family: M.Theme.fontFamily
elide: Text.ElideRight
visible: text !== ""
}
}
}
// Progress
Item {
width: parent.width
height: 20
readonly property real pos: root.player?.position ?? 0
readonly property real dur: root.player?.length ?? 0
readonly property real frac: dur > 0 ? pos / dur : 0
function _fmtTime(ms) {
const s = Math.floor(ms / 1000);
const m = Math.floor(s / 60);
return m + ":" + String(s % 60).padStart(2, "0");
}
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: parent._fmtTime(parent.pos)
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: parent._fmtTime(parent.dur)
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
}
Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 80
height: 4
Rectangle { Rectangle {
anchors.left: parent.left anchors.fill: parent
anchors.right: parent.right color: M.Theme.base02
anchors.bottom: parent.bottom radius: 2
height: 40
visible: _artImg.visible
gradient: Gradient {
GradientStop {
position: 0
color: "transparent"
}
GradientStop {
position: 1
color: M.Theme.base01
}
}
} }
Rectangle {
Text { width: parent.width * Math.min(1, Math.max(0, parent.parent.frac))
anchors.centerIn: parent height: parent.height
text: "\uF001" color: root.accentColor
color: M.Theme.base04 radius: 2
font.pixelSize: 28
font.family: M.Theme.iconFontFamily
visible: _artImg.status !== Image.Ready
} }
} }
}
// Track info // Transport controls
Item { Item {
width: parent.width width: parent.width
height: titleCol.implicitHeight + 8 height: 36
Column {
id: titleCol
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 2
Text {
width: parent.width
text: root.player?.trackTitle || "No track"
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize + 1
font.family: M.Theme.fontFamily
font.bold: true
elide: Text.ElideRight
}
Text {
width: parent.width
text: {
const p = root.player;
if (!p)
return "";
const artist = Array.isArray(p.trackArtists) ? p.trackArtists.join(", ") : (p.trackArtists || "");
return [artist, p.trackAlbum].filter(s => s).join(" \u2014 ");
}
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 1
font.family: M.Theme.fontFamily
elide: Text.ElideRight
visible: text !== ""
}
}
}
// Progress
Item {
width: parent.width
height: 20
readonly property real pos: root.player?.position ?? 0
readonly property real dur: root.player?.length ?? 0
readonly property real frac: dur > 0 ? pos / dur : 0
function _fmtTime(ms) {
const s = Math.floor(ms / 1000);
const m = Math.floor(s / 60);
return m + ":" + String(s % 60).padStart(2, "0");
}
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: parent._fmtTime(parent.pos)
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: parent._fmtTime(parent.dur)
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
}
Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 80
height: 4
Rectangle {
anchors.fill: parent
color: M.Theme.base02
radius: 2
}
Rectangle {
width: parent.width * Math.min(1, Math.max(0, parent.parent.frac))
height: parent.height
color: root.accentColor
radius: 2
}
}
}
// Transport controls
Item {
width: parent.width
height: 36
Row {
anchors.centerIn: parent
spacing: 24
Text {
text: "\uF048"
color: root.player?.canGoPrevious ? M.Theme.base05 : M.Theme.base03
font.pixelSize: M.Theme.fontSize + 4
font.family: M.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
enabled: root.player?.canGoPrevious ?? false
onClicked: root.player.previous()
}
}
Text {
text: root.playing ? "\uF04C" : "\uF04B"
color: root.accentColor
font.pixelSize: M.Theme.fontSize + 8
font.family: M.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.player?.togglePlaying()
}
}
Text {
text: "\uF051"
color: root.player?.canGoNext ? M.Theme.base05 : M.Theme.base03
font.pixelSize: M.Theme.fontSize + 4
font.family: M.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
enabled: root.player?.canGoNext ?? false
onClicked: root.player.next()
}
}
}
}
// Player switcher
Row { Row {
anchors.horizontalCenter: parent.horizontalCenter anchors.centerIn: parent
height: 22 spacing: 24
spacing: 6
Repeater { Text {
model: root._players text: "\uF048"
color: root.player?.canGoPrevious ? M.Theme.base05 : M.Theme.base03
font.pixelSize: M.Theme.fontSize + 4
font.family: M.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
enabled: root.player?.canGoPrevious ?? false
onClicked: root.player.previous()
}
}
delegate: Rectangle { Text {
required property var modelData text: root.playing ? "\uF04C" : "\uF04B"
required property int index color: root.accentColor
font.pixelSize: M.Theme.fontSize + 8
font.family: M.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.player?.togglePlaying()
}
}
readonly property bool _active: index === root._playerIdx Text {
text: "\uF051"
color: root.player?.canGoNext ? M.Theme.base05 : M.Theme.base03
font.pixelSize: M.Theme.fontSize + 4
font.family: M.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
enabled: root.player?.canGoNext ?? false
onClicked: root.player.next()
}
}
}
}
width: _pLabel.implicitWidth + 12 // Player switcher
height: 18 Row {
radius: 9 anchors.horizontalCenter: parent.horizontalCenter
color: _active ? M.Theme.base02 : (pArea.containsMouse ? M.Theme.base02 : "transparent") height: 22
border.color: _active ? root.accentColor : M.Theme.base03 spacing: 6
border.width: _active ? 1 : 0
anchors.verticalCenter: parent.verticalCenter
Text { Repeater {
id: _pLabel model: root._players
anchors.centerIn: parent
text: modelData.identity ?? "Player"
color: _active ? root.accentColor : M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
font.bold: _active
}
MouseArea { delegate: Rectangle {
id: pArea required property var modelData
anchors.fill: parent required property int index
hoverEnabled: true
cursorShape: Qt.PointingHandCursor readonly property bool _active: index === root._playerIdx
onClicked: root._playerIdx = index
} width: _pLabel.implicitWidth + 12
height: 18
radius: 9
color: _active ? M.Theme.base02 : (pArea.containsMouse ? M.Theme.base02 : "transparent")
border.color: _active ? root.accentColor : M.Theme.base03
border.width: _active ? 1 : 0
anchors.verticalCenter: parent.verticalCenter
Text {
id: _pLabel
anchors.centerIn: parent
text: modelData.identity ?? "Player"
color: _active ? root.accentColor : M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
font.bold: _active
}
MouseArea {
id: pArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root._playerIdx = index
} }
} }
} }

View file

@ -121,10 +121,10 @@ M.BarSection {
} }
} }
Loader { LazyLoader {
id: menuLoader id: menuLoader
active: false active: false
sourceComponent: M.NetworkMenu { M.NetworkMenu {
accentColor: root.accentColor accentColor: root.accentColor
screen: root.bar.screen screen: root.bar.screen
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)

View file

@ -77,10 +77,10 @@ M.BarSection {
onTapped: M.NotifService.toggleDnd() onTapped: M.NotifService.toggleDnd()
} }
Loader { LazyLoader {
id: centerLoader id: centerLoader
active: false active: false
sourceComponent: M.NotifCenter { M.NotifCenter {
accentColor: root.accentColor accentColor: root.accentColor
screen: root.bar.screen screen: root.bar.screen
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)

View file

@ -0,0 +1,15 @@
import QtQuick
import "." as M
Rectangle {
property color accentColor: M.Theme.base05
color: M.Theme.base01
opacity: Math.max(M.Theme.barOpacity, 0.85)
topLeftRadius: 0
topRightRadius: 0
bottomLeftRadius: M.Theme.radius
bottomRightRadius: M.Theme.radius
border.color: accentColor
border.width: 1
}

View file

@ -56,16 +56,9 @@ PanelWindow {
anchors.fill: parent anchors.fill: parent
} }
Rectangle { M.PopupBackground {
anchors.fill: parent anchors.fill: parent
color: M.Theme.base01 accentColor: root.accentColor
opacity: Math.max(M.Theme.barOpacity, 0.85)
topLeftRadius: 0
topRightRadius: 0
bottomLeftRadius: M.Theme.radius
bottomRightRadius: M.Theme.radius
border.color: root.accentColor
border.width: 1
} }
Column { Column {

View file

@ -23,11 +23,11 @@ M.BarIcon {
} }
} }
Loader { LazyLoader {
id: menuLoader id: menuLoader
active: false active: false
sourceComponent: M.PowerMenu { M.PowerMenu {
accentColor: parent?.accentColor ?? root.color accentColor: root.accentColor
screen: root.bar.screen screen: root.bar.screen
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
onDismissed: menuLoader.active = false onDismissed: menuLoader.active = false

View file

@ -105,10 +105,10 @@ RowLayout {
} }
// Per-icon context menu window, created on demand // Per-icon context menu window, created on demand
Loader { LazyLoader {
id: menuLoader id: menuLoader
active: false active: false
sourceComponent: M.TrayMenu { M.TrayMenu {
accentColor: root.parent?.accentColor ?? M.Theme.base05 accentColor: root.parent?.accentColor ?? M.Theme.base05
handle: iconItem.modelData.menu handle: iconItem.modelData.menu
screen: root.bar.screen screen: root.bar.screen

View file

@ -1,6 +1,5 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import "." as M import "." as M
@ -40,11 +39,13 @@ M.BarSection {
} }
property bool _expanded: false property bool _expanded: false
property bool _panelHovered: false
property bool _osdActive: false property bool _osdActive: false
readonly property bool _anyHover: root._hovered || _panelHovered readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
readonly property bool _showPanel: _anyHover || _expanded || _osdActive readonly property bool _showPanel: _anyHover || _expanded || _osdActive
on_ShowPanelChanged: if (!_showPanel)
_expanded = false
onVolumeChanged: _flashPanel() onVolumeChanged: _flashPanel()
onMutedChanged: _flashPanel() onMutedChanged: _flashPanel()
@ -104,415 +105,320 @@ M.BarSection {
} }
// Unified volume panel hover shows slider, click expands to show devices // Unified volume panel hover shows slider, click expands to show devices
PanelWindow { M.HoverPanel {
id: panel id: hoverPanel
showPanel: root._showPanel
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
accentColor: root.accentColor
panelNamespace: "nova-volume"
contentWidth: 220
screen: QsWindow.window?.screen ?? null // Compact: slider row
visible: _winVisible Item {
color: "transparent" width: parent.width
height: 36
property bool _winVisible: false // Mute toggle
Text {
id: muteIcon
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root._volumeIcon
color: root._volumeColor
font.pixelSize: M.Theme.fontSize + 2
font.family: M.Theme.iconFontFamily
WlrLayershell.layer: WlrLayer.Overlay MouseArea {
WlrLayershell.exclusiveZone: 0 anchors.fill: parent
WlrLayershell.namespace: "nova-volume" cursorShape: Qt.PointingHandCursor
onClicked: if (root.sink?.audio)
anchors.top: true root.sink.audio.muted = !root.sink.audio.muted
anchors.left: true
margins.top: 0
margins.left: Math.max(0, Math.min(Math.round(root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) - implicitWidth / 2), (panel.screen?.width ?? 1920) - implicitWidth))
implicitWidth: panelContent.width
implicitHeight: panelContent.height
// Show/hide logic
Connections {
target: root
function on_ShowPanelChanged() {
if (root._showPanel) {
panel._winVisible = true;
hideAnim.stop();
showAnim.start();
} else {
root._expanded = false;
showAnim.stop();
hideAnim.start();
} }
} }
}
ParallelAnimation { // Slider
id: showAnim
NumberAnimation {
target: panelContent
property: "opacity"
to: 1
duration: 120
easing.type: Easing.OutCubic
}
NumberAnimation {
target: panelContent
property: "y"
to: 0
duration: 150
easing.type: Easing.OutCubic
}
}
ParallelAnimation {
id: hideAnim
NumberAnimation {
target: panelContent
property: "opacity"
to: 0
duration: 150
easing.type: Easing.InCubic
}
NumberAnimation {
target: panelContent
property: "y"
to: -panelContent.height
duration: 150
easing.type: Easing.InCubic
}
onFinished: panel._winVisible = false
}
// Keep panel open when mouse is over it
HoverHandler {
onHoveredChanged: root._panelHovered = hovered
// Click inside panel doesn't dismiss
}
Rectangle {
x: panelContent.x
y: panelContent.y
width: panelContent.width
height: panelContent.height
color: M.Theme.base01
opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85)
topLeftRadius: 0
topRightRadius: 0
bottomLeftRadius: M.Theme.radius
bottomRightRadius: M.Theme.radius
border.color: root.accentColor
border.width: 1
}
Column {
id: panelContent
width: 220
opacity: 0
y: -height
// Compact: slider row
Item { Item {
width: parent.width id: slider
height: 36 anchors.left: muteIcon.right
anchors.leftMargin: 8
anchors.right: volLabel.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
height: 6
// Mute toggle Rectangle {
Text { anchors.fill: parent
id: muteIcon color: M.Theme.base02
anchors.left: parent.left radius: 3
anchors.leftMargin: 12 }
anchors.verticalCenter: parent.verticalCenter
text: root._volumeIcon Rectangle {
width: parent.width * Math.min(1, Math.max(0, root.volume))
height: parent.height
color: root._volumeColor color: root._volumeColor
font.pixelSize: M.Theme.fontSize + 2 radius: 3
font.family: M.Theme.iconFontFamily
MouseArea { Behavior on width {
anchors.fill: parent NumberAnimation {
cursorShape: Qt.PointingHandCursor duration: 80
onClicked: if (root.sink?.audio)
root.sink.audio.muted = !root.sink.audio.muted
}
}
// Slider
Item {
id: slider
anchors.left: muteIcon.right
anchors.leftMargin: 8
anchors.right: volLabel.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
height: 6
Rectangle {
anchors.fill: parent
color: M.Theme.base02
radius: 3
}
Rectangle {
width: parent.width * Math.min(1, Math.max(0, root.volume))
height: parent.height
color: root._volumeColor
radius: 3
Behavior on width {
NumberAnimation {
duration: 80
}
}
}
MouseArea {
anchors.fill: parent
anchors.margins: -6
cursorShape: Qt.PointingHandCursor
onPressed: mouse => _setVol(mouse)
onPositionChanged: mouse => {
if (pressed)
_setVol(mouse);
}
function _setVol(mouse) {
if (!root.sink?.audio)
return;
root.sink.audio.volume = Math.max(0, Math.min(1, mouse.x / slider.width));
} }
} }
} }
Text { MouseArea {
id: volLabel anchors.fill: parent
anchors.right: parent.right anchors.margins: -6
anchors.rightMargin: 12 cursorShape: Qt.PointingHandCursor
anchors.verticalCenter: parent.verticalCenter onPressed: mouse => _setVol(mouse)
text: Math.round(root.volume * 100) + "%" onPositionChanged: mouse => {
color: root.muted ? M.Theme.base04 : M.Theme.base05 if (pressed)
font.pixelSize: M.Theme.fontSize _setVol(mouse);
font.family: M.Theme.fontFamily }
width: 30 function _setVol(mouse) {
if (!root.sink?.audio)
return;
root.sink.audio.volume = Math.max(0, Math.min(1, mouse.x / slider.width));
}
} }
} }
// Sink name Text {
id: volLabel
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: Math.round(root.volume * 100) + "%"
color: root.muted ? M.Theme.base04 : M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
width: 30
}
}
// Sink name
Text {
width: parent.width
height: 18
horizontalAlignment: Text.AlignHCenter
text: root.sink?.description ?? root.sink?.name ?? ""
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
elide: Text.ElideRight
leftPadding: 12
rightPadding: 12
}
// Expanded: output device list
Column {
id: deviceList
width: parent.width
visible: root._expanded
clip: true
property real _targetHeight: root._expanded ? implicitHeight : 0
height: _targetHeight
Behavior on height {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
// Separator
Rectangle {
width: parent.width - 16
height: 1
anchors.horizontalCenter: parent.horizontalCenter
color: M.Theme.base03
}
// Header
Text { Text {
width: parent.width width: parent.width
height: 18 height: 24
horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter
text: root.sink?.description ?? root.sink?.name ?? ""
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
elide: Text.ElideRight
leftPadding: 12 leftPadding: 12
rightPadding: 12 text: "Output Devices"
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 1
font.family: M.Theme.fontFamily
} }
// Expanded: output device list Repeater {
Column { model: root._sinkList
id: deviceList
width: parent.width
visible: root._expanded
clip: true
property real _targetHeight: root._expanded ? implicitHeight : 0 delegate: Item {
height: _targetHeight required property var modelData
Behavior on height {
NumberAnimation { width: deviceList.width
duration: 200 height: 28
easing.type: Easing.OutCubic
readonly property bool _active: modelData === root.sink
Rectangle {
anchors.fill: parent
anchors.leftMargin: 4
anchors.rightMargin: 4
color: deviceArea.containsMouse ? M.Theme.base02 : "transparent"
radius: M.Theme.radius
}
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: modelData.description || modelData.name || "Unknown"
color: parent._active ? root.accentColor : M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
font.bold: parent._active
elide: Text.ElideRight
}
MouseArea {
id: deviceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Pipewire.preferredDefaultAudioSink = modelData
} }
} }
}
// Streams header (only if there are streams)
Item {
visible: root._streamList.length > 0
width: parent.width
height: visible ? streamSep.height + streamHeader.height : 0
// Separator
Rectangle { Rectangle {
id: streamSep
width: parent.width - 16 width: parent.width - 16
height: 1 height: 1
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: M.Theme.base03 color: M.Theme.base03
} }
// Header
Text { Text {
id: streamHeader
anchors.top: streamSep.bottom
width: parent.width width: parent.width
height: 24 height: 24
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
leftPadding: 12 leftPadding: 12
text: "Output Devices" text: "Applications"
color: M.Theme.base04 color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 1 font.pixelSize: M.Theme.fontSize - 1
font.family: M.Theme.fontFamily font.family: M.Theme.fontFamily
} }
}
Repeater { Repeater {
model: root._sinkList model: root._streamList
delegate: Item { delegate: Item {
required property var modelData id: streamEntry
required property var modelData
width: deviceList.width width: deviceList.width
height: 28 height: 32
readonly property bool _active: modelData === root.sink readonly property string _appName: modelData.properties["application.name"] || modelData.description || modelData.name || "Unknown"
readonly property real _vol: modelData.audio?.volume ?? 0
readonly property bool _muted: modelData.audio?.muted ?? false
Rectangle { Text {
anchors.fill: parent id: streamIcon
anchors.leftMargin: 4 anchors.left: parent.left
anchors.rightMargin: 4 anchors.leftMargin: 12
color: deviceArea.containsMouse ? M.Theme.base02 : "transparent" anchors.verticalCenter: parent.verticalCenter
radius: M.Theme.radius text: streamEntry._muted ? "\uF026" : "\uF028"
} color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05
font.pixelSize: M.Theme.fontSize
Text { font.family: M.Theme.iconFontFamily
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: modelData.description || modelData.name || "Unknown"
color: parent._active ? root.accentColor : M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
font.bold: parent._active
elide: Text.ElideRight
}
MouseArea { MouseArea {
id: deviceArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: Pipewire.preferredDefaultAudioSink = modelData onClicked: if (streamEntry.modelData.audio)
streamEntry.modelData.audio.muted = !streamEntry.modelData.audio.muted
} }
} }
}
// Streams header (only if there are streams)
Item {
visible: root._streamList.length > 0
width: parent.width
height: visible ? streamSep.height + streamHeader.height : 0
Rectangle {
id: streamSep
width: parent.width - 16
height: 1
anchors.horizontalCenter: parent.horizontalCenter
color: M.Theme.base03
}
Text { Text {
id: streamHeader id: streamName
anchors.top: streamSep.bottom anchors.left: streamIcon.right
width: parent.width anchors.leftMargin: 6
height: 24 anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter text: streamEntry._appName
leftPadding: 12 color: M.Theme.base05
text: "Applications"
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 1 font.pixelSize: M.Theme.fontSize - 1
font.family: M.Theme.fontFamily font.family: M.Theme.fontFamily
elide: Text.ElideRight
width: 70
} }
}
Repeater { Item {
model: root._streamList id: streamSlider
anchors.left: streamName.right
anchors.leftMargin: 6
anchors.right: streamVol.left
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
height: 4
delegate: Item { Rectangle {
id: streamEntry anchors.fill: parent
required property var modelData color: M.Theme.base02
radius: 2
width: deviceList.width }
height: 32 Rectangle {
width: parent.width * Math.min(1, Math.max(0, streamEntry._vol))
readonly property string _appName: modelData.properties["application.name"] || modelData.description || modelData.name || "Unknown" height: parent.height
readonly property real _vol: modelData.audio?.volume ?? 0 color: streamEntry._muted ? M.Theme.base04 : root.accentColor
readonly property bool _muted: modelData.audio?.muted ?? false radius: 2
Text {
id: streamIcon
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: streamEntry._muted ? "\uF026" : "\uF028"
color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.iconFontFamily
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: if (streamEntry.modelData.audio)
streamEntry.modelData.audio.muted = !streamEntry.modelData.audio.muted
}
} }
Text { MouseArea {
id: streamName anchors.fill: parent
anchors.left: streamIcon.right anchors.margins: -6
anchors.leftMargin: 6 cursorShape: Qt.PointingHandCursor
anchors.verticalCenter: parent.verticalCenter onPressed: mouse => _set(mouse)
text: streamEntry._appName onPositionChanged: mouse => {
color: M.Theme.base05 if (pressed)
font.pixelSize: M.Theme.fontSize - 1 _set(mouse);
font.family: M.Theme.fontFamily
elide: Text.ElideRight
width: 70
}
Item {
id: streamSlider
anchors.left: streamName.right
anchors.leftMargin: 6
anchors.right: streamVol.left
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
height: 4
Rectangle {
anchors.fill: parent
color: M.Theme.base02
radius: 2
} }
Rectangle { function _set(mouse) {
width: parent.width * Math.min(1, Math.max(0, streamEntry._vol)) if (!streamEntry.modelData.audio)
height: parent.height return;
color: streamEntry._muted ? M.Theme.base04 : root.accentColor streamEntry.modelData.audio.volume = Math.max(0, Math.min(1, mouse.x / streamSlider.width));
radius: 2
} }
MouseArea {
anchors.fill: parent
anchors.margins: -6
cursorShape: Qt.PointingHandCursor
onPressed: mouse => _set(mouse)
onPositionChanged: mouse => {
if (pressed)
_set(mouse);
}
function _set(mouse) {
if (!streamEntry.modelData.audio)
return;
streamEntry.modelData.audio.volume = Math.max(0, Math.min(1, mouse.x / streamSlider.width));
}
}
}
Text {
id: streamVol
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: Math.round(streamEntry._vol * 100) + "%"
color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05
font.pixelSize: M.Theme.fontSize - 1
font.family: M.Theme.fontFamily
width: 28
} }
} }
}
// Bottom padding Text {
Item { id: streamVol
width: 1 anchors.right: parent.right
height: 4 anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: Math.round(streamEntry._vol * 100) + "%"
color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05
font.pixelSize: M.Theme.fontSize - 1
font.family: M.Theme.fontFamily
width: 28
}
} }
} }
// Bottom padding
Item {
width: 1
height: 4
}
} }
} }
} }

View file

@ -13,6 +13,8 @@ Volume 1.0 Volume.qml
Tray 1.0 Tray.qml Tray 1.0 Tray.qml
TrayMenu 1.0 TrayMenu.qml TrayMenu 1.0 TrayMenu.qml
PopupPanel 1.0 PopupPanel.qml PopupPanel 1.0 PopupPanel.qml
PopupBackground 1.0 PopupBackground.qml
HoverPanel 1.0 HoverPanel.qml
PowerMenu 1.0 PowerMenu.qml PowerMenu 1.0 PowerMenu.qml
ScreenCorners 1.0 ScreenCorners.qml ScreenCorners 1.0 ScreenCorners.qml
ThemedIcon 1.0 ThemedIcon.qml ThemedIcon 1.0 ThemedIcon.qml