PDF notebooks, more metadata for PDF items in the list view

This commit is contained in:
Jackson Harper 2023-07-17 12:56:49 +08:00
parent ccff1f421f
commit 43905a47ba
6 changed files with 1499 additions and 32 deletions

View file

@ -40,6 +40,8 @@ import Utils
@State var readerView: Bool = false
@State private var shareLink: ShareLink?
@State private var errorMessage: String?
@State private var showNotebookView = false
@State private var hasPerformedHighlightMutations = false
init(viewModel: PDFViewerViewModel) {
self.viewModel = viewModel
@ -117,6 +119,12 @@ import Utils
style: .plain,
target: controller.searchButtonItem.target,
action: controller.searchButtonItem.action
),
UIBarButtonItem(
image: UIImage(named: "notebook", in: Bundle(url: ViewsPackage.bundleURL), with: nil),
style: .plain,
target: coordinator,
action: #selector(PDFViewCoordinator.toggleNotebookView)
)
]
@ -183,6 +191,15 @@ import Utils
.sheet(item: $shareLink) {
ShareSheet(activityItems: [$0.url])
}
.fullScreenCover(isPresented: $showNotebookView, onDismiss: onNotebookViewDismissal) {
NotebookView(
itemObjectID: viewModel.pdfItem.objectID,
hasHighlightMutations: $hasPerformedHighlightMutations,
onDeleteHighlight: { highlightId in
coordinator.removeHighlightFromPDF(highlightId: highlightId)
}
)
}
} else if let errorMessage = errorMessage {
Text(errorMessage)
} else {
@ -202,6 +219,12 @@ import Utils
}
}
func onNotebookViewDismissal() {
guard hasPerformedHighlightMutations else { return }
hasPerformedHighlightMutations.toggle()
}
class PDFViewCoordinator: NSObject, PDFDocumentViewControllerDelegate, PDFViewControllerDelegate {
let document: Document
let viewModel: PDFViewerViewModel
@ -239,6 +262,22 @@ import Utils
}.store(in: &subscriptions)
}
func removeHighlightFromPDF(highlightId: String) {
for pageIndex in 0 ..< document.pageCount {
let pageHighlights = document.annotations(at: pageIndex, type: HighlightAnnotation.self)
for annotation in pageHighlights {
if let customHighlight = annotation.customData?["omnivoreHighlight"] as? [String: String] {
if customHighlight["id"]?.lowercased() == highlightId {
if !document.remove(annotations: [annotation]) {
Snackbar.show(message: "Error removing highlight")
}
}
}
}
}
}
func highlightsOverlap(left: HighlightAnnotation, right: HighlightAnnotation) -> Bool {
for rect in left.rects ?? [] {
for hrrect in right.rects ?? [] {
@ -382,6 +421,12 @@ import Utils
}
}
@objc public func toggleNotebookView() {
if let viewer = self.viewer {
viewer.showNotebookView = !viewer.showNotebookView
}
}
func shortHighlightIds(_ annotations: [HighlightAnnotation]) -> [String] {
annotations.compactMap { ($0.customData?["omnivoreHighlight"] as? [String: String])?["shortId"] }
}

View file

@ -6,6 +6,8 @@
import SwiftUI
import Views
typealias DeleteHighlightAction = (String) -> Void
struct NotebookView: View {
@EnvironmentObject var dataService: DataService
@Environment(\.presentationMode) private var presentationMode
@ -21,6 +23,7 @@
@State var setLabelsHighlight: Highlight?
@State var showShareView: Bool = false
@State var showConfirmNoteDelete = false
@State var onDeleteHighlight: DeleteHighlightAction?
var emptyView: some View {
Text(LocalText.highlightCardNoHighlightsOnPage)
@ -134,6 +137,9 @@
highlightID: highlightParams.highlightID,
dataService: dataService
)
if let onDeleteHighlight = onDeleteHighlight {
onDeleteHighlight(highlightParams.highlightID)
}
},
onSetLabels: { highlightID in
setLabelsHighlight = Highlight.lookup(byID: highlightID, inContext: dataService.viewContext)

View file

@ -381,6 +381,7 @@ struct AnimatingCellHeight: AnimatableModifier {
.padding(.top, 0)
} else {
Text((FeaturedItemFilter(rawValue: viewModel.featureFilter) ?? .continueReading).emptyMessage)
.padding(.horizontal, UIDevice.isIPad ? 20 : 10)
.font(Font.system(size: 14, weight: .regular))
.foregroundColor(Color(hex: "#898989"))
.frame(maxWidth: geo.size.width)

File diff suppressed because it is too large Load diff

View file

@ -33,44 +33,49 @@ public struct LibraryFeatureCard: View {
var imageBox: some View {
ZStack(alignment: .bottomLeading) {
Group {
if let imageURL = item.imageURL {
AsyncImage(url: imageURL) { phase in
switch phase {
case .empty:
Color.systemBackground
.frame(width: 146, height: 90)
case let .success(image):
image.resizable()
.frame(width: 146, height: 90)
.aspectRatio(contentMode: .fill)
case .failure:
Image(systemName: "photo")
.frame(width: 146, height: 90)
.foregroundColor(Color(hex: "#6A6968"))
.background(Color(hex: "#EBEBEB"))
if let imageURL = item.imageURL {
AsyncImage(url: imageURL) { phase in
switch phase {
case .empty:
EmptyView()
case let .success(image):
image.resizable()
.frame(width: 146, height: 90)
.aspectRatio(contentMode: .fill)
case .failure:
fallbackImage
@unknown default:
// Since the AsyncImagePhase enum isn't frozen,
// we need to add this currently unused fallback
// to handle any new cases that might be added
// in the future:
EmptyView()
}
@unknown default:
// Since the AsyncImagePhase enum isn't frozen,
// we need to add this currently unused fallback
// to handle any new cases that might be added
// in the future:
EmptyView()
}
} else {
Image(systemName: "photo")
.frame(width: 146, height: 90)
.foregroundColor(Color(hex: "#6A6968"))
.background(Color(hex: "#EBEBEB"))
}
Color(hex: "#D9D9D9")?.opacity(0.65).frame(width: 146, height: 5)
Color(hex: "#FFD234").frame(width: 146 * (item.readingProgress / 100), height: 5)
} else {
fallbackImage
}
Color(hex: "#D9D9D9")?.opacity(0.65).frame(width: 146, height: 5)
Color(hex: "#FFD234").frame(width: 146 * (item.readingProgress / 100), height: 5)
}
.cornerRadius(5)
}
var fallbackImage: some View {
HStack {
Text(item.unwrappedTitle.prefix(1))
.font(Font.system(size: 128, weight: .bold))
.offset(CGSize(width: -48, height: 32))
.frame(alignment: .bottomLeading)
.foregroundColor(Gradient.randomColor(str: item.unwrappedTitle, offset: 1))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Gradient.randomColor(str: item.unwrappedTitle, offset: 0))
.background(LinearGradient(gradient: Gradient(fromStr: item.unwrappedTitle)!, startPoint: .top, endPoint: .bottom))
.frame(width: 146, height: 90)
}
var title: some View {
Text(item.unwrappedTitle.trimmingCharacters(in: .whitespacesAndNewlines))
.multilineTextAlignment(.leading)

View file

@ -82,13 +82,21 @@ public struct LibraryItemCard: View {
if item.wordsCount > 0 {
return "\(String(format: "%d", Int(item.readingProgress)))%"
}
if item.isPDF {
// base estimated reading time on page count
return "\(String(format: "%d", Int(item.readingProgress)))%"
}
return ""
}
var hasMultipleInfoItems: Bool {
item.wordsCount > 0 || item.highlights?.first { ($0 as? Highlight)?.annotation != nil } != nil
}
var highlightsText: String {
if let highlights = item.highlights, highlights.count > 0 {
let fmted = LocalText.pluralizedText(key: "number_of_highlights", count: highlights.count)
if item.wordsCount > 0 {
if item.wordsCount > 0 || item.isPDF {
return "\(fmted)"
}
return fmted
@ -106,7 +114,7 @@ public struct LibraryItemCard: View {
if let notes = notes, notes.count > 0 {
let fmted = LocalText.pluralizedText(key: "number_of_notes", count: notes.count)
if item.wordsCount > 0 {
if hasMultipleInfoItems {
return "\(fmted)"
}
return fmted