diff --git a/Zotero/Scenes/Detail/HTML:EPUB/HtmlEpubCoordinator.swift b/Zotero/Scenes/Detail/HTML:EPUB/HtmlEpubCoordinator.swift index 2797d3d4e..85fd73778 100644 --- a/Zotero/Scenes/Detail/HTML:EPUB/HtmlEpubCoordinator.swift +++ b/Zotero/Scenes/Detail/HTML:EPUB/HtmlEpubCoordinator.swift @@ -22,6 +22,7 @@ protocol HtmlEpubSidebarCoordinatorDelegate: AnyObject { for annotation: HtmlEpubAnnotation, userId: Int, library: Library, + highlightFont: UIFont, sender: UIButton, userInterfaceStyle: UIUserInterfaceStyle, saveAction: @escaping AnnotationEditSaveAction, @@ -188,6 +189,7 @@ extension HtmlEpubCoordinator: HtmlEpubSidebarCoordinatorDelegate { for annotation: HtmlEpubAnnotation, userId: Int, library: Library, + highlightFont: UIFont, sender: UIButton, userInterfaceStyle: UIUserInterfaceStyle, saveAction: @escaping AnnotationEditSaveAction, @@ -196,6 +198,8 @@ extension HtmlEpubCoordinator: HtmlEpubSidebarCoordinatorDelegate { let navigationController = NavigationViewController() navigationController.overrideUserInterfaceStyle = userInterfaceStyle + let highlightText: NSAttributedString = (self.navigationController?.viewControllers.first as? HtmlEpubAnnotationsDelegate)? + .parseAndCacheIfNeededAttributedText(for: annotation, with: highlightFont) ?? .init(string: "") let coordinator = AnnotationEditCoordinator( data: AnnotationEditState.Data( type: annotation.type, @@ -203,7 +207,8 @@ extension HtmlEpubCoordinator: HtmlEpubSidebarCoordinatorDelegate { color: annotation.color, lineWidth: 0, pageLabel: annotation.pageLabel, - highlightText: annotation.text ?? "", + highlightText: highlightText, + highlightFont: highlightFont, fontSize: 12 ), saveAction: saveAction, @@ -241,7 +246,11 @@ extension HtmlEpubCoordinator: HtmlEpubSidebarCoordinatorDelegate { let navigationController = NavigationViewController() navigationController.overrideUserInterfaceStyle = userInterfaceStyle let author = viewModel.state.library.identifier == .custom(.myLibrary) ? "" : annotation.author - let comment = viewModel.state.comments[annotation.key] ?? NSAttributedString() + let comment: NSAttributedString = (self.navigationController?.viewControllers.first as? HtmlEpubAnnotationsDelegate)? + .parseAndCacheIfNeededAttributedComment(for: annotation) ?? .init(string: "") + let highlightFont = viewModel.state.textFont + let highlightText: NSAttributedString = (self.navigationController?.viewControllers.first as? HtmlEpubAnnotationsDelegate)? + .parseAndCacheIfNeededAttributedText(for: annotation, with: highlightFont) ?? .init(string: "") let editability = annotation.editability(currentUserId: viewModel.state.userId, library: viewModel.state.library) let data = AnnotationPopoverState.Data( libraryId: viewModel.state.library.identifier, @@ -252,7 +261,8 @@ extension HtmlEpubCoordinator: HtmlEpubSidebarCoordinatorDelegate { color: annotation.color, lineWidth: 0, pageLabel: annotation.pageLabel, - highlightText: annotation.text ?? "", + highlightText: highlightText, + highlightFont: highlightFont, tags: annotation.tags, showsDeleteButton: editability != .notEditable ) diff --git a/Zotero/Scenes/Detail/HTML:EPUB/Models/HtmlEpubReaderAction.swift b/Zotero/Scenes/Detail/HTML:EPUB/Models/HtmlEpubReaderAction.swift index 257133ae9..f2f08e7e1 100644 --- a/Zotero/Scenes/Detail/HTML:EPUB/Models/HtmlEpubReaderAction.swift +++ b/Zotero/Scenes/Detail/HTML:EPUB/Models/HtmlEpubReaderAction.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Corporation for Digital Scholarship. All rights reserved. // -import Foundation +import UIKit enum HtmlEpubReaderAction { case changeFilter(AnnotationsFilter?) @@ -18,6 +18,7 @@ enum HtmlEpubReaderAction { case initialiseReader case loadDocument case parseAndCacheComment(key: String, comment: String) + case parseAndCacheText(key: String, text: String, font: UIFont) case removeAnnotation(String) case removeSelectedAnnotations case saveAnnotations([String: Any]) @@ -36,5 +37,5 @@ enum HtmlEpubReaderAction { case setViewState([String: Any]) case showAnnotationPopover(key: String, rect: CGRect) case toggleTool(AnnotationTool) - case updateAnnotationProperties(key: String, color: String, lineWidth: CGFloat, pageLabel: String, updateSubsequentLabels: Bool, highlightText: String) + case updateAnnotationProperties(key: String, color: String, lineWidth: CGFloat, pageLabel: String, updateSubsequentLabels: Bool, highlightText: NSAttributedString) } diff --git a/Zotero/Scenes/Detail/HTML:EPUB/Models/HtmlEpubReaderState.swift b/Zotero/Scenes/Detail/HTML:EPUB/Models/HtmlEpubReaderState.swift index 4e9e6b964..4bad724ef 100644 --- a/Zotero/Scenes/Detail/HTML:EPUB/Models/HtmlEpubReaderState.swift +++ b/Zotero/Scenes/Detail/HTML:EPUB/Models/HtmlEpubReaderState.swift @@ -65,6 +65,7 @@ struct HtmlEpubReaderState: ViewModelState { let userId: Int let username: String let commentFont: UIFont + let textFont: UIFont var documentData: DocumentData? var settings: HtmlEpubSettings @@ -83,6 +84,7 @@ struct HtmlEpubReaderState: ViewModelState { var annotationPopoverRect: CGRect? var documentSearchTerm: String? var comments: [String: NSAttributedString] + var texts: [String: (String, [UIFont: NSAttributedString])] var changes: Changes var error: Error? /// Updates that need to be performed on html/epub document @@ -110,9 +112,11 @@ struct HtmlEpubReaderState: ViewModelState { self.userId = userId self.username = username commentFont = PDFReaderLayout.annotationLayout.font + textFont = PDFReaderLayout.annotationLayout.font sortedKeys = [] annotations = [:] comments = [:] + texts = [:] sidebarEditingEnabled = false selectedAnnotationCommentActive = false toolColors = [ diff --git a/Zotero/Scenes/Detail/HTML:EPUB/ViewModels/HtmlEpubReaderActionHandler.swift b/Zotero/Scenes/Detail/HTML:EPUB/ViewModels/HtmlEpubReaderActionHandler.swift index ba22b2d10..6dafa747e 100644 --- a/Zotero/Scenes/Detail/HTML:EPUB/ViewModels/HtmlEpubReaderActionHandler.swift +++ b/Zotero/Scenes/Detail/HTML:EPUB/ViewModels/HtmlEpubReaderActionHandler.swift @@ -113,6 +113,9 @@ final class HtmlEpubReaderActionHandler: ViewModelActionHandler, BackgroundDbPro state.comments[key] = self.htmlAttributedStringConverter.convert(text: comment, baseAttributes: [.font: viewModel.state.commentFont]) } + case .parseAndCacheText(let key, let text, let font): + updateTextCache(key: key, text: text, font: font, viewModel: viewModel) + case .updateAnnotationProperties(let key, let color, let lineWidth, let pageLabel, let updateSubsequentLabels, let highlightText): set(color: color, lineWidth: lineWidth, pageLabel: pageLabel, updateSubsequentLabels: updateSubsequentLabels, highlightText: highlightText, key: key, viewModel: viewModel) @@ -148,6 +151,17 @@ final class HtmlEpubReaderActionHandler: ViewModelActionHandler, BackgroundDbPro } } + private func updateTextCache(key: String, text: String, font: UIFont, viewModel: ViewModel) { + update(viewModel: viewModel, notifyListeners: false) { state in + var (cachedText, attributedTextByFont) = state.texts[key, default: (text, [:])] + if cachedText != text { + attributedTextByFont = [:] + } + attributedTextByFont[font] = htmlAttributedStringConverter.convert(text: text, baseAttributes: [.font: font]) + state.texts[key] = (text, attributedTextByFont) + } + } + private func changeIdleTimer(disabled: Bool, in viewModel: ViewModel) { guard viewModel.state.settings.idleTimerDisabled != disabled else { return } var settings = viewModel.state.settings @@ -314,10 +328,19 @@ final class HtmlEpubReaderActionHandler: ViewModelActionHandler, BackgroundDbPro } } - private func set(color: String, lineWidth: CGFloat, pageLabel: String, updateSubsequentLabels: Bool, highlightText: String, key: String, viewModel: ViewModel) { + private func set( + color: String, + lineWidth: CGFloat, + pageLabel: String, + updateSubsequentLabels: Bool, + highlightText: NSAttributedString, + key: String, + viewModel: ViewModel + ) { + let text = htmlAttributedStringConverter.convert(attributedString: highlightText) let values = [ KeyBaseKeyPair(key: FieldKeys.Item.Annotation.pageLabel, baseKey: nil): pageLabel, - KeyBaseKeyPair(key: FieldKeys.Item.Annotation.text, baseKey: nil): highlightText, + KeyBaseKeyPair(key: FieldKeys.Item.Annotation.text, baseKey: nil): text, KeyBaseKeyPair(key: FieldKeys.Item.Annotation.color, baseKey: nil): color, KeyBaseKeyPair(key: FieldKeys.Item.Annotation.Position.lineWidth, baseKey: FieldKeys.Item.Annotation.position): "\(Decimal(lineWidth).rounded(to: 3))" ] @@ -697,6 +720,7 @@ final class HtmlEpubReaderActionHandler: ViewModelActionHandler, BackgroundDbPro // Get sorted database keys var keys = viewModel.state.snapshotKeys ?? viewModel.state.sortedKeys var annotations: [String: HtmlEpubAnnotation] = viewModel.state.annotations + var texts = viewModel.state.texts var comments = viewModel.state.comments var selectionDeleted = false var popoverWasInserted = false @@ -730,7 +754,28 @@ final class HtmlEpubReaderActionHandler: ViewModelActionHandler, BackgroundDbPro if item.changeType == .sync { // Update comment if it's remote sync change DDLogInfo("HtmlEpubReaderActionHandler: update comment") - comments[key] = htmlAttributedStringConverter.convert(text: annotation.comment, baseAttributes: [.font: viewModel.state.commentFont]) + let textCacheTuple: (String, [UIFont: NSAttributedString])? + let comment: NSAttributedString? + // Annotation text + switch annotation.type { + case .highlight, .underline: + textCacheTuple = annotation.text.flatMap({ + ($0, [viewModel.state.textFont: htmlAttributedStringConverter.convert(text: $0, baseAttributes: [.font: viewModel.state.textFont])]) + }) + + case .note, .image, .ink, .freeText: + textCacheTuple = nil + } + texts[key] = textCacheTuple + // Annotation comment + switch annotation.type { + case .note, .highlight, .image, .underline: + comment = htmlAttributedStringConverter.convert(text: annotation.comment, baseAttributes: [.font: viewModel.state.commentFont]) + + case .ink, .freeText: + comment = nil + } + comments[key] = comment } } } @@ -810,6 +855,7 @@ final class HtmlEpubReaderActionHandler: ViewModelActionHandler, BackgroundDbPro state.annotations = annotations state.documentUpdate = HtmlEpubReaderState.DocumentUpdate(deletions: deletedPdfAnnotations, insertions: insertedPdfAnnotations, modifications: updatedPdfAnnotations) state.comments = comments + state.texts = texts // Filter updated keys to include only keys that are actually available in `sortedKeys`. If filter/search is turned on and an item is edited so that it disappears from the filter/search, // `updatedKeys` will try to update it while the key will be deleted from data source at the same time. state.updatedAnnotationKeys = updatedKeys.filter({ state.sortedKeys.contains($0) }) diff --git a/Zotero/Scenes/Detail/HTML:EPUB/Views/HtmlEpubReaderViewController.swift b/Zotero/Scenes/Detail/HTML:EPUB/Views/HtmlEpubReaderViewController.swift index 2d68cde8f..d1d1d1f05 100644 --- a/Zotero/Scenes/Detail/HTML:EPUB/Views/HtmlEpubReaderViewController.swift +++ b/Zotero/Scenes/Detail/HTML:EPUB/Views/HtmlEpubReaderViewController.swift @@ -510,3 +510,28 @@ extension HtmlEpubReaderViewController: UIPopoverPresentationControllerDelegate } extension HtmlEpubReaderViewController: HtmlEpubReaderContainerDelegate {} + +extension HtmlEpubReaderViewController: HtmlEpubAnnotationsDelegate { + func parseAndCacheIfNeededAttributedText(for annotation: HtmlEpubAnnotation, with font: UIFont) -> NSAttributedString? { + guard let text = annotation.text, !text.isEmpty else { return nil } + + if let attributedText = viewModel.state.texts[annotation.key]?.1[font] { + return attributedText + } + + viewModel.process(action: .parseAndCacheText(key: annotation.key, text: text, font: font)) + return viewModel.state.texts[annotation.key]?.1[font] + } + + func parseAndCacheIfNeededAttributedComment(for annotation: HtmlEpubAnnotation) -> NSAttributedString? { + let comment = annotation.comment + guard !comment.isEmpty else { return nil } + + if let attributedComment = viewModel.state.comments[annotation.key] { + return attributedComment + } + + viewModel.process(action: .parseAndCacheComment(key: annotation.key, comment: comment)) + return viewModel.state.comments[annotation.key] + } +} diff --git a/Zotero/Scenes/Detail/HTML:EPUB/Views/HtmlEpubSidebarViewController.swift b/Zotero/Scenes/Detail/HTML:EPUB/Views/HtmlEpubSidebarViewController.swift index 057c9670d..789db95cf 100644 --- a/Zotero/Scenes/Detail/HTML:EPUB/Views/HtmlEpubSidebarViewController.swift +++ b/Zotero/Scenes/Detail/HTML:EPUB/Views/HtmlEpubSidebarViewController.swift @@ -10,6 +10,11 @@ import UIKit import RxSwift +protocol HtmlEpubAnnotationsDelegate: AnyObject { + func parseAndCacheIfNeededAttributedText(for annotation: HtmlEpubAnnotation, with font: UIFont) -> NSAttributedString? + func parseAndCacheIfNeededAttributedComment(for annotation: HtmlEpubAnnotation) -> NSAttributedString? +} + class HtmlEpubSidebarViewController: UIViewController { private static let cellId = "AnnotationCell" private let viewModel: ViewModel @@ -274,17 +279,18 @@ class HtmlEpubSidebarViewController: UIViewController { for: annotation, userId: viewModel.state.userId, library: viewModel.state.library, + highlightFont: viewModel.state.textFont, sender: sender, userInterfaceStyle: viewModel.state.settings.appearance.userInterfaceStyle, - saveAction: { [weak self] color, lineWidth, _, pageLabel, updateSubsequentLabels, highlightText in + saveAction: { [weak self] data, updateSubsequentLabels in self?.viewModel.process( action: .updateAnnotationProperties( key: key, - color: color, - lineWidth: lineWidth, - pageLabel: pageLabel, + color: data.color, + lineWidth: data.lineWidth, + pageLabel: data.pageLabel, updateSubsequentLabels: updateSubsequentLabels, - highlightText: highlightText + highlightText: data.highlightText ) ) }, diff --git a/Zotero/Scenes/Detail/PDF/PDFCoordinator.swift b/Zotero/Scenes/Detail/PDF/PDFCoordinator.swift index 5c8239bab..ff0d23fac 100644 --- a/Zotero/Scenes/Detail/PDF/PDFCoordinator.swift +++ b/Zotero/Scenes/Detail/PDF/PDFCoordinator.swift @@ -203,9 +203,9 @@ extension PDFCoordinator: PdfReaderCoordinatorDelegate { navigationController.overrideUserInterfaceStyle = userInterfaceStyle let author = viewModel.state.library.identifier == .custom(.myLibrary) ? "" : annotation.author(displayName: viewModel.state.displayName, username: viewModel.state.username) - let comment: NSAttributedString = (self.navigationController?.viewControllers.first as? AnnotationsDelegate)?.parseAndCacheIfNeededAttributedComment(for: annotation) ?? .init(string: "") + let comment: NSAttributedString = (self.navigationController?.viewControllers.first as? PDFAnnotationsDelegate)?.parseAndCacheIfNeededAttributedComment(for: annotation) ?? .init(string: "") let highlightFont = viewModel.state.textEditorFont - let highlightText: NSAttributedString = (self.navigationController?.viewControllers.first as? AnnotationsDelegate)? + let highlightText: NSAttributedString = (self.navigationController?.viewControllers.first as? PDFAnnotationsDelegate)? .parseAndCacheIfNeededAttributedText(for: annotation, with: highlightFont) ?? .init(string: "") let editability = annotation.editability(currentUserId: viewModel.state.userId, library: viewModel.state.library) @@ -625,7 +625,7 @@ extension PDFCoordinator: PdfAnnotationsCoordinatorDelegate { let navigationController = NavigationViewController() navigationController.overrideUserInterfaceStyle = userInterfaceStyle - let highlightText: NSAttributedString = (self.navigationController?.viewControllers.first as? AnnotationsDelegate)? + let highlightText: NSAttributedString = (self.navigationController?.viewControllers.first as? PDFAnnotationsDelegate)? .parseAndCacheIfNeededAttributedText(for: annotation, with: highlightFont) ?? .init(string: "") let coordinator = AnnotationEditCoordinator( data: AnnotationEditState.Data( diff --git a/Zotero/Scenes/Detail/PDF/Views/PDFAnnotationsViewController.swift b/Zotero/Scenes/Detail/PDF/Views/PDFAnnotationsViewController.swift index 8cf8e3d31..e19ac8b78 100644 --- a/Zotero/Scenes/Detail/PDF/Views/PDFAnnotationsViewController.swift +++ b/Zotero/Scenes/Detail/PDF/Views/PDFAnnotationsViewController.swift @@ -15,7 +15,7 @@ import RxSwift typealias AnnotationsViewControllerAction = (AnnotationView.Action, Annotation, UIButton) -> Void -protocol AnnotationsDelegate: AnyObject { +protocol PDFAnnotationsDelegate: AnyObject { func parseAndCacheIfNeededAttributedText(for annotation: PDFAnnotation, with font: UIFont) -> NSAttributedString? func parseAndCacheIfNeededAttributedComment(for annotation: PDFAnnotation) -> NSAttributedString? } @@ -36,7 +36,7 @@ final class PDFAnnotationsViewController: UIViewController { private var dataSource: TableViewDiffableDataSource! private var searchController: UISearchController! - weak var parentDelegate: (PDFReaderContainerDelegate & SidebarDelegate & AnnotationsDelegate)? + weak var parentDelegate: (PDFReaderContainerDelegate & SidebarDelegate & PDFAnnotationsDelegate)? weak var coordinatorDelegate: PdfAnnotationsCoordinatorDelegate? weak var boundingBoxConverter: AnnotationBoundingBoxConverter? diff --git a/Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift b/Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift index 5d0f33cf5..6d4d776a0 100644 --- a/Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift +++ b/Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift @@ -807,7 +807,7 @@ extension PDFReaderViewController: SidebarDelegate { } } -extension PDFReaderViewController: AnnotationsDelegate { +extension PDFReaderViewController: PDFAnnotationsDelegate { func parseAndCacheIfNeededAttributedText(for annotation: any PDFAnnotation, with font: UIFont) -> NSAttributedString? { guard let text = annotation.text, !text.isEmpty else { return nil } diff --git a/Zotero/Scenes/Detail/PDF/Views/PDFSidebarViewController.swift b/Zotero/Scenes/Detail/PDF/Views/PDFSidebarViewController.swift index 85f6b1346..97cc1eba2 100644 --- a/Zotero/Scenes/Detail/PDF/Views/PDFSidebarViewController.swift +++ b/Zotero/Scenes/Detail/PDF/Views/PDFSidebarViewController.swift @@ -47,7 +47,7 @@ class PDFSidebarViewController: UIViewController { private weak var controllerContainer: UIView! private weak var currentController: UIViewController? private var controllerDisposeBag: DisposeBag? - weak var parentDelegate: (PDFReaderContainerDelegate & SidebarDelegate & AnnotationsDelegate)? + weak var parentDelegate: (PDFReaderContainerDelegate & SidebarDelegate & PDFAnnotationsDelegate)? weak var coordinatorDelegate: PdfAnnotationsCoordinatorDelegate? weak var boundingBoxConverter: AnnotationBoundingBoxConverter?