import QtQuick
import QtQuick.Controls.Basic
// The "Format Mix" task pane: what's playing, a sparkline to seek with, and the stems as a chart.
Rectangle {
id: pane
property var win: Window.window
color: "#FFFFFF"
Rectangle { width: 1; height: parent.height; color: X.ribbonLine }
Item {
id: head
x: 1
width: parent.width - 1
height: 40
T { x: 14; height: parent.height; text: "Format Mix"; font.pixelSize: 17; color: X.ink }
Ico { anchors.right: parent.right; anchors.rightMargin: 34; anchors.verticalCenter: parent.verticalCenter; name: "dropdown"; size: 12 }
Item {
anchors { right: parent.right; rightMargin: 6; verticalCenter: parent.verticalCenter }
width: 24
height: 24
Rectangle { anchors.fill: parent; color: cm.containsMouse ? X.hover : "transparent" }
Ico { anchors.centerIn: parent; name: "close"; size: 20 }
MouseArea { id: cm; anchors.fill: parent; hoverEnabled: true; onClicked: pane.win.toggleMixer() }
}
}
Flickable {
id: flick
x: 1
y: head.height
width: parent.width - 1
height: parent.height - y
contentHeight: col.height + 20
clip: true
boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: XScrollBar { policy: flick.contentHeight > flick.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff }
Column {
id: col
x: 14
width: flick.width - 34
spacing: 10
// ---- now playing ----
Row {
width: parent.width
spacing: 12
Rectangle {
width: 128
height: 128
color: "#F2F2F2"
border.color: "#D9D9D9"
Image {
id: art
anchors.fill: parent
anchors.margins: 1
source: player.track.art || ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
sourceSize.width: 256
sourceSize.height: 256
}
Ico { visible: art.status !== Image.Ready; anchors.centerIn: parent; name: "excel"; size: 48; opacity: 0.25 }
}
Column {
width: parent.width - 140
anchors.verticalCenter: parent.verticalCenter
spacing: 3
Text {
width: parent.width
text: player.track.title || "Nothing playing"
font.family: uiFont
font.pixelSize: 16
font.bold: true
color: "#222222"
wrapMode: Text.WordWrap
maximumLineCount: 3
elide: Text.ElideRight
}
T { width: parent.width; text: player.track.artist || "Double-click a song to play it"; font.pixelSize: 13; color: X.inkSoft }
Text {
width: parent.width
visible: !!player.track.album
text: (player.track.album || "") + (player.track.year ? " ยท " + player.track.year : "")
font.family: uiFont
font.pixelSize: 12
color: X.inkFaint
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
}
}
}
// ---- the sparkline seek bar ----
Item {
id: spark
width: parent.width
height: 46
readonly property real dur: engine.duration > 0 ? engine.duration : (player.track.durationMs || 0) / 1000
readonly property real frac: dur > 0 ? Math.min(1, pane.win.playPos / dur) : 0
readonly property var bars: player.wave.length ? player.wave : []
readonly property int n: bars.length ? bars.length : 80
property real hoverFrac: -1
Rectangle { anchors.fill: parent; color: "#FFFFFF"; border.color: "#D9D9D9" }
Canvas {
id: sparkCanvas
anchors.fill: parent
anchors.margins: 3
property real frac: spark.frac
property var bars: spark.bars
onFracChanged: requestPaint()
onBarsChanged: requestPaint()
onWidthChanged: requestPaint()
onPaint: {
var c = getContext("2d")
c.reset()
var n = spark.n, w = width / n, h = height
for (var i = 0; i < n; i++) {
var v = bars.length ? bars[i] : 0.12
var bh = Math.max(2, v * h)
c.fillStyle = (i + 0.5) / n <= frac ? "#217346" : "#A9D08E"
c.fillRect(i * w + w * 0.15, h - bh, Math.max(1, w * 0.7), bh)
}
}
}
Rectangle { // playhead
visible: spark.dur > 0
x: 3 + spark.frac * (parent.width - 6) - 1
y: 2
width: 2
height: parent.height - 4
color: "#C00000"
}
Rectangle {
visible: spark.hoverFrac >= 0
x: Math.max(0, Math.min(parent.width - width, spark.hoverFrac * parent.width - width / 2))
y: -22
width: hv.implicitWidth + 10
height: 18
color: "#FFFFFF"
border.color: "#BEBEBE"
T { id: hv; anchors.centerIn: parent; font.pixelSize: X.fsSmall; text: X.time(spark.hoverFrac * spark.dur) }
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
function f(m) { return Math.max(0, Math.min(1, (m.x - 3) / (width - 6))) }
onPositionChanged: (m) => { spark.hoverFrac = f(m); if (pressed) pane.win.seekTo(f(m) * spark.dur) }
onExited: spark.hoverFrac = -1
onPressed: (m) => pane.win.seekTo(f(m) * spark.dur)
}
}
Item {
width: parent.width
height: 14
y: -6
T { font.pixelSize: X.fsSmall; color: X.inkSoft; text: X.time(pane.win.playPos) }
T {
anchors.right: parent.right
font.pixelSize: X.fsSmall
color: X.inkSoft
text: spark.dur > 0 ? "-" + X.time(spark.dur - pane.win.playPos) : ""
}
}
// ---- transport ----
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 6
PaneButton { icon: "shuffle"; checked: player.shuffleOn; tip: "Shuffle"; onClicked: player.toggleShuffle() }
PaneButton { icon: "previous"; tip: "Previous"; onClicked: player.previous() }
PaneButton {
icon: player.status === "playing" ? "pause" : "play"
size: 40
tip: "Play / Pause (Space)"
onClicked: player.toggle()
}
PaneButton { icon: "next"; tip: "Next"; onClicked: player.next() }
PaneButton {
icon: player.repeatMode === "one" ? "repeat_one" : "repeat"
checked: player.repeatMode !== "off"
tip: player.repeatMode === "off" ? "Repeat" : player.repeatMode === "all" ? "Repeat all" : "Repeat one"
onClicked: player.cycleRepeat()
}
}
// output meter, as a pair of data bars
Column {
width: parent.width
spacing: 2
Repeater {
model: 2
delegate: Rectangle {
required property int index
width: parent.width
height: 5
color: "#EDEDED"
Rectangle {
readonly property real lv: index === 0 ? pane.win.levelL : pane.win.levelR
readonly property real db: 20 * Math.log(Math.max(lv, 1e-5)) / Math.LN10
width: parent.width * Math.max(0, Math.min(1, (db + 48) / 48))
height: parent.height
color: db > -1 ? "#C00000" : db > -9 ? "#FFC000" : X.dataBar
}
}
}
}
// ---- section: Stem Mix ----
Row {
spacing: 6
height: 24
Ico { anchors.verticalCenter: parent.verticalCenter; name: "dropdown"; size: 10 }
T { anchors.verticalCenter: parent.verticalCenter; text: "Stem Mix"; font.bold: true; font.pixelSize: 13 }
}
Rectangle { // "comment" for songs without stems
visible: player.sourceKind === "original"
width: parent.width
height: noteTxt.implicitHeight + 16
color: X.note
border.color: "#B2B2B2"
Text {
id: noteTxt
x: 8
y: 8
width: parent.width - 16
wrapMode: Text.WordWrap
font.family: uiFont
font.pixelSize: X.fsUi
textFormat: Text.StyledText
text: "Stemify:
This song hasn't been separated yet, so it plays the original file. The server's stemd will get to it."
}
}
StemChart {
visible: player.sourceKind === "stems"
width: parent.width
height: 250
}
Row {
visible: player.sourceKind === "stems"
spacing: 8
T { height: 22; text: "Mix style:" }
Rectangle {
width: 150
height: 22
border.color: pm.containsMouse ? X.green : "#ABABAB"
T { x: 6; height: parent.height; width: parent.width - 24; text: player.presetLabel }
Ico { anchors.right: parent.right; anchors.rightMargin: 5; anchors.verticalCenter: parent.verticalCenter; name: "dropdown"; size: 10 }
MouseArea { id: pm; anchors.fill: parent; hoverEnabled: true; onClicked: presetMenu.popup() }
}
}
XCheck {
text: "Keep the mix for the next song"
checked: player.keepMix
onToggled: player.setKeepMix(!player.keepMix)
}
}
}
XMenu {
id: presetMenu
Instantiator {
model: player.presets
delegate: MenuItem {
required property int index
required property var modelData
text: modelData.label
enabled: modelData.usable
checkable: true
checked: modelData.active
onTriggered: player.applyPreset(index)
}
onObjectAdded: (i, o) => presetMenu.insertItem(i, o)
onObjectRemoved: (i, o) => presetMenu.removeItem(o)
}
}
component PaneButton: Item {
id: pb
property string icon
property bool checked: false
property int size: 32
property string tip: ""
signal clicked()
width: size
height: size
Rectangle {
anchors.fill: parent
radius: pb.size / 2
color: pb.checked ? X.checked : pbm.pressed ? X.pressed : pbm.containsMouse ? X.hover : "transparent"
}
Ico { anchors.centerIn: parent; name: pb.icon; size: Math.round(pb.size * 0.55) }
MouseArea { id: pbm; anchors.fill: parent; hoverEnabled: true; onClicked: pb.clicked() }
Tip { target: pbm; text: pb.tip }
}
}