Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/v1-presentation-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,11 @@ remain open acceptance gates.
G9 must preserve V1's idle note, previous/play/next controls, fixed-width
marquee, playing pulse, tooltip, cover/progress panel, and selectable
FULL/muse outcome: 24-band real Cava, vinyl mark, transport state, click
play/pause, and wheel previous/next. State, player selection, source selection,
play/pause, and wheel previous/next. An optional Icon presentation keeps the
default row's play/pause control and playing pulse but drops the skip controls
and title marquee, so a playing track adds little width on narrow outputs and
does not push the bar into a responsive stage that hides G9; the lazy panel
keeps the full controls. State, player selection, source selection,
and every transport action come from the single official `omarchy.media`
service. One lazy process-wide Shibumi spectrum service may own the bounded
Cava process and degraded state; no per-output, per-panel, or per-view process
Expand Down
8 changes: 5 additions & 3 deletions hancore.shibumi.control-center/WidgetAppearanceWorkbench.qml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ Column {
]
readonly property var mediaStyleOptions: [
{ value: "default", label: "Default" },
{ value: "full", label: "Compact" }
{ value: "full", label: "Compact" },
{ value: "icon", label: "Icon" }
]
readonly property var v1CompactGroupIds: [
"G4", "G5", "G6", "G11", "G12", "G13", "G14", "G15", "G18"
Expand Down Expand Up @@ -484,8 +485,9 @@ Column {
const catalogGroup = String(catalogGroupValue
|| catalogGroupForSettingsGroup(group) || "")
if (catalogGroup === "G9")
return String(widgetSetting(group, "mediaStyle", "default")) === "full"
? "full" : "default"
return ["full", "icon"].indexOf(
String(widgetSetting(group, "mediaStyle", "default"))) >= 0
? String(widgetSetting(group, "mediaStyle", "default")) : "default"
const stored = String(widgetSetting(group, "displayMode", ""))
const mode = stored !== "" ? stored
: widgetSetting(group, "compact", false) === true ? "icon" : "full"
Expand Down
22 changes: 16 additions & 6 deletions hancore.shibumi.media/BarWidget.qml
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,18 @@ Ui.Panel {
? tokens.widgetContentColor(settings,
bar ? bar.urgent : Commons.Color.accent)
: (bar ? bar.urgent : Commons.Color.accent)
// Now Playing follows the two-presentation contract shared by the original
// Now Playing follows the presentation contract shared by the original
// V1 and V2 shells. It is deliberately not part of the generic
// icon/text/full content-mode family used by the other V2 widgets.
readonly property string mediaStyle:
String(setting("mediaStyle", "default")) === "full"
? "full" : "default"
readonly property bool defaultMode: mediaStyle === "default"
// "icon" is Default without the skip controls and the title marquee, so
// a playing track adds little width and narrow outputs do not drop into a
// responsive stage that hides this group; the panel keeps the full controls.
readonly property string mediaStyle: {
const style = String(setting("mediaStyle", "default"))
return style === "full" || style === "icon" ? style : "default"
}
readonly property bool iconStyle: mediaStyle === "icon"
readonly property bool defaultMode: mediaStyle === "default" || iconStyle
readonly property bool fullMode: mediaStyle === "full"
readonly property bool iconMode: false
readonly property bool textMode: false
Expand All @@ -64,6 +69,7 @@ Ui.Panel {
readonly property bool textVisible: activeText.visible
readonly property bool iconVisible: activeIcon.visible
readonly property bool v1FullVisible: activeRow.visible
readonly property bool marqueeVisible: marqueeClip.visible

visible: mediaService !== null
implicitWidth: visible ? (bar && bar.vertical
Expand Down Expand Up @@ -194,6 +200,7 @@ Ui.Panel {
spacing: root.tokens ? root.tokens.compactGap : Commons.Style.space(4)

MediaControl {
visible: !root.iconStyle
icon: "skip_previous"
enabled: root.activePlayer && root.activePlayer.canGoPrevious === true
action: "previous"
Expand All @@ -208,6 +215,7 @@ Ui.Panel {
}

MediaControl {
visible: !root.iconStyle
icon: "skip_next"
enabled: root.activePlayer && root.activePlayer.canGoNext === true
action: "next"
Expand All @@ -234,14 +242,15 @@ Ui.Panel {
Item {
id: marqueeClip
width: Commons.Style.space(88)
visible: !root.iconStyle
height: mediaSurface.height
anchors.verticalCenter: parent.verticalCenter

function reset() {
marqueeAnimation.stop()
marqueeText.x = 0
Qt.callLater(function() {
if (root.visible && root.playing && !root.opened
if (root.visible && !root.iconStyle && root.playing && !root.opened
&& marqueeText.implicitWidth > marqueeClip.width)
marqueeAnimation.start()
})
Expand Down Expand Up @@ -288,6 +297,7 @@ Ui.Panel {
function onPlayingChanged() { marqueeClip.reset() }
function onOpenedChanged() { marqueeClip.reset() }
function onVisibleChanged() { marqueeClip.reset() }
function onIconStyleChanged() { marqueeClip.reset() }
}

SequentialAnimation {
Expand Down
10 changes: 8 additions & 2 deletions hancore.shibumi.state/Service.qml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ Item {
? appearance[variant] : ({})
}

// G9 Now Playing presentations: Default, Full (muse) and Icon.
function normalizedMediaStyle(value) {
const style = String(value || "default")
return style === "full" || style === "icon" ? style : "default"
}

function normalizedDisplayMode(groupId, variantValue, value) {
const group = String(groupId || "")
const variant = normalizedVariant(variantValue)
Expand Down Expand Up @@ -215,7 +221,7 @@ Item {
&& !Object.prototype.hasOwnProperty.call(settings, name)
&& settings.compact === true)
value = "full"
value = String(value || "default") === "full" ? "full" : "default"
value = normalizedMediaStyle(value)
}
return value
}
Expand Down Expand Up @@ -334,7 +340,7 @@ Item {
else if (name === "compact")
normalizedValue = value === true
else if (name === "mediaStyle")
normalizedValue = String(value || "") === "full" ? "full" : "default"
normalizedValue = normalizedMediaStyle(value)
else if (name === "batteryOrientation")
normalizedValue = value === "vertical" ? "vertical" : "horizontal"

Expand Down
13 changes: 11 additions & 2 deletions tests/audio-media-plugins-regression.sh
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,18 @@ rg -q 'target: "shibumi-media-spectrum"' \
rg -Fq 'readonly property string mediaStyle:' \
"$repo_root/hancore.shibumi.media/BarWidget.qml" \
|| fail "G9 does not normalize its two-presentation style"
rg -Fq 'String(setting("mediaStyle", "default")) === "full"' \
rg -Fq 'return style === "full" || style === "icon" ? style : "default"' \
"$repo_root/hancore.shibumi.media/BarWidget.qml" \
|| fail "G9 does not expose Default and Full presentations"
|| fail "G9 does not expose Default, Full and Icon presentations"
rg -Fq 'visible: !root.iconStyle' \
"$repo_root/hancore.shibumi.media/BarWidget.qml" \
|| fail "G9 Icon does not drop the skip controls and marquee"
rg -Fq '{ value: "icon", label: "Icon" }' \
"$repo_root/hancore.shibumi.control-center/WidgetAppearanceWorkbench.qml" \
|| fail "Control Center does not offer the G9 Icon presentation"
rg -Fq 'return style === "full" || style === "icon" ? style : "default"' \
"$repo_root/hancore.shibumi.state/Service.qml" \
|| fail "State does not preserve the G9 Icon presentation"
rg -q 'readonly property bool museMode: fullMode' \
"$repo_root/hancore.shibumi.media/BarWidget.qml" \
|| fail "G9 does not share FULL/muse across V1 and V2"
Expand Down
17 changes: 13 additions & 4 deletions tests/control-center-smoke.qml
Original file line number Diff line number Diff line change
Expand Up @@ -837,16 +837,23 @@ ShellRoot {
appearance.showWidgetOverview()
if (!appearance.openWidgetDetails("G9", "")
|| appearance.selectedWidgetMode !== "default"
|| appearance.selectedWidgetModeOptions.length !== 2
|| appearance.selectedWidgetModeOptions.length !== 3
|| appearance.selectedWidgetModeOptions[0].value !== "default"
|| appearance.selectedWidgetModeOptions[0].label !== "Default"
|| appearance.selectedWidgetModeOptions[1].value !== "full"
|| appearance.selectedWidgetModeOptions[1].label !== "Compact"
|| appearance.selectedWidgetModeOptions[2].value !== "icon"
|| appearance.selectedWidgetModeOptions[2].label !== "Icon"
|| !appearance.cycleSelectedWidgetMode()
|| appearance.selectedWidgetMode !== "full"
|| stateService.groupAppearanceSettingForVariant(
"G9", "v2", "mediaStyle", "") !== "default")
return root.fail("V1 Now Playing Default/Compact contract drifted")
return root.fail("V1 Now Playing Default/Compact/Icon contract drifted")
if (!appearance.cycleSelectedWidgetMode()
|| appearance.selectedWidgetMode !== "icon"
|| stateService.groupAppearanceSettingForVariant(
"G9", "v1", "mediaStyle", "") !== "icon")
return root.fail("V1 Now Playing Icon style did not persist")
if (!appearance.cycleSelectedWidgetMode()
|| appearance.selectedWidgetMode !== "default")
return root.fail("V1 Now Playing style did not restore")
Expand Down Expand Up @@ -876,16 +883,18 @@ ShellRoot {
panel.v2LayoutActive = true
if (!appearance.openWidgetDetails("G9", "")
|| appearance.selectedWidgetMode !== "default"
|| appearance.selectedWidgetModeOptions.length !== 2
|| appearance.selectedWidgetModeOptions.length !== 3
|| appearance.selectedWidgetModeOptions[0].value !== "default"
|| appearance.selectedWidgetModeOptions[0].label !== "Default"
|| appearance.selectedWidgetModeOptions[1].value !== "full"
|| appearance.selectedWidgetModeOptions[1].label !== "Compact"
|| appearance.selectedWidgetModeOptions[2].value !== "icon"
|| appearance.selectedWidgetModeOptions[2].label !== "Icon"
|| !appearance.cycleSelectedWidgetMode()
|| appearance.selectedWidgetMode !== "full"
|| stateService.groupAppearanceSettingForVariant(
"G9", "v1", "mediaStyle", "") !== "default")
return root.fail("V2 Now Playing Default/Compact contract drifted")
return root.fail("V2 Now Playing Default/Compact/Icon contract drifted")
if (!appearance.openWidgetDetails("G4", "")
|| appearance.selectedWidgetMode !== "full")
return root.fail("V2 appearance did not remain independent")
Expand Down
30 changes: 25 additions & 5 deletions tests/media-plugin-smoke.qml
Original file line number Diff line number Diff line change
Expand Up @@ -282,14 +282,34 @@ ShellRoot {
if (!media.fullMode || !media.museMode || !media.museVisible
|| media.v1Presentation || fakeSpectrum.clientCount !== 1)
return root.fail("V2 FULL muse presentation/lease")
root.mediaSettings = ({ spectrum: false, mediaStyle: "default" })
root.mediaSettings = ({ spectrum: false, mediaStyle: "icon" })
} else if (root.phase === 3) {
// Icon keeps the default row's play/pause and pulse but drops the
// skip controls and title, so a playing track stays narrow.
if (!media.iconStyle || !media.defaultMode || media.fullMode
|| !media.v1FullVisible || media.museVisible
|| fakeSpectrum.clientCount !== 0)
return root.fail("icon presentation")
// Width follows the row after a polish, which this headless window
// does not run; the real-bar width drop is covered by the hidden
// marquee and the static check on the skip controls.
if (media.marqueeVisible)
return root.fail("icon presentation still shows the marquee")
media.interactionTarget.triggerPress(Qt.LeftButton)
if (mediaState.lastAction !== "playPause" || playerA.isPlaying)
return root.fail("icon play/pause action forwarding")
media.interactionTarget.triggerPress(Qt.LeftButton)
if (!playerA.isPlaying)
return root.fail("icon play/pause did not resume")
root.mediaSettings = ({ spectrum: false, mediaStyle: "default" })
} else if (root.phase === 4) {
if (media.fullMode || !media.defaultMode || !media.v1FullVisible
|| media.iconStyle || !media.marqueeVisible
|| media.museVisible || fakeSpectrum.clientCount !== 0)
return root.fail("shared default presentation")
fakeShell.spectrumBackend = null
media.interactionTarget.triggerPress(Qt.RightButton)
} else if (root.phase === 4) {
} else if (root.phase === 5) {
if (!media.opened || !media.panelLoaded || !media.panelItem) {
if (++root.waits > 45) return root.fail("real media panel did not load")
return
Expand Down Expand Up @@ -362,15 +382,15 @@ ShellRoot {
|| mediaState.activePlayer !== playerB)
return root.fail("source selection forwarding")
media.close()
} else if (root.phase === 5) {
} else if (root.phase === 6) {
if (media.opened || media.panelLoaded || media.activePlayer !== playerB)
return root.fail("lazy panel close")
mediaState.activePlayer = null
} else if (root.phase === 6) {
} else if (root.phase === 7) {
if (media.active || media.implicitWidth >= root.activeWidth)
return root.fail("idle V1 presentation")
media.interactionTarget.triggerPress(Qt.LeftButton)
} else if (root.phase === 7) {
} else if (root.phase === 8) {
if (!media.opened || !media.panelLoaded) return
media.close()
mediaLoader.active = false
Expand Down