import SwiftUI struct BookmarksView: View { @Bindable var viewModel: BookmarksViewModel let onDisconnect: () -> Void @Environment(\.openURL) private var openURL @State private var showSettings = false @State private var showCollections = false @State private var showAddBookmark = false @State private var editingBookmark: Bookmark? @State private var browsingBookmark: Bookmark? @State private var showFullPlayer = false @State private var showPodcastLibrary = false @State private var readingProgress: [String: Double] = ReadingProgress.all() var body: some View { NavigationStack { List { ForEach(viewModel.bookmarks) { bookmark in BookmarkRow( bookmark: bookmark, readingProgress: readingProgress[bookmark.url] ?? 0, onPodcast: { viewModel.podcastPlayer.start( articleUrl: bookmark.url, articleTitle: bookmark.displayTitle, claude: viewModel.claude ) showFullPlayer = true } ) .onTapGesture { browsingBookmark = bookmark } .onAppear { maybeLoadMore(bookmark) } .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)) .listRowSeparator(.visible) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button(role: .destructive) { Task { await viewModel.delete(bookmark) } } label: { Label("Delete", systemImage: "trash") } } .swipeActions(edge: .leading) { Button { Task { await viewModel.archive(bookmark) } } label: { Label("Archive", systemImage: "archivebox") } .tint(.orange) Button { editingBookmark = bookmark } label: { Label("Edit", systemImage: "pencil") } .tint(.blue) } .contextMenu { Button { editingBookmark = bookmark } label: { Label("Edit", systemImage: "pencil") } Button { browsingBookmark = bookmark } label: { Label("Open", systemImage: "globe") } Button { if let url = URL(string: bookmark.url) { openURL(url) } } label: { Label("Open in Safari", systemImage: "safari") } Button { viewModel.podcastPlayer.start( articleUrl: bookmark.url, articleTitle: bookmark.displayTitle, claude: viewModel.claude ) showFullPlayer = true } label: { Label("Convert to Podcast", systemImage: "headphones") } Divider() Button { Task { await viewModel.archive(bookmark) } } label: { Label("Archive", systemImage: "archivebox") } Button(role: .destructive) { Task { await viewModel.delete(bookmark) } } label: { Label("Delete", systemImage: "trash") } } } if viewModel.isLoadingMore { HStack { Spacer(); ProgressView(); Spacer() } .listRowSeparator(.hidden) } } .listStyle(.plain) .navigationTitle(viewModel.unreadFilter ? "Unread" : "Bookmarks") .navigationBarTitleDisplayMode(.large) .toolbar { ToolbarItem(placement: .topBarLeading) { Menu { Button { Task { await viewModel.generateSmartCollections() } showCollections = true } label: { Label("Smart Collections", systemImage: "sparkles") } Button { Task { await viewModel.enrichAll() } } label: { Label("Enrich All with AI", systemImage: "wand.and.stars") } Button { showPodcastLibrary = true } label: { Label("Podcasts", systemImage: "headphones") } } label: { Image(systemName: "sparkles") } } ToolbarItem(placement: .topBarTrailing) { HStack(spacing: 16) { Button { showAddBookmark = true } label: { Image(systemName: "plus") } Button { Task { await viewModel.toggleUnreadFilter() } } label: { Image(systemName: viewModel.unreadFilter ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle") } Button { showSettings = true } label: { Image(systemName: "gearshape") } } } } .overlay { if viewModel.isLoading && viewModel.bookmarks.isEmpty { ProgressView() } if !viewModel.isLoading && viewModel.bookmarks.isEmpty { ContentUnavailableView("No Bookmarks", systemImage: "bookmark", description: Text("Bookmarks you save will appear here.")) } } .overlay(alignment: .bottom) { VStack(spacing: 8) { if !viewModel.podcastPlayer.currentArticleUrl.isEmpty { MiniPlayerView(vm: viewModel.podcastPlayer) { showFullPlayer = true } .transition(.move(edge: .bottom).combined(with: .opacity)) } if viewModel.enrichmentProgress > 0 { enrichmentBanner } } .animation(.spring(duration: 0.3), value: !viewModel.podcastPlayer.currentArticleUrl.isEmpty) .padding(.bottom, 8) } } .sheet(isPresented: $showSettings) { SettingsView(viewModel: viewModel, onDisconnect: onDisconnect) } .sheet(isPresented: $showCollections) { CollectionsView(viewModel: viewModel) } .sheet(isPresented: $showAddBookmark) { AddBookmarkView(viewModel: viewModel) } .sheet(item: $editingBookmark) { bookmark in EditBookmarkView(viewModel: viewModel, bookmark: bookmark) } .sheet(item: $browsingBookmark) { bookmark in if let url = URL(string: bookmark.url) { BrowserView(url: url, title: bookmark.displayTitle, claude: viewModel.claude) } } .onChange(of: browsingBookmark) { _, new in if new == nil { readingProgress = ReadingProgress.all() } } .sheet(isPresented: $showFullPlayer) { PodcastPlayerView( vm: viewModel.podcastPlayer, articleUrl: viewModel.podcastPlayer.currentArticleUrl, articleTitle: viewModel.podcastPlayer.currentArticleTitle, claude: viewModel.claude, stopOnDismiss: false ) } .sheet(isPresented: $showPodcastLibrary) { PodcastLibraryView(vm: viewModel.podcastPlayer, claude: viewModel.claude) } .refreshable { await viewModel.load() } .task { if viewModel.bookmarks.isEmpty { await viewModel.load() } } .alert("Error", isPresented: Binding( get: { viewModel.error != nil }, set: { if !$0 { viewModel.error = nil } } )) { Button("OK") { viewModel.error = nil } } message: { Text(viewModel.error ?? "") } } private var enrichmentBanner: some View { HStack(spacing: 10) { ProgressView(value: viewModel.enrichmentProgress) .progressViewStyle(.linear) .tint(.primary) Text("Adding AI summaries…") .font(.system(size: 13)) .foregroundStyle(.secondary) } .padding(.horizontal, 20) .padding(.vertical, 12) .background(.regularMaterial) .clipShape(RoundedRectangle(cornerRadius: 12)) .padding(.horizontal, 16) } private func maybeLoadMore(_ bookmark: Bookmark) { guard let last = viewModel.bookmarks.last, last.id == bookmark.id, viewModel.nextPageUrl != nil, !viewModel.isLoadingMore else { return } Task { await viewModel.loadMore() } } }