diff --git a/Zotero.xcodeproj/project.pbxproj b/Zotero.xcodeproj/project.pbxproj index e7531b3e4..29cfc4ed7 100644 --- a/Zotero.xcodeproj/project.pbxproj +++ b/Zotero.xcodeproj/project.pbxproj @@ -515,9 +515,8 @@ B33E8A4C27B6A7BE00CBC7DE /* SearchableCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B36181EB24C96B0500B30D56 /* SearchableCollection.swift */; }; B33E8A4D27B6AAE600CBC7DE /* CollectionCellContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34673CE27B14F0D00444C96 /* CollectionCellContentView.swift */; }; B33E8A4E27B6AAF000CBC7DE /* CollectionCellContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B37D8E6324DC21D300F526C5 /* CollectionCellContentView.xib */; }; - B3401D572567D8F700BB8D6E /* AnnotationPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3401D552567D8F700BB8D6E /* AnnotationPopoverViewController.swift */; }; B33EB2BA2B076657003255DA /* Localizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37080512AA72135006F56B9 /* Localizable.swift */; }; - B3401D572567D8F700BB8D6E /* AnnotationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3401D552567D8F700BB8D6E /* AnnotationViewController.swift */; }; + B3401D572567D8F700BB8D6E /* AnnotationPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3401D552567D8F700BB8D6E /* AnnotationPopoverViewController.swift */; }; B3401D5B2567DAAE00BB8D6E /* AnnotationPopoverCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3401D5A2567DAAE00BB8D6E /* AnnotationPopoverCoordinator.swift */; }; B3401D612568047D00BB8D6E /* AnnotationViewTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3401D602568047D00BB8D6E /* AnnotationViewTextView.swift */; }; B340692224A60D6A009ECE48 /* Rounding+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B340692124A60D6A009ECE48 /* Rounding+Extensions.swift */; }; @@ -909,6 +908,7 @@ B3ADAE4D2833BED300D46271 /* LookupActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3ADAE4C2833BED300D46271 /* LookupActionHandler.swift */; }; B3ADAE4F2833BEDC00D46271 /* LookupState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3ADAE4E2833BEDC00D46271 /* LookupState.swift */; }; B3ADAE512833BEE300D46271 /* LookupAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3ADAE502833BEE300D46271 /* LookupAction.swift */; }; + B3AED7212B98828F000FCD18 /* PSPDFKItAnnotation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AED7202B98828F000FCD18 /* PSPDFKItAnnotation+Extensions.swift */; }; B3AFB83C2A0CE82C008C2374 /* EmptyDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AFB83B2A0CE82C008C2374 /* EmptyDecodable.swift */; }; B3AFB83D2A0CF1EE008C2374 /* EmptyDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AFB83B2A0CE82C008C2374 /* EmptyDecodable.swift */; }; B3B1C5842664E23A00883597 /* ReadItemsForUploadDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B1C5832664E23A00883597 /* ReadItemsForUploadDbRequest.swift */; }; @@ -1030,7 +1030,6 @@ B3DCDF1F240945B40039ED0D /* CollectionsPickerActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3DCDF1E240945B30039ED0D /* CollectionsPickerActionHandler.swift */; }; B3DCDF21240945D80039ED0D /* CollectionsPickerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3DCDF20240945D80039ED0D /* CollectionsPickerState.swift */; }; B3DCDF23240945FA0039ED0D /* CollectionsPickerAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3DCDF22240945FA0039ED0D /* CollectionsPickerAction.swift */; }; - B3DD197B2B0CA7840042C46D /* Localizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37080512AA72135006F56B9 /* Localizable.swift */; }; B3DDACE928E6F52E0063407E /* CustomURLController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3DDACE828E6F52E0063407E /* CustomURLController.swift */; }; B3DDC0CB2667824D00B2DFD1 /* RegularExpression+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3DDC0CA2667824D00B2DFD1 /* RegularExpression+Extensions.swift */; }; B3DDC0CC2667825E00B2DFD1 /* RegularExpression+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3DDC0CA2667824D00B2DFD1 /* RegularExpression+Extensions.swift */; }; @@ -1884,6 +1883,7 @@ B3ADAE4C2833BED300D46271 /* LookupActionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LookupActionHandler.swift; sourceTree = ""; }; B3ADAE4E2833BEDC00D46271 /* LookupState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LookupState.swift; sourceTree = ""; }; B3ADAE502833BEE300D46271 /* LookupAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LookupAction.swift; sourceTree = ""; }; + B3AED7202B98828F000FCD18 /* PSPDFKItAnnotation+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PSPDFKItAnnotation+Extensions.swift"; sourceTree = ""; }; B3AFB83B2A0CE82C008C2374 /* EmptyDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyDecodable.swift; sourceTree = ""; }; B3B1C5832664E23A00883597 /* ReadItemsForUploadDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadItemsForUploadDbRequest.swift; sourceTree = ""; }; B3B1C58526651E2A00883597 /* StorageSettingsActionHandlerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageSettingsActionHandlerSpec.swift; sourceTree = ""; }; @@ -3693,6 +3693,7 @@ B310280C2B1E16F300E41554 /* PDFThumbnailsAction.swift */, B373C98E2B1F5431007FD56C /* PDFThumbnailsLayout.swift */, B310280A2B1E16EC00E41554 /* PDFThumbnailsState.swift */, + B3AED7202B98828F000FCD18 /* PSPDFKItAnnotation+Extensions.swift */, B37BF0AA25A5E2AD00AE0268 /* SquareAnnotation.swift */, B3A47C3729015FDD00E7D90D /* TableOfContentsAction.swift */, B3A47C3929015FE800E7D90D /* TableOfContentsState.swift */, @@ -5249,6 +5250,7 @@ B30BDDE42836642D007034E8 /* FilenameFormatter.swift in Sources */, B3E381D325F2D78A00F046CE /* ApiLogger.swift in Sources */, B3E8FE192714297200F51458 /* CiteAction.swift in Sources */, + B3AED7212B98828F000FCD18 /* PSPDFKItAnnotation+Extensions.swift in Sources */, B3A2AEDC2656511D004BF3A4 /* StylesRequest.swift in Sources */, B3F3D62C255EBE3500F310C2 /* AnnotationViewHeader.swift in Sources */, B3E8FE34271429C300F51458 /* ExportLocalePickerView.swift in Sources */, diff --git a/Zotero/Controllers/AnnotationConverter.swift b/Zotero/Controllers/AnnotationConverter.swift index de069709a..e292ed9a6 100644 --- a/Zotero/Controllers/AnnotationConverter.swift +++ b/Zotero/Controllers/AnnotationConverter.swift @@ -210,9 +210,10 @@ struct AnnotationConverter { username: String, boundingBoxConverter: AnnotationBoundingBoxConverter ) -> [PSPDFKit.Annotation] { - return items.map({ item in + return items.compactMap({ item in + guard let annotation = PDFDatabaseAnnotation(item: item) else { return nil } return self.annotation( - from: PDFDatabaseAnnotation(item: item), + from: annotation, type: type, interfaceStyle: interfaceStyle, currentUserId: currentUserId, diff --git a/Zotero/Controllers/Database/Requests/CreatePDFAnnotationsDbRequest.swift b/Zotero/Controllers/Database/Requests/CreatePDFAnnotationsDbRequest.swift index 5c37b0d41..9d3870eca 100644 --- a/Zotero/Controllers/Database/Requests/CreatePDFAnnotationsDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/CreatePDFAnnotationsDbRequest.swift @@ -22,17 +22,17 @@ struct CreatePDFAnnotationsDbRequest: DbRequest { var needsWrite: Bool { return true } func process(in database: Realm) throws { - guard let parent = database.objects(RItem.self).filter(.key(self.attachmentKey, in: self.libraryId)).first else { return } + guard let parent = database.objects(RItem.self).filter(.key(attachmentKey, in: libraryId)).first else { return } - for annotation in self.annotations { - self.create(annotation: annotation, parent: parent, in: database) + for annotation in annotations { + create(annotation: annotation, parent: parent, in: database) } } private func create(annotation: PDFDocumentAnnotation, parent: RItem, in database: Realm) { let item: RItem - if let _item = database.objects(RItem.self).filter(.key(annotation.key, in: self.libraryId)).first { + if let _item = database.objects(RItem.self).filter(.key(annotation.key, in: libraryId)).first { if !_item.deleted { // If item exists and is not deleted locally, we can ignore this request return @@ -46,12 +46,13 @@ struct CreatePDFAnnotationsDbRequest: DbRequest { item = RItem() item.key = annotation.key item.rawType = ItemTypes.annotation - item.localizedType = self.schemaController.localized(itemType: ItemTypes.annotation) ?? "" - item.libraryId = self.libraryId + item.localizedType = schemaController.localized(itemType: ItemTypes.annotation) ?? "" + item.libraryId = libraryId item.dateAdded = annotation.dateModified database.add(item) } + item.annotationType = annotation.type.rawValue item.syncState = .synced item.changeType = .user item.htmlFreeContent = annotation.comment.isEmpty ? nil : annotation.comment.strippedRichTextTags @@ -117,12 +118,12 @@ struct CreatePDFAnnotationsDbRequest: DbRequest { } private func add(rects: [CGRect], to item: RItem, changes: inout RItemChanges, database: Realm) { - guard !rects.isEmpty else { return } + guard !rects.isEmpty, let annotation = PDFDatabaseAnnotation(item: item) else { return } - let page = UInt(PDFDatabaseAnnotation(item: item).page) + let page = UInt(annotation.page) for rect in rects { - let dbRect = self.boundingBoxConverter.convertToDb(rect: rect, page: page) ?? rect + let dbRect = boundingBoxConverter.convertToDb(rect: rect, page: page) ?? rect let rRect = RRect() rRect.minX = Double(dbRect.minX) @@ -135,16 +136,16 @@ struct CreatePDFAnnotationsDbRequest: DbRequest { } private func add(paths: [[CGPoint]], to item: RItem, changes: inout RItemChanges, database: Realm) { - guard !paths.isEmpty else { return } + guard !paths.isEmpty, let annotation = PDFDatabaseAnnotation(item: item) else { return } - let page = UInt(PDFDatabaseAnnotation(item: item).page) + let page = UInt(annotation.page) for (idx, path) in paths.enumerated() { let rPath = RPath() rPath.sortIndex = idx for (idy, point) in path.enumerated() { - let dbPoint = self.boundingBoxConverter.convertToDb(point: point, page: page) ?? point + let dbPoint = boundingBoxConverter.convertToDb(point: point, page: page) ?? point let rXCoordinate = RPathCoordinate() rXCoordinate.value = Double(dbPoint.x) diff --git a/Zotero/Controllers/Database/Requests/EditAnnotationPathsDbRequest.swift b/Zotero/Controllers/Database/Requests/EditAnnotationPathsDbRequest.swift index e1993b17d..f17bc9ecc 100644 --- a/Zotero/Controllers/Database/Requests/EditAnnotationPathsDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/EditAnnotationPathsDbRequest.swift @@ -19,13 +19,13 @@ struct EditAnnotationPathsDbRequest: DbRequest { var needsWrite: Bool { return true } func process(in database: Realm) throws { - guard let item = database.objects(RItem.self).filter(.key(self.key, in: self.libraryId)).first else { return } - let page = UInt(PDFDatabaseAnnotation(item: item).page) - let dbPaths = self.paths.map { path in - return path.map({ self.boundingBoxConverter.convertToDb(point: $0, page: page) ?? $0 }) + guard let item = database.objects(RItem.self).filter(.key(key, in: libraryId)).first, let annotation = PDFDatabaseAnnotation(item: item) else { return } + let page = UInt(annotation.page) + let dbPaths = paths.map { path in + return path.map({ boundingBoxConverter.convertToDb(point: $0, page: page) ?? $0 }) } - guard self.paths(dbPaths, differFrom: item.paths) else { return } - self.sync(paths: dbPaths, in: item, database: database) + guard paths(dbPaths, differFrom: item.paths) else { return } + sync(paths: dbPaths, in: item, database: database) } private func sync(paths: [[CGPoint]], in item: RItem, database: Realm) { diff --git a/Zotero/Controllers/Database/Requests/EditAnnotationRectsDbRequest.swift b/Zotero/Controllers/Database/Requests/EditAnnotationRectsDbRequest.swift index c5883f988..c9b6fd037 100644 --- a/Zotero/Controllers/Database/Requests/EditAnnotationRectsDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/EditAnnotationRectsDbRequest.swift @@ -19,11 +19,11 @@ struct EditAnnotationRectsDbRequest: DbRequest { var needsWrite: Bool { return true } func process(in database: Realm) throws { - guard let item = database.objects(RItem.self).filter(.key(self.key, in: self.libraryId)).first else { return } - let page = UInt(PDFDatabaseAnnotation(item: item).page) - let dbRects = self.rects.map({ self.boundingBoxConverter.convertToDb(rect: $0, page: page) ?? $0 }) - guard self.rects(dbRects, differFrom: item.rects) else { return } - self.sync(rects: dbRects, in: item, database: database) + guard let item = database.objects(RItem.self).filter(.key(key, in: libraryId)).first, let annotation = PDFDatabaseAnnotation(item: item) else { return } + let page = UInt(annotation.page) + let dbRects = rects.map({ boundingBoxConverter.convertToDb(rect: $0, page: page) ?? $0 }) + guard rects(dbRects, differFrom: item.rects) else { return } + sync(rects: dbRects, in: item, database: database) } private func sync(rects: [CGRect], in item: RItem, database: Realm) { diff --git a/Zotero/Controllers/Database/Requests/ReadAnnotationsDbRequest.swift b/Zotero/Controllers/Database/Requests/ReadAnnotationsDbRequest.swift index 66169086b..51eadb2ec 100644 --- a/Zotero/Controllers/Database/Requests/ReadAnnotationsDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/ReadAnnotationsDbRequest.swift @@ -19,9 +19,11 @@ struct ReadAnnotationsDbRequest: DbResponseRequest { var needsWrite: Bool { return false } func process(in database: Realm) throws -> Results { + let supportedTypes = AnnotationType.allCases.filter({ AnnotationsConfig.supported.contains($0.kind) }).map({ $0.rawValue }) return database.objects(RItem.self).filter(.parent(self.attachmentKey, in: self.libraryId)) .filter(.items(type: ItemTypes.annotation, notSyncState: .dirty)) .filter(.deleted(false)) + .filter("annotationType in %@", supportedTypes) .sorted(byKeyPath: "annotationSortIndex", ascending: true) } } diff --git a/Zotero/Controllers/Database/Requests/SplitAnnotationsDbRequest.swift b/Zotero/Controllers/Database/Requests/SplitAnnotationsDbRequest.swift index 4467c7ba5..d168d2457 100644 --- a/Zotero/Controllers/Database/Requests/SplitAnnotationsDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/SplitAnnotationsDbRequest.swift @@ -112,6 +112,7 @@ struct SplitAnnotationsDbRequest: DbRequest { let new = RItem() new.key = KeyGenerator.newKey new.rawType = item.rawType + new.annotationType = item.annotationType new.localizedType = item.localizedType new.dateAdded = item.dateAdded new.dateModified = item.dateModified diff --git a/Zotero/Controllers/Database/Requests/StoreItemsDbResponseRequest.swift b/Zotero/Controllers/Database/Requests/StoreItemsDbResponseRequest.swift index 00d4de7fa..af0cbebe0 100644 --- a/Zotero/Controllers/Database/Requests/StoreItemsDbResponseRequest.swift +++ b/Zotero/Controllers/Database/Requests/StoreItemsDbResponseRequest.swift @@ -223,6 +223,9 @@ struct StoreItemDbRequest: DbResponseRequest { case (FieldKeys.Item.Annotation.comment, _) where item.rawType == ItemTypes.annotation: item.htmlFreeContent = value.isEmpty ? nil : value.strippedRichTextTags + case (FieldKeys.Item.Annotation.type, _) where item.rawType == ItemTypes.annotation: + item.annotationType = value + case (FieldKeys.Item.date, _): date = value diff --git a/Zotero/Models/API/ItemResponse.swift b/Zotero/Models/API/ItemResponse.swift index 7012da33e..c120142e0 100644 --- a/Zotero/Models/API/ItemResponse.swift +++ b/Zotero/Models/API/ItemResponse.swift @@ -373,28 +373,16 @@ struct ItemResponse { for object in json { switch object.key { - case FieldKeys.Item.Annotation.Position.pageIndex: - if (object.value as? Int) == nil { - throw SchemaError.invalidValue(value: "\(object.value)", field: FieldKeys.Item.Annotation.Position.pageIndex, key: key) - } - - case FieldKeys.Item.Annotation.Position.lineWidth: - if (object.value as? Double) == nil { - throw SchemaError.invalidValue(value: "\(object.value)", field: FieldKeys.Item.Annotation.Position.lineWidth, key: key) - } - case FieldKeys.Item.Annotation.Position.paths: - guard let parsedPaths = object.value as? [[Double]], !parsedPaths.isEmpty && !parsedPaths.contains(where: { $0.count % 2 != 0 }) else { - throw SchemaError.invalidValue(value: "\(object.value)", field: FieldKeys.Item.Annotation.Position.paths, key: key) + if let parsedPaths = object.value as? [[Double]], !parsedPaths.isEmpty && !parsedPaths.contains(where: { $0.count % 2 != 0 }) { + paths = parsedPaths } - paths = parsedPaths continue case FieldKeys.Item.Annotation.Position.rects: - guard let parsedRects = object.value as? [[Double]], !parsedRects.isEmpty && !parsedRects.contains(where: { $0.count != 4 }) else { - throw SchemaError.invalidValue(value: "\(object.value)", field: FieldKeys.Item.Annotation.Position.rects, key: key) + if let parsedRects = object.value as? [[Double]], !parsedRects.isEmpty && !parsedRects.contains(where: { $0.count != 4 }) { + rects = parsedRects } - rects = parsedRects continue default: break @@ -424,7 +412,7 @@ struct ItemResponse { private static func validate(fields: [KeyBaseKeyPair: String], itemType: String, key: String, hasPaths: Bool, hasRects: Bool) throws { switch itemType { case ItemTypes.annotation: - // `position` values are validated in `parsePositionFields(from:key:fields:)` where we have access to their original value, instead of just `String`. + // `position` values are not validated at this point. They depend on content type of parent (attachment) item, which is unknown here, so they are validated when this item is being opened. guard let rawType = fields[KeyBaseKeyPair(key: FieldKeys.Item.Annotation.type, baseKey: nil)] else { throw SchemaError.missingField(key: key, field: FieldKeys.Item.Annotation.type, itemType: itemType) } diff --git a/Zotero/Models/AnnotationType.swift b/Zotero/Models/AnnotationType.swift index 55dcd4a2c..3e446b96a 100644 --- a/Zotero/Models/AnnotationType.swift +++ b/Zotero/Models/AnnotationType.swift @@ -8,7 +8,7 @@ import Foundation -enum AnnotationType: String { +enum AnnotationType: String, CaseIterable { case note case highlight case image diff --git a/Zotero/Models/AnnotationsConfig.swift b/Zotero/Models/AnnotationsConfig.swift index df421eeac..b4080edd7 100644 --- a/Zotero/Models/AnnotationsConfig.swift +++ b/Zotero/Models/AnnotationsConfig.swift @@ -13,7 +13,17 @@ import PSPDFKit struct AnnotationsConfig { static let defaultActiveColor = "#ffd400" static let allColors: [String] = ["#ffd400", "#ff6666", "#5fb236", "#2ea8e5", "#a28ae5", "#e56eee", "#f19837", "#aaaaaa", "#000000"] - static let colorNames: [String: String] = ["#ffd400": "Yellow", "#ff6666": "Red", "#5fb236": "Green", "#2ea8e5": "Blue", "#a28ae5": "Purple", "#e56eee": "Magenta", "#f19837": "Orange", "#aaaaaa": "Gray", "#000000": "Black"] + static let colorNames: [String: String] = [ + "#ffd400": "Yellow", + "#ff6666": "Red", + "#5fb236": "Green", + "#2ea8e5": "Blue", + "#a28ae5": "Purple", + "#e56eee": "Magenta", + "#f19837": "Orange", + "#aaaaaa": "Gray", + "#000000": "Black" + ] // Maps different variations colors to their base color static let colorVariationMap: [String: String] = createColorVariationMap() static let keyKey = "Zotero:Key" @@ -22,7 +32,8 @@ struct AnnotationsConfig { // Size of note annotation in PDF document. static let noteAnnotationSize: CGSize = CGSize(width: 22, height: 22) static let positionSizeLimit = 65000 - static let supported: PSPDFKit.Annotation.Kind = [.note, .highlight, .square, .ink, .underline, .freeText] + // TODO: Enable when text/underline annotations are fully available + static let supported: PSPDFKit.Annotation.Kind = [.note, .highlight, .square, .ink]//, .underline, .freeText] static func colors(for type: AnnotationType) -> [String] { switch type { diff --git a/Zotero/Models/Database/Database.swift b/Zotero/Models/Database/Database.swift index 351916b66..1b2acd10b 100644 --- a/Zotero/Models/Database/Database.swift +++ b/Zotero/Models/Database/Database.swift @@ -13,7 +13,7 @@ import RealmSwift import Network struct Database { - private static let schemaVersion: UInt64 = 44 + private static let schemaVersion: UInt64 = 45 static func mainConfiguration(url: URL, fileStorage: FileStorage) -> Realm.Configuration { var config = Realm.Configuration( @@ -83,6 +83,24 @@ struct Database { if schemaVersion < 42 { extractEmojisFromTags(migration: migration) } + if schemaVersion < 45 { + extractAnnotationTypeFromItems(migration: migration) + } + } + } + + private static func extractAnnotationTypeFromItems(migration: Migration) { + migration.enumerateObjects(ofType: RItem.className()) { oldObject, newObject in + guard let rawType = oldObject?["rawType"] as? String, let fields = oldObject?["fields"] as? List else { return } + + switch rawType { + case ItemTypes.annotation: + guard let annotationType = fields.first(where: { $0["key"] as? String == FieldKeys.Item.Annotation.type })?["value"] as? String, !annotationType.isEmpty else { return } + newObject?["annotationType"] = annotationType + + default: + return + } } } diff --git a/Zotero/Models/Database/RItem.swift b/Zotero/Models/Database/RItem.swift index c7eb1b6d3..30286c754 100644 --- a/Zotero/Models/Database/RItem.swift +++ b/Zotero/Models/Database/RItem.swift @@ -119,6 +119,8 @@ final class RItem: Object { /// Indicates whether this instance has nonempty publicationTitle, helper variable, used in sorting so that we can show items with titles /// first and sort them in any order we want (asd/desc) and all other items later @Persisted var hasPublicationTitle: Bool + /// Type of annotation + @Persisted var annotationType: String /// Sort index for annotations @Persisted(indexed: true) var annotationSortIndex: String // MARK: - Sync data diff --git a/Zotero/Scenes/Detail/PDF/Models/PDFDatabaseAnnotation.swift b/Zotero/Scenes/Detail/PDF/Models/PDFDatabaseAnnotation.swift index d80c0c832..54839478e 100644 --- a/Zotero/Scenes/Detail/PDF/Models/PDFDatabaseAnnotation.swift +++ b/Zotero/Scenes/Detail/PDF/Models/PDFDatabaseAnnotation.swift @@ -14,30 +14,31 @@ import RxSwift struct PDFDatabaseAnnotation { let item: RItem + let type: AnnotationType - var key: String { - return self.item.key - } - - var _type: AnnotationType? { - guard let rawValue = self.item.fieldValue(for: FieldKeys.Item.Annotation.type) else { - DDLogError("DatabaseAnnotation: \(self.key) missing annotation type!") + init?(item: RItem) { + guard let _type = AnnotationType(rawValue: item.annotationType) else { + DDLogWarn("DatabaseAnnotation: \(item.key) unknown annotation type \(item.annotationType)") return nil } - guard let type = AnnotationType(rawValue: rawValue) else { - DDLogWarn("DatabaseAnnotation: \(self.key) unknown annotation type \(rawValue)") + guard AnnotationsConfig.supported.contains(_type.kind) else { return nil } - return type + self.item = item + type = _type + } + + var key: String { + return item.key } var _page: Int? { - guard let rawValue = self.item.fieldValue(for: FieldKeys.Item.Annotation.Position.pageIndex) else { - DDLogError("DatabaseAnnotation: \(self.key) missing page!") + guard let rawValue = item.fieldValue(for: FieldKeys.Item.Annotation.Position.pageIndex) else { + DDLogError("DatabaseAnnotation: \(key) missing page!") return nil } guard let page = Int(rawValue) else { - DDLogError("DatabaseAnnotation: \(self.key) page incorrect format \(rawValue)") + DDLogError("DatabaseAnnotation: \(key) page incorrect format \(rawValue)") // Page is not an int, try double or fail return Double(rawValue).flatMap(Int.init) } @@ -45,19 +46,19 @@ struct PDFDatabaseAnnotation { } var _pageLabel: String? { - guard let label = self.item.fieldValue(for: FieldKeys.Item.Annotation.pageLabel) else { - DDLogError("DatabaseAnnotation: \(self.key) missing page label!") + guard let label = item.fieldValue(for: FieldKeys.Item.Annotation.pageLabel) else { + DDLogError("DatabaseAnnotation: \(key) missing page label!") return nil } return label } var lineWidth: CGFloat? { - return (self.item.fields.filter(.key(FieldKeys.Item.Annotation.Position.lineWidth)).first?.value).flatMap(Double.init).flatMap(CGFloat.init) + return (item.fields.filter(.key(FieldKeys.Item.Annotation.Position.lineWidth)).first?.value).flatMap(Double.init).flatMap(CGFloat.init) } func isAuthor(currentUserId: Int) -> Bool { - return self.item.libraryId == .custom(.myLibrary) ? true : self.item.createdBy?.identifier == currentUserId + return item.libraryId == .custom(.myLibrary) ? true : item.createdBy?.identifier == currentUserId } func author(displayName: String, username: String) -> String { @@ -65,7 +66,7 @@ struct PDFDatabaseAnnotation { return authorName } - if let createdBy = self.item.createdBy { + if let createdBy = item.createdBy { if !createdBy.name.isEmpty { return createdBy.name } @@ -87,40 +88,40 @@ struct PDFDatabaseAnnotation { } var _color: String? { - guard let color = self.item.fieldValue(for: FieldKeys.Item.Annotation.color) else { - DDLogError("DatabaseAnnotation: \(self.key) missing color!") + guard let color = item.fieldValue(for: FieldKeys.Item.Annotation.color) else { + DDLogError("DatabaseAnnotation: \(key) missing color!") return nil } return color } var comment: String { - return self.item.fieldValue(for: FieldKeys.Item.Annotation.comment) ?? "" + return item.fieldValue(for: FieldKeys.Item.Annotation.comment) ?? "" } var text: String? { - return self.item.fields.filter(.key(FieldKeys.Item.Annotation.text)).first?.value + return item.fields.filter(.key(FieldKeys.Item.Annotation.text)).first?.value } var fontSize: UInt? { - return (self.item.fields.filter(.key(FieldKeys.Item.Annotation.Position.fontSize)).first?.value).flatMap(UInt.init) + return (item.fields.filter(.key(FieldKeys.Item.Annotation.Position.fontSize)).first?.value).flatMap(UInt.init) } var rotation: UInt? { - guard let rotation = (self.item.fields.filter(.key(FieldKeys.Item.Annotation.Position.rotation)).first?.value).flatMap(Double.init) else { return nil } + guard let rotation = (item.fields.filter(.key(FieldKeys.Item.Annotation.Position.rotation)).first?.value).flatMap(Double.init) else { return nil } return UInt(round(rotation)) } var sortIndex: String { - return self.item.annotationSortIndex + return item.annotationSortIndex } var dateModified: Date { - return self.item.dateModified + return item.dateModified } var tags: [Tag] { - return self.item.tags.map({ Tag(tag: $0) }) + return item.tags.map({ Tag(tag: $0) }) } func editability(currentUserId: Int, library: Library) -> AnnotationEditability { @@ -132,21 +133,22 @@ struct PDFDatabaseAnnotation { if !library.metadataEditable { return .notEditable } - return self.isAuthor(currentUserId: currentUserId) ? .editable : .deletable + return isAuthor(currentUserId: currentUserId) ? .editable : .deletable } } func rects(boundingBoxConverter: AnnotationBoundingBoxConverter) -> [CGRect] { - guard let page = self._page else { return [] } - return self.item.rects.map({ CGRect(x: $0.minX, y: $0.minY, width: ($0.maxX - $0.minX), height: ($0.maxY - $0.minY)) }) - .compactMap({ boundingBoxConverter.convertFromDb(rect: $0, page: PageIndex(page))?.rounded(to: 3) }) + guard let page = _page else { return [] } + return item.rects + .map({ CGRect(x: $0.minX, y: $0.minY, width: ($0.maxX - $0.minX), height: ($0.maxY - $0.minY)) }) + .compactMap({ boundingBoxConverter.convertFromDb(rect: $0, page: PageIndex(page))?.rounded(to: 3) }) } func paths(boundingBoxConverter: AnnotationBoundingBoxConverter) -> [[CGPoint]] { - guard let page = self._page else { return [] } + guard let page = _page else { return [] } let pageIndex = PageIndex(page) var paths: [[CGPoint]] = [] - for path in self.item.paths.sorted(byKeyPath: "sortIndex") { + for path in item.paths.sorted(byKeyPath: "sortIndex") { guard path.coordinates.count % 2 == 0 else { continue } let sortedCoordinates = path.coordinates.sorted(byKeyPath: "sortIndex") let lines = (0..<(path.coordinates.count / 2)).compactMap({ idx -> CGPoint? in @@ -157,31 +159,23 @@ struct PDFDatabaseAnnotation { } return paths } - - init(item: RItem) { - self.item = item - } } extension PDFDatabaseAnnotation: PDFAnnotation { var readerKey: PDFReaderState.AnnotationKey { - return .init(key: self.key, type: .database) - } - - var type: AnnotationType { - return self._type ?? .note + return .init(key: key, type: .database) } var page: Int { - return self._page ?? 0 + return _page ?? 0 } var pageLabel: String { - return self._pageLabel ?? "" + return _pageLabel ?? "" } var color: String { - return self._color ?? "#000000" + return _color ?? "#000000" } var isSyncable: Bool { @@ -191,7 +185,7 @@ extension PDFDatabaseAnnotation: PDFAnnotation { extension RItem { fileprivate func fieldValue(for key: String) -> String? { - let value = self.fields.filter(.key(key)).first?.value + let value = fields.filter(.key(key)).first?.value if value == nil { DDLogError("DatabaseAnnotation: missing value for `\(key)`") } diff --git a/Zotero/Scenes/Detail/PDF/Models/PSPDFKItAnnotation+Extensions.swift b/Zotero/Scenes/Detail/PDF/Models/PSPDFKItAnnotation+Extensions.swift new file mode 100644 index 000000000..d675fb230 --- /dev/null +++ b/Zotero/Scenes/Detail/PDF/Models/PSPDFKItAnnotation+Extensions.swift @@ -0,0 +1,119 @@ +// +// PSPDFKItAnnotation+Extensions.swift +// Zotero +// +// Created by Michal Rentka on 06.03.2024. +// Copyright © 2024 Corporation for Digital Scholarship. All rights reserved. +// + +import Foundation + +import PSPDFKit + +extension PSPDFKit.Annotation.Tool { + var toolbarTool: AnnotationTool? { + switch self { + case .eraser: + return .eraser + + case .highlight: + return .highlight + + case .square: + return .image + + case .ink: + return .ink + + case .note: + return .note + + case .freeText: + return .freeText + + case .underline: + return .underline + + default: + return nil + } + } +} + +extension AnnotationTool { + var pspdfkitTool: PSPDFKit.Annotation.Tool { + switch self { + case .eraser: + return .eraser + + case .highlight: + return .highlight + + case .image: + return .square + + case .ink: + return .ink + + case .note: + return .note + + case .freeText: + return .freeText + + case .underline: + return .underline + } + } +} + +extension PSPDFKit.Annotation.Kind { + var annotationType: AnnotationType? { + switch self { + case .note: + return .note + + case .highlight: + return .highlight + + case .square: + return .image + + case .ink: + return .ink + + case .underline: + return .underline + + case .freeText: + return .freeText + + default: + return nil + } + } +} + +extension AnnotationType { + var kind: PSPDFKit.Annotation.Kind { + switch self { + case .note: + return .note + + case .highlight: + return .highlight + + case .image: + return .square + + case .ink: + return .ink + + case .underline: + return .underline + + case .freeText: + return .freeText + } + } +} diff --git a/Zotero/Scenes/Detail/PDF/ViewModels/PDFReaderActionHandler.swift b/Zotero/Scenes/Detail/PDF/ViewModels/PDFReaderActionHandler.swift index af4686f06..0e5dbfa4f 100644 --- a/Zotero/Scenes/Detail/PDF/ViewModels/PDFReaderActionHandler.swift +++ b/Zotero/Scenes/Detail/PDF/ViewModels/PDFReaderActionHandler.swift @@ -1689,8 +1689,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi boundingBoxConverter: AnnotationBoundingBoxConverter, in viewModel: ViewModel ) -> (Int, (PDFReaderState.AnnotationKey, AnnotationDocumentLocation)?) { - if let key = viewModel.state.selectedAnnotationKey, let item = databaseAnnotations.filter(.key(key.key)).first { - let annotation = PDFDatabaseAnnotation(item: item) + if let key = viewModel.state.selectedAnnotationKey, let item = databaseAnnotations.filter(.key(key.key)).first, let annotation = PDFDatabaseAnnotation(item: item) { let page = annotation._page ?? storedPage let boundingBox = annotation.boundingBox(boundingBoxConverter: boundingBoxConverter) return (page, (key, (page, boundingBox))) @@ -1776,7 +1775,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi private func createSortedKeys(fromDatabaseAnnotations databaseAnnotations: Results, documentAnnotations: [String: PDFDocumentAnnotation]) -> [PDFReaderState.AnnotationKey] { var keys: [(PDFReaderState.AnnotationKey, String)] = [] for item in databaseAnnotations { - guard self.validate(databaseAnnotation: PDFDatabaseAnnotation(item: item)) else { continue } + guard let annotation = PDFDatabaseAnnotation(item: item), self.validate(databaseAnnotation: annotation) else { continue } keys.append((PDFReaderState.AnnotationKey(key: item.key, type: .database), item.annotationSortIndex)) } for annotation in documentAnnotations.values { @@ -1940,8 +1939,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi } let key = keys[index] - guard let item = objects.filter(.key(key.key)).first else { continue } - let annotation = PDFDatabaseAnnotation(item: item) + guard let item = objects.filter(.key(key.key)).first, let annotation = PDFDatabaseAnnotation(item: item) else { continue } if canUpdate(key: key, item: item, at: index, viewModel: viewModel) { DDLogInfo("PDFReaderActionHandler: update key \(key)") @@ -1954,8 +1952,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi } } - guard item.changeType == .sync, - let pdfAnnotation = viewModel.state.document.annotations(at: PageIndex(annotation.page)).first(where: { $0.key == key.key }) else { continue } + guard item.changeType == .sync, let pdfAnnotation = viewModel.state.document.annotations(at: PageIndex(annotation.page)).first(where: { $0.key == key.key }) else { continue } DDLogInfo("PDFReaderActionHandler: update PDF annotation") updatedPdfAnnotations.append((pdfAnnotation, annotation)) @@ -1979,8 +1976,9 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi selectionDeleted = true } - let oldAnnotation = PDFDatabaseAnnotation(item: viewModel.state.databaseAnnotations[index]) - guard let pdfAnnotation = viewModel.state.document.annotations(at: PageIndex(oldAnnotation.page)).first(where: { $0.key == oldAnnotation.key }) else { continue } + guard let oldAnnotation = PDFDatabaseAnnotation(item: viewModel.state.databaseAnnotations[index]), + let pdfAnnotation = viewModel.state.document.annotations(at: PageIndex(oldAnnotation.page)).first(where: { $0.key == oldAnnotation.key }) + else { continue } DDLogInfo("PDFReaderActionHandler: delete PDF annotation") deletedPdfAnnotations.append(pdfAnnotation) } @@ -2001,7 +1999,11 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi keys.insert(PDFReaderState.AnnotationKey(key: item.key, type: .database), at: index) DDLogInfo("PDFReaderActionHandler: insert key \(item.key)") - let annotation = PDFDatabaseAnnotation(item: item) + guard let annotation = PDFDatabaseAnnotation(item: item) else { + DDLogWarn("PDFReaderActionHandler: tried inserting unsupported annotation (\(item.annotationType))! keys.count=\(keys.count); index=\(index); deletions=\(deletions); insertions=\(insertions); modifications=\(modifications)") + shouldCancelUpdate = true + break + } switch item.changeType { case .user: diff --git a/Zotero/Scenes/Detail/PDF/Views/PDFAnnotationsViewController.swift b/Zotero/Scenes/Detail/PDF/Views/PDFAnnotationsViewController.swift index 3fe45bc66..4fe7b8174 100644 --- a/Zotero/Scenes/Detail/PDF/Views/PDFAnnotationsViewController.swift +++ b/Zotero/Scenes/Detail/PDF/Views/PDFAnnotationsViewController.swift @@ -349,8 +349,9 @@ final class PDFAnnotationsViewController: UIViewController { } } - for annotation in self.viewModel.state.databaseAnnotations { - processAnnotation(PDFDatabaseAnnotation(item: annotation)) + for dbAnnotation in self.viewModel.state.databaseAnnotations { + guard let annotation = PDFDatabaseAnnotation(item: dbAnnotation) else { continue } + processAnnotation(annotation) } for annotation in self.viewModel.state.documentAnnotations.values { processAnnotation(annotation) diff --git a/Zotero/Scenes/Detail/PDF/Views/PDFDocumentViewController.swift b/Zotero/Scenes/Detail/PDF/Views/PDFDocumentViewController.swift index 73fa2eeef..f045679b3 100644 --- a/Zotero/Scenes/Detail/PDF/Views/PDFDocumentViewController.swift +++ b/Zotero/Scenes/Detail/PDF/Views/PDFDocumentViewController.swift @@ -460,7 +460,7 @@ final class PDFDocumentViewController: UIViewController { view.removeFromSuperview() } - guard let selection = annotation, selection.type == .highlight && selection.page == Int(pageView.pageIndex) else { return } + guard let selection = annotation, (selection.type == .highlight || selection.type == .underline) && selection.page == Int(pageView.pageIndex) else { return } // Add custom highlight selection view if needed let frame = pageView.convert(selection.boundingBox(boundingBoxConverter: self), from: pageView.pdfCoordinateSpace).insetBy(dx: -SelectionView.inset, dy: -SelectionView.inset) let selectionView = SelectionView() diff --git a/Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift b/Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift index 889b8b240..c98a0cbf1 100644 --- a/Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift +++ b/Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift @@ -220,7 +220,8 @@ class PDFReaderViewController: UIViewController { separator.translatesAutoresizingMaskIntoConstraints = false separator.backgroundColor = Asset.Colors.annotationSidebarBorderColor.color - let annotationToolbar = AnnotationToolbarViewController(tools: [.highlight, .note, .image, .ink, .underline, .freeText, .eraser], undoRedoEnabled: true, size: navigationBarHeight) + // TODO: Add .underline and .freeText tools when those annotations are fully available + let annotationToolbar = AnnotationToolbarViewController(tools: [.highlight, .note, .image, .ink, .eraser], undoRedoEnabled: true, size: navigationBarHeight) annotationToolbar.delegate = self add(controller: documentController) @@ -910,60 +911,3 @@ extension PDFReaderViewController: PDFSearchDelegate { documentController.highlightSelectedSearchResult(result) } } - -extension PSPDFKit.Annotation.Tool { - fileprivate var toolbarTool: AnnotationTool? { - switch self { - case .eraser: - return .eraser - - case .highlight: - return .highlight - - case .square: - return .image - - case .ink: - return .ink - - case .note: - return .note - - case .freeText: - return .freeText - - case .underline: - return .underline - - default: - return nil - } - } -} - -extension AnnotationTool { - fileprivate var pspdfkitTool: PSPDFKit.Annotation.Tool { - switch self { - case .eraser: - return .eraser - - case .highlight: - return .highlight - - case .image: - return .square - - case .ink: - return .ink - - case .note: - return .note - - case .freeText: - return .freeText - - case .underline: - return .underline - } - } -}