Files

164 lines
8.0 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
/// The mixer bar's presets: order them, delete or add them, bring back deleted built-ins and unlock a locked one.
/// This list is also the rotation for the Mix button in CarPlay, on the lock screen and in the widgets.
struct StemEditorScreen: View {
@Environment(\.palette) private var p
@Environment(Ui.self) private var ui
/// The preset being made or edited; a blank one for New Preset.
@State private var editing: CustomPreset?
private var g: AppGraph { .shared }
var body: some View {
let s = g.settings
let hidden = MixPresets.hidden(s)
let order = MixPresets.ordered(s)
let shown = order.filter { !hidden.contains($0.id) }
let off = order.filter { hidden.contains($0.id) }
let mine = shown.filter(\.custom).count
List {
if let l = MixPresets.locked(s) {
Section {
HStack(spacing: 14) {
SettingsIcon(symbol: "lock.fill", bg: 0xFF9500)
Text(l.label).foregroundStyle(p.label)
Spacer()
Button("Unlock") { s.lockedPreset = "" }
}
.themedRow()
} header: { Text("Locked").foregroundStyle(p.label2) } footer: {
Text("Every song starts with this mix, whether or not Keep Stem Mix Between Songs is on. Long press a preset in the mixer to lock or unlock it.")
.foregroundStyle(p.label2)
}
}
Section {
ForEach(shown) { pr in
HStack(spacing: 14) {
SettingsIcon(symbol: pr.custom ? "person.fill" : "slider.horizontal.3", bg: pr.custom ? 0xAF52DE : 0x5AC8FA)
Text(pr.label).foregroundStyle(p.label)
Spacer()
if pr.custom { Image(systemName: "slider.vertical.3").foregroundStyle(p.label3) }
}
.contentShape(Rectangle())
.onTapGesture { if pr.custom { editing = CustomPreset.load(s).first { $0.name == pr.label } } }
.themedRow()
.deleteDisabled(pr == MixPresets.full)
}
.onMove { from, to in
var l = shown
l.move(fromOffsets: from, toOffset: to)
MixPresets.setOrder(s, l + off)
}
.onDelete { rows in
for pr in rows.map({ shown[$0] }) {
if pr.custom { CustomPreset.remove(pr.label) } else { MixPresets.setHidden(s, pr.id, true) }
if s.lockedPreset == pr.id { s.lockedPreset = "" }
}
}
Button { editing = CustomPreset(name: "", mix: [:]) } label: { Label("New Preset", systemImage: "plus.circle.fill") }
.themedRow()
} header: { Text("Mixer Bar").foregroundStyle(p.label2) } footer: {
Text("Drag to reorder and tap one of yours to change it. The Mix button in CarPlay, on the lock screen and in the widgets goes through this list in this order.")
.foregroundStyle(p.label2)
}
Section {
Button { MixPresets.restoreDefaults(s) } label: {
HStack(spacing: 14) {
SettingsIcon(symbol: "arrow.counterclockwise", bg: 0x34C759)
Text("Restore Deleted Presets").foregroundStyle(off.isEmpty ? p.label3 : p.label)
Spacer()
if !off.isEmpty { Text("\(off.count)").foregroundStyle(p.label2) }
}
}
.disabled(off.isEmpty)
.themedRow()
Button {
let what = mine == 1 ? "your preset" : "all \(mine) of your presets"
ui.overlays.confirm("Delete \(what)?", "This can’t be undone. The built-in presets stay.", "Delete") {
CustomPreset.removeAll()
}
} label: {
HStack(spacing: 14) {
SettingsIcon(symbol: "trash", bg: 0xFF3B30)
Text("Delete All Custom Presets").foregroundStyle(mine == 0 ? p.label3 : p.red)
}
}
.disabled(mine == 0)
.themedRow()
} footer: {
Text(off.isEmpty ? "Every built-in preset is in the list." : "Brings back \(off.map(\.label).joined(separator: ", ")).")
.foregroundStyle(p.label2)
}
}
.listStyle(.insetGrouped)
.themedList()
.environment(\.editMode, .constant(.active))
.navigationTitle("Stem Editor")
.sheet(item: $editing) { PresetSheet(original: $0) }
}
}
/// Builds a preset on the same faders and M / S buttons as the Now Playing mixer, plus a name.
private struct PresetSheet: View {
let original: CustomPreset
@Environment(\.dismiss) private var dismiss
@Environment(\.palette) private var p
@State private var name = ""
@State private var mix: [String: StemSetting] = [:]
var body: some View {
let n = name.trimmingCharacters(in: .whitespaces)
let anySolo = mix.values.contains { $0.solo }
NavigationStack {
List {
Section { TextField("Preset name", text: $name).themedRow() }
Section {
ForEach(StemPlaylists.allStems, id: \.self) { st in
let set = mix[st] ?? StemSetting()
let silent = set.muted || (anySolo && !set.solo)
let color = Color(hex: Stems.color(st))
FaderRow(dot: silent ? p.label3 : color, label: Stems.label(st), percent: Int(stemTaper(set.level) * 100), dim: silent) {
FaderSlider(value: set.level, fill: silent ? p.label3 : color, detent: StemSetting.unity) { mix[st, default: StemSetting()].level = $0 }
} trailing: {
ToggleBox(text: "M", on: set.muted, onColor: p.red) { mix[st, default: StemSetting()].muted.toggle() }
ToggleBox(text: "S", on: set.solo, onColor: Color(hex: 0xFFCC00)) { mix[st, default: StemSetting()].solo.toggle() }
}
.themedRow()
}
} header: {
HStack {
Text("Stems").foregroundStyle(p.label2)
Spacer()
Button("Reset") { mix = [:] }.font(.footnote)
}
} footer: {
Text("Stems a song doesn't have are skipped when the preset is applied.").foregroundStyle(p.label2)
}
}
.listStyle(.insetGrouped)
.themedList()
.navigationTitle(original.name.isEmpty ? "New Preset" : "Edit Preset")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } }
ToolbarItem(placement: .confirmationAction) {
Button("Save") {
let s = AppGraph.shared.settings
let old = original.name
let locked = !old.isEmpty && s.lockedPreset == "c:" + old
if !old.isEmpty && old != n {
CustomPreset.remove(old)
s.presetOrder = s.presetOrder.split(separator: "\n").map { $0 == "c:" + old ? "c:" + n : String($0) }.joined(separator: "\n")
}
CustomPreset.add(n, mix)
if locked { s.lockedPreset = "c:" + n }
dismiss()
}
.disabled(n.isEmpty)
}
}
}
.onAppear { name = original.name; mix = original.mix }
}
}