Add mini-player, per-row podcast button, and resume-from-position
Some checks failed
CI / build-and-deploy (push) Failing after 9s

- MiniPlayerView: persistent bar above tab bar, shows title/progress,
  play/pause and close buttons, slides in when audio starts
- PodcastPlayerViewModel lifted to BookmarksViewModel so it persists
  across sheet dismissals — audio keeps playing in background
- Headphone button on every bookmark row (filled = cached, outline = not)
- start() is idempotent: re-tapping same article resumes without restart
- stop() saves playback position before teardown (PodcastProgress)
- setupPlayer restores saved position on next open
- BrowserView keeps its own local VM (stopOnDismiss: true)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krishna Kumar
2026-05-22 16:19:21 -05:00
parent 65efe708ee
commit abe35f47d6
5 changed files with 238 additions and 43 deletions

View File

@@ -10,17 +10,28 @@ struct BookmarksView: View {
@State private var showAddBookmark = false
@State private var editingBookmark: Bookmark?
@State private var browsingBookmark: Bookmark?
@State private var podcastBookmark: Bookmark?
@State private var showFullPlayer = 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)
.onTapGesture {
browsingBookmark = bookmark
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)
@@ -62,7 +73,12 @@ struct BookmarksView: View {
Label("Open in Safari", systemImage: "safari")
}
Button {
podcastBookmark = bookmark
viewModel.podcastPlayer.start(
articleUrl: bookmark.url,
articleTitle: bookmark.displayTitle,
claude: viewModel.claude
)
showFullPlayer = true
} label: {
Label("Convert to Podcast", systemImage: "headphones")
}
@@ -133,9 +149,19 @@ struct BookmarksView: View {
}
}
.overlay(alignment: .bottom) {
if viewModel.enrichmentProgress > 0 {
enrichmentBanner
VStack(spacing: 8) {
if viewModel.podcastPlayer.player != nil {
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.player != nil)
.padding(.bottom, 8)
}
}
.sheet(isPresented: $showSettings) {
@@ -158,8 +184,14 @@ struct BookmarksView: View {
.onChange(of: browsingBookmark) { _, new in
if new == nil { readingProgress = ReadingProgress.all() }
}
.sheet(item: $podcastBookmark) { bookmark in
PodcastPlayerView(articleUrl: bookmark.url, claude: viewModel.claude)
.sheet(isPresented: $showFullPlayer) {
PodcastPlayerView(
vm: viewModel.podcastPlayer,
articleUrl: viewModel.podcastPlayer.currentArticleUrl,
articleTitle: viewModel.podcastPlayer.currentArticleTitle,
claude: viewModel.claude,
stopOnDismiss: false
)
}
.refreshable {
await viewModel.load()
@@ -193,7 +225,6 @@ struct BookmarksView: View {
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding(.horizontal, 16)
.padding(.bottom, 8)
}
private func maybeLoadMore(_ bookmark: Bookmark) {