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>
This commit is contained in:
150
Jervis/JarvisApp.swift
Normal file
150
Jervis/JarvisApp.swift
Normal file
@@ -0,0 +1,150 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user