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:
parent
47f8f73664
commit
cf06a3147d
@ -18,6 +18,7 @@ protocol PAPI {
|
|||||||
func deleteBookmark(id: String) async throws
|
func deleteBookmark(id: String) async throws
|
||||||
func searchBookmarks(search: String) async throws -> BookmarksPageDto
|
func searchBookmarks(search: String) async throws -> BookmarksPageDto
|
||||||
func getBookmarkLabels() async throws -> [BookmarkLabelDto]
|
func getBookmarkLabels() async throws -> [BookmarkLabelDto]
|
||||||
|
func getBookmarkAnnotations(bookmarkId: String) async throws -> [AnnotationDto]
|
||||||
}
|
}
|
||||||
|
|
||||||
class API: PAPI {
|
class API: PAPI {
|
||||||
@ -435,15 +436,29 @@ class API: PAPI {
|
|||||||
logger.debug("Fetching bookmark labels")
|
logger.debug("Fetching bookmark labels")
|
||||||
let endpoint = "/api/bookmarks/labels"
|
let endpoint = "/api/bookmarks/labels"
|
||||||
logger.logNetworkRequest(method: "GET", url: await self.baseURL + endpoint)
|
logger.logNetworkRequest(method: "GET", url: await self.baseURL + endpoint)
|
||||||
|
|
||||||
let result = try await makeJSONRequest(
|
let result = try await makeJSONRequest(
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
responseType: [BookmarkLabelDto].self
|
responseType: [BookmarkLabelDto].self
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("Successfully fetched \(result.count) bookmark labels")
|
logger.info("Successfully fetched \(result.count) bookmark labels")
|
||||||
return result
|
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 {
|
enum HTTPMethod: String {
|
||||||
|
|||||||
21
readeck/Data/API/DTOs/AnnotationDto.swift
Normal file
21
readeck/Data/API/DTOs/AnnotationDto.swift
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
24
readeck/Data/Repository/AnnotationsRepository.swift
Normal file
24
readeck/Data/Repository/AnnotationsRepository.swift
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
readeck/Domain/Model/Annotation.swift
Normal file
19
readeck/Domain/Model/Annotation.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
3
readeck/Domain/Protocols/PAnnotationsRepository.swift
Normal file
3
readeck/Domain/Protocols/PAnnotationsRepository.swift
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
protocol PAnnotationsRepository {
|
||||||
|
func fetchAnnotations(bookmarkId: String) async throws -> [Annotation]
|
||||||
|
}
|
||||||
17
readeck/Domain/UseCase/GetBookmarkAnnotationsUseCase.swift
Normal file
17
readeck/Domain/UseCase/GetBookmarkAnnotationsUseCase.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
120
readeck/UI/BookmarkDetail/AnnotationsListView.swift
Normal file
120
readeck/UI/BookmarkDetail/AnnotationsListView.swift
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
29
readeck/UI/BookmarkDetail/AnnotationsListViewModel.swift
Normal file
29
readeck/UI/BookmarkDetail/AnnotationsListViewModel.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,18 +29,19 @@ struct BookmarkDetailLegacyView: View {
|
|||||||
@State private var initialContentEndPosition: CGFloat = 0
|
@State private var initialContentEndPosition: CGFloat = 0
|
||||||
@State private var showingFontSettings = false
|
@State private var showingFontSettings = false
|
||||||
@State private var showingLabelsSheet = false
|
@State private var showingLabelsSheet = false
|
||||||
|
@State private var showingAnnotationsSheet = false
|
||||||
@State private var readingProgress: Double = 0.0
|
@State private var readingProgress: Double = 0.0
|
||||||
@State private var lastSentProgress: Double = 0.0
|
@State private var lastSentProgress: Double = 0.0
|
||||||
@State private var showJumpToProgressButton: Bool = false
|
@State private var showJumpToProgressButton: Bool = false
|
||||||
@State private var scrollPosition = ScrollPosition(edge: .top)
|
@State private var scrollPosition = ScrollPosition(edge: .top)
|
||||||
@State private var showingImageViewer = false
|
@State private var showingImageViewer = false
|
||||||
|
|
||||||
// MARK: - Envs
|
// MARK: - Envs
|
||||||
|
|
||||||
@EnvironmentObject var playerUIState: PlayerUIState
|
@EnvironmentObject var playerUIState: PlayerUIState
|
||||||
@EnvironmentObject var appSettings: AppSettings
|
@EnvironmentObject var appSettings: AppSettings
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
private let headerHeight: CGFloat = 360
|
private let headerHeight: CGFloat = 360
|
||||||
|
|
||||||
init(bookmarkId: String, useNativeWebView: Binding<Bool>, viewModel: BookmarkDetailViewModel = BookmarkDetailViewModel()) {
|
init(bookmarkId: String, useNativeWebView: Binding<Bool>, viewModel: BookmarkDetailViewModel = BookmarkDetailViewModel()) {
|
||||||
@ -86,7 +87,8 @@ struct BookmarkDetailLegacyView: View {
|
|||||||
if webViewHeight != height {
|
if webViewHeight != height {
|
||||||
webViewHeight = height
|
webViewHeight = height
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
selectedAnnotationId: viewModel.selectedAnnotationId
|
||||||
)
|
)
|
||||||
.frame(height: webViewHeight)
|
.frame(height: webViewHeight)
|
||||||
.cornerRadius(14)
|
.cornerRadius(14)
|
||||||
@ -220,6 +222,12 @@ struct BookmarkDetailLegacyView: View {
|
|||||||
Image(systemName: "tag")
|
Image(systemName: "tag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
showingAnnotationsSheet = true
|
||||||
|
}) {
|
||||||
|
Image(systemName: "pencil.line")
|
||||||
|
}
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
showingFontSettings = true
|
showingFontSettings = true
|
||||||
}) {
|
}) {
|
||||||
@ -252,6 +260,11 @@ struct BookmarkDetailLegacyView: View {
|
|||||||
.sheet(isPresented: $showingLabelsSheet) {
|
.sheet(isPresented: $showingLabelsSheet) {
|
||||||
BookmarkLabelsView(bookmarkId: bookmarkId, initialLabels: viewModel.bookmarkDetail.labels)
|
BookmarkLabelsView(bookmarkId: bookmarkId, initialLabels: viewModel.bookmarkDetail.labels)
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showingAnnotationsSheet) {
|
||||||
|
AnnotationsListView(bookmarkId: bookmarkId) { annotationId in
|
||||||
|
viewModel.selectedAnnotationId = annotationId
|
||||||
|
}
|
||||||
|
}
|
||||||
.sheet(isPresented: $showingImageViewer) {
|
.sheet(isPresented: $showingImageViewer) {
|
||||||
ImageViewerView(imageUrl: viewModel.bookmarkDetail.imageUrl)
|
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
|
.onChange(of: viewModel.readProgress) { _, progress in
|
||||||
showJumpToProgressButton = progress > 0 && progress < 100
|
showJumpToProgressButton = progress > 0 && progress < 100
|
||||||
}
|
}
|
||||||
|
.onChange(of: viewModel.selectedAnnotationId) { _, _ in
|
||||||
|
// Trigger WebView reload when annotation is selected
|
||||||
|
}
|
||||||
.task {
|
.task {
|
||||||
await viewModel.loadBookmarkDetail(id: bookmarkId)
|
await viewModel.loadBookmarkDetail(id: bookmarkId)
|
||||||
await viewModel.loadArticleContent(id: bookmarkId)
|
await viewModel.loadArticleContent(id: bookmarkId)
|
||||||
|
|||||||
@ -14,6 +14,7 @@ struct BookmarkDetailView2: View {
|
|||||||
@State private var initialContentEndPosition: CGFloat = 0
|
@State private var initialContentEndPosition: CGFloat = 0
|
||||||
@State private var showingFontSettings = false
|
@State private var showingFontSettings = false
|
||||||
@State private var showingLabelsSheet = false
|
@State private var showingLabelsSheet = false
|
||||||
|
@State private var showingAnnotationsSheet = false
|
||||||
@State private var readingProgress: Double = 0.0
|
@State private var readingProgress: Double = 0.0
|
||||||
@State private var lastSentProgress: Double = 0.0
|
@State private var lastSentProgress: Double = 0.0
|
||||||
@State private var showJumpToProgressButton: Bool = false
|
@State private var showJumpToProgressButton: Bool = false
|
||||||
@ -50,6 +51,11 @@ struct BookmarkDetailView2: View {
|
|||||||
.sheet(isPresented: $showingLabelsSheet) {
|
.sheet(isPresented: $showingLabelsSheet) {
|
||||||
BookmarkLabelsView(bookmarkId: bookmarkId, initialLabels: viewModel.bookmarkDetail.labels)
|
BookmarkLabelsView(bookmarkId: bookmarkId, initialLabels: viewModel.bookmarkDetail.labels)
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showingAnnotationsSheet) {
|
||||||
|
AnnotationsListView(bookmarkId: bookmarkId) { annotationId in
|
||||||
|
viewModel.selectedAnnotationId = annotationId
|
||||||
|
}
|
||||||
|
}
|
||||||
.sheet(isPresented: $showingImageViewer) {
|
.sheet(isPresented: $showingImageViewer) {
|
||||||
ImageViewerView(imageUrl: viewModel.bookmarkDetail.imageUrl)
|
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
|
.onChange(of: viewModel.readProgress) { _, progress in
|
||||||
showJumpToProgressButton = progress > 0 && progress < 100
|
showJumpToProgressButton = progress > 0 && progress < 100
|
||||||
}
|
}
|
||||||
|
.onChange(of: viewModel.selectedAnnotationId) { _, _ in
|
||||||
|
// Trigger WebView reload when annotation is selected
|
||||||
|
}
|
||||||
.task {
|
.task {
|
||||||
await viewModel.loadBookmarkDetail(id: bookmarkId)
|
await viewModel.loadBookmarkDetail(id: bookmarkId)
|
||||||
await viewModel.loadArticleContent(id: bookmarkId)
|
await viewModel.loadArticleContent(id: bookmarkId)
|
||||||
@ -254,6 +270,12 @@ struct BookmarkDetailView2: View {
|
|||||||
Image(systemName: "tag")
|
Image(systemName: "tag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
showingAnnotationsSheet = true
|
||||||
|
}) {
|
||||||
|
Image(systemName: "pencil.line")
|
||||||
|
}
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
showingFontSettings = true
|
showingFontSettings = true
|
||||||
}) {
|
}) {
|
||||||
@ -437,7 +459,8 @@ struct BookmarkDetailView2: View {
|
|||||||
if webViewHeight != height {
|
if webViewHeight != height {
|
||||||
webViewHeight = height
|
webViewHeight = height
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
selectedAnnotationId: viewModel.selectedAnnotationId
|
||||||
)
|
)
|
||||||
.frame(height: webViewHeight)
|
.frame(height: webViewHeight)
|
||||||
.cornerRadius(14)
|
.cornerRadius(14)
|
||||||
|
|||||||
@ -8,7 +8,7 @@ class BookmarkDetailViewModel {
|
|||||||
private let loadSettingsUseCase: PLoadSettingsUseCase
|
private let loadSettingsUseCase: PLoadSettingsUseCase
|
||||||
private let updateBookmarkUseCase: PUpdateBookmarkUseCase
|
private let updateBookmarkUseCase: PUpdateBookmarkUseCase
|
||||||
private var addTextToSpeechQueueUseCase: PAddTextToSpeechQueueUseCase?
|
private var addTextToSpeechQueueUseCase: PAddTextToSpeechQueueUseCase?
|
||||||
|
|
||||||
var bookmarkDetail: BookmarkDetail = BookmarkDetail.empty
|
var bookmarkDetail: BookmarkDetail = BookmarkDetail.empty
|
||||||
var articleContent: String = ""
|
var articleContent: String = ""
|
||||||
var articleParagraphs: [String] = []
|
var articleParagraphs: [String] = []
|
||||||
@ -18,7 +18,8 @@ class BookmarkDetailViewModel {
|
|||||||
var errorMessage: String?
|
var errorMessage: String?
|
||||||
var settings: Settings?
|
var settings: Settings?
|
||||||
var readProgress: Int = 0
|
var readProgress: Int = 0
|
||||||
|
var selectedAnnotationId: String?
|
||||||
|
|
||||||
private var factory: UseCaseFactory?
|
private var factory: UseCaseFactory?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private let readProgressSubject = PassthroughSubject<(id: String, progress: Double, anchor: String?), Never>()
|
private let readProgressSubject = PassthroughSubject<(id: String, progress: Double, anchor: String?), Never>()
|
||||||
@ -29,7 +30,7 @@ class BookmarkDetailViewModel {
|
|||||||
self.loadSettingsUseCase = factory.makeLoadSettingsUseCase()
|
self.loadSettingsUseCase = factory.makeLoadSettingsUseCase()
|
||||||
self.updateBookmarkUseCase = factory.makeUpdateBookmarkUseCase()
|
self.updateBookmarkUseCase = factory.makeUpdateBookmarkUseCase()
|
||||||
self.factory = factory
|
self.factory = factory
|
||||||
|
|
||||||
readProgressSubject
|
readProgressSubject
|
||||||
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
|
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
|
||||||
.sink { [weak self] (id, progress, anchor) in
|
.sink { [weak self] (id, progress, anchor) in
|
||||||
@ -67,17 +68,17 @@ class BookmarkDetailViewModel {
|
|||||||
@MainActor
|
@MainActor
|
||||||
func loadArticleContent(id: String) async {
|
func loadArticleContent(id: String) async {
|
||||||
isLoadingArticle = true
|
isLoadingArticle = true
|
||||||
|
|
||||||
do {
|
do {
|
||||||
articleContent = try await getBookmarkArticleUseCase.execute(id: id)
|
articleContent = try await getBookmarkArticleUseCase.execute(id: id)
|
||||||
processArticleContent()
|
processArticleContent()
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Error loading article"
|
errorMessage = "Error loading article"
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingArticle = false
|
isLoadingArticle = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func processArticleContent() {
|
private func processArticleContent() {
|
||||||
let paragraphs = articleContent
|
let paragraphs = articleContent
|
||||||
.components(separatedBy: .newlines)
|
.components(separatedBy: .newlines)
|
||||||
|
|||||||
@ -11,7 +11,8 @@ struct NativeWebView: View {
|
|||||||
let settings: Settings
|
let settings: Settings
|
||||||
let onHeightChange: (CGFloat) -> Void
|
let onHeightChange: (CGFloat) -> Void
|
||||||
var onScroll: ((Double) -> Void)? = nil
|
var onScroll: ((Double) -> Void)? = nil
|
||||||
|
var selectedAnnotationId: String?
|
||||||
|
|
||||||
@State private var webPage = WebPage()
|
@State private var webPage = WebPage()
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
@ -27,6 +28,9 @@ struct NativeWebView: View {
|
|||||||
.onChange(of: colorScheme) { _, _ in
|
.onChange(of: colorScheme) { _, _ in
|
||||||
loadStyledContent()
|
loadStyledContent()
|
||||||
}
|
}
|
||||||
|
.onChange(of: selectedAnnotationId) { _, _ in
|
||||||
|
loadStyledContent()
|
||||||
|
}
|
||||||
.onChange(of: webPage.isLoading) { _, isLoading in
|
.onChange(of: webPage.isLoading) { _, isLoading in
|
||||||
if !isLoading {
|
if !isLoading {
|
||||||
// Update height when content finishes loading
|
// Update height when content finishes loading
|
||||||
@ -197,6 +201,49 @@ struct NativeWebView: View {
|
|||||||
th { font-weight: 600; }
|
th { font-weight: 600; }
|
||||||
|
|
||||||
hr { border: none; height: 1px; background-color: #ccc; margin: 24px 0; }
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -242,6 +289,9 @@ struct NativeWebView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scheduleHeightCheck();
|
scheduleHeightCheck();
|
||||||
|
|
||||||
|
// Scroll to selected annotation
|
||||||
|
\(generateScrollToAnnotationJS())
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -273,6 +323,37 @@ struct NativeWebView: View {
|
|||||||
case .monospace: return "'SF Mono', Menlo, Monaco, monospace"
|
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)
|
// MARK: - Hybrid WebView (Not Currently Used)
|
||||||
|
|||||||
@ -6,6 +6,7 @@ struct WebView: UIViewRepresentable {
|
|||||||
let settings: Settings
|
let settings: Settings
|
||||||
let onHeightChange: (CGFloat) -> Void
|
let onHeightChange: (CGFloat) -> Void
|
||||||
var onScroll: ((Double) -> Void)? = nil
|
var onScroll: ((Double) -> Void)? = nil
|
||||||
|
var selectedAnnotationId: String?
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
func makeUIView(context: Context) -> WKWebView {
|
func makeUIView(context: Context) -> WKWebView {
|
||||||
@ -235,6 +236,49 @@ struct WebView: UIViewRepresentable {
|
|||||||
--separator-color: #e0e0e0;
|
--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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -264,6 +308,9 @@ struct WebView: UIViewRepresentable {
|
|||||||
document.querySelectorAll('img').forEach(img => {
|
document.querySelectorAll('img').forEach(img => {
|
||||||
img.addEventListener('load', debouncedHeightUpdate);
|
img.addEventListener('load', debouncedHeightUpdate);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Scroll to selected annotation
|
||||||
|
\(generateScrollToAnnotationJS())
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -305,6 +352,37 @@ struct WebView: UIViewRepresentable {
|
|||||||
return "'SF Mono', Menlo, Monaco, Consolas, 'Liberation Mono', monospace"
|
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 {
|
class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
|
||||||
|
|||||||
@ -21,6 +21,7 @@ protocol UseCaseFactory {
|
|||||||
func makeLoadCardLayoutUseCase() -> PLoadCardLayoutUseCase
|
func makeLoadCardLayoutUseCase() -> PLoadCardLayoutUseCase
|
||||||
func makeSaveCardLayoutUseCase() -> PSaveCardLayoutUseCase
|
func makeSaveCardLayoutUseCase() -> PSaveCardLayoutUseCase
|
||||||
func makeCheckServerReachabilityUseCase() -> PCheckServerReachabilityUseCase
|
func makeCheckServerReachabilityUseCase() -> PCheckServerReachabilityUseCase
|
||||||
|
func makeGetBookmarkAnnotationsUseCase() -> PGetBookmarkAnnotationsUseCase
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
|||||||
private let settingsRepository: PSettingsRepository = SettingsRepository()
|
private let settingsRepository: PSettingsRepository = SettingsRepository()
|
||||||
private lazy var infoApiClient: PInfoApiClient = InfoApiClient(tokenProvider: tokenProvider)
|
private lazy var infoApiClient: PInfoApiClient = InfoApiClient(tokenProvider: tokenProvider)
|
||||||
private lazy var serverInfoRepository: PServerInfoRepository = ServerInfoRepository(apiClient: infoApiClient)
|
private lazy var serverInfoRepository: PServerInfoRepository = ServerInfoRepository(apiClient: infoApiClient)
|
||||||
|
private lazy var annotationsRepository: PAnnotationsRepository = AnnotationsRepository(api: api)
|
||||||
|
|
||||||
static let shared = DefaultUseCaseFactory()
|
static let shared = DefaultUseCaseFactory()
|
||||||
|
|
||||||
@ -119,4 +121,8 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
|||||||
func makeCheckServerReachabilityUseCase() -> PCheckServerReachabilityUseCase {
|
func makeCheckServerReachabilityUseCase() -> PCheckServerReachabilityUseCase {
|
||||||
return CheckServerReachabilityUseCase(repository: serverInfoRepository)
|
return CheckServerReachabilityUseCase(repository: serverInfoRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeGetBookmarkAnnotationsUseCase() -> PGetBookmarkAnnotationsUseCase {
|
||||||
|
return GetBookmarkAnnotationsUseCase(repository: annotationsRepository)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,6 +88,10 @@ class MockUseCaseFactory: UseCaseFactory {
|
|||||||
func makeSaveCardLayoutUseCase() -> PSaveCardLayoutUseCase {
|
func makeSaveCardLayoutUseCase() -> PSaveCardLayoutUseCase {
|
||||||
MockSaveCardLayoutUseCase()
|
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 {
|
extension Bookmark {
|
||||||
static let mock: Bookmark = .init(
|
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)
|
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)
|
||||||
|
|||||||
@ -87,11 +87,11 @@ struct PadSidebarView: View {
|
|||||||
case .all:
|
case .all:
|
||||||
BookmarksView(state: .all, type: [.article, .video, .photo], selectedBookmark: $selectedBookmark)
|
BookmarksView(state: .all, type: [.article, .video, .photo], selectedBookmark: $selectedBookmark)
|
||||||
case .unread:
|
case .unread:
|
||||||
BookmarksView(state: .unread, type: [.article], selectedBookmark: $selectedBookmark)
|
BookmarksView(state: .unread, type: [.article, .video, .photo], selectedBookmark: $selectedBookmark)
|
||||||
case .favorite:
|
case .favorite:
|
||||||
BookmarksView(state: .favorite, type: [.article], selectedBookmark: $selectedBookmark)
|
BookmarksView(state: .favorite, type: [.article, .video, .photo], selectedBookmark: $selectedBookmark)
|
||||||
case .archived:
|
case .archived:
|
||||||
BookmarksView(state: .archived, type: [.article], selectedBookmark: $selectedBookmark)
|
BookmarksView(state: .archived, type: [.article, .video, .photo], selectedBookmark: $selectedBookmark)
|
||||||
case .settings:
|
case .settings:
|
||||||
SettingsView()
|
SettingsView()
|
||||||
case .article:
|
case .article:
|
||||||
|
|||||||
@ -153,7 +153,7 @@ struct PhoneTabView: View {
|
|||||||
|
|
||||||
// Hidden NavigationLink to remove disclosure indicator
|
// Hidden NavigationLink to remove disclosure indicator
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
BookmarkDetailView(bookmarkId: bookmark.id)
|
BookmarkDetailView(bookmarkId: bookmark.id)
|
||||||
} label: {
|
} label: {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
@ -234,11 +234,11 @@ struct PhoneTabView: View {
|
|||||||
case .all:
|
case .all:
|
||||||
BookmarksView(state: .all, type: [.article, .video, .photo], selectedBookmark: .constant(nil))
|
BookmarksView(state: .all, type: [.article, .video, .photo], selectedBookmark: .constant(nil))
|
||||||
case .unread:
|
case .unread:
|
||||||
BookmarksView(state: .unread, type: [.article], selectedBookmark: .constant(nil))
|
BookmarksView(state: .unread, type: [.article, .video, .photo], selectedBookmark: .constant(nil))
|
||||||
case .favorite:
|
case .favorite:
|
||||||
BookmarksView(state: .favorite, type: [.article], selectedBookmark: .constant(nil))
|
BookmarksView(state: .favorite, type: [.article, .video, .photo], selectedBookmark: .constant(nil))
|
||||||
case .archived:
|
case .archived:
|
||||||
BookmarksView(state: .archived, type: [.article], selectedBookmark: .constant(nil))
|
BookmarksView(state: .archived, type: [.article, .video, .photo], selectedBookmark: .constant(nil))
|
||||||
case .search:
|
case .search:
|
||||||
EmptyView() // search is directly implemented
|
EmptyView() // search is directly implemented
|
||||||
case .settings:
|
case .settings:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user