// Settings tab: bridge connection, playback buffer, prefetch-ahead policy, and // offline storage management. import SwiftUI import LilithDesignTokens struct SettingsScreen: View { @EnvironmentObject private var settings: BridgeSettings @EnvironmentObject private var downloads: DownloadManager @State private var portText = "" var body: some View { NavigationStack { Form { Section { LabeledContent("Host") { TextField("10.0.0.11", text: $settings.host) .textInputAutocapitalization(.never) .autocorrectionDisabled() .multilineTextAlignment(.trailing) .accessibilityHint("Home LAN address of the bridge — saved on this device") } LabeledContent("Fallback host") { TextField("10.9.0.4", text: $settings.fallbackHost) .textInputAutocapitalization(.never) .autocorrectionDisabled() .multilineTextAlignment(.trailing) .accessibilityHint("WireGuard mesh address — used automatically when home LAN is unreachable") } LabeledContent("Port") { TextField("8787", text: $portText) .keyboardType(.numberPad) .multilineTextAlignment(.trailing) .accessibilityHint("Bridge HTTP port — saved on this device") .onChange(of: portText) { _, new in if let p = Int(new), p > 0, p < 65536 { settings.port = p } } } LabeledContent("Token") { SecureField("optional", text: $settings.token) .multilineTextAlignment(.trailing) .accessibilityHint("Optional bridge auth token — saved on this device") } } header: { Text("Bridge") } footer: { Text("Host is tried first on the home LAN; fallback (WireGuard) takes over when you open the app off-LAN. Connected via \(settings.activeHost). All bridge fields persist on this phone.") } Section { VStack(alignment: .leading) { Text("Buffer: \(settings.networkCachingMs) ms") Slider( value: Binding( get: { Double(settings.networkCachingMs) }, set: { settings.networkCachingMs = Int($0) } ), in: 300...8000, step: 100 ) .accessibilityHint("VLCKit network buffer — higher absorbs jitter but slows seeking") } } header: { Text("Playback") } footer: { Text("Higher buffer absorbs more network jitter but makes seeking slower. 1500 ms is a good default over the mesh. Saved on this device.") } Section { Toggle("Prefetch ahead", isOn: $settings.prefetchEnabled) .accessibilityHint("Download the next episodes while you watch a series") if settings.prefetchEnabled { Stepper("Keep next \(settings.prefetchCount) episode\(settings.prefetchCount == 1 ? "" : "s")", value: $settings.prefetchCount, in: 1...10) .accessibilityHint("How many upcoming episodes to keep downloaded") } } header: { Text("Offline") } footer: { Text("While you watch a series, the next episodes download automatically so they're ready offline. Settings persist on this device.") } Section { Toggle("Auto-pack on open", isOn: $settings.packEnabled) .accessibilityHint("Run flight pack when the app opens") Stepper("Next \(settings.packEpisodesPerShow) per show", value: $settings.packEpisodesPerShow, in: 1...10) .accessibilityHint("Episodes per in-progress show to keep downloaded") Stepper("Budget: \(settings.packBudgetGB) GB", value: $settings.packBudgetGB, in: 5...100, step: 5) .accessibilityHint("Total storage cap for flight pack downloads") } header: { Text("Flight pack") } footer: { Text("Keeps the next episodes of every show you're watching downloaded, up to the budget. Watched episodes are evicted to make room; unwatched never are. Run it manually from Downloads. All values persist on this device.") } Section("Storage") { LabeledContent("Downloaded", value: "\(downloads.entries.count) episodes") LabeledContent("On disk", value: ByteCountFormatter.string(fromByteCount: downloads.totalBytes, countStyle: .file)) if !downloads.entries.isEmpty { Button("Delete all offline", role: .destructive) { for e in downloads.entries { downloads.delete(episodeId: e.episodeId) } } .accessibilityHint("Remove every downloaded episode from this phone") } } } .navigationTitle("Settings") .onAppear { portText = String(settings.port) } } } }