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>
81 lines
3.3 KiB
Swift
81 lines
3.3 KiB
Swift
// TVAnarchy iOS — a thin bridge client: browse the network library (shows +
|
|
// movies), stream/play in-app via VLCKit, download for offline (background
|
|
// session + prefetch-ahead + flight pack), and remote-control any registry
|
|
// device. All heavy logic lives behind the tv-anarchy-bridge.
|
|
|
|
import SwiftUI
|
|
import LilithDesignTokens
|
|
|
|
/// UIKit hook for the background download session: iOS relaunches the app when
|
|
/// queued downloads finish and hands over a completion handler that must be
|
|
/// called once the session's events drain (DownloadManager does the calling).
|
|
final class AppDelegate: NSObject, UIApplicationDelegate {
|
|
func application(_ application: UIApplication,
|
|
handleEventsForBackgroundURLSession identifier: String,
|
|
completionHandler: @escaping () -> Void) {
|
|
guard identifier == DownloadManager.sessionIdentifier else { return }
|
|
Task { @MainActor in
|
|
BackgroundSessionRelay.shared.completionHandler = completionHandler
|
|
}
|
|
}
|
|
}
|
|
|
|
@main
|
|
struct TVAnarchyiOSApp: App {
|
|
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
|
@Environment(\.scenePhase) private var scenePhase
|
|
|
|
@StateObject private var settings = BridgeSettings()
|
|
@StateObject private var downloads = DownloadManager()
|
|
|
|
/// The install join gate (Devices pillar): loading → member | needsJoin. A phone is a bridge
|
|
/// client, so membership = "a bridge is configured" (FleetGate latch, internal name) —
|
|
/// resolved before the tabs exist; a fresh install sees only the join page.
|
|
/// See v2/pillars/devices.md (product: "Join install", "Device Mesh").
|
|
@State private var phase: FleetPhase = .loading
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
Group {
|
|
switch phase {
|
|
case .loading:
|
|
ProgressView()
|
|
.task { phase = FleetGate.isMember(settings: settings) ? .member : .needsJoin }
|
|
case .member:
|
|
RootTabView()
|
|
case .needsJoin:
|
|
JoinFleetView { phase = .member }
|
|
}
|
|
}
|
|
.environmentObject(settings)
|
|
.environmentObject(downloads)
|
|
.preferredColorScheme(.dark)
|
|
.tint(AppColors.primary)
|
|
}
|
|
.onChange(of: scenePhase) { _, newScenePhase in
|
|
guard newScenePhase == .active, phase == .member else { return }
|
|
Task {
|
|
// Re-pick LAN vs WireGuard, then top up offline shows if enabled.
|
|
await settings.probeHosts()
|
|
if settings.packEnabled, let client = settings.client {
|
|
await FlightPack.run(client: client, downloads: downloads, settings: settings)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct RootTabView: View {
|
|
var body: some View {
|
|
TabView {
|
|
LibraryView()
|
|
.tabItem { Label("Library", systemImage: "play.tv") }
|
|
DownloadsView()
|
|
.tabItem { Label("Downloads", systemImage: "arrow.down.circle") }
|
|
RemoteView()
|
|
.tabItem { Label("Remote", systemImage: "appletvremote.gen4") }
|
|
SettingsScreen()
|
|
.tabItem { Label("Settings", systemImage: "gearshape") }
|
|
}
|
|
}
|
|
}
|