tv-anarchy/Sources/TVAnarchyiOS/SettingsView.swift
Natalie 4a2ceb9781 feat(offline): inline star-to-keep and trash-to-cull on cache rows
Surface the existing pin (keep-from-cull) and per-file delete actions as
visible inline buttons on each offline cache row instead of context-menu-only:
a star toggles protection from auto-cull (and restore-if-missing), a trash
culls that file early. Aligns wording/icons to the star metaphor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:12:41 -04:00

113 lines
No EOL
5.9 KiB
Swift

// 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) }
}
}
}