- Python package stemroller/ -> stemify/, Android package com.stemroller.mobile -> com.stemify.mobile, app names, QML module, icons, desktop entries, MPRIS and Plex product names - Done-markers are now .stemify.json; stemd renames the old ones once at startup so songs separated before the rename aren't separated again - A top-level README covering the server, the Android and Android Auto app, the EQ, Stem Now and the desktop apps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
5.2 KiB
QML
148 lines
5.2 KiB
QML
import QtQuick
|
|
import QtQuick.Controls.Basic
|
|
|
|
// One lane per stem: color strip, solo / mute / gain / export, waveform texture.
|
|
Item {
|
|
id: lane
|
|
required property int index
|
|
required property var modelData
|
|
property string jobId: ""
|
|
property real viewStart: 0
|
|
property real viewEnd: 1
|
|
property int waveW: 100
|
|
property int waveH: 60
|
|
property real waveX: 0
|
|
property bool anySolo: false
|
|
property bool solo: false
|
|
property bool mute: false
|
|
property real gainDb: 0
|
|
signal soloToggled(bool on)
|
|
readonly property string stem: modelData.name
|
|
readonly property color tone: modelData.color
|
|
readonly property bool audible: anySolo ? solo : !mute
|
|
height: 92
|
|
|
|
Card {
|
|
anchors.fill: parent
|
|
clip: false
|
|
|
|
// color strip with soft glow
|
|
Rectangle {
|
|
x: 0
|
|
width: 4
|
|
height: parent.height
|
|
topLeftRadius: 8
|
|
bottomLeftRadius: 8
|
|
color: lane.tone
|
|
}
|
|
Rectangle {
|
|
x: 4
|
|
width: 24
|
|
height: parent.height
|
|
gradient: Gradient {
|
|
orientation: Gradient.Horizontal
|
|
GradientStop { position: 0; color: Theme.alpha(lane.tone, 0.14) }
|
|
GradientStop { position: 1; color: Theme.alpha(lane.tone, 0.0) }
|
|
}
|
|
}
|
|
|
|
Column {
|
|
id: controls
|
|
x: 18
|
|
width: lane.waveX - 30
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 6
|
|
Row {
|
|
spacing: 6
|
|
width: parent.width
|
|
Txt {
|
|
width: parent.width - 3 * 30 - 12
|
|
text: lane.stem.charAt(0).toUpperCase() + lane.stem.slice(1)
|
|
font.pixelSize: Theme.fs16
|
|
font.weight: Font.DemiBold
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
IconButton {
|
|
size: 30; iconSize: 16
|
|
iconName: "headphones"
|
|
checkable: true
|
|
checked: lane.solo
|
|
activeColor: Theme.warning
|
|
tip: "Solo"
|
|
onClicked: lane.soloToggled(checked)
|
|
}
|
|
IconButton {
|
|
size: 30; iconSize: 16
|
|
iconName: lane.mute ? "volume_off" : "volume"
|
|
checkable: true
|
|
checked: lane.mute
|
|
activeColor: Theme.error
|
|
tip: "Mute"
|
|
onClicked: { lane.mute = checked; audio.setMute(lane.index, checked) }
|
|
}
|
|
IconButton {
|
|
size: 30; iconSize: 16
|
|
iconName: "download"
|
|
tip: "Export " + lane.stem
|
|
onClicked: app.exportStem(lane.stem)
|
|
}
|
|
}
|
|
Row {
|
|
spacing: 8
|
|
width: parent.width
|
|
AeroSlider {
|
|
id: gain
|
|
width: parent.width - 52
|
|
from: -24
|
|
to: 12
|
|
stepSize: 0.5
|
|
value: lane.gainDb
|
|
tone: lane.tone
|
|
onMoved: { lane.gainDb = value; audio.setGain(lane.index, value) }
|
|
// double-click resets to unity
|
|
TapHandler { onDoubleTapped: { lane.gainDb = 0; audio.setGain(lane.index, 0) } }
|
|
}
|
|
Txt {
|
|
width: 44
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
horizontalAlignment: Text.AlignRight
|
|
text: (lane.gainDb > 0 ? "+" : "") + lane.gainDb.toFixed(1) + " dB"
|
|
font.pixelSize: 11
|
|
font.features: { "tnum": 1 }
|
|
color: lane.gainDb === 0 ? Theme.textLo : Theme.textHi
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { // waveform well
|
|
x: lane.waveX
|
|
y: 8
|
|
width: lane.waveW
|
|
height: parent.height - 16
|
|
radius: 6
|
|
color: Qt.rgba(0, 0, 0, 0.22)
|
|
border.color: Qt.rgba(1, 1, 1, 0.04)
|
|
Rectangle {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: parent.width
|
|
height: 1
|
|
color: Theme.alpha(lane.tone, 0.18)
|
|
}
|
|
Image {
|
|
anchors.fill: parent
|
|
asynchronous: true
|
|
retainWhileLoading: true
|
|
cache: false
|
|
smooth: true
|
|
opacity: lane.audible ? 1 : 0.28
|
|
Behavior on opacity { NumberAnimation { duration: Theme.med } }
|
|
source: lane.jobId && lane.waveW > 4
|
|
? "image://wave/" + lane.jobId + "/" + lane.stem + "/"
|
|
+ Math.round(lane.waveW * Screen.devicePixelRatio) + "x" + Math.round((lane.height - 16) * Screen.devicePixelRatio)
|
|
+ "/" + lane.viewStart.toFixed(3) + "/" + lane.viewEnd.toFixed(3) + "/" + Theme.hex(lane.tone)
|
|
: ""
|
|
}
|
|
}
|
|
}
|
|
}
|