Fix ShareExtension "Open Marks first" + add widget + BookmarkListRow polish
All checks were successful
CI / build-and-deploy (push) Successful in 41s
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>
This commit is contained in:
@@ -1,10 +1,57 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Skeleton loading row
|
||||
|
||||
private struct SkeletonRow: View {
|
||||
var delay: Double = 0
|
||||
@State private var dimmed = false
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 11) {
|
||||
RoundedRectangle(cornerRadius: 7)
|
||||
.fill(Color(.systemGray5))
|
||||
.frame(width: 32, height: 32)
|
||||
VStack(alignment: .leading, spacing: 7) {
|
||||
Capsule()
|
||||
.fill(Color(.systemGray5))
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 13)
|
||||
Capsule()
|
||||
.fill(Color(.systemGray6))
|
||||
.frame(width: 140, height: 10)
|
||||
}
|
||||
.padding(.top, 4)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 14)
|
||||
.opacity(dimmed ? 0.45 : 1)
|
||||
.onAppear {
|
||||
withAnimation(
|
||||
.easeInOut(duration: 0.9)
|
||||
.repeatForever(autoreverses: true)
|
||||
.delay(delay)
|
||||
) { dimmed = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Row press ButtonStyle
|
||||
|
||||
struct RowPressStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.scaleEffect(configuration.isPressed ? 0.97 : 1)
|
||||
.opacity(configuration.isPressed ? 0.75 : 1)
|
||||
.animation(.spring(duration: 0.18, bounce: 0), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BookmarksView
|
||||
|
||||
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
|
||||
@@ -17,84 +64,23 @@ struct BookmarksView: View {
|
||||
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
|
||||
if viewModel.isLoading && viewModel.bookmarks.isEmpty {
|
||||
ForEach(0..<3, id: \.self) { i in
|
||||
SkeletonRow(delay: Double(i) * 0.13)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
||||
.listRowSeparator(.visible)
|
||||
}
|
||||
} else {
|
||||
ForEach(viewModel.bookmarks) { bookmark in
|
||||
BookmarkListRow(
|
||||
bookmark: bookmark,
|
||||
viewModel: viewModel,
|
||||
readingProgress: readingProgress[bookmark.url] ?? 0,
|
||||
onOpen: { browsingBookmark = bookmark },
|
||||
onEdit: { editingBookmark = bookmark }
|
||||
)
|
||||
.onAppear { maybeLoadMore(bookmark) }
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
||||
.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 {
|
||||
@@ -103,6 +89,7 @@ struct BookmarksView: View {
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.animation(.spring(duration: 0.35), value: viewModel.bookmarks.isEmpty)
|
||||
.navigationTitle(viewModel.unreadFilter ? "Unread" : "Bookmarks")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.toolbar {
|
||||
@@ -139,6 +126,7 @@ struct BookmarksView: View {
|
||||
Image(systemName: viewModel.unreadFilter
|
||||
? "line.3.horizontal.decrease.circle.fill"
|
||||
: "line.3.horizontal.decrease.circle")
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
}
|
||||
Button { showSettings = true } label: {
|
||||
Image(systemName: "gearshape")
|
||||
@@ -147,11 +135,13 @@ struct BookmarksView: View {
|
||||
}
|
||||
}
|
||||
.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."))
|
||||
ContentUnavailableView(
|
||||
"No Bookmarks",
|
||||
systemImage: "bookmark",
|
||||
description: Text("Bookmarks you save will appear here.")
|
||||
)
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
@@ -169,6 +159,7 @@ struct BookmarksView: View {
|
||||
.animation(.spring(duration: 0.3), value: !viewModel.podcastPlayer.currentArticleUrl.isEmpty)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
.sensoryFeedback(.selection, trigger: browsingBookmark?.id)
|
||||
}
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsView(viewModel: viewModel, onDisconnect: onDisconnect)
|
||||
@@ -184,7 +175,9 @@ struct BookmarksView: View {
|
||||
}
|
||||
.sheet(item: $browsingBookmark) { bookmark in
|
||||
if let url = URL(string: bookmark.url) {
|
||||
BrowserView(url: url, title: bookmark.displayTitle, claude: viewModel.claude)
|
||||
BrowserView(url: url, title: bookmark.displayTitle, claude: viewModel.claude, podcastPlayer: viewModel.podcastPlayer) {
|
||||
await viewModel.archive(bookmark)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: browsingBookmark) { _, new in
|
||||
|
||||
Reference in New Issue
Block a user