feat(transport): redesign transport bar
This commit is contained in:
@@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user