feat: Add annotations support with color-coded highlighting

Add comprehensive annotations feature to bookmark detail views:
- Implement annotations list view with date formatting and state machine
- Add CSS-based highlighting for rd-annotation tags in WebView components
- Support Readeck color scheme (yellow, green, blue, red) for annotations
- Enable tap-to-scroll functionality to navigate to selected annotations
- Integrate annotations button in bookmark detail toolbar
- Add API endpoint and repository layer for fetching annotations
This commit is contained in:
Ilyas Hallak 2025-10-22 15:25:55 +02:00
parent 47f8f73664
commit cf06a3147d
17 changed files with 494 additions and 21 deletions

View File

@ -18,6 +18,7 @@ protocol PAPI {
func deleteBookmark(id: String) async throws
func searchBookmarks(search: String) async throws -> BookmarksPageDto
func getBookmarkLabels() async throws -> [BookmarkLabelDto]
func getBookmarkAnnotations(bookmarkId: String) async throws -> [AnnotationDto]
}
class API: PAPI {
@ -435,15 +436,29 @@ class API: PAPI {
logger.debug("Fetching bookmark labels")
let endpoint = "/api/bookmarks/labels"
logger.logNetworkRequest(method: "GET", url: await self.baseURL + endpoint)
let result = try await makeJSONRequest(
endpoint: endpoint,
responseType: [BookmarkLabelDto].self
)
logger.info("Successfully fetched \(result.count) bookmark labels")
return result
}
func getBookmarkAnnotations(bookmarkId: String) async throws -> [AnnotationDto] {
logger.debug("Fetching annotations for bookmark: \(bookmarkId)")
let endpoint = "/api/bookmarks/\(bookmarkId)/annotations"
logger.logNetworkRequest(method: "GET", url: await self.baseURL + endpoint)
let result = try await makeJSONRequest(
endpoint: endpoint,
responseType: [AnnotationDto].self
)
logger.info("Successfully fetched \(result.count) annotations for bookmark: \(bookmarkId)")
return result
}
}
enum HTTPMethod: String {

View File

@ -0,0 +1,21 @@
import Foundation
struct AnnotationDto: Codable {
let id: String
let text: String
let created: String
let startOffset: Int
let endOffset: Int
let startSelector: String
let endSelector: String
enum CodingKeys: String, CodingKey {
case id
case text
case created
case startOffset = "start_offset"
case endOffset = "end_offset"
case startSelector = "start_selector"
case endSelector = "end_selector"
}
}

View File

@ -0,0 +1,24 @@
import Foundation
class AnnotationsRepository: PAnnotationsRepository {
private let api: PAPI
init(api: PAPI) {
self.api = api
}
func fetchAnnotations(bookmarkId: String) async throws -> [Annotation] {
let annotationDtos = try await api.getBookmarkAnnotations(bookmarkId: bookmarkId)
return annotationDtos.map { dto in
Annotation(
id: dto.id,
text: dto.text,
created: dto.created,
startOffset: dto.startOffset,
endOffset: dto.endOffset,
startSelector: dto.startSelector,
endSelector: dto.endSelector
)
}
}
}

View File

@ -0,0 +1,19 @@
import Foundation
struct Annotation: Identifiable, Hashable {
let id: String
let text: String
let created: String
let startOffset: Int
let endOffset: Int
let startSelector: String
let endSelector: String
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: Annotation, rhs: Annotation) -> Bool {
lhs.id == rhs.id
}
}

View File

@ -0,0 +1,3 @@
protocol PAnnotationsRepository {
func fetchAnnotations(bookmarkId: String) async throws -> [Annotation]
}

View File

@ -0,0 +1,17 @@
import Foundation
protocol PGetBookmarkAnnotationsUseCase {
func execute(bookmarkId: String) async throws -> [Annotation]
}
class GetBookmarkAnnotationsUseCase: PGetBookmarkAnnotationsUseCase {
private let repository: PAnnotationsRepository
init(repository: PAnnotationsRepository) {
self.repository = repository
}
func execute(bookmarkId: String) async throws -> [Annotation] {
return try await repository.fetchAnnotations(bookmarkId: bookmarkId)
}
}

