2026-06-18 18:04:59 +03:00
|
|
|
// JarvisApp.swift
|
|
|
|
|
|
|
|
|
|
import SwiftUI
|
|
|
|
|
import SwiftData
|
2026-06-18 22:17:41 +03:00
|
|
|
import UIKit
|
|
|
|
|
import BackgroundTasks
|
|
|
|
|
|
2026-06-21 02:15:23 +03:00
|
|
|
// MARK: - Logo + splash
|
|
|
|
|
|
|
|
|
|
/// The three-bar logo that mirrors the app icon.
|
|
|
|
|
struct JarvisLogoMark: View {
|
|
|
|
|
var width: CGFloat = 180
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
VStack(alignment: .center, spacing: 8) {
|
|
|
|
|
// Orange (short) · White (longest) · Grey (medium) — same ratios as icon
|
|
|
|
|
Capsule().fill(Palette.orange)
|
|
|
|
|
.frame(width: width * 0.48, height: width * 0.075)
|
|
|
|
|
.shadow(color: Palette.orange.opacity(0.55), radius: 10, y: 2)
|
|
|
|
|
Capsule().fill(Color.white)
|
|
|
|
|
.frame(width: width, height: width * 0.075)
|
|
|
|
|
Capsule().fill(Color(hex: "888888"))
|
|
|
|
|
.frame(width: width * 0.74, height: width * 0.075)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct SplashView: View {
|
|
|
|
|
var body: some View {
|
|
|
|
|
ZStack {
|
|
|
|
|
Color(hex: "0A0A0A").ignoresSafeArea()
|
|
|
|
|
VStack(spacing: 28) {
|
|
|
|
|
JarvisLogoMark(width: 180)
|
|
|
|
|
JarvisWordmark(size: 34)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-18 22:17:41 +03:00
|
|
|
final class AppDelegate: NSObject, UIApplicationDelegate {
|
|
|
|
|
func application(_ application: UIApplication,
|
|
|
|
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
2026-06-21 01:42:20 +03:00
|
|
|
// Briefing notifications refresh
|
2026-06-18 22:17:41 +03:00
|
|
|
BGTaskScheduler.shared.register(forTaskWithIdentifier: jarvisBriefingRefreshID, using: nil) { task in
|
|
|
|
|
Task { @MainActor in
|
|
|
|
|
await NotificationManager.shared.applySettings()
|
|
|
|
|
NotificationManager.shared.scheduleBackgroundRefresh()
|
|
|
|
|
task.setTaskCompleted(success: true)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-21 01:42:20 +03:00
|
|
|
// Story feed cache refresh
|
|
|
|
|
BGTaskScheduler.shared.register(forTaskWithIdentifier: jarvisFeedRefreshID, using: nil) { task in
|
|
|
|
|
guard let refresh = task as? BGAppRefreshTask else {
|
|
|
|
|
task.setTaskCompleted(success: false); return
|
|
|
|
|
}
|
|
|
|
|
BackgroundRefreshManager.handleFeedRefresh(task: refresh)
|
|
|
|
|
}
|
2026-06-18 22:17:41 +03:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-18 18:04:59 +03:00
|
|
|
|
|
|
|
|
@main
|
|
|
|
|
struct JarvisApp: App {
|
2026-06-18 22:17:41 +03:00
|
|
|
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
2026-06-21 01:30:36 +03:00
|
|
|
@AppStorage("appearanceMode") private var appearanceMode: AppearanceMode = .dark
|
2026-06-18 18:04:59 +03:00
|
|
|
@StateObject private var settings = ServerSettings.shared
|
|
|
|
|
@StateObject private var store = StoryStore.shared
|
|
|
|
|
@StateObject private var ws = WebSocketManager.shared
|
|
|
|
|
@StateObject private var connectivity = ConnectivitySettings.shared
|
|
|
|
|
@StateObject private var connManager = ConnectivityManager.shared
|
2026-06-18 22:17:41 +03:00
|
|
|
@StateObject private var notifications = NotificationManager.shared
|
2026-06-21 02:15:23 +03:00
|
|
|
@State private var showSplash = true
|
2026-06-18 18:04:59 +03:00
|
|
|
|
2026-06-21 01:42:20 +03:00
|
|
|
// Single ModelContainer instance shared with BackgroundRefreshManager.
|
|
|
|
|
private let container: ModelContainer = {
|
|
|
|
|
let schema = Schema([CachedStory.self, CachedArticle.self,
|
|
|
|
|
ReadStory.self, SavedStory.self, SeenStory.self])
|
|
|
|
|
return try! ModelContainer(for: schema)
|
|
|
|
|
}()
|
|
|
|
|
|
2026-06-18 18:04:59 +03:00
|
|
|
var body: some Scene {
|
|
|
|
|
WindowGroup {
|
2026-06-21 02:15:23 +03:00
|
|
|
ZStack {
|
|
|
|
|
if showSplash {
|
|
|
|
|
SplashView()
|
|
|
|
|
.transition(.opacity)
|
2026-06-18 18:04:59 +03:00
|
|
|
} else {
|
2026-06-21 02:15:23 +03:00
|
|
|
Group {
|
|
|
|
|
if settings.isConfigured {
|
|
|
|
|
RootTabView()
|
|
|
|
|
} else {
|
|
|
|
|
OnboardingView()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.transition(.opacity)
|
2026-06-18 18:04:59 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-21 02:15:23 +03:00
|
|
|
.animation(.easeOut(duration: 0.5), value: showSplash)
|
2026-06-21 01:30:36 +03:00
|
|
|
.preferredColorScheme(appearanceMode.colorScheme)
|
2026-06-18 18:04:59 +03:00
|
|
|
.environmentObject(settings)
|
|
|
|
|
.environmentObject(store)
|
|
|
|
|
.environmentObject(ws)
|
|
|
|
|
.environmentObject(connectivity)
|
|
|
|
|
.environmentObject(connManager)
|
2026-06-18 22:17:41 +03:00
|
|
|
.environmentObject(notifications)
|
2026-06-18 18:04:59 +03:00
|
|
|
.task {
|
2026-06-21 01:42:20 +03:00
|
|
|
BackgroundRefreshManager.container = container
|
|
|
|
|
BackgroundRefreshManager.scheduleFeedRefresh()
|
2026-06-18 18:04:59 +03:00
|
|
|
connManager.start()
|
|
|
|
|
await connManager.resolveAndActivate()
|
2026-06-18 22:17:41 +03:00
|
|
|
await notifications.bootstrap()
|
2026-06-21 02:15:23 +03:00
|
|
|
// Hold splash until setup is done, minimum 1.3 s for legibility.
|
|
|
|
|
try? await Task.sleep(nanoseconds: 1_300_000_000)
|
|
|
|
|
showSplash = false
|
2026-06-18 18:04:59 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-21 01:42:20 +03:00
|
|
|
.modelContainer(container)
|
2026-06-18 18:04:59 +03:00
|
|
|
}
|
|
|
|
|
}
|