import SwiftUI private let defaultConfig = ServerConfig( host: "linkding-production-f7e0.up.railway.app", port: nil, path: "", token: "04c3388f543a6f4401ae41958b4a459a0125c4bf", useHttps: true ) @main struct MarksApp: App { @State private var serverConfig: ServerConfig = ServerConfig.load() ?? defaultConfig var body: some Scene { WindowGroup { MainContainer(config: serverConfig) { defaultConfig.save() serverConfig = defaultConfig } .task { serverConfig.save() } } } } private struct IdentifiableURL: Identifiable { let id = UUID() let url: URL } // Holds the API + ViewModel so they survive re-renders struct MainContainer: View { let config: ServerConfig let onDisconnect: () -> Void @State private var viewModel: BookmarksViewModel @State private var deepLinkBrowser: IdentifiableURL? @State private var showDeepLinkPlayer = false @State private var selectedTab: AppTab = .bookmarks @State private var router = IntentRouter.shared @Environment(\.scenePhase) private var scenePhase init(config: ServerConfig, onDisconnect: @escaping () -> Void) { self.config = config self.onDisconnect = onDisconnect let api = LinkdingAPI(config: config) _viewModel = State(wrappedValue: BookmarksViewModel(api: api, cacheKey: config.host)) } var body: some View { TabView(selection: $selectedTab) { Tab("Bookmarks", systemImage: "bookmark", value: AppTab.bookmarks) { BookmarksView(viewModel: viewModel, onDisconnect: onDisconnect) } Tab("Tags", systemImage: "tag", value: AppTab.tags) { TagsView(viewModel: viewModel) } Tab("Podcasts", systemImage: "headphones", value: AppTab.podcasts) { PodcastLibraryView(vm: viewModel.podcastPlayer, claude: viewModel.claude) } Tab(value: AppTab.search, role: .search) { SearchView(viewModel: viewModel) } } .onOpenURL { url in handleDeepLink(url) } .task { applyPendingIntent() } .onChange(of: router.openBookmarkURL) { _, _ in applyPendingIntent() } .onChange(of: router.searchRequest) { _, _ in applyPendingIntent() } .onChange(of: scenePhase) { old, new in // Refresh when returning to the foreground (cold launch is already // covered by BookmarksView's .task, so only react to a real re-entry). if new == .active && old != .active { Task { await viewModel.load() } } } .sheet(item: $deepLinkBrowser) { item in BrowserView(url: item.url, title: item.url.host ?? "", claude: viewModel.claude, podcastPlayer: viewModel.podcastPlayer) } .sheet(isPresented: $showDeepLinkPlayer) { PodcastPlayerView( vm: viewModel.podcastPlayer, articleUrl: viewModel.podcastPlayer.currentArticleUrl, articleTitle: viewModel.podcastPlayer.currentArticleTitle, claude: viewModel.claude, stopOnDismiss: false ) } } /// Reacts to requests an App Intent placed on `IntentRouter` (opening a /// bookmark, or searching). Also runs once at launch so cold-starts via an /// intent are honored. private func applyPendingIntent() { if let urlString = router.openBookmarkURL { router.openBookmarkURL = nil if let url = URL(string: urlString) { deepLinkBrowser = IdentifiableURL(url: url) } } if let query = router.searchRequest { selectedTab = .search Task { viewModel.searchQuery = query await viewModel.search() } // SearchView consumes the text from the router on appear; leave it // set until then, then SearchView clears it. } } private func handleDeepLink(_ url: URL) { guard url.scheme == "marks", let comps = URLComponents(url: url, resolvingAgainstBaseURL: false), let articleUrl = comps.queryItems?.first(where: { $0.name == "url" })?.value else { return } switch url.host { case "bookmark": if let browserURL = URL(string: articleUrl) { deepLinkBrowser = IdentifiableURL(url: browserURL) } case "podcast": let podcasts = WidgetDataStore.loadPodcasts() let found = podcasts.first { $0.articleUrl == articleUrl } viewModel.podcastPlayer.start( articleUrl: articleUrl, articleTitle: found?.title ?? "", claude: viewModel.claude ) showDeepLinkPlayer = true default: break } } }