media: rebrand to Bento

popouts: instantly set empty input region on exit
This commit is contained in:
bbedward committed 2026-09-22 13:44:12 -04:00
1 parent 4eb163538c
commit 17d52d3c88
18 files changed
+133 -93

No files matched your search

+3 -2
View File
@@ -194,8 +194,6 @@ Singleton {
readonly property real mediaCardMargin: Theme.spacingL
readonly property real mediaArtSize: 150
readonly property real mediaArtSizeDash: 336
readonly property real mediaArtLowResRatio: 0.5
readonly property real mediaArtLowResScale: 0.85
readonly property real mediaArtSizeMaterial: 200
readonly property real mediaArtRadius: Theme.cornerRadiusXL
readonly property real mediaArtSoftRadiusRatio: 0.3
@@ -204,12 +202,15 @@ Singleton {
readonly property real mediaPlayWidthRatio: 1.5
readonly property real mediaPlayWidthStep: 0.75
readonly property real mediaTransportPressScale: 1.2
readonly property real mediaDeviceNameWidthRatio: 0.42
readonly property real accentSelectedAlpha: 0.2
readonly property real groupIdleAlpha: 0.1
readonly property int mediaPositionPollInterval: 1000
readonly property int mediaVolumeEchoTimeout: 1500
readonly property int mediaTransitionGraceInterval: 3000
readonly property int mediaPlayerLossGraceInterval: 1500
readonly property real mediaTextScrollSpeed: 40
readonly property int mediaTextScrollStep: 30
readonly property int mediaLyricsRequestTimeout: 16000
readonly property int mediaLyricsLoadingDelay: 300
readonly property real mediaLyricsPositionTolerance: 0.05
+1 -1
View File
@@ -122,7 +122,7 @@ Singleton {
"value": "slanted",
"text": I18n.tr("Slanted", "Album artwork shape")
}
]), choice("titleFont", I18n.tr("Title font"), MediaOptions.defaultTitleFont, Theme.fontChoices), toggle("seekbar", I18n.tr("Show seekbar"), MediaOptions.defaults.seekbar), toggle("waveProgress", I18n.tr("Wave progress bars"), MediaOptions.defaults.waveProgress), toggle("albumArtBackdrop", I18n.tr("Album art backdrop"), MediaOptions.defaults.albumArtBackdrop), toggle("albumArtAccent", I18n.tr("Use album art accent"), MediaOptions.defaults.albumArtAccent), toggle("animatedArt", I18n.tr("Apple Music animated covers"), MediaOptions.defaults.animatedArt, I18n.tr("Sends the playing artist and album name to Apple")), toggle("lyrics", I18n.tr("Lyrics", "Media player lyrics button"), MediaOptions.defaults.lyrics, I18n.tr("Sends the playing track, artist and album name to enabled lyrics providers"))]
]), choice("titleFont", I18n.tr("Title font"), MediaOptions.defaultTitleFont, Theme.fontChoices), toggle("deviceName", I18n.tr("Show device name", "Media player option that puts the audio output device name on its button"), MediaOptions.defaults.deviceName), toggle("seekbar", I18n.tr("Show seekbar"), MediaOptions.defaults.seekbar), toggle("waveProgress", I18n.tr("Wave progress bars"), MediaOptions.defaults.waveProgress), toggle("albumArtBackdrop", I18n.tr("Album art backdrop"), MediaOptions.defaults.albumArtBackdrop), toggle("albumArtAccent", I18n.tr("Use album art accent"), MediaOptions.defaults.albumArtAccent), toggle("animatedArt", I18n.tr("Apple Music animated covers"), MediaOptions.defaults.animatedArt, I18n.tr("Sends the playing artist and album name to Apple")), toggle("lyrics", I18n.tr("Lyrics", "Media player lyrics button"), MediaOptions.defaults.lyrics, I18n.tr("Sends the playing track, artist and album name to enabled lyrics providers"))]
},
{
"id": "wallpaper",
@@ -12,27 +12,25 @@ MediaChromeBase {
readonly property bool compact: width < Theme.smallBreakpoint || halfWidth < headerHeight + controls.implicitHeight
readonly property real halfWidth: Math.max(0, (width - padding * 2 - Theme.spacingXL) / 2)
readonly property real bodyHeight: Math.max(0, height - padding * 2)
// !TODO: expose this variant? playerPaneOpen false gives art beside lyrics, or art alone with hover transport
readonly property bool playerPane: root.player.playerPaneOpen
readonly property bool controlsColumn: playerPane
readonly property bool lyricsColumn: root.player.lyricsOpen && !playerPane
readonly property bool splitPanes: controlsColumn || lyricsColumn
readonly property bool artTransport: !playerPane && !!root.presentation
readonly property real naturalArtSize: Math.max(0, Math.min(artSize, compact ? width / 2 : halfWidth))
readonly property real artworkSize: compact ? naturalArtSize : Math.max(naturalArtSize, Math.min(halfWidth, bodyHeight))
readonly property real naturalArtSize: Math.round(Math.max(0, Math.min(artSize, compact ? width / 2 : halfWidth)))
// Sized from the controls column, never from this item's own height: reading back the height
// the artwork itself sets makes the split settle wherever the current track happened to put it.
readonly property real artworkSize: Math.round(compact ? naturalArtSize : Math.min(halfWidth, Math.max(naturalArtSize, headerHeight + controls.implicitHeight)))
readonly property real columnHeight: compact ? artworkSize : Math.max(artworkSize, paneHeight)
readonly property real paneHeight: Math.max(artworkSize, headerHeight + controls.implicitHeight)
readonly property real paneMinHeight: Math.max(naturalArtSize, headerHeight + controls.implicitHeight)
readonly property real headerHeight: sourceRow.height + Theme.spacingS
readonly property real renderScale: Window.window?.devicePixelRatio ?? Screen.devicePixelRatio
readonly property int artSide: TrackArtService.resolvedArtSide
readonly property bool lowResArt: artSide > 0 && artSide < artworkSize * renderScale * DashMetrics.mediaArtLowResRatio
artSize: DashMetrics.mediaArtSizeDash
surfaceColor: Theme.foregroundColor(Theme.hostSurface)
baseHeight: Math.max(DashMetrics.tabMinHeight, padding * 2 + (compact ? naturalArtSize + Theme.spacingXL + paneMinHeight : Math.max(naturalArtSize, paneMinHeight)))
focusTargets: (viewToggle.visible ? [viewToggle] : []).concat(artTransportLoader.item?.focusTargets ?? [], [playerButton], sourceGroup.focusTargets, transport.focusTargets, seekbar.canSeek && seekBlock.visible ? [seekbar] : [])
focusTargets: (viewToggle.visible ? [viewToggle] : []).concat(artTransportLoader.item?.focusTargets ?? [], [playerButton], sourceGroup.focusTargets, transport.focusTargets, seekbar.canSeek && root.hasSeekbar ? [seekbar] : [])
panelButtons: [playerButton].concat(sourceGroup.panelButtons)
inlineVolume: true
@@ -56,10 +54,8 @@ MediaChromeBase {
Item {
id: art
readonly property real scaledSize: root.artworkSize * (root.lowResArt ? DashMetrics.mediaArtLowResScale : 1)
anchors.centerIn: parent
width: root.compact ? scaledSize : Math.min(root.artworkSize, Math.max(scaledSize, root.headerHeight + controls.implicitHeight))
width: root.artworkSize
height: width
HoverHandler {
@@ -282,6 +278,7 @@ MediaChromeBase {
loop: true
fadeEdges: true
pxPerMs: DashMetrics.mediaTextScrollSpeed / 1000
stepInterval: DashMetrics.mediaTextScrollStep
}
ScrollingText {
@@ -294,6 +291,7 @@ MediaChromeBase {
loop: true
fadeEdges: true
pxPerMs: DashMetrics.mediaTextScrollSpeed / 1000
stepInterval: DashMetrics.mediaTextScrollStep
}
}
}
@@ -327,11 +325,13 @@ MediaChromeBase {
visible: hasControls
}
// Kept in the column for a length-less stream so the pane, and with it the
// artwork it sizes, does not resize when one comes up in the queue.
Item {
id: seekBlock
width: parent.width
height: Theme.buttonHeightXS
visible: root.hasSeekbar
visible: root.seekbarEnabled && !!root.presentation
LayoutMirroring.enabled: false
LayoutMirroring.childrenInherit: true
@@ -339,6 +339,7 @@ MediaChromeBase {
id: elapsed
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
visible: root.hasSeekbar
text: Format.formatDuration(seekbar.value * (root.presentation?.length ?? 0))
reserveText: duration.text.replace(/\d/g, "8")
}
@@ -349,6 +350,7 @@ MediaChromeBase {
anchors.right: duration.left
anchors.margins: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
visible: root.hasSeekbar
player: root.player
}
@@ -356,6 +358,7 @@ MediaChromeBase {
id: duration
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: root.hasSeekbar
text: Format.formatDuration(root.presentation?.length ?? 0)
reserveText: text.replace(/\d/g, "8")
}
@@ -39,7 +39,7 @@ Row {
panelId: "volume"
iconName: root.player.getVolumeIcon()
enabled: root.player.volumeAvailable
tooltipText: I18n.tr("Volume") + ": " + Math.round(root.player.currentVolume * 100) + "%"
tooltipText: (root.player.usePlayerVolume ? I18n.tr("Media volume") : I18n.tr("Volume")) + ": " + Math.round(root.player.currentVolume * 100) + "%"
MediaVolumeWheel {
player: root.player
@@ -50,7 +50,7 @@ Row {
GroupButton {
id: devicesButton
panelId: "devices"
tooltipText: I18n.tr("Devices", "Media player output device picker")
tooltipText: I18n.tr("Devices", "Media player output device picker") + ": " + AudioService.displayName(AudioService.sink)
iconName: AudioService.sinkIcon(AudioService.sink)
MediaSinkWheel {}
@@ -151,7 +151,7 @@ DankBottomSheet {
subtitle: [modelData?.trackTitle, modelData?.trackArtist].filter(Boolean).join(" · ")
selected: modelData === root.player.activePlayer
onActivated: {
MprisController.switchActivePlayer(modelData);
MprisController.setActivePlayer(modelData);
root.player.panel = "";
}
}
@@ -4,6 +4,7 @@ import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.DankDash
import "../../../DankCommon/Common/FocusNavigation.js" as FocusNavigation
Item {
@@ -11,26 +12,13 @@ Item {
required property var player
readonly property var focusTargets: [volumeButton, devicesButton]
readonly property var panelButtons: [volumeButton, devicesButton]
readonly property var focusTargets: [volumeButton, deviceChip, deviceIcon]
readonly property var panelButtons: [volumeButton, deviceChip, deviceIcon]
readonly property string devicesLabel: I18n.tr("Devices", "Media player output device picker")
readonly property string sinkName: AudioService.displayName(AudioService.sink)
readonly property string fittedSinkName: {
const room = devicesButton.maximumLabelWidth;
// advanceWidth() is not reactive, this read re-evaluates once the font resolves
if (labelMetrics.averageCharacterWidth <= 0)
return "";
if (Math.ceil(labelMetrics.advanceWidth(sinkName)) <= room)
return sinkName;
const words = sinkName.split(/\s+/);
while (words.length > 1) {
words.pop();
const shortened = words.join(" ");
if (Math.ceil(labelMetrics.advanceWidth(shortened)) <= room)
return shortened;
}
return "";
}
readonly property string deviceTooltip: devicesLabel + ": " + sinkName
readonly property bool showSinkName: root.player.options?.deviceName ?? MediaOptions.defaults.deviceName
readonly property string volumeLabel: root.player.usePlayerVolume ? I18n.tr("Media volume") : I18n.tr("Volume")
readonly property bool muted: player.usePlayerVolume ? player.currentVolume === 0 : AudioService.sinkSilent
readonly property real volumeRatio: player.maxVolumePercent > 0 ? player.currentVolume * 100 / player.maxVolumePercent : 0
@@ -38,13 +26,6 @@ Item {
Keys.onPressed: event => event.accepted = FocusNavigation.handleHorizontalKey(event, focusTargets, I18n.isRtl)
FontMetrics {
id: labelMetrics
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeMedium
font.weight: Theme.fontWeightMedium
}
DankRingGauge {
id: volumeRing
@@ -72,7 +53,7 @@ Item {
iconName: root.player.getVolumeIcon()
iconColor: root.muted ? Theme.onSurfaceVariant : Theme.onSurface
enabled: root.player.volumeAvailable
tooltipText: I18n.tr("Volume") + ": " + Math.round(root.player.currentVolume * 100) + "%"
tooltipText: root.volumeLabel + ": " + Math.round(root.player.currentVolume * 100) + "%"
onClicked: root.player.togglePanel(panelId)
MediaVolumeWheel {
@@ -83,22 +64,41 @@ Item {
}
DankButton {
id: devicesButton
id: deviceChip
readonly property string panelId: "devices"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
maximumWidth: Math.max(0, root.width - volumeRing.width - Theme.spacingXS)
text: root.fittedSinkName || root.devicesLabel
visible: root.showSinkName
maximumWidth: Math.round(root.width * DashMetrics.mediaDeviceNameWidthRatio)
text: root.sinkName
iconName: AudioService.sinkIcon(AudioService.sink)
tooltipText: root.fittedSinkName === root.sinkName ? root.devicesLabel : root.sinkName
Accessible.name: root.devicesLabel + ": " + root.sinkName
tooltipText: root.deviceTooltip
Accessible.name: root.deviceTooltip
buttonHeight: Theme.buttonHeightXS
backgroundColor: MediaAccentService.accentSecondaryContainer
textColor: MediaAccentService.onAccentSecondaryContainer
horizontalPadding: Theme.spacingM
onClicked: root.player.togglePanel("devices")
onClicked: root.player.togglePanel(panelId)
MediaSinkWheel {}
}
DankActionButton {
id: deviceIcon
readonly property string panelId: "devices"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: !root.showSinkName
buttonSize: volumeRing.width
iconName: AudioService.sinkIcon(AudioService.sink)
iconColor: MediaAccentService.onAccentSecondaryContainer
backgroundColor: MediaAccentService.accentSecondaryContainer
tooltipText: root.deviceTooltip
onClicked: root.player.togglePanel(panelId)
MediaSinkWheel {}
}
+30 -4
View File
@@ -45,9 +45,22 @@ Item {
}
readonly property bool volumeAvailable: !!((activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio))
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
readonly property real reportedVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
readonly property real currentVolume: pendingVolume >= 0 ? pendingVolume : reportedVolume
readonly property real maxVolumePercent: usePlayerVolume ? 100 : AudioService.sinkMaxVolume
// MPRIS volume round-trips through the player, so successive steps would each read a stale
// activePlayer.volume and collapse into one. Drive them from the value we last asked for.
property real pendingVolume: -1
onReportedVolumeChanged: {
if (pendingVolume >= 0 && Math.abs(reportedVolume - pendingVolume) < 0.005)
clearPendingVolume();
}
onUsePlayerVolumeChanged: clearPendingVolume()
onActivePlayerChanged: clearPendingVolume()
implicitWidth: DashMetrics.contentWidthFor(SettingsData.showWeekNumber, DashMetrics.panelColumnsFor(entryId))
implicitHeight: mediaChrome.item?.implicitHeight ?? DashMetrics.tabMinHeight
@@ -93,6 +106,12 @@ Item {
}
}
Timer {
id: pendingVolumeTimer
interval: DashMetrics.mediaVolumeEchoTimeout
onTriggered: root.pendingVolume = -1
}
MediaPresentation {
id: mediaPresentation
player: root
@@ -155,12 +174,19 @@ Item {
return AudioService.sinkVolumeIconName;
}
function clearPendingVolume() {
pendingVolume = -1;
pendingVolumeTimer.stop();
}
function setVolume(ratio) {
if (!volumeAvailable)
return;
const clamped = Math.min(maxVolumePercent / 100, Math.max(0, ratio));
SessionData.suppressOSDTemporarily();
if (usePlayerVolume) {
pendingVolume = clamped;
pendingVolumeTimer.restart();
activePlayer.volume = clamped;
return;
}
@@ -281,12 +307,12 @@ Item {
Loader {
id: mediaChrome
anchors.fill: parent
sourceComponent: root.playerStyle === "material" ? materialChrome : zurvanChrome
sourceComponent: root.playerStyle === "material" ? materialChrome : bentoChrome
}
Component {
id: zurvanChrome
ZurvanChrome {
id: bentoChrome
BentoChrome {
player: root
}
}
@@ -47,6 +47,7 @@ CcSheetDialog {
iconName: "settings"
onClicked: {
root.dismiss();
PopoutService.closeDankDash();
PopoutService.openSettingsWithTab(root.entryId === "media" ? "media_player" : "weather");
}
}
@@ -26,7 +26,7 @@ Card {
readonly property string artUrl: TrackArtService.resolvedArtUrl
readonly property color accent: MediaAccentService.accent
readonly property bool showSeekbar: options.seekbar !== false && !tiny
readonly property bool zurvan: options.playerStyle === "zurvan"
readonly property bool bento: options.playerStyle === "bento"
readonly property bool inlineMetadata: compact && titleLabel.implicitHeight + artistLabel.implicitHeight + trackText.spacing > transport.y - Theme.spacingXS
readonly property var playbackFocusTargets: [playButton, previousButton, nextButton].concat(seekbar.canSeek ? [seekbar] : [])
@@ -167,7 +167,7 @@ Card {
id: transport
readonly property bool stacked: root.narrow && !root.tiny
readonly property bool centered: root.zurvan && !stacked && !root.tiny
readonly property bool centered: root.bento && !stacked && !root.tiny
readonly property bool medium: !root.compact && width >= Theme.buttonHeightM * 4
readonly property real buttonHeight: medium ? Theme.buttonHeightM : Theme.buttonHeightS
readonly property real spacing: root.compact || stacked ? Theme.spacingXS : Theme.spacingS
@@ -194,7 +194,7 @@ Card {
checkable: true
checked: root.playing
iconFilled: false
radius: Theme.buttonRadius(width, height, buttonSize, pressed, !root.zurvan && checked)
radius: Theme.buttonRadius(width, height, buttonSize, pressed, !root.bento && checked)
iconName: root.playing ? "pause" : "play_arrow"
Accessible.name: root.playing ? I18n.tr("Pause") : I18n.tr("Play")
enabled: !!root.activePlayer?.canTogglePlaying
@@ -209,11 +209,11 @@ Card {
anchors.bottom: parent.bottom
size: transport.medium ? "m" : "s"
width: transport.stacked ? (parent.width - transport.spacing) / 2 : (parent.width - playButton.width - transport.spacing * 2) / 2
variant: root.zurvan ? "tonal" : "standard"
round: !root.zurvan
variant: root.bento ? "tonal" : "standard"
round: !root.bento
radius: Theme.buttonRadius(width, height, buttonSize, pressed, round)
containerColor: MediaAccentService.accentSecondaryContainer
contentColor: root.zurvan ? MediaAccentService.onAccentSecondaryContainer : Theme.onSecondaryContainer
contentColor: root.bento ? MediaAccentService.onAccentSecondaryContainer : Theme.onSecondaryContainer
iconName: "skip_previous"
Accessible.name: I18n.tr("Previous")
enabled: !!root.activePlayer?.canGoPrevious || (!!root.activePlayer?.canSeek && root.activePlayer.position > 8)
+4 -1
View File
@@ -279,7 +279,10 @@ Item {
DankButton {
text: I18n.tr("Settings")
iconName: "settings"
onClicked: PopoutService.openSettingsWithTab("weather")
onClicked: {
PopoutService.closeDankDash();
PopoutService.openSettingsWithTab("weather");
}
}
}
}