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>
113 lines
No EOL
5.9 KiB
Swift
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) }
|
|
}
|
|
}
|
|
} |