All checks were successful
CI / build-and-deploy (push) Successful in 41s
- Save serverConfig to App Group on every app launch so ShareExtension can always find credentials without requiring a disconnect/reconnect flow - Add MarksWidget target with RandomBookmarkWidget (random saved bookmark, reloads hourly) using file-based App Group storage to avoid CFPreferences issues - Extract BookmarkListRow as shared component with editorial typography tweaks (17pt semibold title, 13pt domain, relative date, 32×32 favicon, italic AI summary, 20pt podcast icon, 16pt horizontal insets) - Add MarksWidget and ShareExtension Xcode schemes for direct run/debug Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
101 lines
3.2 KiB
Swift
101 lines
3.2 KiB
Swift
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
|
|
|
|
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 {
|
|
Tab("Bookmarks", systemImage: "bookmark") {
|
|
BookmarksView(viewModel: viewModel, onDisconnect: onDisconnect)
|
|
}
|
|
Tab("Tags", systemImage: "tag") {
|
|
TagsView(viewModel: viewModel)
|
|
}
|
|
Tab(role: .search) {
|
|
SearchView(viewModel: viewModel)
|
|
}
|
|
}
|
|
.onOpenURL { url in
|
|
handleDeepLink(url)
|
|
}
|
|
.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
|
|
)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|