mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-17 00:34:01 -04:00
media: add album art to bar widget
settings: keep the page still when a rebuilt row drops focus fixes #3673
This commit is contained in:
+2726
-815
File diff suppressed because it is too large
Load Diff
@@ -128,6 +128,7 @@ var DEFAULTS = {
|
||||
mediaSize: 1,
|
||||
mediaAdaptiveWidthEnabled: true,
|
||||
mediaShowLyrics: true,
|
||||
mediaShowCoverArt: true,
|
||||
audioScrollMode: "volume"
|
||||
},
|
||||
focusedWindow: {
|
||||
|
||||
@@ -44,7 +44,9 @@ FocusScope {
|
||||
} else if (contains(content, focusedItem)) {
|
||||
activePane = "content";
|
||||
content.rememberFocus();
|
||||
content._reveal(focusedItem);
|
||||
// Focus that falls back to a container when its control is destroyed must not scroll the page
|
||||
if (keyboardDriven() || focusedItem.cursorPosition !== undefined)
|
||||
content._reveal(focusedItem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modules.DankBar
|
||||
import qs.Modules.Plugins
|
||||
@@ -27,6 +28,14 @@ BasePill {
|
||||
property var widgetData: null
|
||||
readonly property bool adaptiveWidthEnabled: SettingsData.widgetOption("music", widgetData, "mediaAdaptiveWidthEnabled")
|
||||
readonly property bool lyricsEnabled: SettingsData.widgetOption("music", widgetData, "mediaShowLyrics") && LyricsService.allowed
|
||||
readonly property bool coverArtEnabled: SettingsData.widgetOption("music", widgetData, "mediaShowCoverArt")
|
||||
readonly property string coverArtUrl: coverArtEnabled && activePlayer && TrackArtService.artReadyFor(activePlayer) ? TrackArtService.resolvedArtUrl : ""
|
||||
readonly property bool hasCoverArt: coverArtUrl !== ""
|
||||
readonly property bool visualizerEnabled: CavaService.cavaAvailable && SettingsData.audioVisualizerEnabled
|
||||
readonly property bool showIconSlot: visualizerEnabled || !hasCoverArt
|
||||
readonly property real badgeSize: BarMetrics.mediaControlSize * contentScale
|
||||
readonly property real badgeExtent: (hasCoverArt ? badgeSize + Theme.spacingXS : 0) + (showIconSlot ? badgeSize + Theme.spacingXS : 0)
|
||||
readonly property string trackSummary: [MprisController.stableTitle, MprisController.stableArtist, MprisController.stableAlbum].filter(part => part).join(" • ")
|
||||
readonly property bool lyricActive: {
|
||||
if (!lyricsEnabled)
|
||||
return false;
|
||||
@@ -59,9 +68,8 @@ BasePill {
|
||||
if (!isVerticalOrientation) {
|
||||
return contentThickness;
|
||||
}
|
||||
const audioVizHeight = BarMetrics.mediaControlSize * root.contentScale;
|
||||
const playButtonHeight = 24 * root.contentScale;
|
||||
return audioVizHeight + Theme.spacingXS + playButtonHeight;
|
||||
return badgeExtent + playButtonHeight;
|
||||
}
|
||||
|
||||
property real scrollAccumulatorY: 0
|
||||
@@ -71,6 +79,27 @@ BasePill {
|
||||
active: root.lyricsEnabled && root.playerAvailable
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: tooltipLoader
|
||||
active: false
|
||||
sourceComponent: DankTooltip {}
|
||||
}
|
||||
|
||||
function showTrackTooltip() {
|
||||
if (!root.lyricActive || !root.parentScreen)
|
||||
return;
|
||||
tooltipLoader.active = true;
|
||||
const anchor = root.contextMenuAnchor();
|
||||
tooltipLoader.item.show(root.trackSummary, anchor.x, anchor.y, anchor.screen, anchor.isVertical && anchor.edge === "left", anchor.isVertical && anchor.edge === "right");
|
||||
}
|
||||
|
||||
function hideTrackTooltip() {
|
||||
tooltipLoader.item?.hide();
|
||||
tooltipLoader.active = false;
|
||||
}
|
||||
|
||||
onClicked: hideTrackTooltip()
|
||||
|
||||
onWheel: function (wheelEvent) {
|
||||
if (SettingsData.widgetOption("music", widgetData, "audioScrollMode") === "nothing")
|
||||
return;
|
||||
@@ -135,6 +164,42 @@ BasePill {
|
||||
}
|
||||
}
|
||||
|
||||
component CoverArt: ClippingRectangle {
|
||||
width: root.badgeSize
|
||||
height: root.badgeSize
|
||||
radius: Theme.cornerRadiusXS
|
||||
color: "transparent"
|
||||
visible: root.hasCoverArt
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: root.coverArtUrl
|
||||
sourceSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr))
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
|
||||
component MediaIcon: Item {
|
||||
width: root.badgeSize
|
||||
height: root.badgeSize
|
||||
visible: root.showIconSlot
|
||||
|
||||
AudioVisualization {
|
||||
enabled: root.surfaceLive
|
||||
anchors.fill: parent
|
||||
visible: root.visualizerEnabled
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.fill: parent
|
||||
name: "music_note"
|
||||
size: parent.width
|
||||
color: Theme.primary
|
||||
visible: !root.visualizerEnabled
|
||||
}
|
||||
}
|
||||
|
||||
component PlayButton: Item {
|
||||
width: Theme.iconSize * root.contentScale
|
||||
height: width
|
||||
@@ -177,15 +242,18 @@ BasePill {
|
||||
}
|
||||
readonly property int horizontalContentWidth: {
|
||||
const controlsWidth = 64 * root.contentScale + Theme.spacingXS * 2;
|
||||
const audioVizWidth = BarMetrics.mediaControlSize * root.contentScale;
|
||||
const baseWidth = audioVizWidth + Theme.spacingXS + controlsWidth;
|
||||
return baseWidth + (measuredTextWidth > 0 ? measuredTextWidth + Theme.spacingXS : 0);
|
||||
return root.badgeExtent + controlsWidth + (measuredTextWidth > 0 ? measuredTextWidth + Theme.spacingXS : 0);
|
||||
}
|
||||
|
||||
implicitWidth: root.playerAvailable ? (root.isVerticalOrientation ? root.currentContentWidth : horizontalContentWidth) : 0
|
||||
implicitHeight: root.playerAvailable ? root.currentContentHeight : 0
|
||||
opacity: root.playerAvailable ? 1 : 0
|
||||
|
||||
HoverHandler {
|
||||
enabled: root.lyricActive
|
||||
onHoveredChanged: hovered ? root.showTrackTooltip() : root.hideTrackTooltip()
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
@@ -215,22 +283,17 @@ BasePill {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Item {
|
||||
width: BarMetrics.mediaControlSize * root.contentScale
|
||||
height: BarMetrics.mediaControlSize * root.contentScale
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: root.badgeSize
|
||||
height: badges.height
|
||||
|
||||
AudioVisualization {
|
||||
enabled: root.surfaceLive
|
||||
anchors.fill: parent
|
||||
visible: CavaService.cavaAvailable && SettingsData.audioVisualizerEnabled
|
||||
}
|
||||
Column {
|
||||
id: badges
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
anchors.fill: parent
|
||||
name: "music_note"
|
||||
size: BarMetrics.mediaControlSize * root.contentScale
|
||||
color: Theme.primary
|
||||
visible: !CavaService.cavaAvailable || !SettingsData.audioVisualizerEnabled
|
||||
CoverArt {}
|
||||
|
||||
MediaIcon {}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -272,24 +335,12 @@ BasePill {
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Item {
|
||||
width: BarMetrics.mediaControlSize * root.contentScale
|
||||
height: BarMetrics.mediaControlSize * root.contentScale
|
||||
CoverArt {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
AudioVisualization {
|
||||
enabled: root.surfaceLive
|
||||
anchors.fill: parent
|
||||
visible: CavaService.cavaAvailable && SettingsData.audioVisualizerEnabled
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.fill: parent
|
||||
name: "music_note"
|
||||
size: BarMetrics.mediaControlSize * root.contentScale
|
||||
color: Theme.primary
|
||||
visible: !CavaService.cavaAvailable || !SettingsData.audioVisualizerEnabled
|
||||
}
|
||||
MediaIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
||||
@@ -38,7 +38,7 @@ Item {
|
||||
property real wordProgress: 1
|
||||
|
||||
implicitHeight: Math.ceil(line.implicitHeight * textScale) + Theme.spacingS
|
||||
opacity: reach === 0 || !emphasize ? 1 : reach === 1 ? DashMetrics.lyricsNearOpacity : reach === 2 ? DashMetrics.lyricsFarOpacity : 0
|
||||
opacity: reach === 0 || !emphasize ? 1 : reach === 1 ? DashMetrics.lyricsNearOpacity : DashMetrics.lyricsFarOpacity
|
||||
onWordRevisionChanged: updateWordProgress()
|
||||
onAnimateWordsChanged: updateWordProgress()
|
||||
onSweepChanged: updateWordProgress()
|
||||
|
||||
@@ -46,6 +46,14 @@ Column {
|
||||
onToggled: checked => root.page.set("mediaShowLyrics", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
resetStore: root.page
|
||||
resetKeys: ["mediaShowCoverArt"]
|
||||
text: I18n.tr("Cover art", "media widget option showing the album cover in the pill")
|
||||
checked: root.page.value("mediaShowCoverArt")
|
||||
onToggled: checked => root.page.set("mediaShowCoverArt", checked)
|
||||
}
|
||||
|
||||
SettingsDropdownRow {
|
||||
resetStore: root.page
|
||||
resetKeys: ["audioScrollMode"]
|
||||
|
||||
@@ -189,24 +189,25 @@ Item {
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: SettingsData.notificationRules
|
||||
// Count model: a rule edit must not rebuild the row holding the focused toggle
|
||||
model: SettingsData.notificationRules.length
|
||||
|
||||
delegate: SettingsRow {
|
||||
id: ruleRow
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
readonly property var badges: root.outcomeBadges(modelData)
|
||||
readonly property var rule: SettingsData.notificationRules[index] ?? ({})
|
||||
readonly property var badges: root.outcomeBadges(rule)
|
||||
|
||||
title: modelData.pattern || I18n.tr("Rule %1", "notification rule heading, %1 is the rule number").arg(index + 1)
|
||||
titleColor: modelData.enabled !== false ? Theme.surfaceText : Theme.surfaceVariantText
|
||||
subtitle: root.matchSummary(modelData)
|
||||
title: rule.pattern || I18n.tr("Rule %1", "notification rule heading, %1 is the rule number").arg(index + 1)
|
||||
titleColor: rule.enabled !== false ? Theme.surfaceText : Theme.surfaceVariantText
|
||||
subtitle: root.matchSummary(rule)
|
||||
|
||||
DankToggle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
hideText: true
|
||||
text: ruleRow.title
|
||||
checked: ruleRow.modelData.enabled !== false
|
||||
checked: ruleRow.rule.enabled !== false
|
||||
onToggled: checked => SettingsData.updateNotificationRuleField(ruleRow.index, "enabled", checked)
|
||||
}
|
||||
|
||||
@@ -214,7 +215,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "edit"
|
||||
Accessible.name: I18n.tr("Edit rule")
|
||||
onClicked: root.openEditor(ruleRow.index, ruleRow.modelData)
|
||||
onClicked: root.openEditor(ruleRow.index, ruleRow.rule)
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
|
||||
@@ -32,6 +32,7 @@ Column {
|
||||
SettingsData.barConfigs;
|
||||
SettingsData.dockConfigs;
|
||||
const bars = SettingsData.barConfigs.filter(config => !SettingsData.isDotBarConfig(config)).map(config => ({
|
||||
key: "bar:" + config.id,
|
||||
kind: "bar",
|
||||
id: config.id,
|
||||
name: config.name || config.id,
|
||||
@@ -40,6 +41,7 @@ Column {
|
||||
transparency: config.transparency ?? 1
|
||||
}));
|
||||
const docks = SettingsData.dockConfigs.map(config => ({
|
||||
key: "dock:" + config.id,
|
||||
kind: "dock",
|
||||
id: config.id,
|
||||
name: config.name,
|
||||
@@ -372,37 +374,38 @@ Column {
|
||||
SettingsToggleRow {
|
||||
id: overrideRow
|
||||
|
||||
required property var modelData
|
||||
required property string modelData
|
||||
readonly property var target: root.opacityTargets.find(entry => entry.key === modelData) ?? null
|
||||
|
||||
tab: "theme"
|
||||
tags: ["surface", "opacity", "transparency", "bar", "dock", "override"]
|
||||
settingKey: "surfaceOpacity_" + modelData.kind + "_" + modelData.id
|
||||
text: modelData.name
|
||||
settingKey: "surfaceOpacity_" + modelData.replace(":", "_")
|
||||
text: target?.name ?? ""
|
||||
description: I18n.tr("Override")
|
||||
checked: modelData.override
|
||||
modified: modelData.override
|
||||
checked: target?.override ?? false
|
||||
modified: target?.override ?? false
|
||||
resetByKeys: false
|
||||
onResetRequested: root.setOpacityOverride(modelData, {
|
||||
onResetRequested: root.setOpacityOverride(target, {
|
||||
followInterfaceStyle: true,
|
||||
transparency: 1
|
||||
})
|
||||
onToggled: checked => root.setOpacityOverride(modelData, {
|
||||
onToggled: checked => root.setOpacityOverride(target, {
|
||||
followInterfaceStyle: !checked
|
||||
})
|
||||
|
||||
body: SettingsSliderRow {
|
||||
width: parent.width
|
||||
enabled: overrideRow.modelData.override
|
||||
enabled: overrideRow.target?.override ?? false
|
||||
text: I18n.tr("Opacity")
|
||||
value: Math.round(overrideRow.modelData.transparency * 100)
|
||||
value: Math.round((overrideRow.target?.transparency ?? 1) * 100)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
modified: value !== 100
|
||||
resetByKeys: false
|
||||
onResetRequested: root.setOpacityOverride(overrideRow.modelData, {
|
||||
onResetRequested: root.setOpacityOverride(overrideRow.target, {
|
||||
transparency: 1
|
||||
})
|
||||
onSliderDragFinished: finalValue => root.setOpacityOverride(overrideRow.modelData, {
|
||||
onSliderDragFinished: finalValue => root.setOpacityOverride(overrideRow.target, {
|
||||
transparency: finalValue / 100
|
||||
})
|
||||
}
|
||||
@@ -430,6 +433,9 @@ Column {
|
||||
readonly property var targets: root.opacityTargets.filter(target => target.kind === modelData.kind)
|
||||
readonly property var activeTargets: targets.filter(target => target.enabled)
|
||||
readonly property var hiddenTargets: targets.filter(target => !target.enabled)
|
||||
// String keys: a config edit must not rebuild the row holding the focused control
|
||||
readonly property string activeKeys: activeTargets.map(target => target.key).join("\n")
|
||||
readonly property string hiddenKeys: hiddenTargets.map(target => target.key).join("\n")
|
||||
property bool showHidden: false
|
||||
|
||||
tab: "theme"
|
||||
@@ -439,7 +445,7 @@ Column {
|
||||
visible: targets.length > 0
|
||||
|
||||
Repeater {
|
||||
model: targetCard.activeTargets
|
||||
model: targetCard.activeKeys ? targetCard.activeKeys.split("\n") : []
|
||||
delegate: opacityTargetRow
|
||||
}
|
||||
|
||||
@@ -461,7 +467,7 @@ Column {
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: targetCard.showHidden ? targetCard.hiddenTargets : []
|
||||
model: targetCard.showHidden && targetCard.hiddenKeys ? targetCard.hiddenKeys.split("\n") : []
|
||||
delegate: opacityTargetRow
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ Singleton {
|
||||
readonly property string displayName: {
|
||||
switch (compositor) {
|
||||
case "niri":
|
||||
return "Niri";
|
||||
return "niri";
|
||||
case "hyprland":
|
||||
return "Hyprland";
|
||||
case "mango":
|
||||
|
||||
@@ -9933,6 +9933,14 @@
|
||||
"runtimeType": "barWidgetOption",
|
||||
"optionFile": "IslandOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:MusicOptions.qml:4",
|
||||
"label": "Cover art",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
"runtimeType": "barWidgetOption",
|
||||
"optionFile": "MusicOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:RunningAppsOptions.qml:4",
|
||||
"label": "Current display",
|
||||
@@ -10294,7 +10302,7 @@
|
||||
"optionFile": "IslandOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:MusicOptions.qml:5",
|
||||
"section": "barWidgetOption:MusicOptions.qml:6",
|
||||
"label": "Media player",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
@@ -10438,7 +10446,7 @@
|
||||
"optionFile": "PrivacyOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:MusicOptions.qml:4",
|
||||
"section": "barWidgetOption:MusicOptions.qml:5",
|
||||
"label": "Scroll wheel",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
|
||||
Reference in New Issue
Block a user