Files
jarvis/Jervis/JarvisApp.swift
Kutesir bd632a1592
Some checks are pending
CI / Build · Debug (push) Waiting to run
CI / Build · Release (push) Waiting to run
rename: Jarvis -> Jervis target, scheme, and folder throughout
The bundle-ID and display-name renames weren't enough — the actual
Xcode target/product name was still "Jarvis", which drives CFBundleName,
Xcode's Organizer archive list, the .xcodeproj filename, and the scheme
name. Renamed all the way through:

- project.yml: top-level name, target key, PRODUCT_NAME, source/info paths
- Jarvis/ -> Jervis/ (source folder, git-tracked as renames, no content
  diffs on the moved files)
- .gitignore, CI workflows, README, CONTRIBUTING: Jarvis.xcodeproj /
  scheme Jarvis -> Jervis.xcodeproj / scheme Jervis

Verified: xcodebuild -scheme Jervis succeeds, produces Jervis.app,
CFBundleName/CFBundleExecutable/CFBundleDisplayName all read "Jervis".
CFBundleIdentifier intentionally stays com.kisani.jarvis (per prior
decision — bundle ID isn't user-facing anywhere including Organizer).

Swift type names (JarvisApp, JarvisWordmark, etc.) and the Gitea repo
name/URL are unchanged — pure internal source identifiers, not user or
developer-facing product identity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-13 03:49:31 +03:00

151 lines
5.7 KiB
Swift

// JarvisApp.swift
import SwiftUI
import SwiftData
import UIKit // needed for AppDelegate
import BackgroundTasks
// MARK: - Splash
struct SplashView: View {
var onDismiss: () -> Void
@State private var iconScale: CGFloat = 0.82
@State private var iconOpacity: Double = 0
@State private var markOpacity: Double = 0
@State private var rootOpacity: Double = 1
var body: some View {
ZStack {
Color(hex: "0A0A0A").ignoresSafeArea()
VStack(spacing: 22) {
Image("JervisIcon")
.resizable()
.frame(width: 108, height: 108)
.clipShape(Circle())
.shadow(color: Palette.orange.opacity(0.35), radius: 22, y: 4)
.scaleEffect(iconScale)
.opacity(iconOpacity)
JarvisWordmark(size: 28)
.opacity(markOpacity)
}
}
.opacity(rootOpacity)
.onAppear {
withAnimation(.spring(response: 0.48, dampingFraction: 0.74).delay(0.08)) {
iconScale = 1
iconOpacity = 1
}
withAnimation(.easeOut(duration: 0.28).delay(0.32)) {
markOpacity = 1
}
withAnimation(.easeIn(duration: 0.26).delay(1.6)) {
rootOpacity = 0
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.9) {
onDismiss()
}
}
}
}
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
// Briefing notifications refresh
BGTaskScheduler.shared.register(forTaskWithIdentifier: jarvisBriefingRefreshID, using: nil) { task in
Task { @MainActor in
await NotificationManager.shared.applySettings()
NotificationManager.shared.scheduleBackgroundRefresh()
task.setTaskCompleted(success: true)
}
}
// 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)
}
return true
}
}
@main
struct JarvisApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@AppStorage("appearanceMode") private var appearanceMode: AppearanceMode = .dark
@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
@StateObject private var notifications = NotificationManager.shared
@Environment(\.scenePhase) private var scenePhase
@State private var splashDone = false
// 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)
}()
var body: some Scene {
WindowGroup {
ZStack {
// Main app always rendered underneath so it's ready when splash fades.
Group {
if settings.isConfigured {
RootTabView()
} else {
OnboardingView()
}
}
if !splashDone {
SplashView { splashDone = true }
}
}
.preferredColorScheme(appearanceMode.colorScheme)
.environmentObject(settings)
.environmentObject(store)
.environmentObject(ws)
.environmentObject(connectivity)
.environmentObject(connManager)
.environmentObject(notifications)
.task {
BackgroundRefreshManager.container = container
BackgroundRefreshManager.scheduleFeedRefresh()
// Pre-populate the feed from SwiftData cache before the first API
// fetch completes prevents the cold-launch empty state.
store.bootstrap(container: container)
connManager.start()
await connManager.resolveAndActivate()
await notifications.bootstrap()
store.startForegroundSync()
}
.onChange(of: scenePhase) { _, phase in
switch phase {
case .active:
store.startForegroundSync()
// Only trigger a load if connectivity has already been resolved.
// On cold launch activeHost is nil until resolveAndActivate()
// completes calling loadFeedFull() before that always throws
// APIError.notConnected and leaves lastSyncedAt permanently nil.
// activate() calls loadFeedFull() itself once the host is confirmed.
if connManager.activeHost != nil {
Task { await store.loadFeedFull() }
}
case .background:
store.stopForegroundSync()
default:
break
}
}
}
.modelContainer(container)
}
}