feat(transport): redesign transport bar

This commit is contained in:
jack
2026-06-29 14:57:52 -07:00
parent c9cb7ab8d8
commit db21f201f6
+321 -85
View File
@@ -3,101 +3,337 @@ import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import ISC import ISC
RowLayout { // ===== Control Bar =====
spacing: 2 // Mode-aware footer: cross-fades between Live and Review layouts.
// Both variants render as a single centered pill — no full-width bar.
component TBtn: Button { Item {
implicitWidth: 44 id: bar
implicitHeight: 32 Layout.fillWidth: true
hoverEnabled: true implicitHeight: hasContent ? 80 : 0
font.pixelSize: 13 Behavior on implicitHeight {
background: Rectangle { NumberAnimation { duration: Theme.durationBase; easing.type: Theme.easeStandard }
color: parent.pressed ? Theme.transportButtonHover
: parent.hovered ? Theme.transportButtonBg
: Theme.transportBackground
radius: Theme.radiusSm
}
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
} }
clip: true
// Frame counter badge on the left // Hide entirely when in review mode with no file loaded
Rectangle { readonly property bool hasContent: {
id: frameCounter if (streamController.mode === "live") return true;
implicitWidth: frameCounterText.implicitWidth + 16 return streamController.loadedFile !== "";
implicitHeight: 32 }
color: Theme.fieldBackground readonly property bool isLive: streamController.mode === "live"
border.color: Theme.cardBorder
border.width: Theme.borderThin
radius: Theme.radiusSm
Text { // ── LIVE MODE ─────────────────────────────────────────────────
id: frameCounterText Item {
id: liveContent
anchors.fill: parent
opacity: bar.isLive ? 1.0 : 0.0
visible: opacity > 0
Behavior on opacity {
NumberAnimation { duration: Theme.durationBase; easing.type: Theme.easeStandard }
}
// Single centered pill for Received + Errors + Stop
Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
text: "Frame " + (streamController.frameIndex + 1) + " / " + streamController.frameTotal implicitWidth: liveRow.implicitWidth + 48
color: Theme.headingColor implicitHeight: 68
font.pixelSize: 11 radius: Theme.radiusMd
font.weight: Font.Medium color: Theme.sidePanelBackground
border.color: Theme.sideBorder
border.width: 1
Row {
id: liveRow
anchors.centerIn: parent
spacing: 24
// Received counter
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Received: " + streamController.receivedCount
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontMd
font.weight: Font.Medium
}
// Separator
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 36
color: Theme.sideBorder
}
// Errors counter
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Errors: " + streamController.errorCount
color: streamController.errorCount > 0
? Theme.statusWarningColor : Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontMd
font.weight: Font.Medium
}
// Separator
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 36
color: Theme.sideBorder
}
// STOP button
Button {
id: stopBtn
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 112
implicitHeight: 48
hoverEnabled: true
text: "STOP"
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontSm
font.weight: Font.Medium
font.letterSpacing: 1.0
background: Rectangle {
radius: 8
color: stopBtn.pressed ? Theme.transportButtonHover
: Theme.transportButtonBg
border.color: Theme.sideBorder
border.width: 1
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
}
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: streamController.stopStream()
}
}
} }
} }
// Left fill — centers the button cluster // ── REVIEW MODE ──────────────────────────────────────────────
Item { Layout.fillWidth: true } Item {
id: reviewContent
TBtn { text: "⏮"; onClicked: streamController.step(-1) } anchors.fill: parent
TBtn { text: "⏹"; onClicked: streamController.stop() } opacity: !bar.isLive ? 1.0 : 0.0
TBtn { text: "⏸"; onClicked: streamController.pause() } visible: opacity > 0
// Play: slightly wider; leftPadding nudge compensates for ▶ optical offset Behavior on opacity {
TBtn { NumberAnimation { duration: Theme.durationBase; easing.type: Theme.easeStandard }
text: "▶"
implicitWidth: 52
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
leftPadding: 2
} }
onClicked: streamController.play()
}
TBtn { text: "⏭"; onClicked: streamController.step(1) }
// Right fill — balances centering // Single centered pill for frame counter + transport + speed
Item { Layout.fillWidth: true } Rectangle {
anchors.centerIn: parent
implicitWidth: reviewRow.implicitWidth + 48
implicitHeight: 68
radius: Theme.radiusMd
color: Theme.sidePanelBackground
border.color: Theme.sideBorder
border.width: 1
// Speed cycle Row {
Button { id: reviewRow
id: speedBtn anchors.centerIn: parent
property var speeds: [1, 2, 5] spacing: 24
property int idx: 0
implicitWidth: 44 // Frame counter
implicitHeight: 32 Text {
hoverEnabled: true anchors.verticalCenter: parent.verticalCenter
text: speeds[idx] + "×" text: "Frame " + (streamController.frameIndex + 1)
font.pixelSize: 11 + " / " + streamController.frameTotal
font.weight: Font.Medium color: Theme.sideMutedText
background: Rectangle { font.family: Theme.uiFontFamily
color: parent.pressed ? Theme.transportButtonHover font.pixelSize: Theme.fontMd
: parent.hovered ? Theme.transportButtonBg font.weight: Font.Medium
: Theme.transportBackground leftPadding: 8
radius: Theme.radiusSm }
}
contentItem: Text { // Separator 1
text: parent.text Rectangle {
color: Theme.bodyColor anchors.verticalCenter: parent.verticalCenter
font: parent.font width: 1
horizontalAlignment: Text.AlignHCenter height: 36
verticalAlignment: Text.AlignVCenter color: Theme.sideBorder
} }
onClicked: {
idx = (idx + 1) % speeds.length // Media Buttons Row
streamController.setSpeed(speeds[idx]) Row {
anchors.verticalCenter: parent.verticalCenter
spacing: 12
// Skip-start
Button {
id: skipStartBtn
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 56
implicitHeight: 56
hoverEnabled: true
text: "⏮"
font.pixelSize: 24
background: Rectangle {
radius: 8
color: skipStartBtn.pressed ? Theme.transportButtonHover
: skipStartBtn.hovered ? Theme.transportButtonBg
: "transparent"
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
}
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: streamController.step(-1)
}
// Stop
Button {
id: reviewStopBtn
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 56
implicitHeight: 56
hoverEnabled: true
text: "■"
font.pixelSize: 22
background: Rectangle {
radius: 8
color: reviewStopBtn.pressed ? Theme.transportButtonHover
: reviewStopBtn.hovered ? Theme.transportButtonBg
: "transparent"
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
}
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: streamController.stop()
}
// Play / Pause toggle
Button {
id: playPauseBtn
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 64
implicitHeight: 56
hoverEnabled: true
text: streamController.playing ? "⏸" : "▶"
font.pixelSize: 24
background: Rectangle {
radius: 8
color: playPauseBtn.pressed ? Theme.transportButtonHover
: playPauseBtn.hovered ? Theme.transportButtonBg
: "transparent"
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
}
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
leftPadding: parent.text === "▶" ? 4 : 0
}
onClicked: {
if (streamController.playing) {
streamController.pause();
} else {
streamController.play();
}
}
}
// Skip-end
Button {
id: skipEndBtn
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 56
implicitHeight: 56
hoverEnabled: true
text: "⏭"
font.pixelSize: 24
background: Rectangle {
radius: 8
color: skipEndBtn.pressed ? Theme.transportButtonHover
: skipEndBtn.hovered ? Theme.transportButtonBg
: "transparent"
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
}
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: streamController.step(1)
}
}
// Separator 2
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 36
color: Theme.sideBorder
}
// Speed capsule button
Button {
id: speedBtn
anchors.verticalCenter: parent.verticalCenter
property var speeds: [1, 2, 4]
property int idx: 0
implicitWidth: 72
implicitHeight: 48
hoverEnabled: true
text: speeds[idx] + "x"
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontSm
font.weight: Font.Medium
background: Rectangle {
radius: 8
color: speedBtn.pressed ? Theme.transportButtonHover
: Theme.transportButtonBg
border.color: Theme.sideBorder
border.width: 1
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
}
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: {
idx = (idx + 1) % speeds.length;
streamController.setSpeed(speeds[idx]);
}
}
}
} }
} }
} }