Compare commits

..

No commits in common. "0fd3c78fb035f10e0ec5bf353d9a7ddba95b8f82" and "c5067c4e7ffabf719c204ba4137bd11ba14aa68d" have entirely different histories.

13 changed files with 896 additions and 726 deletions

View file

@ -1,6 +1,7 @@
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 {
@ -11,8 +12,9 @@ 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 || hoverPanel.panelHovered || _osdActive readonly property bool _showPanel: root._hovered || _panelHovered || _osdActive
onPercentChanged: if (percent > 0) onPercentChanged: if (percent > 0)
_flashPanel() _flashPanel()
@ -95,83 +97,174 @@ M.BarSection {
onWheel: event => root.adjust(event.angleDelta.y) onWheel: event => root.adjust(event.angleDelta.y)
} }
M.HoverPanel { PanelWindow {
id: hoverPanel id: panel
showPanel: root._showPanel
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
accentColor: root.accentColor
panelNamespace: "nova-backlight"
contentWidth: 200
Item { screen: QsWindow.window?.screen ?? null
width: parent.width visible: _winVisible
height: 36 color: "transparent"
Text { property bool _winVisible: false
id: blIcon
anchors.left: parent.left WlrLayershell.layer: WlrLayer.Overlay
anchors.leftMargin: 12 WlrLayershell.exclusiveZone: 0
anchors.verticalCenter: parent.verticalCenter WlrLayershell.namespace: "nova-backlight"
text: "\uF185"
color: root.accentColor anchors.top: true
font.pixelSize: M.Theme.fontSize + 2 anchors.left: true
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 {
id: slider width: parent.width
anchors.left: blIcon.right height: 36
anchors.leftMargin: 8
anchors.right: blLabel.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
height: 6
Rectangle { Text {
anchors.fill: parent id: blIcon
color: M.Theme.base02 anchors.left: parent.left
radius: 3 anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "\uF185"
color: root.accentColor
font.pixelSize: M.Theme.fontSize + 2
font.family: M.Theme.iconFontFamily
} }
Rectangle { Item {
width: parent.width * root.percent / 100 id: slider
height: parent.height anchors.left: blIcon.right
color: root.accentColor anchors.leftMargin: 8
radius: 3 anchors.right: blLabel.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
height: 6
Behavior on width { Rectangle {
NumberAnimation { anchors.fill: parent
duration: 80 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);
} }
} }
} }
MouseArea { Text {
anchors.fill: parent id: blLabel
anchors.margins: -6 anchors.right: parent.right
cursorShape: Qt.PointingHandCursor anchors.rightMargin: 12
onPressed: mouse => _set(mouse) anchors.verticalCenter: parent.verticalCenter
onPositionChanged: mouse => { text: root.percent + "%"
if (pressed) color: M.Theme.base05
_set(mouse); font.pixelSize: M.Theme.fontSize
} font.family: M.Theme.fontFamily
function _set(mouse) { width: 30
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 {
} }
} }
LazyLoader { Loader {
id: menuLoader id: menuLoader
active: false active: false
M.BluetoothMenu { sourceComponent: 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,9 +83,16 @@ PanelWindow {
opacity: 0 opacity: 0
y: -height y: -height
M.PopupBackground { Rectangle {
anchors.fill: parent anchors.fill: parent
accentColor: M.FlyoutState.accentColor 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: M.FlyoutState.accentColor
border.width: 1
} }
Text { Text {

View file

@ -1,115 +0,0 @@
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,6 +1,7 @@
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
@ -57,8 +58,9 @@ 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 || hoverPanel.panelHovered readonly property bool _anyHover: root._hovered || _panelHovered
readonly property bool _showPanel: _anyHover || _pinned readonly property bool _showPanel: _anyHover || _pinned
on_AnyHoverChanged: { on_AnyHoverChanged: {
@ -95,290 +97,389 @@ M.BarSection {
} }
} }
M.HoverPanel { PanelWindow {
id: hoverPanel id: panel
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
// Album art screen: QsWindow.window?.screen ?? null
Item { visible: _winVisible
width: parent.width color: "transparent"
height: _artImg._hasArt ? 140 : 60
clip: true
Rectangle { property bool _winVisible: false
anchors.fill: parent
color: M.Theme.base02 WlrLayershell.layer: WlrLayer.Overlay
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 {
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: 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
Row {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: parent.height * 0.6
spacing: 2
visible: root.playing
opacity: 0.5
Repeater {
model: 16
Rectangle {
required property int index
width: (parent.width - 15 * parent.spacing) / 16
height: parent.height * (root._cavaBars[index] ?? 0)
anchors.bottom: parent.bottom
color: root.accentColor
radius: 1
Behavior on height {
NumberAnimation {
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
}
} }
Image { // Track info
id: _artImg Item {
anchors.fill: parent width: parent.width
fillMode: Image.PreserveAspectCrop height: titleCol.implicitHeight + 8
visible: _hasArt
asynchronous: true
source: root._cachedArt
property bool _hasArt: false Column {
onStatusChanged: if (status === Image.Ready) id: titleCol
_hasArt = true anchors.left: parent.left
Connections { anchors.right: parent.right
target: root anchors.verticalCenter: parent.verticalCenter
function on_CachedArtChanged() { anchors.leftMargin: 12
if (!root._cachedArt) anchors.rightMargin: 12
_artImg._hasArt = false; 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 !== ""
} }
} }
} }
// Visualizer bars // Progress
Row { Item {
anchors.left: parent.left width: parent.width
anchors.right: parent.right height: 20
anchors.bottom: parent.bottom
height: parent.height * 0.6 readonly property real pos: root.player?.position ?? 0
spacing: 2 readonly property real dur: root.player?.length ?? 0
visible: root.playing readonly property real frac: dur > 0 ? pos / dur : 0
opacity: 0.5
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
Repeater {
model: 16
Rectangle { Rectangle {
required property int index anchors.fill: parent
width: (parent.width - 15 * parent.spacing) / 16 color: M.Theme.base02
height: parent.height * (root._cavaBars[index] ?? 0) radius: 2
anchors.bottom: parent.bottom }
Rectangle {
width: parent.width * Math.min(1, Math.max(0, parent.parent.frac))
height: parent.height
color: root.accentColor color: root.accentColor
radius: 1 radius: 2
}
}
}
Behavior on height { // Transport controls
NumberAnimation { Item {
duration: 50 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()
} }
} }
} }
} }
Rectangle { // Player switcher
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 {
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 { Row {
anchors.centerIn: parent anchors.horizontalCenter: parent.horizontalCenter
spacing: 24 height: 22
spacing: 6
Text { Repeater {
text: "\uF048" model: root._players
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 { delegate: Rectangle {
text: root.playing ? "\uF04C" : "\uF04B" required property var modelData
color: root.accentColor required property int index
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 { readonly property bool _active: index === root._playerIdx
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 width: _pLabel.implicitWidth + 12
Row { height: 18
anchors.horizontalCenter: parent.horizontalCenter radius: 9
height: 22 color: _active ? M.Theme.base02 : (pArea.containsMouse ? M.Theme.base02 : "transparent")
spacing: 6 border.color: _active ? root.accentColor : M.Theme.base03
border.width: _active ? 1 : 0
anchors.verticalCenter: parent.verticalCenter
Repeater { Text {
model: root._players 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
}
delegate: Rectangle { MouseArea {
required property var modelData id: pArea
required property int index anchors.fill: parent
hoverEnabled: true
readonly property bool _active: index === root._playerIdx cursorShape: Qt.PointingHandCursor
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 {
} }
} }
LazyLoader { Loader {
id: menuLoader id: menuLoader
active: false active: false
M.NetworkMenu { sourceComponent: 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()
} }
LazyLoader { Loader {
id: centerLoader id: centerLoader
active: false active: false
M.NotifCenter { sourceComponent: 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

@ -1,15 +0,0 @@
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,9 +56,16 @@ PanelWindow {
anchors.fill: parent anchors.fill: parent
} }
M.PopupBackground { Rectangle {
anchors.fill: parent anchors.fill: parent
accentColor: root.accentColor 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: root.accentColor
border.width: 1
} }
Column { Column {

View file

@ -23,11 +23,11 @@ M.BarIcon {
} }
} }
LazyLoader { Loader {
id: menuLoader id: menuLoader
active: false active: false
M.PowerMenu { sourceComponent: M.PowerMenu {
accentColor: root.accentColor accentColor: parent?.accentColor ?? root.color
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
LazyLoader { Loader {
id: menuLoader id: menuLoader
active: false active: false
M.TrayMenu { sourceComponent: 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,5 +1,6 @@
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
@ -39,13 +40,11 @@ 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 || hoverPanel.panelHovered readonly property bool _anyHover: root._hovered || _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()
@ -105,319 +104,414 @@ M.BarSection {
} }
// Unified volume panel hover shows slider, click expands to show devices // Unified volume panel hover shows slider, click expands to show devices
M.HoverPanel { PanelWindow {
id: hoverPanel id: panel
showPanel: root._showPanel
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
accentColor: root.accentColor
panelNamespace: "nova-volume"
contentWidth: 220
// Compact: slider row screen: QsWindow.window?.screen ?? null
Item { visible: _winVisible
width: parent.width color: "transparent"
height: 36
// Mute toggle property bool _winVisible: false
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
MouseArea { WlrLayershell.layer: WlrLayer.Overlay
anchors.fill: parent WlrLayershell.exclusiveZone: 0
cursorShape: Qt.PointingHandCursor WlrLayershell.namespace: "nova-volume"
onClicked: if (root.sink?.audio)
root.sink.audio.muted = !root.sink.audio.muted 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
// 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();
} }
} }
}
// Slider 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
}
// 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 {
id: slider width: parent.width
anchors.left: muteIcon.right height: 36
anchors.leftMargin: 8
anchors.right: volLabel.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
height: 6
Rectangle { // Mute toggle
anchors.fill: parent Text {
color: M.Theme.base02 id: muteIcon
radius: 3 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
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: if (root.sink?.audio)
root.sink.audio.muted = !root.sink.audio.muted
}
} }
Rectangle { // Slider
width: parent.width * Math.min(1, Math.max(0, root.volume)) Item {
height: parent.height id: slider
color: root._volumeColor anchors.left: muteIcon.right
radius: 3 anchors.leftMargin: 8
anchors.right: volLabel.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
height: 6
Behavior on width { Rectangle {
NumberAnimation { anchors.fill: parent
duration: 80 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));
} }
} }
} }
MouseArea { Text {
anchors.fill: parent id: volLabel
anchors.margins: -6 anchors.right: parent.right
cursorShape: Qt.PointingHandCursor anchors.rightMargin: 12
onPressed: mouse => _setVol(mouse) anchors.verticalCenter: parent.verticalCenter
onPositionChanged: mouse => { text: Math.round(root.volume * 100) + "%"
if (pressed) color: root.muted ? M.Theme.base04 : M.Theme.base05
_setVol(mouse); font.pixelSize: M.Theme.fontSize
} font.family: M.Theme.fontFamily
function _setVol(mouse) { width: 30
if (!root.sink?.audio)
return;
root.sink.audio.volume = Math.max(0, Math.min(1, mouse.x / slider.width));
}
} }
} }
Text { // Sink name
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: 24 height: 18
verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter
leftPadding: 12 text: root.sink?.description ?? root.sink?.name ?? ""
text: "Output Devices"
color: M.Theme.base04 color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 1 font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily font.family: M.Theme.fontFamily
elide: Text.ElideRight
leftPadding: 12
rightPadding: 12
} }
Repeater { // Expanded: output device list
model: root._sinkList Column {
id: deviceList
width: parent.width
visible: root._expanded
clip: true
delegate: Item { property real _targetHeight: root._expanded ? implicitHeight : 0
required property var modelData height: _targetHeight
Behavior on height {
width: deviceList.width NumberAnimation {
height: 28 duration: 200
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: "Applications" text: "Output Devices"
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._streamList model: root._sinkList
delegate: Item { delegate: Item {
id: streamEntry required property var modelData
required property var modelData
width: deviceList.width width: deviceList.width
height: 32 height: 28
readonly property string _appName: modelData.properties["application.name"] || modelData.description || modelData.name || "Unknown" readonly property bool _active: modelData === root.sink
readonly property real _vol: modelData.audio?.volume ?? 0
readonly property bool _muted: modelData.audio?.muted ?? false
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 {
id: streamName
anchors.left: streamIcon.right
anchors.leftMargin: 6
anchors.verticalCenter: parent.verticalCenter
text: streamEntry._appName
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize - 1
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 { Rectangle {
anchors.fill: parent anchors.fill: parent
color: M.Theme.base02 anchors.leftMargin: 4
radius: 2 anchors.rightMargin: 4
color: deviceArea.containsMouse ? M.Theme.base02 : "transparent"
radius: M.Theme.radius
} }
Rectangle {
width: parent.width * Math.min(1, Math.max(0, streamEntry._vol)) Text {
height: parent.height anchors.left: parent.left
color: streamEntry._muted ? M.Theme.base04 : root.accentColor anchors.leftMargin: 12
radius: 2 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
anchors.margins: -6 hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => _set(mouse) onClicked: Pipewire.preferredDefaultAudioSink = modelData
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 // Streams header (only if there are streams)
Item { Item {
width: 1 visible: root._streamList.length > 0
height: 4 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 {
id: streamHeader
anchors.top: streamSep.bottom
width: parent.width
height: 24
verticalAlignment: Text.AlignVCenter
leftPadding: 12
text: "Applications"
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 1
font.family: M.Theme.fontFamily
}
}
Repeater {
model: root._streamList
delegate: Item {
id: streamEntry
required property var modelData
width: deviceList.width
height: 32
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
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 {
id: streamName
anchors.left: streamIcon.right
anchors.leftMargin: 6
anchors.verticalCenter: parent.verticalCenter
text: streamEntry._appName
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize - 1
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 {
width: parent.width * Math.min(1, Math.max(0, streamEntry._vol))
height: parent.height
color: streamEntry._muted ? M.Theme.base04 : root.accentColor
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
Item {
width: 1
height: 4
}
} }
} }
} }

View file

@ -13,8 +13,6 @@ 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