1 Commits
Author SHA1 Message Date
rootandClaude Opus 5.5 47514127d8 publish.sh: dev uploads go to the main admin with ?source=developer (dev-admin :8097 is gone)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 15:24:55 -08:00
+30 -29
View File
@@ -681,10 +681,8 @@ private struct MixerPanel: View {
let names = pm.stemNames let names = pm.stemNames
let stems = pm.source == .Stems let stems = pm.source == .Stems
GeometryReader { geo in GeometryReader { geo in
// Spread spare height across the stem rows instead of leaving it all at the bottom. // Spread spare height across the rows (stems + master) instead of leaving it all at the bottom.
let count = CGFloat(max(names.count, 1)) let rowH = roomy ? Self.roomyRow : min(62, max(46, (geo.size.height - 80) / CGFloat(names.count + 1)))
let rowH = roomy ? min(80, max(Self.roomyRow, (geo.size.height - Self.roomyChrome) / count))
: min(62, max(46, (geo.size.height - 64) / count))
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
if pm.source == .Original { if pm.source == .Original {
@@ -752,7 +750,11 @@ private struct MixerPanel: View {
} }
} }
} }
Spacer(minLength: roomy ? 8 : 16) p.separator.frame(height: 0.5).padding(.vertical, 6)
FaderRow(dot: nil, label: "Master", percent: Int(pm.master * 100), dim: false, height: rowH, stacked: roomy) {
FaderSlider(value: pm.master, fill: p.label, thumb: roomy ? 30 : 24, onChange: pm.setMaster)
} trailing: { EmptyView() }
Spacer(minLength: 16)
} }
.padding(.horizontal, 24) .padding(.horizontal, 24)
} }
@@ -764,11 +766,9 @@ private struct MixerPanel: View {
} }
extension MixerPanel { extension MixerPanel {
static let roomyRow: CGFloat = 66 static let roomyRow: CGFloat = 60
/// Preset chips plus the bottom gap. /// Height that shows the presets, every stem and the master without scrolling, roomy rows.
static let roomyChrome: CGFloat = 56 static func roomyHeight(_ stems: Int) -> CGFloat { 48 + CGFloat(stems + 1) * roomyRow + 13 + 16 }
/// Height that shows the presets and every stem without scrolling, roomy rows.
static func roomyHeight(_ stems: Int) -> CGFloat { roomyChrome + CGFloat(stems) * roomyRow }
} }
/// [Chip] in the sheet's own colours: selected is the label colour, so it reads on any tint. /// [Chip] in the sheet's own colours: selected is the label colour, so it reads on any tint.
@@ -806,11 +806,21 @@ private struct StemFader: View {
var body: some View { var body: some View {
let color = Color(hex: Stems.color(name)) let color = Color(hex: Stems.color(name))
FaderRow(dot: setting.muted ? p.label3 : color, label: Stems.label(name), percent: Int(stemTaper(setting.level) * 100), dim: setting.muted, height: height, stacked: roomy) { FaderRow(dot: setting.muted ? p.label3 : color, label: Stems.label(name), percent: Int(stemTaper(setting.level) * 100), dim: setting.muted, height: height, stacked: roomy) {
// Live level, -48..0 dBFS, drawn as a light band inside the fader's fill. VStack(spacing: 0) {
let f = CGFloat(min(1, max(0, (20 * log10(level + 1e-5) + 48) / 48))) FaderSlider(value: setting.level, fill: setting.muted ? p.label3 : color, detent: StemSetting.unity, enabled: enabled, thumb: roomy ? 30 : 24) {
FaderSlider(value: setting.level, fill: setting.muted ? p.label3 : color, detent: StemSetting.unity, enabled: enabled, thumb: roomy ? 26 : 24, meter: enabled && !setting.muted ? f : nil) {
pm.setStemLevel(name, $0) pm.setStemLevel(name, $0)
} }
// Live level under the fader, -48..0 dBFS.
GeometryReader { geo in
let f = CGFloat(min(1, max(0, (20 * log10(level + 1e-5) + 48) / 48)))
ZStack(alignment: .leading) {
Capsule().fill(p.fill)
Capsule().fill(color).frame(width: geo.size.width * f)
}
}
.frame(height: 3)
.padding(.horizontal, 12)
}
} trailing: { } trailing: {
if enabled { if enabled {
ToggleBox(text: "M", on: setting.muted, onColor: p.red, big: roomy) { pm.toggleMute(name) } ToggleBox(text: "M", on: setting.muted, onColor: p.red, big: roomy) { pm.toggleMute(name) }
@@ -835,8 +845,7 @@ struct FaderRow<Fader: View, Trailing: View>: View {
@Environment(\.palette) private var p @Environment(\.palette) private var p
var body: some View { var body: some View {
if stacked { if stacked {
// Bottom-aligned with a gap under, so each name sits clear of the meter above it. VStack(alignment: .leading, spacing: 0) {
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 6) { HStack(spacing: 6) {
if let dot { Circle().fill(dot).frame(width: 8, height: 8) } if let dot { Circle().fill(dot).frame(width: 8, height: 8) }
Text(label).font(.subheadline.weight(.semibold)).foregroundStyle(dim ? p.label2 : p.label).lineLimit(1) Text(label).font(.subheadline.weight(.semibold)).foregroundStyle(dim ? p.label2 : p.label).lineLimit(1)
@@ -846,8 +855,7 @@ struct FaderRow<Fader: View, Trailing: View>: View {
.frame(height: 18) .frame(height: 18)
HStack(spacing: 8) { fader; trailing } HStack(spacing: 8) { fader; trailing }
} }
.padding(.bottom, 4) .frame(height: height)
.frame(height: height, alignment: .bottom)
} else { } else {
row row
} }
@@ -880,7 +888,7 @@ struct ToggleBox: View {
var body: some View { var body: some View {
Button(action: action) { Button(action: action) {
Text(text).font(.footnote.bold()).foregroundStyle(on ? .black : p.label2) Text(text).font(.footnote.bold()).foregroundStyle(on ? .black : p.label2)
.frame(width: big ? 34 : 30, height: big ? 28 : 26) .frame(width: big ? 38 : 30, height: big ? 32 : 26)
.background(on ? onColor : p.fill, in: RoundedRectangle(cornerRadius: 7, style: .continuous)) .background(on ? onColor : p.fill, in: RoundedRectangle(cornerRadius: 7, style: .continuous))
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@@ -895,19 +903,17 @@ struct FaderSlider: View {
var detent: Float? var detent: Float?
var enabled = true var enabled = true
var thumb: CGFloat = 24 var thumb: CGFloat = 24
/// 0...1 live level, shown as a lighter band over the filled part of the track.
var meter: CGFloat?
let onChange: (Float) -> Void let onChange: (Float) -> Void
@Environment(\.palette) private var p @Environment(\.palette) private var p
@State private var start: Float? @State private var start: Float?
@State private var caught = false @State private var caught = false
init(value: Float, fill: Color, detent: Float? = nil, enabled: Bool = true, thumb: CGFloat = 24, meter: CGFloat? = nil, onChange: @escaping (Float) -> Void) { init(value: Float, fill: Color, detent: Float? = nil, enabled: Bool = true, thumb: CGFloat = 24, onChange: @escaping (Float) -> Void) {
self.value = value; self.fill = fill; self.detent = detent; self.enabled = enabled; self.thumb = thumb; self.meter = meter; self.onChange = onChange self.value = value; self.fill = fill; self.detent = detent; self.enabled = enabled; self.thumb = thumb; self.onChange = onChange
} }
private var bar: CGFloat { meter == nil ? (thumb >= 26 ? 5 : 4) : 6 } private var bar: CGFloat { thumb >= 28 ? 6 : 4 }
var body: some View { var body: some View {
GeometryReader { geo in GeometryReader { geo in
@@ -916,11 +922,6 @@ struct FaderSlider: View {
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
Capsule().fill(p.fillStrong).frame(height: bar).padding(.horizontal, thumb / 2) Capsule().fill(p.fillStrong).frame(height: bar).padding(.horizontal, thumb / 2)
Capsule().fill(fill).frame(width: x + thumb / 2, height: bar).padding(.leading, thumb / 2 - 0.01) Capsule().fill(fill).frame(width: x + thumb / 2, height: bar).padding(.leading, thumb / 2 - 0.01)
if let meter, meter > 0.01 {
Capsule().fill(.white.opacity(0.5)).frame(width: meter * (x + thumb / 2), height: bar)
.padding(.leading, thumb / 2 - 0.01)
.animation(.linear(duration: 1 / 30), value: meter)
}
if let detent { if let detent {
p.label3.frame(width: 2, height: 10).offset(x: CGFloat(detent) * track + thumb / 2 - 1) p.label3.frame(width: 2, height: 10).offset(x: CGFloat(detent) * track + thumb / 2 - 1)
} }
@@ -965,7 +966,7 @@ struct NowPlayingDeck: View {
Text("Stems").font(.headline).foregroundStyle(c.label) Text("Stems").font(.headline).foregroundStyle(c.label)
.frame(maxWidth: .infinity, alignment: .leading).padding(.horizontal, 24) .frame(maxWidth: .infinity, alignment: .leading).padding(.horizontal, 24)
// As much as the stems need to show without scrolling, leaving the queue a few rows. // As much as the stems need to show without scrolling, leaving the queue a few rows.
MixerPanel(roomy: true).frame(height: min(MixerPanel.roomyHeight(max(g.playback.stemNames.count, 4)), geo.size.height * 0.72)) MixerPanel(roomy: true).frame(height: min(MixerPanel.roomyHeight(max(g.playback.stemNames.count, 4)), geo.size.height * 0.68))
Rectangle().fill(c.label3.opacity(0.35)).frame(height: 0.5).padding(.horizontal, 24).padding(.bottom, 8) Rectangle().fill(c.label3.opacity(0.35)).frame(height: 0.5).padding(.horizontal, 24).padding(.bottom, 8)
QueuePanel() QueuePanel()
} }