import DesignSystem import MusubiCore import SwiftUI struct ContentView: View { let sourceList: SourceListModel let coordinator: Coordinator @AppStorage("detailTab") private var tab = DetailTab.screens enum DetailTab: String { case screens, sound } var body: some View { NavigationSplitView { SourceListView(sourceList: sourceList, audio: coordinator.audio) .navigationSplitViewColumnWidth(min: 220, ideal: 260) } detail: { switch tab { case .screens: ScreensView(coordinator: coordinator) case .sound: SoundView(audio: coordinator.audio, sourceList: sourceList) } } .navigationTitle("Musubi") .navigationSubtitle("NDI rein. Bild, Ton und Stimme raus.") .toolbar { ToolbarItem(placement: .navigation) { if coordinator.settings.role == .control { SceneMenu(coordinator: coordinator) } else { Text("Knoten").font(Theme.Font.caption).foregroundStyle(Theme.Color.nezumi) } } ToolbarItem(placement: .principal) { Picker("Ansicht", selection: $tab) { Text("Bildschirme").tag(DetailTab.screens) Text("Ton").tag(DetailTab.sound) } .pickerStyle(.segmented) .fixedSize() } if coordinator.outputs.isSleepBlocked { ToolbarItem(placement: .primaryAction) { Text("Ruhezustand blockiert") .font(Theme.Font.caption) .foregroundStyle(Theme.Color.nezumi) } } } } } private struct SourceListView: View { let sourceList: SourceListModel let audio: AudioController var body: some View { List { Section("NDI") { switch sourceList.state { case .unavailable: HintRow(text: "NDI lässt sich nicht starten. Prüfe die Installation des NDI SDK.") case .idle, .searching: if sourceList.sources.isEmpty { HintRow(text: "Keine NDI-Quellen gefunden. Prüfe, ob das Technik-Netz an en7 hängt.") } else { ForEach(sourceList.sources, id: \.ndiName) { source in SourceRow(source: source) .draggable(source.ndiName) } } } } Section("Mikrofone") { ForEach(audio.matrix.inputDevices) { device in Label { Text(verbatim: device.name) .font(Theme.Font.body) .foregroundStyle(Theme.Color.washi) } icon: { Image(systemName: "mic").foregroundStyle(Theme.Color.nezumi) } .draggable("input:\(device.uid)") } } } .scrollContentBackground(.hidden) .background(Theme.Color.kasumi) .safeAreaInset(edge: .top, spacing: 0) { Text("Quellen") .font(Theme.Font.title) .foregroundStyle(Theme.Color.washi) .frame(maxWidth: .infinity, alignment: .leading) .padding(Theme.Space.card) } } } private struct HintRow: View { let text: LocalizedStringKey var body: some View { Text(text) .font(Theme.Font.caption) .foregroundStyle(Theme.Color.nezumi) .fixedSize(horizontal: false, vertical: true) } } private struct SourceRow: View { let source: Source var body: some View { VStack(alignment: .leading, spacing: Theme.Space.unit / 2) { Text(verbatim: source.label) .font(Theme.Font.body) .foregroundStyle(Theme.Color.washi) Text(verbatim: source.ndiName) .font(Theme.Font.caption.monospaced()) .foregroundStyle(Theme.Color.nezumi) } .padding(.vertical, Theme.Space.unit / 2) } }