- Add SwiftUI views for feed list, detail, add feed, settings, and bookmarks - Connect all views to ViewModels using @StateObject - Implement pull-to-refresh for feed list - Add error handling and loading states to all views - Create FeedItemRow view for consistent feed item display - Add toFeedItem() extension to Bookmark for UI integration - Update FeedDetailView to use sync methods - Update BookmarkView to use FeedService for unstar operations Co-Authored-By: Paperclip <noreply@paperclip.ing>
128 lines
4.0 KiB
Swift
128 lines
4.0 KiB
Swift
import SwiftUI
|
|
|
|
struct FeedListView: View {
|
|
@StateObject private var viewModel: FeedViewModel
|
|
@State private var selectedFeedItem: FeedItem?
|
|
@State private var showError: Bool = false
|
|
@State private var errorMessage: String = ""
|
|
|
|
private let feedService: FeedServiceProtocol
|
|
|
|
init(feedService: FeedServiceProtocol = FeedService()) {
|
|
self.feedService = feedService
|
|
_viewModel = StateObject(wrappedValue: FeedViewModel(feedService: feedService))
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationSplitView {
|
|
List {
|
|
switch viewModel.feedState {
|
|
case .idle:
|
|
ContentUnavailableView("No Feed", systemImage: "rss")
|
|
.padding()
|
|
|
|
case .loading:
|
|
ProgressView("Loading...")
|
|
.padding()
|
|
|
|
case .success(let items):
|
|
ForEach(items) { item in
|
|
FeedItemRow(feedItem: item)
|
|
.onTapGesture {
|
|
selectedFeedItem = item
|
|
}
|
|
}
|
|
.onDelete(perform: deleteItems)
|
|
|
|
case .error(let error):
|
|
VStack {
|
|
Text("Error loading feed")
|
|
.foregroundColor(.red)
|
|
Text(error)
|
|
.font(.caption)
|
|
Button("Retry") {
|
|
refresh()
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
.navigationTitle("Feeds")
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
Button(action: refresh) {
|
|
Image(systemName: "arrow.clockwise")
|
|
}
|
|
}
|
|
}
|
|
.refreshable {
|
|
refresh()
|
|
}
|
|
.sheet(item: $selectedFeedItem) { item in
|
|
FeedDetailView(feedItem: item, feedService: feedService)
|
|
}
|
|
.alert("Error", isPresented: $showError) {
|
|
Button("OK", role: .cancel) { errorMessage = "" }
|
|
} message: {
|
|
Text(errorMessage)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func refresh() {
|
|
if let subscriptionId = viewModel.currentSubscriptionId {
|
|
viewModel.refresh(subscriptionId: subscriptionId)
|
|
}
|
|
}
|
|
|
|
private func deleteItems(offsets: IndexSet) {
|
|
guard let subscriptionId = viewModel.currentSubscriptionId else { return }
|
|
|
|
Task {
|
|
let items = viewModel.feedItems
|
|
for index in offsets {
|
|
let item = items[index]
|
|
_ = feedService.markItemAsRead(itemId: item.id)
|
|
}
|
|
viewModel.refresh(subscriptionId: subscriptionId)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct FeedItemRow: View {
|
|
let feedItem: FeedItem
|
|
|
|
var body: some View {
|
|
HStack {
|
|
VStack(alignment: .leading) {
|
|
Text(feedItem.title)
|
|
.font(.headline)
|
|
.lineLimit(2)
|
|
|
|
if let author = feedItem.author {
|
|
Text(author)
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
if let published = feedItem.published {
|
|
Text(published, format: Date.FormatStyle(date: .abbreviated, time: .shortened))
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
Spacer()
|
|
if !feedItem.read {
|
|
Circle()
|
|
.fill(Color.blue)
|
|
.frame(width: 8, height: 8)
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
FeedListView()
|
|
}
|