Block B3. The owner's verdict on the four blocks that landed before it was that the miniplayer was "no draggeable, no hay controles de ningun tipo. Ni de reproducción, ni para cambiar entre modos, no es pinneable." All of that lived here. qml/miniplayer/ — MiniFooter with its three modes, and the five primitives the reference declares as inner components: MiniSeek, MiniTransport, MiniVolume, TBtn and CapBtn, one file each because a 396-line Slint footer folded into one QML file lands well past the size rule. MiniWindowControls is the hover capsule: five surface tabs, a separator, then expand, pin, backdrop, drag and close, plus the collapsed square trigger. The transport in full and compact is centred by a PHANTOM SPACER the width of the volume button, not by a centred layout — micro has no volume and is the one mode genuinely centred. The seek carries the reference's pending machine: the -1.0 sentinel, the 0.02 convergence window and the 8s watchdog. The capsule is a conditional MOUNT, so leaving the card destroys its expanded flag and its 250ms timer with it. ALWAYS-ON-TOP is new work, not a port: three comments in the Slint tree promise it and no code implements it. The owner asked for it back because Tauri had it and Slint structurally cannot. Qt.WindowStaysOnTopHint, a pin button, and a persisted pref that defaults ON. COLOUR LITERALS: six were carried over from the reference verbatim and every one of them was WRONG. Slint writes #RRGGBBAA; Qt parses #AARRGGBB, so #ffffff33 is not white at 20% but opaque pale yellow — which is exactly how the capsule rendered until it was driven and looked at. This is the second time this trap has landed in this port. Each converted literal now carries the reference form beside it. One of the six is MiniShell's, from B2. mini_qt.rs — surface 0 leaves the temporary clamp, because the micro footer IS the card and now exists. Surfaces 3 and 4 stay clamped until B4. Driven and seen, not inferred: the footer in artwork and micro, the capsule revealing on hover and collapsing on leave, the micro card at 380x57 with its centred transport, and the pin persisting mini_always_on_top across a toggle. The drag handle is wired to startSystemMove on pressed; the VNC platform cannot move a window, so that one is owed a real compositor. cc
147 lines
5.6 KiB
QML
147 lines
5.6 KiB
QML
// MiniVolume — the footer's volume trigger and its popup (2026-08-03
|
|
// miniplayer/tray contract A-29, §4.3.3), port of `component MiniVolume
|
|
// inherits Rectangle` at `crates/qbz-ui/ui/miniplayer/MiniFooter.slint:106-164`.
|
|
//
|
|
// THE POPUP OPENS TO THE RIGHT, not below: `x: root.btn + 8px`,
|
|
// `y: (root.height - 34px) / 2` (:122-123) — 8 px past the trigger's right edge
|
|
// and vertically centred on the trigger's own row, so it sits over the
|
|
// transport rather than over the seek bar.
|
|
//
|
|
// INHERITED ODDITY, REPRODUCED (§12-P5): the popup's mute button is hard-coded
|
|
// `volume-x.svg` and never reflects the mute state (:139), where the TRIGGER
|
|
// does swap between volume-x and volume-2 (:114). Not a bug worth diverging
|
|
// for — it is a mute BUTTON, not a mute indicator.
|
|
//
|
|
// WHY THE DISMISS LAYER IS A HUGE RECTANGLE. Slint gets close-on-click-outside
|
|
// from `PopupWindow`'s `close-policy` (:126). Qt's equivalent would be a
|
|
// QtQuick.Controls Popup, whose overlay would take focus from the frameless
|
|
// mini and whose window-level surface is exactly what §4.3.4 refuses for the
|
|
// capsule. So dismissal is a plain MouseArea sized far past this component and
|
|
// CLIPPED BY THE CARD — MiniShell sets `clip: true`, and Qt clips hit-testing to
|
|
// the clip rect as well as painting, so the layer covers the whole card and
|
|
// nothing beyond it. It sits under the popup (z 1 vs 2) and over the trigger,
|
|
// which is why clicking the trigger a second time closes rather than reopens —
|
|
// 1:1 with a close-on-click-outside policy consuming that click.
|
|
//
|
|
// The reference's 24 px drop shadow (:132-133) is dropped by the port's
|
|
// standing convention for Slint shadows (QbzToast.qml:30, QbzTooltip.qml:51-61,
|
|
// MyQbzModals.qml:23, AddToMixtapeModal.qml:19).
|
|
//
|
|
// NOT PORTED, and it is a REFERENCE GAP rather than an omission: the main bar
|
|
// persists the settled volume on drag-end (`QbzSlider.onReleased` ->
|
|
// QbzPlayer.persistVolume, qml/shell/PlayerBar.qml:582), and the Slint mini
|
|
// does NOT — its slider binds `changed` only (:151) even though
|
|
// `NowPlayingState.persist-volume` exists beside `set-volume`
|
|
// (state.slint:4482-4483). Reported as a divergence PROPOSAL rather than taken,
|
|
// because §13.0 rule 5 is that a divergence never lands without its §13 row.
|
|
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
import "../controls"
|
|
import "../theme"
|
|
|
|
Rectangle {
|
|
id: root
|
|
|
|
property int btn: 30
|
|
/// The TRIGGER's glyph size — the SEVENTH per-mode value, cached by the
|
|
/// footer beside the other six (contract §4.3.2).
|
|
property int iconSize: 18
|
|
/// Read by MiniFooter so it can raise this whole subtree above the
|
|
/// transport while the popup is up.
|
|
property bool popupOpen: false
|
|
|
|
function closePopup() { root.popupOpen = false }
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
width: root.btn
|
|
height: root.btn
|
|
color: "transparent"
|
|
|
|
TBtn {
|
|
name: (QbzPlayer.npMuted || QbzPlayer.npVolume === 0) ? "volume-x" : "volume-2"
|
|
iconSize: root.iconSize
|
|
btn: root.btn
|
|
onClicked: root.popupOpen = true
|
|
}
|
|
|
|
// Close-on-click-outside. See the header for why it is shaped like this.
|
|
MouseArea {
|
|
z: 1
|
|
visible: root.popupOpen
|
|
enabled: root.popupOpen
|
|
x: -1000
|
|
y: -1000
|
|
width: 2000
|
|
height: 2000
|
|
// hoverEnabled stays FALSE so the card's HoverHandler — the thing that
|
|
// mounts the capsule — keeps seeing the pointer through this layer.
|
|
onClicked: root.closePopup()
|
|
}
|
|
|
|
Rectangle {
|
|
id: popup
|
|
z: 2
|
|
visible: root.popupOpen
|
|
x: root.btn + 8
|
|
y: Math.round((root.height - height) / 2)
|
|
width: 172
|
|
height: 34
|
|
radius: 17
|
|
antialiasing: true
|
|
color: theme.surfaceCard
|
|
border.width: 1
|
|
border.color: theme.alphaTier(12)
|
|
|
|
// Swallow clicks on the popup's own background so they do not fall
|
|
// through to the dismiss layer (the Cortinilla.qml:227-232 idiom).
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.AllButtons
|
|
onClicked: {}
|
|
}
|
|
|
|
Row {
|
|
x: 8
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 7
|
|
|
|
TBtn {
|
|
// Always volume-x — see the header (§12-P5).
|
|
name: "volume-x"
|
|
iconSize: 14
|
|
btn: 20
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: QbzPlayer.toggleMute()
|
|
}
|
|
|
|
QbzSlider {
|
|
width: 96
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
minimum: 0
|
|
// 0..1000, not 0..100: the slider is integer-stepped, so a
|
|
// 0..100 scale quantizes the volume to 1 % where 0.1 % drags
|
|
// fluidly (:146-149).
|
|
maximum: 1000
|
|
value: Math.round(QbzPlayer.npVolume * 1000)
|
|
onChanged: function (v) { QbzPlayer.setVolume(v / 1000.0) }
|
|
}
|
|
|
|
Text {
|
|
// 26, not the reference's `min-width: 22`: 8 + 20 + 7 + 96 + 7
|
|
// + 26 = 164 leaves exactly the 8 px right padding the Slint
|
|
// layout ends with, where a hard 22 would leave 12. The number
|
|
// it renders is at most three digits at 10 px.
|
|
width: 26
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: Math.round(QbzPlayer.npVolume * 100)
|
|
color: theme.textMuted
|
|
font.pixelSize: 10
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
}
|
|
}
|