improvements

This commit is contained in:
2026-08-11 17:35:26 +02:00
parent 39f925cb58
commit a868f50ab7
24 changed files with 924 additions and 239 deletions
+72
View File
@@ -0,0 +1,72 @@
import QtQuick
import "root:/config"
import "root:/components"
import "root:/services"
// Secondary input band with an explicit mute action.
Item {
id: root
implicitHeight: 68
Row {
anchors.left: parent.left
anchors.right: muteButton.left
anchors.rightMargin: Theme.padM
anchors.verticalCenter: muteButton.verticalCenter
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: Audio.micIcon
color: Audio.micMuted ? Theme.accent : Theme.text
font.pixelSize: Theme.fsLarge
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - Theme.fsLarge - Theme.padS
spacing: 2
Text {
width: parent.width
text: Audio.sourceName
elide: Text.ElideRight
color: Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
font.italic: true
renderType: Text.NativeRendering
}
Text {
text: Audio.micMuted ? "MUTED" : Audio.micPercent + "%"
color: Audio.micMuted ? Theme.accent : Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
renderType: Text.NativeRendering
}
}
}
P5Button {
id: muteButton
anchors.right: parent.right
anchors.top: parent.top
text: Audio.micMuted ? "Unmute input" : "Mute input"
icon: Audio.micIcon
onClicked: Audio.toggleMicMute()
}
P5Slider {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
value: Audio.micVolume
accent: Theme.primary
enabled: !Audio.micMuted
onMoved: v => Audio.setMicVolume(v)
}
}
+71
View File
@@ -0,0 +1,71 @@
import QtQuick
import "root:/config"
import "root:/components"
import "root:/services"
// Dominant current-output readout for AudioPopout.
Item {
implicitHeight: 112
Text {
id: outputPercent
anchors.left: parent.left
anchors.top: parent.top
text: Audio.volumePercent + "%"
color: Audio.muted ? Theme.muted : Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsHuge
font.weight: Theme.weightDisplay
font.italic: true
renderType: Text.NativeRendering
}
Column {
anchors.left: outputPercent.right
anchors.leftMargin: Theme.padL
anchors.right: parent.right
anchors.verticalCenter: outputPercent.verticalCenter
spacing: 3
Text {
text: "CURRENT OUTPUT"
color: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Text {
width: parent.width
text: Audio.sinkName
elide: Text.ElideRight
color: Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
font.italic: true
renderType: Text.NativeRendering
}
Text {
text: Audio.muted ? "MUTED" : "LIVE"
color: Audio.muted ? Theme.accent : Theme.text
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
renderType: Text.NativeRendering
}
}
P5Slider {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
value: Audio.volume
accent: Theme.accent
enabled: !Audio.muted
onMoved: v => Audio.setVolume(v)
}
}
+35 -28
View File
@@ -5,12 +5,15 @@ import "root:/config"
import "root:/components"
import "root:/services"
// Output and input levels, the sink picker, and a per-app mixer.
// Audio is the expressive vertical slice for rail popouts: output owns the
// composition, input is a clear secondary band, and application streams recede
// into a compact mixer below both device controls.
Popout {
id: root
contentWidth: 360
contentWidth: 400
contentHeight: surface.implicitContentHeight + Theme.padM * 2
initialFocusItem: outputMuteButton
PopoutSurface {
id: surface
@@ -39,29 +42,38 @@ Popout {
spacing: Theme.padM
// ------------------------------------------------------- output
LevelRow {
AudioOutputHero {
width: parent.width
icon: Audio.icon
label: "Output"
readout: Audio.muted ? "MUTED" : Audio.volumePercent + "%"
value: Audio.volume
muted: Audio.muted
accent: Theme.primary
onMoved: v => Audio.setVolume(v)
onToggled: Audio.toggleMute()
}
Column {
width: parent.width
spacing: Theme.padS
P5Button {
id: outputMuteButton
width: parent.width
text: Audio.muted ? "Unmute output" : "Mute output"
icon: Audio.icon
onClicked: Audio.toggleMute()
}
P5Button {
width: parent.width
text: "Open audio mixer"
icon: "󰕾"
onClicked: Actions.openAudioSettings()
}
}
// -------------------------------------------------------- input
LevelRow {
SectionRule {
width: parent.width
text: "Input"
}
AudioInputBand {
width: parent.width
icon: Audio.micIcon
label: "Input"
readout: Audio.micMuted ? "MUTED" : Audio.micPercent + "%"
value: Audio.micVolume
muted: Audio.micMuted
accent: Theme.blue
onMoved: v => Audio.setMicVolume(v)
onToggled: Audio.toggleMicMute()
}
SectionRule {
@@ -98,19 +110,14 @@ Popout {
icon: "󰝚"
label: Audio.streamName(modelData)
readout: modelData.audio ? Math.round(modelData.audio.volume * 100) + "%" : ""
value: modelData.audio?.volume ?? 0
muted: modelData.audio?.muted ?? false
value: modelData.audio ? modelData.audio.volume : 0
muted: modelData.audio ? modelData.audio.muted : false
accent: Theme.glow
onMoved: v => { if (modelData.audio) modelData.audio.volume = v; }
onToggled: { if (modelData.audio) modelData.audio.muted = !modelData.audio.muted; }
}
}
P5Button {
text: "Mixer"
icon: "󰕾"
onClicked: Actions.openAudioSettings()
}
}
}
@@ -178,7 +185,7 @@ Popout {
anchors.right: parent.right
value: levelRow.value
accent: levelRow.accent
enabledControl: !levelRow.muted
enabled: !levelRow.muted
onMoved: v => levelRow.moved(v)
}
}
+73 -27
View File
@@ -155,26 +155,26 @@ Popout {
TransportButton {
glyph: "󰒮"
enabledControl: Media.canGoPrevious
enabled: Media.canGoPrevious
onClicked: Media.previous()
}
TransportButton {
glyph: Media.playing ? "󰏤" : "󰐊"
primary: true
enabledControl: Media.hasPlayer
enabled: Media.hasPlayer
onClicked: Media.playPause()
}
TransportButton {
glyph: "󰒭"
enabledControl: Media.canGoNext
enabled: Media.canGoNext
onClicked: Media.next()
}
TransportButton {
glyph: "󰐒"
enabledControl: Media.hasPlayer
enabled: Media.hasPlayer
onClicked: Media.raise()
}
}
@@ -196,7 +196,7 @@ Popout {
id: elapsed
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: Media.timeString(Media.position)
text: Media.positionSupported ? Media.timeString(Media.position) : "--:--"
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
@@ -207,7 +207,7 @@ Popout {
id: total
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: Media.timeString(Media.length)
text: Media.lengthSupported ? Media.timeString(Media.length) : "LIVE"
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
@@ -222,7 +222,7 @@ Popout {
anchors.verticalCenter: parent.verticalCenter
value: Media.progress
accent: Theme.accent
enabledControl: Media.canSeek
enabled: Media.canSeek
onMoved: v => Media.seekTo(v)
}
}
@@ -238,7 +238,7 @@ Popout {
Repeater {
model: Media.players
Item {
FocusScope {
id: chip
required property var modelData
@@ -246,21 +246,44 @@ Popout {
implicitWidth: chipLabel.implicitWidth + Theme.padM
implicitHeight: 22
focusPolicy: chip.enabled ? Qt.TabFocus : Qt.NoFocus
function activate(): void {
if (chip.enabled)
Media.select(chip.modelData);
}
function activateFromKey(event): void {
if (!event.isAutoRepeat)
chip.activate();
}
Skew {
anchors.fill: parent
color: chip.current
? Theme.alpha(Theme.primary, 0.22)
: (chipHover.hovered ? Theme.alpha(Theme.primary, 0.1) : "transparent")
borderWidth: 1
borderColor: chip.current ? Theme.primary : Theme.alpha(Theme.outline, 0.7)
color: !chip.enabled
? Theme.treatmentDisabledFill
: (chip.activeFocus
? Theme.treatmentFocusFill
: (chip.current
? Theme.alpha(Theme.primary, 0.22)
: (chipHover.hovered ? Theme.alpha(Theme.primary, 0.1) : "transparent")))
borderWidth: chip.activeFocus ? Theme.stroke : 1
borderColor: !chip.enabled
? Theme.treatmentDisabledMark
: (chip.activeFocus
? Theme.treatmentFocusMark
: (chip.current ? Theme.primary : Theme.alpha(Theme.outline, 0.7)))
}
Text {
id: chipLabel
anchors.centerIn: parent
text: chip.modelData.identity
color: chip.current ? Theme.text : Theme.muted
color: !chip.enabled
? Theme.treatmentDisabledText
: (chip.activeFocus
? Theme.treatmentFocusText
: (chip.current ? Theme.text : Theme.muted))
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
@@ -268,39 +291,57 @@ Popout {
HoverHandler {
id: chipHover
enabled: chip.enabled
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: Media.select(chip.modelData)
enabled: chip.enabled
onTapped: chip.activate()
}
Keys.onReturnPressed: event => chip.activateFromKey(event)
Keys.onEnterPressed: event => chip.activateFromKey(event)
Keys.onSpacePressed: event => chip.activateFromKey(event)
}
}
}
}
component TransportButton: Item {
component TransportButton: FocusScope {
id: tb
property string glyph: ""
property bool primary: false
property bool enabledControl: true
signal clicked()
implicitWidth: primary ? 38 : 30
implicitHeight: primary ? 38 : 30
focusPolicy: tb.enabled ? Qt.TabFocus : Qt.NoFocus
function activate(): void {
if (tb.enabled)
tb.clicked();
}
function activateFromKey(event): void {
if (!event.isAutoRepeat)
tb.activate();
}
Skew {
anchors.fill: parent
color: {
if (!tb.enabledControl) return "transparent";
if (!tb.enabled) return "transparent";
if (tb.activeFocus) return Theme.treatmentFocusFill;
if (tbHover.hovered) return tb.primary ? Theme.accent : Theme.alpha(Theme.primary, 0.22);
return tb.primary ? Theme.alpha(Theme.primary, 0.18) : "transparent";
}
borderWidth: tb.primary ? 2 : 1
borderColor: tb.enabledControl
? (tbHover.hovered ? Theme.text : Theme.alpha(Theme.primary, 0.6))
borderWidth: tb.activeFocus ? Theme.stroke * 2 : (tb.primary ? 2 : 1)
borderColor: tb.enabled
? (tb.activeFocus
? Theme.treatmentFocusMark
: (tbHover.hovered ? Theme.text : Theme.alpha(Theme.primary, 0.6)))
: Theme.alpha(Theme.outline, 0.4)
Behavior on color {
@@ -313,26 +354,31 @@ Popout {
text: tb.glyph
font.pixelSize: tb.primary ? Theme.fsTitle : Theme.fsLarge
color: {
if (!tb.enabledControl) return Theme.alpha(Theme.muted, 0.5);
if (!tb.enabled) return Theme.alpha(Theme.muted, 0.5);
if (tb.activeFocus) return Theme.treatmentFocusText;
if (tbHover.hovered && tb.primary) return Theme.text;
return Theme.primary;
}
}
scale: tbHover.hovered && tb.enabledControl ? 1.12 : 1.0
scale: tbHover.hovered && tb.enabled ? 1.12 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
HoverHandler {
id: tbHover
enabled: tb.enabledControl
enabled: tb.enabled
cursorShape: Qt.PointingHandCursor
}
TapHandler {
enabled: tb.enabledControl
onTapped: tb.clicked()
enabled: tb.enabled
onTapped: tb.activate()
}
Keys.onReturnPressed: event => tb.activateFromKey(event)
Keys.onEnterPressed: event => tb.activateFromKey(event)
Keys.onSpacePressed: event => tb.activateFromKey(event)
}
}
+2 -1
View File
@@ -6,12 +6,13 @@ import "root:/components"
// own, so P5Panel's default content property stays usable by callers.
P5Panel {
property color tone: Theme.primary
property bool textured: false
fill: Theme.mantle
fillOpacity: 0.97
border: Theme.alpha(tone, 0.75)
borderWidth: Theme.stroke
halftone: true
halftone: textured
halftoneColor: tone
halftoneOpacity: 0.06
sheen: true
+10
View File
@@ -74,6 +74,16 @@ PopupWindow {
close();
}
function dismissImmediately() {
openAnim.stop();
closeAnim.stop();
shown = false;
holder.opacity = 0;
holder.scale = 0.94;
holder.x = root.bleed;
PopupCoordinator.release(root);
}
// The grab can only take hold once the popup is actually mapped. Asked for
// in the same tick as `shown`, Hyprland has no surface to hand it and
// clears it immediately — which closed the menu the instant it opened.