View File

@ -0,0 +1,120 @@
import SwiftUI
struct AnnotationsListView: View {
let bookmarkId: String
@State private var viewModel = AnnotationsListViewModel()
@Environment(\.dismiss) private var dismiss
var onAnnotationTap: ((String) -> Void)?
enum ViewState {
case loading
case empty
case loaded([Annotation])
case error(String)
}
private var viewState: ViewState {
if viewModel.isLoading {
return .loading
} else if let error = viewModel.errorMessage, viewModel.showErrorAlert {
return .error(error)
} else if viewModel.annotations.isEmpty {
return .empty
} else {
return .loaded(viewModel.annotations)
}
}
var body: some View {
List {
switch viewState {
case .loading:
HStack {
Spacer()
ProgressView()
Spacer()
}
case .empty:
ContentUnavailableView(
"No Annotations",
systemImage: "pencil.slash",
description: Text("This bookmark has no annotations yet.")
)
case .loaded(let annotations):
ForEach(annotations) { annotation in
Button(action: {
onAnnotationTap?(annotation.id)
dismiss()
}) {
VStack(alignment: .leading, spacing: 8) {
if !annotation.text.isEmpty {
Text(annotation.text)
.font(.body)
.foregroundColor(.primary)
}
Text(formatDate(annotation.created))
.font(.caption)
.foregroundStyle(.tertiary)
}
.padding(.vertical, 4)
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
case .error:
EmptyView()
}
}
.navigationTitle("Annotations")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
dismiss()
}
}
}
.task {
await viewModel.loadAnnotations(for: bookmarkId)
}
.alert("Error", isPresented: $viewModel.showErrorAlert) {
Button("OK", role: .cancel) {}
} message: {
if let errorMessage = viewModel.errorMessage {
Text(errorMessage)
}
}
}
private func formatDate(_ dateString: String) -> String {
let isoFormatter = ISO8601DateFormatter()
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let isoFormatterNoMillis = ISO8601DateFormatter()
isoFormatterNoMillis.formatOptions = [.withInternetDateTime]
var date: Date?
if let parsedDate = isoFormatter.date(from: dateString) {
date = parsedDate
} else if let parsedDate = isoFormatterNoMillis.date(from: dateString) {
date = parsedDate
}
if let date = date {
let displayFormatter = DateFormatter()
displayFormatter.dateStyle = .medium
displayFormatter.timeStyle = .short
displayFormatter.locale = .autoupdatingCurrent
return displayFormatter.string(from: date)
}
return dateString
}
}
#Preview {
NavigationStack {
AnnotationsListView(bookmarkId: "123")
}
}

View File

@ -0,0 +1,29 @@
import Foundation
@Observable
class AnnotationsListViewModel {
private let getAnnotationsUseCase: PGetBookmarkAnnotationsUseCase
var annotations: [Annotation] = []
var isLoading = false
var errorMessage: String?
var showErrorAlert = false
init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared) {
self.getAnnotationsUseCase = factory.makeGetBookmarkAnnotationsUseCase()
}
@MainActor
func loadAnnotations(for bookmarkId: String) async {
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
annotations = try await getAnnotationsUseCase.execute(bookmarkId: bookmarkId)
} catch {
errorMessage = "Failed to load annotations"
showErrorAlert = true
}
}
}

View File

