import SwiftUI import FlixyDesign import FlixyPlayback /// Die Qualitätswahl im Player, samt dem, was gerade tatsächlich läuft. /// /// Ein Blatt statt eines Dialogs: die Leiter hat je nach Quelle bis zu /// dreizehn Stufen, und `confirmationDialog` zeigt nur eine Liste von Knöpfen /// ohne Ordnung. Hier stehen sie nach Auflösung gruppiert — und ganz oben, /// was gerade über die Leitung kommt. struct QualitySheet: View { let model: PlayerModel @Environment(\.flixyAccent) private var accent @Environment(PlaybackSettings.self) private var settings @Environment(\.dismiss) private var dismiss @State private var showsEveryRung = false private var everyday: [PlayerQuality] { PlayerQuality.everyday(forSourceHeight: model.sourceHeight) } private var options: [PlayerQuality] { PlayerQuality.available(forSourceHeight: model.sourceHeight) } var body: some View { NavigationStack { List { current Section { ForEach(everyday) { option in row(for: option) } } footer: { Text("quality.audioOnly.hint") } // Aufgeklappt nur, wer es wirklich will. Für den Alltag // reicht „eine Stufe kleiner"; die volle Leiter braucht, wer // eine bestimmte Bitrate erzwingen muss. Section { DisclosureGroup(isExpanded: $showsEveryRung) { ForEach(groupedHeights, id: \.self) { height in ForEach(rungs(height: height), id: \.self) { rung in row(for: .limited(rung), detail: "\(height)p") } } } label: { Text("quality.everyRung") } } } .navigationTitle(Text("player.quality")) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { Button("action.done") { dismiss() } } } } .presentationDetents([.medium, .large]) } /// Was gerade läuft — gemessen, nicht gewünscht. @ViewBuilder private var current: some View { if settings.showsBitrate, let statistics = model.statistics { Section { if let observed = statistics.observedBitrate { LabeledContent("quality.current.bitrate", value: bitrateText(observed)) } if let resolution = statistics.resolution { LabeledContent("quality.current.resolution", value: resolution) } if let dropped = statistics.droppedFrames, dropped > 0 { LabeledContent("quality.current.dropped", value: "\(dropped)") } } header: { Text("quality.current") } footer: { // Der Unterschied ist keine Haarspalterei: die Obergrenze sagt, // was erlaubt ist, die gemessene Rate, was ankommt. Ruckelt es // trotz hoher Stufe, steht die Antwort hier. Text("quality.current.hint") } } } private var groupedHeights: [Int] { var seen = Set() return options.compactMap { option -> Int? in guard case .limited(let rung) = option else { return nil } return seen.insert(rung.height).inserted ? rung.height : nil } } private func rungs(height: Int) -> [PlayerQuality.Rung] { options.compactMap { option in guard case .limited(let rung) = option, rung.height == height else { return nil } return rung } } private func row(for option: PlayerQuality, detail: String? = nil) -> some View { Button { Task { await model.select(quality: option) } dismiss() } label: { HStack { Text(label(for: option)) .foregroundStyle(Color.flixyTextPrimary) if let detail { Text(detail) .font(.flixyCaption) .foregroundStyle(Color.flixyTextMuted) } Spacer() if option == model.quality { Image(systemName: "checkmark").foregroundStyle(accent) } } .contentShape(.rect) } .buttonStyle(.plain) } private func label(for option: PlayerQuality) -> String { switch option { case .automatic: String(localized: "quality.automatic") case .original: String(localized: "quality.original") case .audioOnly: String(localized: "quality.audioOnly") // In der kurzen Liste zählt die Auflösung, nicht die Zahl dahinter: // „720p" sagt mehr als „3 Mbps", wenn man nur kleiner will. case .limited(let rung): "\(rung.height)p · \(bitrateText(rung.bitrate))" } } /// Kilobit unterhalb eines Megabits, darüber Megabit mit einer Stelle. /// /// „1500 kbps" liest sich schlechter als „1,5 Mbps", und „0,064 Mbps" /// schlechter als „64 kbps". private func bitrateText(_ kilobits: Int) -> String { guard kilobits >= 1_000 else { return String(localized: "quality.kilobits \(kilobits)") } let megabits = Double(kilobits) / 1_000 let rounded = megabits.rounded() == megabits ? String(Int(megabits)) : String(format: "%.1f", megabits) return String(localized: "quality.megabits \(rounded)") } }