ReadKeep/readeck/UI/Bookmarks/BookmarksView.swift
Ilyas Hallak 07384215eb Add documentation and tools, refactor BookmarksView for DI, update mocks, and improve project structure
- Add CHANGELOG.md, CODE_OF_CONDUCT.md, and Contribute.md for documentation and community standards
- Add tools/add_spdx_header.sh for SPDX license header management
- Refactor BookmarksView and BookmarksViewModel to support dependency injection via UseCaseFactory
- Add retroactive extension for String: Identifiable in StringExtension.swift
- Update MockUseCaseFactory and MockGetBookmarksUseCase to provide mock data for previews and tests
- Update README.md: add TestFlight info, changelog link, HTTPS/local network note, and move planned features to changelog
2025-07-18 13:36:47 +02:00

175 lines
6.7 KiB
Swift

import Combine
import Foundation
import SwiftUI
struct BookmarksView: View {
// MARK: States
@State private var viewModel: BookmarksViewModel
@State private var showingAddBookmark = false
@State private var selectedBookmarkId: String?
@State private var showingAddBookmarkFromShare = false
@State private var shareURL = ""
@State private var shareTitle = ""
let state: BookmarkState
let type: [BookmarkType]
@Binding var selectedBookmark: Bookmark?
@EnvironmentObject var playerUIState: PlayerUIState
let tag: String?
// MARK: Initializer
init(viewModel: BookmarksViewModel = .init(), state: BookmarkState, type: [BookmarkType], selectedBookmark: Binding<Bookmark?>, tag: String? = nil) {
self.state = state
self.type = type
self._selectedBookmark = selectedBookmark
self.tag = tag
self.viewModel = viewModel
}
// MARK: Environments
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass
var body: some View {
ZStack {
if viewModel.isLoading && viewModel.bookmarks?.bookmarks.isEmpty == true {
ProgressView("Lade \(state.displayName)...")
} else {
List {
ForEach(viewModel.bookmarks?.bookmarks ?? [], id: \.id) { bookmark in
Button(action: {
if UIDevice.isPhone {
selectedBookmarkId = bookmark.id
} else {
if selectedBookmark?.id == bookmark.id {
selectedBookmark = nil
DispatchQueue.main.async {
selectedBookmark = bookmark
}
} else {
selectedBookmark = bookmark
}
}
}) {
BookmarkCardView(
bookmark: bookmark,
currentState: state,
onArchive: { bookmark in
Task {
await viewModel.toggleArchive(bookmark: bookmark)
}
},
onDelete: { bookmark in
Task {
await viewModel.deleteBookmark(bookmark: bookmark)
}
},
onToggleFavorite: { bookmark in
Task {
await viewModel.toggleFavorite(bookmark: bookmark)
}
}
)
.onAppear {
if bookmark.id == viewModel.bookmarks?.bookmarks.last?.id {
Task {
await viewModel.loadMoreBookmarks()
}
}
}
}
.buttonStyle(PlainButtonStyle())
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.listRowSeparator(.hidden)
.listRowBackground(Color(R.color.bookmark_list_bg))
}
}
.listStyle(.plain)
.background(Color(R.color.bookmark_list_bg))
.scrollContentBackground(.hidden)
.refreshable {
await viewModel.refreshBookmarks()
}
.overlay {
if viewModel.bookmarks?.bookmarks.isEmpty == true && !viewModel.isLoading {
ContentUnavailableView(
"Keine Bookmarks",
systemImage: "bookmark",
description: Text(
"Es wurden noch keine Bookmarks in \(state.displayName.lowercased()) gefunden."
)
)
}
}
}
// FAB Button - nur bei "Ungelesen" anzeigen
if state == .unread || state == .all {
VStack {
Spacer()
HStack {
Spacer()
Button(action: {
showingAddBookmark = true
}) {
Image(systemName: "plus")
.font(.title2)
.fontWeight(.semibold)
.foregroundColor(.white)
.frame(width: 56, height: 56)
.background(Color.accentColor)
.clipShape(Circle())
.shadow(color: .black.opacity(0.25), radius: 6, x: 0, y: 3)
}
.padding(.trailing, 20)
.padding(.bottom, 20)
}
}
}
}
.navigationDestination(
item: Binding<String?>(
get: { selectedBookmarkId },
set: { selectedBookmarkId = $0 }
)
) { bookmarkId in
BookmarkDetailView(bookmarkId: bookmarkId)
}
.sheet(isPresented: $showingAddBookmark) {
AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle)
}
.sheet(
isPresented: $viewModel.showingAddBookmarkFromShare,
content: {
AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle)
}
)
.onAppear {
Task {
await viewModel.loadBookmarks(state: state, type: type, tag: tag)
}
}
.onChange(of: showingAddBookmark) { oldValue, newValue in
// Refresh bookmarks when sheet is dismissed
if oldValue && !newValue {
Task {
await viewModel.loadBookmarks(state: state, type: type)
}
}
}
}
}
#Preview {
BookmarksView(
viewModel: .init(MockUseCaseFactory()),
state: .archived,
type: [.article],
selectedBookmark: .constant(nil),
tag: nil)
}