@ -29,18 +29,19 @@ struct BookmarkDetailLegacyView: View {
@State private var initialContentEndPosition: CGFloat = 0
@State private var showingFontSettings = false
@State private var showingLabelsSheet = false
@State private var showingAnnotationsSheet = false
@State private var readingProgress: Double = 0.0
@State private var lastSentProgress: Double = 0.0
@State private var showJumpToProgressButton: Bool = false
@State private var scrollPosition = ScrollPosition(edge: .top)
@State private var showingImageViewer = false
// MARK: - Envs
@EnvironmentObject var playerUIState: PlayerUIState
@EnvironmentObject var appSettings: AppSettings
@Environment(\.dismiss) private var dismiss
private let headerHeight: CGFloat = 360
init(bookmarkId: String, useNativeWebView: Binding<Bool>, viewModel: BookmarkDetailViewModel = BookmarkDetailViewModel()) {
@ -86,7 +87,8 @@ struct BookmarkDetailLegacyView: View {
if webViewHeight != height {
webViewHeight = height
}
}
},
selectedAnnotationId: viewModel.selectedAnnotationId
)
.frame(height: webViewHeight)
.cornerRadius(14)
@ -220,6 +222,12 @@ struct BookmarkDetailLegacyView: View {
Image(systemName: "tag")
}
Button(action: {
showingAnnotationsSheet = true
}) {
Image(systemName: "pencil.line")
}
Button(action: {
showingFontSettings = true
}) {
@ -252,6 +260,11 @@ struct BookmarkDetailLegacyView: View {
.sheet(isPresented: $showingLabelsSheet) {
BookmarkLabelsView(bookmarkId: bookmarkId, initialLabels: viewModel.bookmarkDetail.labels)
}
.sheet(isPresented: $showingAnnotationsSheet) {
AnnotationsListView(bookmarkId: bookmarkId) { annotationId in
viewModel.selectedAnnotationId = annotationId
}
}
.sheet(isPresented: $showingImageViewer) {
ImageViewerView(imageUrl: viewModel.bookmarkDetail.imageUrl)
}
@ -271,9 +284,20 @@ struct BookmarkDetailLegacyView: View {
}
}
}
.onChange(of: showingAnnotationsSheet) { _, isShowing in
if !isShowing {
// Reload bookmark detail when labels sheet is dismissed
Task {
await viewModel.refreshBookmarkDetail(id: bookmarkId)
}
}
}
.onChange(of: viewModel.readProgress) { _, progress in
showJumpToProgressButton = progress > 0 && progress < 100
}
.onChange(of: viewModel.selectedAnnotationId) { _, _ in
// Trigger WebView reload when annotation is selected
}
.task {
await viewModel.loadBookmarkDetail(id: bookmarkId)
await viewModel.loadArticleContent(id: bookmarkId)

View File

@ -14,6 +14,7 @@ struct BookmarkDetailView2: View {
@State private var initialContentEndPosition: CGFloat = 0
@State private var showingFontSettings = false
@State private var showingLabelsSheet = false
@State private var showingAnnotationsSheet = false
@State private var readingProgress: Double = 0.0
@State private var lastSentProgress: Double = 0.0
@State private var showJumpToProgressButton: Bool = false
@ -50,6 +51,11 @@ struct BookmarkDetailView2: View {
.sheet(isPresented: $showingLabelsSheet) {
BookmarkLabelsView(bookmarkId: bookmarkId, initialLabels: viewModel.bookmarkDetail.labels)
}
.sheet(isPresented: $showingAnnotationsSheet) {
AnnotationsListView(bookmarkId: bookmarkId) { annotationId in
viewModel.selectedAnnotationId = annotationId
}
}
.sheet(isPresented: $showingImageViewer) {
ImageViewerView(imageUrl: viewModel.bookmarkDetail.imageUrl)
}
@ -67,9 +73,19 @@ struct BookmarkDetailView2: View {
}
}
}
.onChange(of: showingAnnotationsSheet) { _, isShowing in
if !isShowing {
Task {
await viewModel.refreshBookmarkDetail(id: bookmarkId)
}
}
}
.onChange(of: viewModel.readProgress) { _, progress in
showJumpToProgressButton = progress > 0 && progress < 100
}
.onChange(of: viewModel.selectedAnnotationId) { _, _ in
// Trigger WebView reload when annotation is selected
}
.task {
await viewModel.loadBookmarkDetail(id: bookmarkId)
await viewModel.loadArticleContent(id: bookmarkId)
@ -254,6 +270,12 @@ struct BookmarkDetailView2: View {
Image(systemName: "tag")
}
Button(action: {
showingAnnotationsSheet = true
}) {
Image(systemName: "pencil.line")
}
Button(action: {
showingFontSettings = true
}) {
@ -437,7 +459,8 @@ struct BookmarkDetailView2: View {
if webViewHeight != height {
webViewHeight = height
}
}
},
selectedAnnotationId: viewModel.selectedAnnotationId
)
.frame(height: webViewHeight)
.cornerRadius(14)

View File

@ -8,7 +8,7 @@ class BookmarkDetailViewModel {
private let loadSettingsUseCase: PLoadSettingsUseCase
private let updateBookmarkUseCase: PUpdateBookmarkUseCase
private var addTextToSpeechQueueUseCase: PAddTextToSpeechQueueUseCase?
var bookmarkDetail: BookmarkDetail = BookmarkDetail.empty
var articleContent: String = ""
var articleParagraphs: [String] = []
@ -18,7 +18,8 @@ class BookmarkDetailViewModel {
var errorMessage: String?
var settings: Settings?
var readProgress: Int = 0
var selectedAnnotationId: String?
private var factory: UseCaseFactory?
private var cancellables = Set<AnyCancellable>()
private let readProgressSubject = PassthroughSubject<(id: String, progress: Double, anchor: String?), Never>()
@ -29,7 +30,7 @@ class BookmarkDetailViewModel {
self.loadSettingsUseCase = factory.makeLoadSettingsUseCase()
self.updateBookmarkUseCase = factory.makeUpdateBookmarkUseCase()
self.factory = factory
readProgressSubject
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
.sink { [weak self] (id, progress, anchor) in
@ -67,17 +68,17 @@ class BookmarkDetailViewModel {
@MainActor
func loadArticleContent(id: String) async {
isLoadingArticle = true
do {
articleContent = try await getBookmarkArticleUseCase.execute(id: id)
processArticleContent()
} catch {
errorMessage = "Error loading article"
}
isLoadingArticle = false
}
private func processArticleContent() {
let paragraphs = articleContent
.components(separatedBy: .newlines)

View File

@ -11,7 +11,8 @@ struct NativeWebView: View {
let settings: Settings
let onHeightChange: (CGFloat) -> Void
var onScroll: ((Double) -> Void)? = nil
var selectedAnnotationId: String?
@State private var webPage = WebPage()
@Environment(\.colorScheme) private var colorScheme
@ -27,6 +28,9 @@ struct NativeWebView: View {
.onChange(of: colorScheme) { _, _ in
loadStyledContent()
}
.onChange(of: selectedAnnotationId) { _, _ in
loadStyledContent()
}
.onChange(of: webPage.isLoading) { _, isLoading in
if !isLoading {
// Update height when content finishes loading
@ -197,6 +201,49 @@ struct NativeWebView: View {
th { font-weight: 600; }
hr { border: none; height: 1px; background-color: #ccc; margin: 24px 0; }
/* Annotation Highlighting - for rd-annotation tags */
rd-annotation {
border-radius: 3px;
padding: 2px 0;
transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
/* Yellow annotations */
rd-annotation[data-annotation-color="yellow"] {
background-color: \(isDarkMode ? "rgba(158, 117, 4, 0.4)" : "rgba(107, 79, 3, 0.3)");
}
rd-annotation[data-annotation-color="yellow"].selected {
background-color: \(isDarkMode ? "rgba(158, 117, 4, 0.6)" : "rgba(107, 79, 3, 0.5)");
box-shadow: 0 0 0 2px \(isDarkMode ? "rgba(158, 117, 4, 0.5)" : "rgba(107, 79, 3, 0.6)");
}
/* Green annotations */
rd-annotation[data-annotation-color="green"] {
background-color: \(isDarkMode ? "rgba(132, 204, 22, 0.4)" : "rgba(57, 88, 9, 0.3)");
}
rd-annotation[data-annotation-color="green"].selected {
background-color: \(isDarkMode ? "rgba(132, 204, 22, 0.6)" : "rgba(57, 88, 9, 0.5)");
box-shadow: 0 0 0 2px \(isDarkMode ? "rgba(132, 204, 22, 0.5)" : "rgba(57, 88, 9, 0.6)");
}
/* Blue annotations */
rd-annotation[data-annotation-color="blue"] {
background-color: \(isDarkMode ? "rgba(9, 132, 159, 0.4)" : "rgba(7, 95, 116, 0.3)");
}
rd-annotation[data-annotation-color="blue"].selected {
background-color: \(isDarkMode ? "rgba(9, 132, 159, 0.6)" : "rgba(7, 95, 116, 0.5)");
box-shadow: 0 0 0 2px \(isDarkMode ? "rgba(9, 132, 159, 0.5)" : "rgba(7, 95, 116, 0.6)");
}
/* Red annotations */
rd-annotation[data-annotation-color="red"] {
background-color: \(isDarkMode ? "rgba(152, 43, 43, 0.4)" : "rgba(103, 29, 29, 0.3)");
}
rd-annotation[data-annotation-color="red"].selected {
background-color: \(isDarkMode ? "rgba(152, 43, 43, 0.6)" : "rgba(103, 29, 29, 0.5)");
box-shadow: 0 0 0 2px \(isDarkMode ? "rgba(152, 43, 43, 0.5)" : "rgba(103, 29, 29, 0.6)");
}
</style>
</head>
<body>
@ -242,6 +289,9 @@ struct NativeWebView: View {
}
scheduleHeightCheck();
// Scroll to selected annotation
\(generateScrollToAnnotationJS())
</script>
</body>
</html>
@ -273,6 +323,37 @@ struct NativeWebView: View {
case .monospace: return "'SF Mono', Menlo, Monaco, monospace"
}
}
private func generateScrollToAnnotationJS() -> String {
guard let selectedId = selectedAnnotationId else {
return ""
}
return """
// Scroll to selected annotation and add selected class
function scrollToAnnotation() {
// Remove 'selected' class from all annotations
document.querySelectorAll('rd-annotation.selected').forEach(el => {
el.classList.remove('selected');
});
// Find and highlight selected annotation
const selectedElement = document.querySelector('rd-annotation[data-annotation-id-value="\(selectedId)"]');
if (selectedElement) {
selectedElement.classList.add('selected');
setTimeout(() => {
selectedElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 100);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', scrollToAnnotation);
} else {
setTimeout(scrollToAnnotation, 300);
}
"""
}
}
// MARK: - Hybrid WebView (Not Currently Used)

View File

@ -6,6 +6,7 @@ struct WebView: UIViewRepresentable {
let settings: Settings
let onHeightChange: (CGFloat) -> Void
var onScroll: ((Double) -> Void)? = nil
var selectedAnnotationId: String?
@Environment(\.colorScheme) private var colorScheme
func makeUIView(context: Context) -> WKWebView {
@ -235,6 +236,49 @@ struct WebView: UIViewRepresentable {
--separator-color: #e0e0e0;
}
}
/* Annotation Highlighting - for rd-annotation tags */
rd-annotation {
border-radius: 3px;
padding: 2px 0;
transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
/* Yellow annotations */
rd-annotation[data-annotation-color="yellow"] {
background-color: \(isDarkMode ? "rgba(158, 117, 4, 0.4)" : "rgba(107, 79, 3, 0.3)");
}
rd-annotation[data-annotation-color="yellow"].selected {
background-color: \(isDarkMode ? "rgba(158, 117, 4, 0.6)" : "rgba(107, 79, 3, 0.5)");
box-shadow: 0 0 0 2px \(isDarkMode ? "rgba(158, 117, 4, 0.5)" : "rgba(107, 79, 3, 0.6)");
}
/* Green annotations */
rd-annotation[data-annotation-color="green"] {
background-color: \(isDarkMode ? "rgba(132, 204, 22, 0.4)" : "rgba(57, 88, 9, 0.3)");
}
rd-annotation[data-annotation-color="green"].selected {
background-color: \(isDarkMode ? "rgba(132, 204, 22, 0.6)" : "rgba(57, 88, 9, 0.5)");
box-shadow: 0 0 0 2px \(isDarkMode ? "rgba(132, 204, 22, 0.5)" : "rgba(57, 88, 9, 0.6)");
}
/* Blue annotations */
rd-annotation[data-annotation-color="blue"] {
background-color: \(isDarkMode ? "rgba(9, 132, 159, 0.4)" : "rgba(7, 95, 116, 0.3)");
}
rd-annotation[data-annotation-color="blue"].selected {
background-color: \(isDarkMode ? "rgba(9, 132, 159, 0.6)" : "rgba(7, 95, 116, 0.5)");
box-shadow: 0 0 0 2px \(isDarkMode ? "rgba(9, 132, 159, 0.5)" : "rgba(7, 95, 116, 0.6)");
}
/* Red annotations */
rd-annotation[data-annotation-color="red"] {
background-color: \(isDarkMode ? "rgba(152, 43, 43, 0.4)" : "rgba(103, 29, 29, 0.3)");
}
rd-annotation[data-annotation-color="red"].selected {
background-color: \(isDarkMode ? "rgba(152, 43, 43, 0.6)" : "rgba(103, 29, 29, 0.5)");
box-shadow: 0 0 0 2px \(isDarkMode ? "rgba(152, 43, 43, 0.5)" : "rgba(103, 29, 29, 0.6)");
}
</style>
</head>
<body>
@ -264,6 +308,9 @@ struct WebView: UIViewRepresentable {
document.querySelectorAll('img').forEach(img => {
img.addEventListener('load', debouncedHeightUpdate);
});
// Scroll to selected annotation
\(generateScrollToAnnotationJS())
</script>
</body>
</html>
@ -305,6 +352,37 @@ struct WebView: UIViewRepresentable {
return "'SF Mono', Menlo, Monaco, Consolas, 'Liberation Mono', monospace"
}
}
private func generateScrollToAnnotationJS() -> String {
guard let selectedId = selectedAnnotationId else {
return ""
}
return """
// Scroll to selected annotation and add selected class
function scrollToAnnotation() {
// Remove 'selected' class from all annotations
document.querySelectorAll('rd-annotation.selected').forEach(el => {
el.classList.remove('selected');
});
// Find and highlight selected annotation
const selectedElement = document.querySelector('rd-annotation[data-annotation-id-value="\(selectedId)"]');
if (selectedElement) {
selectedElement.classList.add('selected');
setTimeout(() => {
selectedElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 100);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', scrollToAnnotation);
} else {
setTimeout(scrollToAnnotation, 300);
}
"""
}
}
class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {

View File

@ -21,6 +21,7 @@ protocol UseCaseFactory {
func makeLoadCardLayoutUseCase() -> PLoadCardLayoutUseCase
func makeSaveCardLayoutUseCase() -> PSaveCardLayoutUseCase
func makeCheckServerReachabilityUseCase() -> PCheckServerReachabilityUseCase
func makeGetBookmarkAnnotationsUseCase() -> PGetBookmarkAnnotationsUseCase
}
@ -33,6 +34,7 @@ class DefaultUseCaseFactory: UseCaseFactory {
private let settingsRepository: PSettingsRepository = SettingsRepository()
private lazy var infoApiClient: PInfoApiClient = InfoApiClient(tokenProvider: tokenProvider)
private lazy var serverInfoRepository: PServerInfoRepository = ServerInfoRepository(apiClient: infoApiClient)
private lazy var annotationsRepository: PAnnotationsRepository = AnnotationsRepository(api: api)
static let shared = DefaultUseCaseFactory()
@ -119,4 +121,8 @@ class DefaultUseCaseFactory: UseCaseFactory {
func makeCheckServerReachabilityUseCase() -> PCheckServerReachabilityUseCase {
return CheckServerReachabilityUseCase(repository: serverInfoRepository)
}
func makeGetBookmarkAnnotationsUseCase() -> PGetBookmarkAnnotationsUseCase {
return GetBookmarkAnnotationsUseCase(repository: annotationsRepository)
}
}

View File

@ -88,6 +88,10 @@ class MockUseCaseFactory: UseCaseFactory {
func makeSaveCardLayoutUseCase() -> PSaveCardLayoutUseCase {
MockSaveCardLayoutUseCase()
}
func makeGetBookmarkAnnotationsUseCase() -> PGetBookmarkAnnotationsUseCase {
MockGetBookmarkAnnotationsUseCase()
}
}
@ -238,6 +242,14 @@ class MockCheckServerReachabilityUseCase: PCheckServerReachabilityUseCase {
}
}
class MockGetBookmarkAnnotationsUseCase: PGetBookmarkAnnotationsUseCase {
func execute(bookmarkId: String) async throws -> [Annotation] {
return [
.init(id: "1", text: "bla", created: "", startOffset: 0, endOffset: 1, startSelector: "", endSelector: "")
]
}
}
extension Bookmark {
static let mock: Bookmark = .init(
id: "123", title: "title", url: "https://example.com", href: "https://example.com", description: "description", authors: ["Tom"], created: "", published: "", updated: "", siteName: "example.com", site: "https://example.com", readingTime: 2, wordCount: 20, hasArticle: true, isArchived: false, isDeleted: false, isMarked: true, labels: ["Test"], lang: "EN", loaded: false, readProgress: 0, documentType: "", state: 0, textDirection: "ltr", type: "", resources: .init(article: nil, icon: nil, image: nil, log: nil, props: nil, thumbnail: nil)

View File

@ -87,11 +87,11 @@ struct PadSidebarView: View {
case .all:
BookmarksView(state: .all, type: [.article, .video, .photo], selectedBookmark: $selectedBookmark)
case .unread:
BookmarksView(state: .unread, type: [.article], selectedBookmark: $selectedBookmark)
BookmarksView(state: .unread, type: [.article, .video, .photo], selectedBookmark: $selectedBookmark)
case .favorite:
BookmarksView(state: .favorite, type: [.article], selectedBookmark: $selectedBookmark)
BookmarksView(state: .favorite, type: [.article, .video, .photo], selectedBookmark: $selectedBookmark)
case .archived:
BookmarksView(state: .archived, type: [.article], selectedBookmark: $selectedBookmark)
BookmarksView(state: .archived, type: [.article, .video, .photo], selectedBookmark: $selectedBookmark)
case .settings:
SettingsView()
case .article:

View File

@ -153,7 +153,7 @@ struct PhoneTabView: View {
// Hidden NavigationLink to remove disclosure indicator
NavigationLink {
BookmarkDetailView(bookmarkId: bookmark.id)
BookmarkDetailView(bookmarkId: bookmark.id)
} label: {
EmptyView()
}
@ -234,11 +234,11 @@ struct PhoneTabView: View {
case .all:
BookmarksView(state: .all, type: [.article, .video, .photo], selectedBookmark: .constant(nil))
case .unread:
BookmarksView(state: .unread, type: [.article], selectedBookmark: .constant(nil))
BookmarksView(state: .unread, type: [.article, .video, .photo], selectedBookmark: .constant(nil))
case .favorite:
BookmarksView(state: .favorite, type: [.article], selectedBookmark: .constant(nil))
BookmarksView(state: .favorite, type: [.article, .video, .photo], selectedBookmark: .constant(nil))
case .archived:
BookmarksView(state: .archived, type: [.article], selectedBookmark: .constant(nil))
BookmarksView(state: .archived, type: [.article, .video, .photo], selectedBookmark: .constant(nil))
case .search:
EmptyView() // search is directly implemented
case .settings: