- stemroller/: PySide6/QML desktop app and player for separating music into stems - android/: Plex music player that plays per-stem mixes, with Android Auto support, a graphic + parametric (Equalizer APO-style) EQ, and offline downloads - server/: stemserve (serves stems to the app) and stemd (separates the library on the server's GPU, with "Stem Now" priority requests) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
134 lines
4.8 KiB
QML
134 lines
4.8 KiB
QML
import QtQuick
|
|
import QtQuick.Controls.Basic
|
|
|
|
// Tonal glossy dropdown; menu on surface-2 at elevation 3.
|
|
ComboBox {
|
|
id: control
|
|
property string iconName: ""
|
|
property string subRole: "sub"
|
|
property string caption: ""
|
|
property int popupWidth: 280
|
|
textRole: "label"
|
|
valueRole: "id"
|
|
implicitHeight: 36
|
|
hoverEnabled: true
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fs14
|
|
|
|
background: Item {
|
|
Glow { radius: 18; sigma: 4; opacity: (control.hovered || control.visualFocus) ? 0.3 : 0 }
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: height / 2
|
|
color: control.down || control.popup.visible ? Qt.rgba(1, 1, 1, 0.11) : (control.hovered ? Qt.rgba(1, 1, 1, 0.08) : Qt.rgba(1, 1, 1, 0.05))
|
|
border.color: control.popup.visible ? Theme.alpha(Theme.accentGlow, 0.6) : Qt.rgba(1, 1, 1, 0.10)
|
|
Behavior on color { ColorAnimation { duration: Theme.fast } }
|
|
Rectangle {
|
|
anchors { left: parent.left; right: parent.right; top: parent.top; margins: 1 }
|
|
height: (parent.height - 2) / 2
|
|
topLeftRadius: height
|
|
topRightRadius: height
|
|
gradient: Gradient {
|
|
GradientStop { position: 0; color: Qt.rgba(1, 1, 1, 0.08) }
|
|
GradientStop { position: 1; color: Qt.rgba(1, 1, 1, 0.0) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
contentItem: Row {
|
|
leftPadding: control.iconName.length ? 12 : 16
|
|
spacing: 8
|
|
Icon {
|
|
visible: control.iconName.length > 0
|
|
name: control.iconName
|
|
size: 18
|
|
color: Theme.accentGlow
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
Column {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
Txt {
|
|
visible: control.caption.length > 0
|
|
text: control.caption
|
|
font.pixelSize: 10
|
|
font.weight: Font.DemiBold
|
|
font.letterSpacing: 0.6
|
|
color: Theme.textLo
|
|
}
|
|
Txt {
|
|
text: control.displayText
|
|
font: control.font
|
|
width: control.width - 64
|
|
}
|
|
}
|
|
}
|
|
|
|
indicator: Icon {
|
|
x: control.width - width - 10
|
|
anchors.verticalCenter: control.verticalCenter
|
|
name: "expand"
|
|
size: 18
|
|
color: Theme.textLo
|
|
rotation: control.popup.visible ? 180 : 0
|
|
Behavior on rotation { NumberAnimation { duration: Theme.med; easing.type: Easing.BezierSpline; easing.bezierCurve: Theme.ease } }
|
|
}
|
|
|
|
delegate: ItemDelegate {
|
|
id: d
|
|
required property var modelData
|
|
required property int index
|
|
width: ListView.view ? ListView.view.width : control.popupWidth
|
|
height: 52
|
|
highlighted: control.highlightedIndex === index
|
|
hoverEnabled: true
|
|
contentItem: Column {
|
|
spacing: 2
|
|
leftPadding: 4
|
|
anchors.verticalCenter: parent ? parent.verticalCenter : undefined
|
|
Txt {
|
|
text: d.modelData[control.textRole]
|
|
font.weight: control.currentIndex === d.index ? Font.DemiBold : Font.Normal
|
|
color: control.currentIndex === d.index ? Theme.accentGlow : Theme.textHi
|
|
}
|
|
Txt {
|
|
text: d.modelData[control.subRole] || ""
|
|
visible: text.length > 0
|
|
font.pixelSize: Theme.fs12
|
|
color: Theme.textLo
|
|
}
|
|
}
|
|
background: Rectangle {
|
|
radius: 6
|
|
color: d.highlighted ? Theme.alpha(Theme.primary, 0.18) : (d.hovered ? Qt.rgba(1, 1, 1, 0.05) : "transparent")
|
|
}
|
|
}
|
|
|
|
popup: Popup {
|
|
y: control.height + 6
|
|
width: Math.max(control.width, control.popupWidth)
|
|
padding: 6
|
|
transformOrigin: Item.Top
|
|
implicitHeight: Math.min(contentItem.implicitHeight + 12, 420)
|
|
contentItem: ListView {
|
|
clip: true
|
|
implicitHeight: contentHeight
|
|
model: control.popup.visible ? control.delegateModel : null
|
|
currentIndex: control.highlightedIndex
|
|
spacing: 2
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
}
|
|
background: Rectangle {
|
|
color: Theme.surface2
|
|
radius: 8
|
|
border.color: Theme.outline
|
|
Elevation { level: 3; radius: 8 }
|
|
}
|
|
enter: Transition {
|
|
NumberAnimation { property: "opacity"; from: 0; to: 1; duration: Theme.fast }
|
|
NumberAnimation { property: "scale"; from: 0.96; to: 1; duration: Theme.med; easing.type: Easing.BezierSpline; easing.bezierCurve: Theme.ease }
|
|
}
|
|
exit: Transition { NumberAnimation { property: "opacity"; from: 1; to: 0; duration: Theme.fast } }
|
|
}
|
|
}
|