diff --git a/ZShare/Controllers/TranslationWebViewHandler.swift b/ZShare/Controllers/TranslationWebViewHandler.swift index 753430b7c..3fa534dad 100644 --- a/ZShare/Controllers/TranslationWebViewHandler.swift +++ b/ZShare/Controllers/TranslationWebViewHandler.swift @@ -136,14 +136,14 @@ final class TranslationWebViewHandler { } .flatMap { translators -> Single in DDLogInfo("WebViewHandler: encode translators") - let encodedTranslators = WKWebView.encodeAsJSONForJavascript(translators) + let encodedTranslators = WebViewEncoder.encodeAsJSONForJavascript(translators) return self.webViewHandler.call(javascript: "initTranslators(\(encodedTranslators));") } .flatMap({ _ -> Single in DDLogInfo("WebViewHandler: call translate js") - let encodedHtml = WKWebView.encodeForJavascript(html.data(using: .utf8)) + let encodedHtml = WebViewEncoder.encodeForJavascript(html.data(using: .utf8)) let jsonFramesData = try? JSONSerialization.data(withJSONObject: frames, options: .fragmentsAllowed) - let encodedFrames = jsonFramesData.flatMap({ WKWebView.encodeForJavascript($0) }) ?? "''" + let encodedFrames = jsonFramesData.flatMap({ WebViewEncoder.encodeForJavascript($0) }) ?? "''" return self.webViewHandler.call(javascript: "translate('\(url.absoluteString)', \(encodedHtml), \(encodedFrames));") }) .subscribe(onFailure: { [weak self] error in @@ -169,8 +169,8 @@ final class TranslationWebViewHandler { return Disposables.create() } - let encodedSchema = WKWebView.encodeForJavascript(schemaData) - let encodedFormats = WKWebView.encodeForJavascript(dateFormatData) + let encodedSchema = WebViewEncoder.encodeForJavascript(schemaData) + let encodedFormats = WebViewEncoder.encodeForJavascript(dateFormatData) DDLogInfo("WebViewHandler: loaded bundled files") diff --git a/Zotero.xcodeproj/project.pbxproj b/Zotero.xcodeproj/project.pbxproj index 5d15fe254..c047108f6 100644 --- a/Zotero.xcodeproj/project.pbxproj +++ b/Zotero.xcodeproj/project.pbxproj @@ -388,13 +388,16 @@ B310280B2B1E16EC00E41554 /* PDFThumbnailsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B310280A2B1E16EC00E41554 /* PDFThumbnailsState.swift */; }; B310280D2B1E16F300E41554 /* PDFThumbnailsAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B310280C2B1E16F300E41554 /* PDFThumbnailsAction.swift */; }; B310FA4529E5765800FA2F15 /* AddTagsToItemDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B310FA4429E5765800FA2F15 /* AddTagsToItemDbRequest.swift */; }; + B31173792AFCEF25007D9741 /* AnnotationTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F6AA392AB30663005BC22E /* AnnotationTool.swift */; }; + B311737B2AFCF004007D9741 /* AnnotationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B311737A2AFCF004007D9741 /* AnnotationType.swift */; }; + B311737C2AFCF04D007D9741 /* AnnotationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B311737A2AFCF004007D9741 /* AnnotationType.swift */; }; B311B3222386A39800BCF592 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B311B3202386A39800BCF592 /* MainInterface.storyboard */; }; B311B3262386A39800BCF592 /* ZShare.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B311B31C2386A39800BCF592 /* ZShare.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; B3129855270EE9DE002376F9 /* Error+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3129854270EE9DE002376F9 /* Error+Helpers.swift */; }; B312BB4125519DD30023300A /* ItemDetailLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B312BB4025519DD30023300A /* ItemDetailLayout.swift */; }; B31901942629C9CB00209E33 /* ItemsFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31901922629C9CB00209E33 /* ItemsFilterViewController.swift */; }; B31901952629C9CB00209E33 /* ItemsFilterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = B31901932629C9CB00209E33 /* ItemsFilterViewController.xib */; }; - B31941DD24531F6600BF6296 /* AnnotationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31941DB24531F6600BF6296 /* AnnotationsViewController.swift */; }; + B31941DD24531F6600BF6296 /* PDFAnnotationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31941DB24531F6600BF6296 /* PDFAnnotationsViewController.swift */; }; B319D6B6265CE05000E52132 /* ReadAllItemsForUploadDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B319D6B5265CE05000E52132 /* ReadAllItemsForUploadDbRequest.swift */; }; B319D6B8265CE0C200E52132 /* ReadAllDownloadedAndForUploadItemsDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B319D6B7265CE0C200E52132 /* ReadAllDownloadedAndForUploadItemsDbRequest.swift */; }; B31A5E51286308960026589F /* LookupIdentifierCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31A5E4F286308960026589F /* LookupIdentifierCell.swift */; }; @@ -440,7 +443,7 @@ B325DBB524375DD200EFF0F5 /* DebugPermissionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = B325DBB424375DD200EFF0F5 /* DebugPermissionResponse.swift */; }; B325DBB624375DD500EFF0F5 /* DebugPermissionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = B325DBB424375DD200EFF0F5 /* DebugPermissionResponse.swift */; }; B325EE4D27B28AC7002143AA /* ExpandableCollectionsCollectionViewHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B325EE4C27B28AC7002143AA /* ExpandableCollectionsCollectionViewHandler.swift */; }; - B32861E428BF89CE007B5A5C /* CreateAnnotationsDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32861E328BF89CE007B5A5C /* CreateAnnotationsDbRequest.swift */; }; + B32861E428BF89CE007B5A5C /* CreatePDFAnnotationsDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32861E328BF89CE007B5A5C /* CreatePDFAnnotationsDbRequest.swift */; }; B32861E628BFACD4007B5A5C /* EditItemFieldsDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32861E528BFACD4007B5A5C /* EditItemFieldsDbRequest.swift */; }; B32989D628D1D94F009B61F3 /* RObjectChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32989D528D1D94F009B61F3 /* RObjectChange.swift */; }; B32A273C254841B80081E061 /* CreatorEditViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32A273A254841B80081E061 /* CreatorEditViewController.swift */; }; @@ -458,6 +461,7 @@ B32B4F952475544700F78A05 /* SyncToolbarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32B4F942475544700F78A05 /* SyncToolbarController.swift */; }; B32B4F972475636600F78A05 /* RxTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32B4F962475636600F78A05 /* RxTableViewCell.swift */; }; B32B8A572B18A08900A9A741 /* PDFThumbnailController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32B8A562B18A08900A9A741 /* PDFThumbnailController.swift */; }; + B32CDD3D2AEA9BB100EF5054 /* AnnotationPopoverViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = B32CDD3C2AEA9BB100EF5054 /* AnnotationPopoverViewController.xib */; }; B32DE61F26320271000287EC /* PopoverNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32DE61E26320271000287EC /* PopoverNavigationViewController.swift */; }; B32EBFBD276A89190003897E /* BackgroundUploadProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32EBFB6276A89190003897E /* BackgroundUploadProcessor.swift */; }; B32EBFBE276A89190003897E /* BackgroundUploaderContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32EBFB7276A89190003897E /* BackgroundUploaderContext.swift */; }; @@ -500,7 +504,7 @@ 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 /* 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 */; }; @@ -695,8 +699,6 @@ B361820F24C9B9E800B30D56 /* SelectableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B361820E24C9B9E800B30D56 /* SelectableTextField.swift */; }; B361A3012511F98700271173 /* LinkMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B361A3002511F98700271173 /* LinkMode.swift */; }; B361A3022511F9BE00271173 /* LinkMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B361A3002511F98700271173 /* LinkMode.swift */; }; - B361A3042511F9FB00271173 /* AnnotationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B361A3032511F9FB00271173 /* AnnotationType.swift */; }; - B361A3052511FA0C00271173 /* AnnotationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B361A3032511F9FB00271173 /* AnnotationType.swift */; }; B361C2B429DECB220035BF7E /* ResetSettingsVersionDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B361C2B329DECB220035BF7E /* ResetSettingsVersionDbRequest.swift */; }; B36459DA2644116000A0C2C0 /* TagPickerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3830CD9255451AA00910FE0 /* TagPickerState.swift */; }; B36459DB2644117D00A0C2C0 /* TagPickerActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3830CE1255451C200910FE0 /* TagPickerActionHandler.swift */; }; @@ -740,7 +742,6 @@ B372CEE7248673DF00B423AE /* GroupRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B372CEE22486512500B423AE /* GroupRequest.swift */; }; B372CEE92486748500B423AE /* MarkGroupForResyncSyncAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B372CEE82486748500B423AE /* MarkGroupForResyncSyncAction.swift */; }; B372CEEA248675A200B423AE /* MarkGroupForResyncSyncAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B372CEE82486748500B423AE /* MarkGroupForResyncSyncAction.swift */; }; - B37381A727CE6EDE005AD721 /* PDFSettingsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37381A627CE6EDE005AD721 /* PDFSettingsState.swift */; }; B373877328FEA0C7004E5031 /* PDFSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B373877228FEA0C7004E5031 /* PDFSidebarViewController.swift */; }; B373C98F2B1F5431007FD56C /* PDFThumbnailsLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B373C98E2B1F5431007FD56C /* PDFThumbnailsLayout.swift */; }; B3748D2026566F4800ED05F8 /* ContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3748D1F26566F4800ED05F8 /* ContainerViewController.swift */; }; @@ -810,6 +811,9 @@ B391480726D9093E0016B7B2 /* UIPasteboard+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B391480626D9093E0016B7B2 /* UIPasteboard+Extensions.swift */; }; B391480826D909E40016B7B2 /* UIPasteboard+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B391480626D9093E0016B7B2 /* UIPasteboard+Extensions.swift */; }; B394A0E226A709A900CA307F /* NoteEditorTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B394A0E126A709A900CA307F /* NoteEditorTitleView.swift */; }; + B39505E42AE9466800F73079 /* AnnotationPopoverActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39505E32AE9466800F73079 /* AnnotationPopoverActionHandler.swift */; }; + B39505E72AE9469300F73079 /* AnnotationPopoverAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39505E52AE9469300F73079 /* AnnotationPopoverAction.swift */; }; + B39505E82AE9469300F73079 /* AnnotationPopoverState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39505E62AE9469300F73079 /* AnnotationPopoverState.swift */; }; B395EFE325494B1B00CEBD9F /* CreatorEditAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B395EFE225494B1B00CEBD9F /* CreatorEditAction.swift */; }; B395EFE725494B2300CEBD9F /* CreatorEditState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B395EFE625494B2300CEBD9F /* CreatorEditState.swift */; }; B395EFEB25494B3200CEBD9F /* CreatorEditActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B395EFEA25494B3200CEBD9F /* CreatorEditActionHandler.swift */; }; @@ -887,9 +891,6 @@ 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 */; }; - B3ADBCB827CFB64B00B7041F /* PDFSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3ADBCB727CFB64B00B7041F /* PDFSettingsViewController.swift */; }; - B3ADBCBA27CFB66C00B7041F /* PDFSettingsActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3ADBCB927CFB66C00B7041F /* PDFSettingsActionHandler.swift */; }; - B3ADBCBC27CFB67A00B7041F /* PDFSettingsAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3ADBCBB27CFB67A00B7041F /* PDFSettingsAction.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 */; }; @@ -919,7 +920,6 @@ B3B78C47266FB11D00E91D02 /* WKWebView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B78C45266FAF8500E91D02 /* WKWebView+Extensions.swift */; }; B3B8800525FB546300904235 /* DeviceInfoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B8800425FB546300904235 /* DeviceInfoProvider.swift */; }; B3B8800A25FB590900904235 /* DeviceInfoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B8800425FB546300904235 /* DeviceInfoProvider.swift */; }; - B3B953C32459780600FC96DB /* Annotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B953C22459780600FC96DB /* Annotation.swift */; }; B3B953C8245981A600FC96DB /* PDFReaderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B953C7245981A600FC96DB /* PDFReaderState.swift */; }; B3B953CA245981AD00FC96DB /* PDFReaderAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B953C9245981AD00FC96DB /* PDFReaderAction.swift */; }; B3B953CC245981C100FC96DB /* PDFReaderActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B953CB245981C100FC96DB /* PDFReaderActionHandler.swift */; }; @@ -960,6 +960,7 @@ B3CBB121248A439A00C4228F /* Notification+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CBB120248A439A00C4228F /* Notification+Extensions.swift */; }; B3CBB123248A44D100C4228F /* KeyboardData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CBB122248A44D100C4228F /* KeyboardData.swift */; }; B3CBB125248A46D500C4228F /* NotificationCenter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CBB124248A46D500C4228F /* NotificationCenter+Extensions.swift */; }; + B3CC280B2AA8A8EC00886856 /* AnnotationToolbarHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CC280A2AA8A8EC00886856 /* AnnotationToolbarHandler.swift */; }; B3CCB8D829B72F9B0097520B /* AnnotationEditCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CCB8D729B72F9B0097520B /* AnnotationEditCoordinator.swift */; }; B3CCB8DA29B73DED0097520B /* UnlockPDFViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CCB8D929B73DED0097520B /* UnlockPDFViewController.swift */; }; B3CD66BF29E800E3008180C7 /* ItemsFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CD66BE29E800E3008180C7 /* ItemsFilter.swift */; }; @@ -991,6 +992,9 @@ B3D84BEB27917D07005DDD7C /* CreatorTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D84BE927917BE1005DDD7C /* CreatorTypes.swift */; }; B3D84BED27917E3D005DDD7C /* UpdateCreatorSummaryFormatDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D84BEC27917E3D005DDD7C /* UpdateCreatorSummaryFormatDbRequest.swift */; }; B3D84BF027919FDE005DDD7C /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = B3D84BEF27919FDE005DDD7C /* Starscream */; }; + B3DAF84D2AC5AA5C00F9AB6A /* PDFDocumentAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3DAF84A2AC5AA5C00F9AB6A /* PDFDocumentAnnotation.swift */; }; + B3DAF84E2AC5AA5C00F9AB6A /* PDFAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3DAF84B2AC5AA5C00F9AB6A /* PDFAnnotation.swift */; }; + B3DAF84F2AC5AA5C00F9AB6A /* PDFDatabaseAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3DAF84C2AC5AA5C00F9AB6A /* PDFDatabaseAnnotation.swift */; }; B3DCDEE12408F5200039ED0D /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3DCDEE02408F5200039ED0D /* MainViewController.swift */; }; B3DCDF06240912060039ED0D /* DocumentPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3DCDF01240912060039ED0D /* DocumentPickerViewController.swift */; }; B3DCDF07240912060039ED0D /* NoteEditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3DCDF02240912060039ED0D /* NoteEditorViewController.swift */; }; @@ -1020,7 +1024,6 @@ B3DF9AD62747AB55007933CB /* ApiEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3DF9AD42747AB4F007933CB /* ApiEndpoint.swift */; }; B3DF9AD82747AB63007933CB /* ApiLogParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3DF9AD72747AB63007933CB /* ApiLogParameters.swift */; }; B3DF9AD92747AB65007933CB /* ApiLogParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3DF9AD72747AB63007933CB /* ApiLogParameters.swift */; }; - B3E02C0D26848CA900FFB419 /* AnnotationViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = B3E02C0C26848CA900FFB419 /* AnnotationViewController.xib */; }; B3E1BEC9254B0B9500D9BB27 /* ItemDetailFieldEditCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E1BEC7254B0B9500D9BB27 /* ItemDetailFieldEditCell.swift */; }; B3E1BECA254B0B9500D9BB27 /* ItemDetailFieldEditContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B3E1BEC8254B0B9500D9BB27 /* ItemDetailFieldEditContentView.xib */; }; B3E2FF2029CAE1EF00F85AEB /* ItemsFilterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E2FF1F29CAE1EF00F85AEB /* ItemsFilterCoordinator.swift */; }; @@ -1118,8 +1121,6 @@ B3F3D640255ECA4700F310C2 /* AnnotationViewText.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F3D63F255ECA4700F310C2 /* AnnotationViewText.swift */; }; B3F3D644255ECF3500F310C2 /* AnnotationViewSeparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F3D643255ECF3500F310C2 /* AnnotationViewSeparator.swift */; }; B3F47E0025B5F550006FA96F /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B30B550424B85A6200F94B59 /* Images.xcassets */; }; - B3F49FB228B8C3CB00A1F3E8 /* DocumentAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F49FB128B8C3CB00A1F3E8 /* DocumentAnnotation.swift */; }; - B3F49FB428B8C3D900A1F3E8 /* DatabaseAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F49FB328B8C3D900A1F3E8 /* DatabaseAnnotation.swift */; }; B3F49FB628B8C3F600A1F3E8 /* AnnotationEditability.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F49FB528B8C3F600A1F3E8 /* AnnotationEditability.swift */; }; B3F4E4EC2A4DAA7D00820718 /* UpdatableObjectSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F4E4EB2A4DAA7D00820718 /* UpdatableObjectSpec.swift */; }; B3F4E4EE2A4DC39800820718 /* JSONSerialization+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F4E4ED2A4DC39800820718 /* JSONSerialization+Utils.swift */; }; @@ -1128,15 +1129,22 @@ B3F55A1729EED04700A6716E /* ReadFilteredTagsDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F55A1629EED04700A6716E /* ReadFilteredTagsDbRequest.swift */; }; B3F55A1929EED4CB00A6716E /* ReadAutomaticTagsDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F55A1829EED4CB00A6716E /* ReadAutomaticTagsDbRequest.swift */; }; B3F55A1B29EED4EA00A6716E /* ReadColoredTagsDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F55A1A29EED4EA00A6716E /* ReadColoredTagsDbRequest.swift */; }; - B3F5B07D27D0FFF500AA81C0 /* PDFSettingsSegmentedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F5B07C27D0FFF500AA81C0 /* PDFSettingsSegmentedCell.swift */; }; - B3F5B07F27D1000C00AA81C0 /* PDFSettingsSegmentedCellContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F5B07E27D1000C00AA81C0 /* PDFSettingsSegmentedCellContentView.swift */; }; - B3F5B08127D1001B00AA81C0 /* PDFSettingsSegmentedCellContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B3F5B08027D1001B00AA81C0 /* PDFSettingsSegmentedCellContentView.xib */; }; + B3F6AA3A2AB30663005BC22E /* AnnotationTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F6AA392AB30663005BC22E /* AnnotationTool.swift */; }; B3F7B0682524A78F00E51377 /* CocoaLumberjack in Frameworks */ = {isa = PBXBuildFile; productRef = B3F7B0672524A78F00E51377 /* CocoaLumberjack */; }; B3F7B06A2524A78F00E51377 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B3F7B0692524A78F00E51377 /* CocoaLumberjackSwift */; }; B3F7B06F2524A8B700E51377 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = B3F7B06E2524A8B700E51377 /* Alamofire */; }; B3F7B0732524A8D500E51377 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B3F7B0722524A8D500E51377 /* KeychainSwift */; }; B3F7B0752524A8D800E51377 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = B3F7B0742524A8D800E51377 /* ZIPFoundation */; }; B3F83808263960FA00E128A6 /* ItemAccessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F83807263960FA00E128A6 /* ItemAccessory.swift */; }; + B3F9A4BD2B04CEC300684030 /* ReaderSettingsSegmentedCellContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F9A4BA2B04CEC300684030 /* ReaderSettingsSegmentedCellContentView.swift */; }; + B3F9A4BE2B04CEC300684030 /* ReaderSettingsSegmentedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F9A4BB2B04CEC300684030 /* ReaderSettingsSegmentedCell.swift */; }; + B3F9A4BF2B04CEC300684030 /* ReaderSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F9A4BC2B04CEC300684030 /* ReaderSettingsViewController.swift */; }; + B3F9A4C12B04CED600684030 /* ReaderSettingsSegmentedCellContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B3F9A4C02B04CED600684030 /* ReaderSettingsSegmentedCellContentView.xib */; }; + B3F9A4C42B04D0D900684030 /* ReaderSettingsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F9A4C22B04D0D800684030 /* ReaderSettingsState.swift */; }; + B3F9A4C52B04D0D900684030 /* ReaderSettingsAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F9A4C32B04D0D900684030 /* ReaderSettingsAction.swift */; }; + B3F9A4C72B04D0E400684030 /* ReaderSettingsActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F9A4C62B04D0E400684030 /* ReaderSettingsActionHandler.swift */; }; + B3F9A4CB2B04F28400684030 /* WebViewEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F9A4CA2B04F28400684030 /* WebViewEncoder.swift */; }; + B3F9A4CC2B04F2AC00684030 /* WebViewEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F9A4CA2B04F28400684030 /* WebViewEncoder.swift */; }; B3FBAC1924570BDE00AA00C0 /* AnnotationPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FBAC1824570BDE00AA00C0 /* AnnotationPreviewController.swift */; }; B3FC7431272181E500F55531 /* WebDavWriteRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC7430272181E500F55531 /* WebDavWriteRequest.swift */; }; B3FC74322721857C00F55531 /* WebDavController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B398A914270C6A4300968EE8 /* WebDavController.swift */; }; @@ -1434,6 +1442,7 @@ B310280A2B1E16EC00E41554 /* PDFThumbnailsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFThumbnailsState.swift; sourceTree = ""; }; B310280C2B1E16F300E41554 /* PDFThumbnailsAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFThumbnailsAction.swift; sourceTree = ""; }; B310FA4429E5765800FA2F15 /* AddTagsToItemDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTagsToItemDbRequest.swift; sourceTree = ""; }; + B311737A2AFCF004007D9741 /* AnnotationType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnotationType.swift; sourceTree = ""; }; B311B31C2386A39800BCF592 /* ZShare.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ZShare.appex; sourceTree = BUILT_PRODUCTS_DIR; }; B311B3212386A39800BCF592 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; B311B3232386A39800BCF592 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1441,7 +1450,7 @@ B312BB4025519DD30023300A /* ItemDetailLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemDetailLayout.swift; sourceTree = ""; }; B31901922629C9CB00209E33 /* ItemsFilterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemsFilterViewController.swift; sourceTree = ""; }; B31901932629C9CB00209E33 /* ItemsFilterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ItemsFilterViewController.xib; sourceTree = ""; }; - B31941DB24531F6600BF6296 /* AnnotationsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnotationsViewController.swift; sourceTree = ""; }; + B31941DB24531F6600BF6296 /* PDFAnnotationsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFAnnotationsViewController.swift; sourceTree = ""; }; B319D6B5265CE05000E52132 /* ReadAllItemsForUploadDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadAllItemsForUploadDbRequest.swift; sourceTree = ""; }; B319D6B7265CE0C200E52132 /* ReadAllDownloadedAndForUploadItemsDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadAllDownloadedAndForUploadItemsDbRequest.swift; sourceTree = ""; }; B31A5E4F286308960026589F /* LookupIdentifierCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LookupIdentifierCell.swift; sourceTree = ""; }; @@ -1482,7 +1491,7 @@ B325DBB124375DAC00EFF0F5 /* ConflictResolution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConflictResolution.swift; sourceTree = ""; }; B325DBB424375DD200EFF0F5 /* DebugPermissionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugPermissionResponse.swift; sourceTree = ""; }; B325EE4C27B28AC7002143AA /* ExpandableCollectionsCollectionViewHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableCollectionsCollectionViewHandler.swift; sourceTree = ""; }; - B32861E328BF89CE007B5A5C /* CreateAnnotationsDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAnnotationsDbRequest.swift; sourceTree = ""; }; + B32861E328BF89CE007B5A5C /* CreatePDFAnnotationsDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePDFAnnotationsDbRequest.swift; sourceTree = ""; }; B32861E528BFACD4007B5A5C /* EditItemFieldsDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditItemFieldsDbRequest.swift; sourceTree = ""; }; B32989D528D1D94F009B61F3 /* RObjectChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RObjectChange.swift; sourceTree = ""; }; B32A273A254841B80081E061 /* CreatorEditViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorEditViewController.swift; sourceTree = ""; }; @@ -1500,6 +1509,7 @@ B32B4F942475544700F78A05 /* SyncToolbarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncToolbarController.swift; sourceTree = ""; }; B32B4F962475636600F78A05 /* RxTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxTableViewCell.swift; sourceTree = ""; }; B32B8A562B18A08900A9A741 /* PDFThumbnailController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFThumbnailController.swift; sourceTree = ""; }; + B32CDD3C2AEA9BB100EF5054 /* AnnotationPopoverViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AnnotationPopoverViewController.xib; sourceTree = ""; }; B32DE61E26320271000287EC /* PopoverNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverNavigationViewController.swift; sourceTree = ""; }; B32EBFB5276A89190003897E /* BackgroundUploader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundUploader.swift; sourceTree = ""; }; B32EBFB6276A89190003897E /* BackgroundUploadProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundUploadProcessor.swift; sourceTree = ""; }; @@ -1528,7 +1538,7 @@ B33E8A4527B69FFE00CBC7DE /* AllCollectionPickerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllCollectionPickerState.swift; sourceTree = ""; }; B33E8A4727B6A07100CBC7DE /* AllCollectionPickerAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllCollectionPickerAction.swift; sourceTree = ""; }; B33E8A4927B6A1D000CBC7DE /* AllCollectionPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllCollectionPickerViewController.swift; sourceTree = ""; }; - B3401D552567D8F700BB8D6E /* AnnotationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnotationViewController.swift; sourceTree = ""; }; + B3401D552567D8F700BB8D6E /* AnnotationPopoverViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnotationPopoverViewController.swift; sourceTree = ""; }; B3401D5A2567DAAE00BB8D6E /* AnnotationPopoverCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnotationPopoverCoordinator.swift; sourceTree = ""; }; B3401D602568047D00BB8D6E /* AnnotationViewTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationViewTextView.swift; sourceTree = ""; }; B340692124A60D6A009ECE48 /* Rounding+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Rounding+Extensions.swift"; sourceTree = ""; }; @@ -1685,7 +1695,6 @@ B361820C24C9B9D900B30D56 /* NoRotationHostingController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoRotationHostingController.swift; sourceTree = ""; }; B361820E24C9B9E800B30D56 /* SelectableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableTextField.swift; sourceTree = ""; }; B361A3002511F98700271173 /* LinkMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkMode.swift; sourceTree = ""; }; - B361A3032511F9FB00271173 /* AnnotationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationType.swift; sourceTree = ""; }; B361C2B329DECB220035BF7E /* ResetSettingsVersionDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetSettingsVersionDbRequest.swift; sourceTree = ""; }; B36459E626441A1800A0C2C0 /* TagRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagRow.swift; sourceTree = ""; }; B36459E826441A2000A0C2C0 /* TagRow.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TagRow.xib; sourceTree = ""; }; @@ -1710,7 +1719,6 @@ B372CEE22486512500B423AE /* GroupRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupRequest.swift; sourceTree = ""; }; B372CEE4248670AF00B423AE /* FetchAndStoreGroupSyncAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAndStoreGroupSyncAction.swift; sourceTree = ""; }; B372CEE82486748500B423AE /* MarkGroupForResyncSyncAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkGroupForResyncSyncAction.swift; sourceTree = ""; }; - B37381A627CE6EDE005AD721 /* PDFSettingsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFSettingsState.swift; sourceTree = ""; }; B373877228FEA0C7004E5031 /* PDFSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFSidebarViewController.swift; sourceTree = ""; }; B373C98E2B1F5431007FD56C /* PDFThumbnailsLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFThumbnailsLayout.swift; sourceTree = ""; }; B3748D1F26566F4800ED05F8 /* ContainerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainerViewController.swift; sourceTree = ""; }; @@ -1765,6 +1773,9 @@ B390EE2B29B5F419004F979B /* PDFCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFCoordinator.swift; sourceTree = ""; }; B391480626D9093E0016B7B2 /* UIPasteboard+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIPasteboard+Extensions.swift"; sourceTree = ""; }; B394A0E126A709A900CA307F /* NoteEditorTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteEditorTitleView.swift; sourceTree = ""; }; + B39505E32AE9466800F73079 /* AnnotationPopoverActionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationPopoverActionHandler.swift; sourceTree = ""; }; + B39505E52AE9469300F73079 /* AnnotationPopoverAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnotationPopoverAction.swift; sourceTree = ""; }; + B39505E62AE9469300F73079 /* AnnotationPopoverState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnotationPopoverState.swift; sourceTree = ""; }; B395EFE225494B1B00CEBD9F /* CreatorEditAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorEditAction.swift; sourceTree = ""; }; B395EFE625494B2300CEBD9F /* CreatorEditState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorEditState.swift; sourceTree = ""; }; B395EFEA25494B3200CEBD9F /* CreatorEditActionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorEditActionHandler.swift; sourceTree = ""; }; @@ -1835,9 +1846,6 @@ 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 = ""; }; - B3ADBCB727CFB64B00B7041F /* PDFSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFSettingsViewController.swift; sourceTree = ""; }; - B3ADBCB927CFB66C00B7041F /* PDFSettingsActionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFSettingsActionHandler.swift; sourceTree = ""; }; - B3ADBCBB27CFB67A00B7041F /* PDFSettingsAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFSettingsAction.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 = ""; }; @@ -1867,7 +1875,6 @@ B3B78C45266FAF8500E91D02 /* WKWebView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WKWebView+Extensions.swift"; sourceTree = ""; }; B3B8166723880DC900529605 /* ZShare.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ZShare.entitlements; sourceTree = ""; }; B3B8800425FB546300904235 /* DeviceInfoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoProvider.swift; sourceTree = ""; }; - B3B953C22459780600FC96DB /* Annotation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Annotation.swift; sourceTree = ""; }; B3B953C7245981A600FC96DB /* PDFReaderState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFReaderState.swift; sourceTree = ""; }; B3B953C9245981AD00FC96DB /* PDFReaderAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFReaderAction.swift; sourceTree = ""; }; B3B953CB245981C100FC96DB /* PDFReaderActionHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFReaderActionHandler.swift; sourceTree = ""; }; @@ -1901,6 +1908,7 @@ B3CBB120248A439A00C4228F /* Notification+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Extensions.swift"; sourceTree = ""; }; B3CBB122248A44D100C4228F /* KeyboardData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardData.swift; sourceTree = ""; }; B3CBB124248A46D500C4228F /* NotificationCenter+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationCenter+Extensions.swift"; sourceTree = ""; }; + B3CC280A2AA8A8EC00886856 /* AnnotationToolbarHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationToolbarHandler.swift; sourceTree = ""; }; B3CCB8D729B72F9B0097520B /* AnnotationEditCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationEditCoordinator.swift; sourceTree = ""; }; B3CCB8D929B73DED0097520B /* UnlockPDFViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockPDFViewController.swift; sourceTree = ""; }; B3CD66BE29E800E3008180C7 /* ItemsFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemsFilter.swift; sourceTree = ""; }; @@ -1922,6 +1930,9 @@ B3D59F122A38A69100F6BB7D /* RevertLibraryFilesSyncAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevertLibraryFilesSyncAction.swift; sourceTree = ""; }; B3D84BE927917BE1005DDD7C /* CreatorTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorTypes.swift; sourceTree = ""; }; B3D84BEC27917E3D005DDD7C /* UpdateCreatorSummaryFormatDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCreatorSummaryFormatDbRequest.swift; sourceTree = ""; }; + B3DAF84A2AC5AA5C00F9AB6A /* PDFDocumentAnnotation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFDocumentAnnotation.swift; sourceTree = ""; }; + B3DAF84B2AC5AA5C00F9AB6A /* PDFAnnotation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFAnnotation.swift; sourceTree = ""; }; + B3DAF84C2AC5AA5C00F9AB6A /* PDFDatabaseAnnotation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFDatabaseAnnotation.swift; sourceTree = ""; }; B3DCDEE02408F5200039ED0D /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; B3DCDF01240912060039ED0D /* DocumentPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentPickerViewController.swift; sourceTree = ""; }; B3DCDF02240912060039ED0D /* NoteEditorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoteEditorViewController.swift; sourceTree = ""; }; @@ -1945,7 +1956,6 @@ B3DF9AD12747AAD2007933CB /* ApiRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiRequest.swift; sourceTree = ""; }; B3DF9AD42747AB4F007933CB /* ApiEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiEndpoint.swift; sourceTree = ""; }; B3DF9AD72747AB63007933CB /* ApiLogParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiLogParameters.swift; sourceTree = ""; }; - B3E02C0C26848CA900FFB419 /* AnnotationViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AnnotationViewController.xib; sourceTree = ""; }; B3E1BEC7254B0B9500D9BB27 /* ItemDetailFieldEditCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemDetailFieldEditCell.swift; sourceTree = ""; }; B3E1BEC8254B0B9500D9BB27 /* ItemDetailFieldEditContentView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ItemDetailFieldEditContentView.xib; sourceTree = ""; }; B3E2FF1F29CAE1EF00F85AEB /* ItemsFilterCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemsFilterCoordinator.swift; sourceTree = ""; }; @@ -2030,8 +2040,6 @@ B3F3D643255ECF3500F310C2 /* AnnotationViewSeparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationViewSeparator.swift; sourceTree = ""; }; B3F47C00243B339F004F8B1E /* TranslatorsControllerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslatorsControllerSpec.swift; sourceTree = ""; }; B3F47C04243B4BC0004F8B1E /* Bundled */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Bundled; sourceTree = ""; }; - B3F49FB128B8C3CB00A1F3E8 /* DocumentAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentAnnotation.swift; sourceTree = ""; }; - B3F49FB328B8C3D900A1F3E8 /* DatabaseAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseAnnotation.swift; sourceTree = ""; }; B3F49FB528B8C3F600A1F3E8 /* AnnotationEditability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationEditability.swift; sourceTree = ""; }; B3F4E4EB2A4DAA7D00820718 /* UpdatableObjectSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatableObjectSpec.swift; sourceTree = ""; }; B3F4E4ED2A4DC39800820718 /* JSONSerialization+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONSerialization+Utils.swift"; sourceTree = ""; }; @@ -2039,11 +2047,17 @@ B3F55A1629EED04700A6716E /* ReadFilteredTagsDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadFilteredTagsDbRequest.swift; sourceTree = ""; }; B3F55A1829EED4CB00A6716E /* ReadAutomaticTagsDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadAutomaticTagsDbRequest.swift; sourceTree = ""; }; B3F55A1A29EED4EA00A6716E /* ReadColoredTagsDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadColoredTagsDbRequest.swift; sourceTree = ""; }; - B3F5B07C27D0FFF500AA81C0 /* PDFSettingsSegmentedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFSettingsSegmentedCell.swift; sourceTree = ""; }; - B3F5B07E27D1000C00AA81C0 /* PDFSettingsSegmentedCellContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFSettingsSegmentedCellContentView.swift; sourceTree = ""; }; - B3F5B08027D1001B00AA81C0 /* PDFSettingsSegmentedCellContentView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PDFSettingsSegmentedCellContentView.xib; sourceTree = ""; }; B3F6415F2A28B21E00A78CB0 /* ci_pre_xcodebuild.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = ci_pre_xcodebuild.sh; sourceTree = ""; }; + B3F6AA392AB30663005BC22E /* AnnotationTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationTool.swift; sourceTree = ""; }; B3F83807263960FA00E128A6 /* ItemAccessory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemAccessory.swift; sourceTree = ""; }; + B3F9A4BA2B04CEC300684030 /* ReaderSettingsSegmentedCellContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderSettingsSegmentedCellContentView.swift; sourceTree = ""; }; + B3F9A4BB2B04CEC300684030 /* ReaderSettingsSegmentedCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderSettingsSegmentedCell.swift; sourceTree = ""; }; + B3F9A4BC2B04CEC300684030 /* ReaderSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderSettingsViewController.swift; sourceTree = ""; }; + B3F9A4C02B04CED600684030 /* ReaderSettingsSegmentedCellContentView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReaderSettingsSegmentedCellContentView.xib; sourceTree = ""; }; + B3F9A4C22B04D0D800684030 /* ReaderSettingsState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderSettingsState.swift; sourceTree = ""; }; + B3F9A4C32B04D0D900684030 /* ReaderSettingsAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderSettingsAction.swift; sourceTree = ""; }; + B3F9A4C62B04D0E400684030 /* ReaderSettingsActionHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderSettingsActionHandler.swift; sourceTree = ""; }; + B3F9A4CA2B04F28400684030 /* WebViewEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewEncoder.swift; sourceTree = ""; }; B3FBAC1824570BDE00AA00C0 /* AnnotationPreviewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationPreviewController.swift; sourceTree = ""; }; B3FC7430272181E500F55531 /* WebDavWriteRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebDavWriteRequest.swift; sourceTree = ""; }; B3FE4B91268DDE4900CE123F /* CitationBibliographyExportActionHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CitationBibliographyExportActionHandler.swift; sourceTree = ""; }; @@ -2235,6 +2249,7 @@ B3972688247D403200A8B469 /* UrlDetector.swift */, B324276C25C81F2000567504 /* WebSocketController.swift */, 618404252A4456A9005AAF22 /* IdentifierLookupController.swift */, + B3F9A4CA2B04F28400684030 /* WebViewEncoder.swift */, ); path = Controllers; sourceTree = ""; @@ -2259,12 +2274,12 @@ B358D3B3279590C200A67054 /* CheckAnyItemIsInTrashDbRequest.swift */, B305644423FC051E003304F2 /* CheckItemIsChangedDbRequest.swift */, B3E34888283CFA92002A8A06 /* CleanupUnusedTags.swift */, - B32861E328BF89CE007B5A5C /* CreateAnnotationsDbRequest.swift */, B305645B23FC051E003304F2 /* CreateAttachmentDbRequest.swift */, B39D33702400150B00EF2ACB /* CreateAttachmentsDbRequest.swift */, B305645523FC051E003304F2 /* CreateCollectionDbRequest.swift */, B305644323FC051E003304F2 /* CreateItemFromDetailDbRequest.swift */, B305645D23FC051E003304F2 /* CreateNoteDbRequest.swift */, + B32861E328BF89CE007B5A5C /* CreatePDFAnnotationsDbRequest.swift */, B30BDDE528366F1B007034E8 /* CreateTranslatedItemsDbRequest.swift */, B3100919272C00BA003FC743 /* CreateWebDavDeletionsDbRequest.swift */, B37ACB922730338A002EF4E1 /* DeleteAllWebDavDeletionsDbRequest.swift */, @@ -2538,7 +2553,8 @@ B305655523FC051E003304F2 /* Sync */, B324277025C82D6900567504 /* WebSocket */, B3242CCC246ABBAF00D8748F /* AnnotationsConfig.swift */, - B361A3032511F9FB00271173 /* AnnotationType.swift */, + B3F6AA392AB30663005BC22E /* AnnotationTool.swift */, + B311737A2AFCF004007D9741 /* AnnotationType.swift */, B305656123FC051E003304F2 /* AppGroup.swift */, B305653A23FC051E003304F2 /* Attachment.swift */, B339459026DE6C2900E59A02 /* AttachmentFileDeletedNotification.swift */, @@ -2816,21 +2832,18 @@ children = ( B3482E57257E3ED1008E817A /* Annotation View */, B3B953D32459B62D00FC96DB /* AnnotationCell.swift */, - B31941DB24531F6600BF6296 /* AnnotationsViewController.swift */, + B3CC280A2AA8A8EC00886856 /* AnnotationToolbarHandler.swift */, B340ECA5290FDC9F00EE920D /* AnnotationToolbarViewController.swift */, B38B24B826E7664400BDD1BB /* AnnotationToolOptionsViewController.swift */, B33AB487246D315F00490DDE /* CheckboxButton.swift */, B3A95DA929194BDE00BCCF11 /* DashedView.swift */, + B31941DB24531F6600BF6296 /* PDFAnnotationsViewController.swift */, B3B953CD245981DA00FC96DB /* PDFDocumentViewController.swift */, B32259A127070EBA0047598D /* PDFPlainReaderViewController.swift */, B340ECA7290FDE3B00EE920D /* PDFReaderViewController.swift */, B3593AD524B601AB00CA0B57 /* PDFSearchCell.swift */, B3593AD624B601AB00CA0B57 /* PDFSearchCell.xib */, B3593AD324B6007900CA0B57 /* PDFSearchViewController.swift */, - B3F5B07C27D0FFF500AA81C0 /* PDFSettingsSegmentedCell.swift */, - B3F5B07E27D1000C00AA81C0 /* PDFSettingsSegmentedCellContentView.swift */, - B3F5B08027D1001B00AA81C0 /* PDFSettingsSegmentedCellContentView.xib */, - B3ADBCB727CFB64B00B7041F /* PDFSettingsViewController.swift */, B373877228FEA0C7004E5031 /* PDFSidebarViewController.swift */, B31028062B1E070F00E41554 /* PDFThumbnailsCell.swift */, B31028042B1E012A00E41554 /* PDFThumbnailsViewController.swift */, @@ -2957,6 +2970,7 @@ children = ( B36C5085257521DE00A370D3 /* AnnotationEditActionHandler.swift */, B34DF1A725768AAC0019CCD1 /* AnnotationPageLabelActionHandler.swift */, + B39505E32AE9466800F73079 /* AnnotationPopoverActionHandler.swift */, ); path = ViewModels; sourceTree = ""; @@ -2968,8 +2982,8 @@ B34C40F425710B430057D5F5 /* AnnotationEditViewController.xib */, B34DF19F257684590019CCD1 /* AnnotationPageLabelViewController.swift */, B34DF1A0257684590019CCD1 /* AnnotationPageLabelViewController.xib */, - B3401D552567D8F700BB8D6E /* AnnotationViewController.swift */, - B3E02C0C26848CA900FFB419 /* AnnotationViewController.xib */, + B3401D552567D8F700BB8D6E /* AnnotationPopoverViewController.swift */, + B32CDD3C2AEA9BB100EF5054 /* AnnotationPopoverViewController.xib */, B34C40FD25711C990057D5F5 /* ColorPickerCell.swift */, B3764D9C2574F2DE0024274F /* ColorPickerCircleView.swift */, B398142C257A649D002C755C /* HighlightEditCell.swift */, @@ -2992,7 +3006,9 @@ B36C508D2575221C00A370D3 /* AnnotationEditState.swift */, B34DF1AF25768ADF0019CCD1 /* AnnotationPageLabelAction.swift */, B34DF1AB25768AD40019CCD1 /* AnnotationPageLabelState.swift */, + B39505E52AE9469300F73079 /* AnnotationPopoverAction.swift */, B34C410B257132730057D5F5 /* AnnotationPopoverLayout.swift */, + B39505E62AE9469300F73079 /* AnnotationPopoverState.swift */, ); path = Models; sourceTree = ""; @@ -3064,8 +3080,8 @@ B3593EC2241A61C700760E20 /* Detail */ = { isa = PBXGroup; children = ( - B3401D4E2567D8CF00BB8D6E /* Annotation Popover */, B3B41F0C2848E4E90017CA4B /* Annotation Filter Popover */, + B3401D4E2567D8CF00BB8D6E /* Annotation Popover */, B3FE4B8F268DDE4900CE123F /* CitationBibliographyExport */, 61391B7B2B3C6E98003B314A /* CopyBibliography */, B3593EC3241A61C700760E20 /* ItemDetail */, @@ -3603,23 +3619,21 @@ B3B953C12459780600FC96DB /* Models */ = { isa = PBXGroup; children = ( - B3B953C22459780600FC96DB /* Annotation.swift */, B3F49FB528B8C3F600A1F3E8 /* AnnotationEditability.swift */, B36E9D4825E51B0E00CD1109 /* AnnotationPosition.swift */, B30BE12A297AADB8000AED6A /* AnnotationToolOptionsAction.swift */, B30BE128297AADAF000AED6A /* AnnotationToolOptionsState.swift */, B34C4107257122BF0057D5F5 /* AnnotationViewLayout.swift */, - B3F49FB328B8C3D900A1F3E8 /* DatabaseAnnotation.swift */, - B3F49FB128B8C3CB00A1F3E8 /* DocumentAnnotation.swift */, B30A8C662582690900EC56FB /* HighlightAnnotation.swift */, B3981B75258399AA00F8D15A /* NoteAnnotation.swift */, + B3DAF84B2AC5AA5C00F9AB6A /* PDFAnnotation.swift */, + B3DAF84C2AC5AA5C00F9AB6A /* PDFDatabaseAnnotation.swift */, + B3DAF84A2AC5AA5C00F9AB6A /* PDFDocumentAnnotation.swift */, B385B70D25C03E7E0073CA6F /* PDFExportState.swift */, B3B953C9245981AD00FC96DB /* PDFReaderAction.swift */, B3D0AC97255C14AB007648F5 /* PDFReaderLayout.swift */, B3B953C7245981A600FC96DB /* PDFReaderState.swift */, B35B473B25F11BCD0023B6F4 /* PDFSettings.swift */, - B3ADBCBB27CFB67A00B7041F /* PDFSettingsAction.swift */, - B37381A627CE6EDE005AD721 /* PDFSettingsState.swift */, B310280C2B1E16F300E41554 /* PDFThumbnailsAction.swift */, B373C98E2B1F5431007FD56C /* PDFThumbnailsLayout.swift */, B310280A2B1E16EC00E41554 /* PDFThumbnailsState.swift */, @@ -3635,7 +3649,6 @@ children = ( B30BE126297AAD9E000AED6A /* AnnotationToolOptionsActionHandler.swift */, B3B953CB245981C100FC96DB /* PDFReaderActionHandler.swift */, - B3ADBCB927CFB66C00B7041F /* PDFSettingsActionHandler.swift */, B31028082B1E16E200E41554 /* PDFThumbnailsActionHandler.swift */, B3A47C3529015FCE00E7D90D /* TableOfContentsActionHandler.swift */, ); @@ -3677,9 +3690,9 @@ B3DCDF03240912060039ED0D /* ActivityIndicatorView.swift */, B347F2B924DC4D5D000D5FF6 /* CapHeightLabel.swift */, B3D58D6A25EE437700D8FA31 /* CircularProgressView.swift */, + B37D8E6824DC2BEE00F526C5 /* CollectionRow.swift */, B3C8DD8527B54EE70084E1AD /* CollectionsPickerViewController.swift */, B3C8DD8627B54EE70084E1AD /* CollectionsPickerViewController.xib */, - B37D8E6824DC2BEE00F526C5 /* CollectionRow.swift */, B3748D2126566F6900ED05F8 /* ContainerViewController.swift */, B338E81F273AA9B20003DECD /* DisappearActionHostingController.swift */, B3DCDF01240912060039ED0D /* DocumentPickerViewController.swift */, @@ -3694,6 +3707,10 @@ B3DCDF02240912060039ED0D /* NoteEditorViewController.swift */, B37C5B6E26454130009A37E5 /* NoteEditorViewController.xib */, B32DE61E26320271000287EC /* PopoverNavigationViewController.swift */, + B3F9A4BB2B04CEC300684030 /* ReaderSettingsSegmentedCell.swift */, + B3F9A4BA2B04CEC300684030 /* ReaderSettingsSegmentedCellContentView.swift */, + B3F9A4C02B04CED600684030 /* ReaderSettingsSegmentedCellContentView.xib */, + B3F9A4BC2B04CEC300684030 /* ReaderSettingsViewController.swift */, B32B4F962475636600F78A05 /* RxTableViewCell.swift */, B361820E24C9B9E800B30D56 /* SelectableTextField.swift */, B3DCDF1A24091EBE0039ED0D /* SinglePickerRow.swift */, @@ -3703,10 +3720,10 @@ B3830CF525545EE400910FE0 /* TagPickerCell.xib */, B3830CE8255451DC00910FE0 /* TagPickerViewController.swift */, B3830CE7255451DC00910FE0 /* TagPickerViewController.xib */, + 61BD13942A5831EF008A0704 /* TextKit1TextView.swift */, B35EDEC9263ADD0C003574AC /* TextPreviewViewController.swift */, B35EDECA263ADD0C003574AC /* TextPreviewViewController.xib */, B3F184A3253F067100498F32 /* WebViewController.swift */, - 61BD13942A5831EF008A0704 /* TextKit1TextView.swift */, ); path = Views; sourceTree = ""; @@ -3717,6 +3734,7 @@ B3DCDF1E240945B30039ED0D /* CollectionsPickerActionHandler.swift */, B361820724C9B53600B30D56 /* KeyboardResponder.swift */, B37C5B6826453C33009A37E5 /* NoteEditorActionHandler.swift */, + B3F9A4C62B04D0E400684030 /* ReaderSettingsActionHandler.swift */, B3DCDF0D240912500039ED0D /* SinglePickerActionHandler.swift */, B3830CE1255451C200910FE0 /* TagPickerActionHandler.swift */, ); @@ -3731,6 +3749,8 @@ B3F83807263960FA00E128A6 /* ItemAccessory.swift */, B37C5B6C26453C58009A37E5 /* NoteEditorAction.swift */, B37C5B6A26453C4F009A37E5 /* NoteEditorState.swift */, + B3F9A4C32B04D0D900684030 /* ReaderSettingsAction.swift */, + B3F9A4C22B04D0D800684030 /* ReaderSettingsState.swift */, B3DCDF09240912220039ED0D /* SinglePickerAction.swift */, B3DCDF11240914F10039ED0D /* SinglePickerModel.swift */, B3DCDF0B240912370039ED0D /* SinglePickerState.swift */, @@ -4310,6 +4330,7 @@ B34DF1A2257684590019CCD1 /* AnnotationPageLabelViewController.xib in Resources */, B3593F32241A61C700760E20 /* ItemDetailAbstractContentView.xib in Resources */, B3830CF725545EE400910FE0 /* TagPickerCell.xib in Resources */, + B3F9A4C12B04CED600684030 /* ReaderSettingsSegmentedCellContentView.xib in Resources */, B3E1BECA254B0B9500D9BB27 /* ItemDetailFieldEditContentView.xib in Resources */, B30B550524B85A6200F94B59 /* Images.xcassets in Resources */, B39D42D429BF85930035CDA9 /* TagFilterContentView.xib in Resources */, @@ -4327,16 +4348,15 @@ B31CC57D286468C80055C114 /* ManualLookupViewController.xib in Resources */, B35DC8E6261D9E7C00ED30F4 /* ItemDetailFieldMultilineEditContentView.xib in Resources */, B30B405F2490CAFC00FAAF6D /* ItemCell.xib in Resources */, - B3E02C0D26848CA900FFB419 /* AnnotationViewController.xib in Resources */, B31A5E52286308960026589F /* LookupIdentifierCell.xib in Resources */, B32A495B2A44730D00080945 /* licenses in Resources */, B34C40F625710B430057D5F5 /* AnnotationEditViewController.xib in Resources */, - B3F5B08127D1001B00AA81C0 /* PDFSettingsSegmentedCellContentView.xib in Resources */, B39D42DA29C0B82F0035CDA9 /* TagFilterCell.xib in Resources */, B3ADAE4B2833B56F00D46271 /* LookupViewController.xib in Resources */, B3C8DD8827B54EE70084E1AD /* CollectionsPickerViewController.xib in Resources */, B3593F2C241A61C700760E20 /* ItemDetailTagContentView.xib in Resources */, B3B78C44266FAF0800E91D02 /* citation in Resources */, + B32CDD3D2AEA9BB100EF5054 /* AnnotationPopoverViewController.xib in Resources */, B30D59652206F60500884C4A /* LaunchScreen.storyboard in Resources */, B337A5A8244F1DEB00AFD13D /* Localizable.strings in Resources */, B30BA8C4255D415600361D25 /* Bundled in Resources */, @@ -4566,6 +4586,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B311737B2AFCF004007D9741 /* AnnotationType.swift in Sources */, B3A95DAA29194BDE00BCCF11 /* DashedView.swift in Sources */, B3593F1C2668E86100FA4BB2 /* SyncStylesDbRequest.swift in Sources */, B3E9180C25ECE30A002B77AF /* CoreGraphics+Extensions.swift in Sources */, @@ -4577,6 +4598,7 @@ B331F9B5265400D70099F6A6 /* ReadStylesDbRequest.swift in Sources */, B39D33712400150B00EF2ACB /* CreateAttachmentsDbRequest.swift in Sources */, B3A081E125D2D8050088EC24 /* CollectionEditHostingViewController.swift in Sources */, + B3CC280B2AA8A8EC00886856 /* AnnotationToolbarHandler.swift in Sources */, B336FE812A2F6D9E00B52F4A /* UploadFixSyncAction.swift in Sources */, B3486B8B26CFAFEA0036A267 /* SingleCitationAction.swift in Sources */, B3593F3C241A61C700760E20 /* ItemsSortType.swift in Sources */, @@ -4598,7 +4620,6 @@ B30566BF23FC051F003304F2 /* AttachmentUpload.swift in Sources */, B3E4463F248FBD2E007FE8AB /* RRelation.swift in Sources */, B3B1C5842664E23A00883597 /* ReadItemsForUploadDbRequest.swift in Sources */, - B3F49FB428B8C3D900A1F3E8 /* DatabaseAnnotation.swift in Sources */, B39649182869B0D0000BCB6C /* ISBNParser.swift in Sources */, B305662123FC051F003304F2 /* SubmitDeletionSyncAction.swift in Sources */, B305661B23FC051E003304F2 /* SyncVersionsSyncAction.swift in Sources */, @@ -4609,6 +4630,7 @@ B3100927272C2441003FC743 /* DeleteWebDavFilesSyncAction.swift in Sources */, B305661D23FC051E003304F2 /* SyncGroupVersionsSyncAction.swift in Sources */, B30566B223FC051F003304F2 /* UpdatesResponse.swift in Sources */, + B39505E42AE9466800F73079 /* AnnotationPopoverActionHandler.swift in Sources */, B3593F39241A61C700760E20 /* ItemsActionHandler.swift in Sources */, B30565C923FC051E003304F2 /* UpdateItemLocaleDbRequest.swift in Sources */, B3BD2BB325C98D2900275EF9 /* BackgroundTimer.swift in Sources */, @@ -4633,6 +4655,7 @@ B305661523FC051E003304F2 /* UploadAttachmentSyncAction.swift in Sources */, B349182B25E00771006C3313 /* ReadVersionDbRequest.swift in Sources */, B33979D12493ACA800A923C3 /* ImagePreviewViewController.swift in Sources */, + B3F9A4C52B04D0D900684030 /* ReaderSettingsAction.swift in Sources */, B3A351E527157EC6002E597A /* WebDavPropParserDelegate.swift in Sources */, B3A2AECC26552248004BF3A4 /* SettingsCoordinator.swift in Sources */, B32B8A572B18A08900A9A741 /* PDFThumbnailController.swift in Sources */, @@ -4651,7 +4674,7 @@ B3DCDF0E240912500039ED0D /* SinglePickerActionHandler.swift in Sources */, B3593F162668E29C00FA4BB2 /* Style.swift in Sources */, B3593F37241A61C700760E20 /* ItemDetailViewController.swift in Sources */, - B31941DD24531F6600BF6296 /* AnnotationsViewController.swift in Sources */, + B31941DD24531F6600BF6296 /* PDFAnnotationsViewController.swift in Sources */, B3BC6B20250FA437006A8B9C /* SchemaError.swift in Sources */, B3863FD02AD83698005082F0 /* EditTypeItemDetailDbRequest.swift in Sources */, B31901942629C9CB00209E33 /* ItemsFilterViewController.swift in Sources */, @@ -4699,7 +4722,6 @@ B3DF9AD52747AB4F007933CB /* ApiEndpoint.swift in Sources */, B322B4772673A41200BC3D08 /* ReadStyleDbRequest.swift in Sources */, B3E8FE8827143BDD00F51458 /* SyncSettingsAction.swift in Sources */, - B3F5B07D27D0FFF500AA81C0 /* PDFSettingsSegmentedCell.swift in Sources */, B37BF0AB25A5E2AD00AE0268 /* SquareAnnotation.swift in Sources */, B361820024C9A7C000B30D56 /* LoginAction.swift in Sources */, B37080522AA72135006F56B9 /* Localizable.swift in Sources */, @@ -4728,6 +4750,7 @@ B3593F182668E6A700FA4BB2 /* StyleParserDelegate.swift in Sources */, B3981427257A5A16002C755C /* AnnotationView.swift in Sources */, B31028072B1E070F00E41554 /* PDFThumbnailsCell.swift in Sources */, + B3DAF84F2AC5AA5C00F9AB6A /* PDFDatabaseAnnotation.swift in Sources */, B36181FD24C9A7C000B30D56 /* LoginError.swift in Sources */, B3F55A1B29EED4EA00A6716E /* ReadColoredTagsDbRequest.swift in Sources */, B3A47C3629015FCE00E7D90D /* TableOfContentsActionHandler.swift in Sources */, @@ -4739,7 +4762,6 @@ B3DCDF1D24091EC40039ED0D /* SinglePickerView.swift in Sources */, B32EBFBD276A89190003897E /* BackgroundUploadProcessor.swift in Sources */, B3E2FF2029CAE1EF00F85AEB /* ItemsFilterCoordinator.swift in Sources */, - B3F5B07F27D1000C00AA81C0 /* PDFSettingsSegmentedCellContentView.swift in Sources */, B3593F4A241A61C700760E20 /* LibrariesState.swift in Sources */, B3E8FE072714292E00F51458 /* StorageSettingsEmptyView.swift in Sources */, B3E8FE5027142BAB00F51458 /* DebuggingActionHandler.swift in Sources */, @@ -4813,6 +4835,7 @@ B305660C23FC051E003304F2 /* VersionsRequest.swift in Sources */, B33AB488246D315F00490DDE /* CheckboxButton.swift in Sources */, B378F4CF242D0CC200B88A05 /* RepoRequest.swift in Sources */, + B3F9A4C72B04D0E400684030 /* ReaderSettingsActionHandler.swift in Sources */, B3100918272BF9DC003FC743 /* RWebDavDeletion.swift in Sources */, B30565B923FC051E003304F2 /* DeleteItemsFromCollectionDbRequest.swift in Sources */, B3EC44612718490A001A9150 /* AnnotationPreviewBoundingBoxCalculator.swift in Sources */, @@ -4854,10 +4877,12 @@ B3B8800525FB546300904235 /* DeviceInfoProvider.swift in Sources */, B3B41F192848E5A90017CA4B /* AnnotationsFilterState.swift in Sources */, B36E9D4925E51B0E00CD1109 /* AnnotationPosition.swift in Sources */, + B3F9A4C42B04D0D900684030 /* ReaderSettingsState.swift in Sources */, B3C9D60824DA9D40003EA1EE /* CollectionsSearchState.swift in Sources */, B34DF1B525768BB70019CCD1 /* TextFieldCell.swift in Sources */, B3593F50241A61C700760E20 /* CollectionsState.swift in Sources */, B3401D5B2567DAAE00BB8D6E /* AnnotationPopoverCoordinator.swift in Sources */, + B39505E82AE9469300F73079 /* AnnotationPopoverState.swift in Sources */, B3B953D22459B4D800FC96DB /* UITableViewCell+SwiftUI.swift in Sources */, B353F204242E52610062EE24 /* Database.swift in Sources */, B30565E023FC051E003304F2 /* ReadAnyChangedObjectsInLibraryDbRequest.swift in Sources */, @@ -4865,7 +4890,6 @@ B305660D23FC051E003304F2 /* AttachmentUploadRequest.swift in Sources */, B30565D723FC051E003304F2 /* ReadLibrariesDataDbRequest.swift in Sources */, B3F09AE829CAFF8D0084E4D8 /* TagFilterState.swift in Sources */, - B3ADBCBC27CFB67A00B7041F /* PDFSettingsAction.swift in Sources */, B30565E723FC051E003304F2 /* ObjectUserChangeObserver.swift in Sources */, B305669F23FC051F003304F2 /* RCustomLibrary.swift in Sources */, B37AA28628994B7600A1C643 /* ItemDetailFieldContentView.swift in Sources */, @@ -4885,6 +4909,7 @@ B3E8FE172714297200F51458 /* CiteSearchActionHandler.swift in Sources */, B34A9F6825BF1D27007C9A4A /* AnnotationConverter.swift in Sources */, B3A081C425D2ADCD0088EC24 /* PerformDeletionsSyncAction.swift in Sources */, + B3DAF84D2AC5AA5C00F9AB6A /* PDFDocumentAnnotation.swift in Sources */, B361C2B429DECB220035BF7E /* ResetSettingsVersionDbRequest.swift in Sources */, B30565B623FC051E003304F2 /* StoreItemsDbResponseRequest.swift in Sources */, B395EFEB25494B3200CEBD9F /* CreatorEditActionHandler.swift in Sources */, @@ -4900,6 +4925,7 @@ B3830CDB255451AB00910FE0 /* TagPickerAction.swift in Sources */, B3593F40241A61C700760E20 /* ItemCell.swift in Sources */, B305679023FC1D9B003304F2 /* CollectionDifference+Separated.swift in Sources */, + B3F6AA3A2AB30663005BC22E /* AnnotationTool.swift in Sources */, B34ACC7A2514EAAB00040C17 /* AnnotationColorGenerator.swift in Sources */, B3ADAE4F2833BEDC00D46271 /* LookupState.swift in Sources */, B3FC7431272181E500F55531 /* WebDavWriteRequest.swift in Sources */, @@ -4907,7 +4933,6 @@ B305660A23FC051E003304F2 /* RegisterUploadRequest.swift in Sources */, B31CC57F28646D8E0055C114 /* ManualLookupAction.swift in Sources */, B3E8FE582714323200F51458 /* SavingSettingsAction.swift in Sources */, - B3ADBCB827CFB64B00B7041F /* PDFSettingsViewController.swift in Sources */, B305662B23FC051F003304F2 /* SyncProgressHandler.swift in Sources */, B3F09AE629CAFF860084E4D8 /* TagFilterActionHandler.swift in Sources */, B30566B523FC051F003304F2 /* LoginResponse.swift in Sources */, @@ -4924,6 +4949,8 @@ B305660F23FC051E003304F2 /* CrashUploadRequest.swift in Sources */, B39E0132283276830091CE4A /* WebViewHandler.swift in Sources */, B37AA28A28995A5800A1C643 /* ItemDetailAbstractContentView.swift in Sources */, + B3F9A4BF2B04CEC300684030 /* ReaderSettingsViewController.swift in Sources */, + B3DAF84E2AC5AA5C00F9AB6A /* PDFAnnotation.swift in Sources */, B3F4E4EE2A4DC39800820718 /* JSONSerialization+Utils.swift in Sources */, B3CCB8DA29B73DED0097520B /* UnlockPDFViewController.swift in Sources */, B331F9B12653F59C0099F6A6 /* RStyle.swift in Sources */, @@ -4949,6 +4976,7 @@ B31DFE7D2746917A005CD69B /* FileRequest.swift in Sources */, B325DBB224375DAC00EFF0F5 /* ConflictResolution.swift in Sources */, B3422F4A289BBA8C00C53DD2 /* ItemDetailAbstractEditContentView.swift in Sources */, + B3F9A4BE2B04CEC300684030 /* ReaderSettingsSegmentedCell.swift in Sources */, B30565D023FC051E003304F2 /* CreateCollectionDbRequest.swift in Sources */, B30565C123FC051E003304F2 /* MarkObjectsAsDeletedDbRequest.swift in Sources */, B3830CDC255451AB00910FE0 /* TagPickerState.swift in Sources */, @@ -5010,7 +5038,6 @@ B351BD0E25EF7E78000451E2 /* ItemAction.swift in Sources */, B30565E823FC051E003304F2 /* CreatorSummaryFormatter.swift in Sources */, B3F3D640255ECA4700F310C2 /* AnnotationViewText.swift in Sources */, - B3F49FB228B8C3CB00A1F3E8 /* DocumentAnnotation.swift in Sources */, B30566AB23FC051F003304F2 /* ItemTypes.swift in Sources */, B3DCDF18240917500039ED0D /* ItemTypePickerViewModelCreator.swift in Sources */, B3A2AECE26553DD8004BF3A4 /* NavigationViewController.swift in Sources */, @@ -5026,10 +5053,8 @@ B30566AD23FC051F003304F2 /* ItemResponse.swift in Sources */, B3D0994726DCCEF700B04FAA /* RPathCoordinate.swift in Sources */, B31FACD225DBC7E900DD5F14 /* ActiveObjectDeletedConflictReceiverHandler.swift in Sources */, - B361A3042511F9FB00271173 /* AnnotationType.swift in Sources */, B36181FF24C9A7C000B30D56 /* LoginState.swift in Sources */, B3E8FE3627142A0900F51458 /* RemoteStyle.swift in Sources */, - B3B953C32459780600FC96DB /* Annotation.swift in Sources */, B3F55A1729EED04700A6716E /* ReadFilteredTagsDbRequest.swift in Sources */, B34A9F5E25BF12F7007C9A4A /* ReadFilenameDbRequest.swift in Sources */, B3B3FF7B266A1FAB0061E3B2 /* StoreStyleDbRequest.swift in Sources */, @@ -5072,7 +5097,7 @@ B32861E628BFACD4007B5A5C /* EditItemFieldsDbRequest.swift in Sources */, B36C07DE26FB264800C855A9 /* UITableView+Extensions.swift in Sources */, B3972689247D403200A8B469 /* UrlDetector.swift in Sources */, - B32861E428BF89CE007B5A5C /* CreateAnnotationsDbRequest.swift in Sources */, + B32861E428BF89CE007B5A5C /* CreatePDFAnnotationsDbRequest.swift in Sources */, B305660923FC051E003304F2 /* SettingsRequest.swift in Sources */, B305662423FC051F003304F2 /* LoadLibraryDataSyncAction.swift in Sources */, B324276D25C81F2000567504 /* WebSocketController.swift in Sources */, @@ -5093,8 +5118,8 @@ B30565BF23FC051E003304F2 /* CheckItemIsChangedDbRequest.swift in Sources */, B3E8FE6E2714344D00F51458 /* SettingsListButtonRow.swift in Sources */, B3DCDF21240945D80039ED0D /* CollectionsPickerState.swift in Sources */, + B3F9A4CB2B04F28400684030 /* WebViewEncoder.swift in Sources */, B3D58D5625ED856F00D8FA31 /* DebugLogUploadRequest.swift in Sources */, - B37381A727CE6EDE005AD721 /* PDFSettingsState.swift in Sources */, B3E1BEC9254B0B9500D9BB27 /* ItemDetailFieldEditCell.swift in Sources */, B361820F24C9B9E800B30D56 /* SelectableTextField.swift in Sources */, B39EE32B223A5E3500302E29 /* TestAppDelegate.swift in Sources */, @@ -5120,6 +5145,7 @@ B3DCDF23240945FA0039ED0D /* CollectionsPickerAction.swift in Sources */, B37673FB262DB25700C3BFAF /* ItemsToolbarController.swift in Sources */, B30565DA23FC051E003304F2 /* UpdateVersionsDbRequest.swift in Sources */, + B3F9A4BD2B04CEC300684030 /* ReaderSettingsSegmentedCellContentView.swift in Sources */, B34DF1A1257684590019CCD1 /* AnnotationPageLabelViewController.swift in Sources */, B3593F7E241A76F400760E20 /* CollectionEditingCoordinator.swift in Sources */, B3C6AB2D248E3F840009AC96 /* AsynchronousOperation.swift in Sources */, @@ -5178,7 +5204,7 @@ B3DCDF102409127A0039ED0D /* CreatorTypePickerViewModelCreator.swift in Sources */, B3E8FE33271429C300F51458 /* ExportSettingsView.swift in Sources */, B340ECA6290FDC9F00EE920D /* AnnotationToolbarViewController.swift in Sources */, - B3401D572567D8F700BB8D6E /* AnnotationViewController.swift in Sources */, + B3401D572567D8F700BB8D6E /* AnnotationPopoverViewController.swift in Sources */, B3830CE2255451C200910FE0 /* TagPickerActionHandler.swift in Sources */, 618404262A4456A9005AAF22 /* IdentifierLookupController.swift in Sources */, B305662223FC051F003304F2 /* StoreVersionSyncAction.swift in Sources */, @@ -5191,10 +5217,10 @@ B3300A4B27C64A0700427FA8 /* AttachmentDownloadOperation.swift in Sources */, B3E8FE6C2714344D00F51458 /* SettingsAction.swift in Sources */, B3401D612568047D00BB8D6E /* AnnotationViewTextView.swift in Sources */, + B39505E72AE9469300F73079 /* AnnotationPopoverAction.swift in Sources */, B30B40612490D84300FAAF6D /* ItemCellModel.swift in Sources */, B31DDA9E27299BD1002CFA05 /* StoreMtimeForAttachmentDbRequest.swift in Sources */, B3C9D60A24DA9D49003EA1EE /* CollectionsSearchAction.swift in Sources */, - B3ADBCBA27CFB66C00B7041F /* PDFSettingsActionHandler.swift in Sources */, B31D973C27F5E51100ED3DA2 /* ItemsToolbarDownloadProgressView.swift in Sources */, B3C9D60524DA921B003EA1EE /* CollectionsSearchViewController.swift in Sources */, B3100921272C0960003FC743 /* ReadWebDavDeletionsDbRequest.swift in Sources */, @@ -5427,6 +5453,7 @@ B30566EC23FC082D003304F2 /* CrashUploadRequest.swift in Sources */, B361A3022511F9BE00271173 /* LinkMode.swift in Sources */, B3FC74342721858200F55531 /* WebDavSessionStorage.swift in Sources */, + B311737C2AFCF04D007D9741 /* AnnotationType.swift in Sources */, B36459E2264411DB00A0C2C0 /* TagPickerAction.swift in Sources */, B3BC6B1E250F79C4006A8B9C /* FieldKeys.swift in Sources */, B30566F123FC0845003304F2 /* CheckItemIsChangedDbRequest.swift in Sources */, @@ -5466,6 +5493,7 @@ B305670323FC08A4003304F2 /* PerformDeletionsDbRequest.swift in Sources */, B30566E823FC0823003304F2 /* SubmitDeletionsRequest.swift in Sources */, B34341BA260A49E800093E63 /* ReadLibraryDbRequest.swift in Sources */, + B31173792AFCEF25007D9741 /* AnnotationTool.swift in Sources */, B372CEEA248675A200B423AE /* MarkGroupForResyncSyncAction.swift in Sources */, B372CEE6248673C700B423AE /* FetchAndStoreGroupSyncAction.swift in Sources */, B3D58D6725EE280300D8FA31 /* DebugResponseParserDelegate.swift in Sources */, @@ -5519,7 +5547,6 @@ B305670523FC08A8003304F2 /* ReadAllGroupsDbRequest.swift in Sources */, B305677C23FC0C7B003304F2 /* KeyGenerator.swift in Sources */, B30566F023FC0843003304F2 /* AssignItemsToCollectionsDbRequest.swift in Sources */, - B361A3052511FA0C00271173 /* AnnotationType.swift in Sources */, B305670E23FC08BF003304F2 /* ReadLibrariesDataDbRequest.swift in Sources */, B3A081D725D2C1680088EC24 /* ConflictCoordinator.swift in Sources */, B305671223FC08C7003304F2 /* ReadUpdatedObjectUpdateParametersDbRequest.swift in Sources */, @@ -5607,6 +5634,7 @@ B305672D23FC091A003304F2 /* MarkGroupAsLocalOnlySyncAction.swift in Sources */, B3B8800A25FB590900904235 /* DeviceInfoProvider.swift in Sources */, B30566FC23FC0862003304F2 /* MarkCollectionAndItemsAsDeletedDbRequest.swift in Sources */, + B3F9A4CC2B04F2AC00684030 /* WebViewEncoder.swift in Sources */, B3D59F142A38A6C100F6BB7D /* RevertLibraryFilesSyncAction.swift in Sources */, B377F2A02493725E00022943 /* UrlDetector.swift in Sources */, B305670A23FC08B5003304F2 /* ReadCollectionsDbRequest.swift in Sources */, diff --git a/Zotero/Assets/en.lproj/Localizable.strings b/Zotero/Assets/en.lproj/Localizable.strings index 816ebf16d..f75ad8956 100644 --- a/Zotero/Assets/en.lproj/Localizable.strings +++ b/Zotero/Assets/en.lproj/Localizable.strings @@ -479,6 +479,7 @@ "errors.pdf.cant_update_annotation" = "Can't update annotation."; "errors.pdf.cant_add_annotations" = "Can't add annotations."; "errors.pdf.cant_delete_annotations" = "Can't delete annotations."; +"errors.pdf.incompatible_document" = "This document is not supported."; "errors.pdf.page_index_not_int" = "Incorrect format of page stored for this document."; "accessibility.untitled" = "Untitled"; diff --git a/Zotero/Controllers/AnnotationConverter.swift b/Zotero/Controllers/AnnotationConverter.swift index 1db28710f..405666474 100644 --- a/Zotero/Controllers/AnnotationConverter.swift +++ b/Zotero/Controllers/AnnotationConverter.swift @@ -61,7 +61,7 @@ struct AnnotationConverter { username: String, displayName: String, boundingBoxConverter: AnnotationBoundingBoxConverter? - ) -> DocumentAnnotation? { + ) -> PDFDocumentAnnotation? { guard let document = annotation.document, AnnotationsConfig.supported.contains(annotation.type) else { return nil } let key = annotation.key ?? annotation.uuid @@ -113,7 +113,7 @@ struct AnnotationConverter { return nil } - return DocumentAnnotation( + return PDFDocumentAnnotation( key: key, type: type, page: page, @@ -191,7 +191,7 @@ struct AnnotationConverter { ) -> [PSPDFKit.Annotation] { return items.map({ item in return self.annotation( - from: DatabaseAnnotation(item: item), + from: PDFDatabaseAnnotation(item: item), type: type, interfaceStyle: interfaceStyle, currentUserId: currentUserId, @@ -204,7 +204,7 @@ struct AnnotationConverter { } static func annotation( - from zoteroAnnotation: DatabaseAnnotation, + from zoteroAnnotation: PDFDatabaseAnnotation, type: Kind, interfaceStyle: UIUserInterfaceStyle, currentUserId: Int, @@ -260,7 +260,7 @@ struct AnnotationConverter { /// Creates corresponding `SquareAnnotation`. /// - parameter annotation: Zotero annotation. - private static func areaAnnotation(from annotation: Annotation, type: Kind, color: UIColor, boundingBoxConverter: AnnotationBoundingBoxConverter) -> PSPDFKit.SquareAnnotation { + private static func areaAnnotation(from annotation: PDFAnnotation, type: Kind, color: UIColor, boundingBoxConverter: AnnotationBoundingBoxConverter) -> PSPDFKit.SquareAnnotation { let square: PSPDFKit.SquareAnnotation switch type { case .export: @@ -280,7 +280,7 @@ struct AnnotationConverter { /// Creates corresponding `HighlightAnnotation`. /// - parameter annotation: Zotero annotation. private static func highlightAnnotation( - from annotation: Annotation, + from annotation: PDFAnnotation, type: Kind, color: UIColor, alpha: CGFloat, @@ -305,7 +305,7 @@ struct AnnotationConverter { /// Creates corresponding `NoteAnnotation`. /// - parameter annotation: Zotero annotation. - private static func noteAnnotation(from annotation: Annotation, type: Kind, color: UIColor, boundingBoxConverter: AnnotationBoundingBoxConverter) -> PSPDFKit.NoteAnnotation { + private static func noteAnnotation(from annotation: PDFAnnotation, type: Kind, color: UIColor, boundingBoxConverter: AnnotationBoundingBoxConverter) -> PSPDFKit.NoteAnnotation { let note: PSPDFKit.NoteAnnotation switch type { case .export: @@ -323,7 +323,7 @@ struct AnnotationConverter { return note } - private static func inkAnnotation(from annotation: Annotation, type: Kind, color: UIColor, boundingBoxConverter: AnnotationBoundingBoxConverter) -> PSPDFKit.InkAnnotation { + private static func inkAnnotation(from annotation: PDFAnnotation, type: Kind, color: UIColor, boundingBoxConverter: AnnotationBoundingBoxConverter) -> PSPDFKit.InkAnnotation { let lines = annotation.paths(boundingBoxConverter: boundingBoxConverter).map({ group in return group.map({ DrawingPoint(cgPoint: $0) }) }) diff --git a/Zotero/Controllers/Citation/CitationController.swift b/Zotero/Controllers/Citation/CitationController.swift index 0f9ec60e5..6916fb43d 100644 --- a/Zotero/Controllers/Citation/CitationController.swift +++ b/Zotero/Controllers/Citation/CitationController.swift @@ -174,7 +174,7 @@ class CitationController: NSObject { } itemsData.append(data) } - return WKWebView.encodeAsJSONForJavascript(itemsData) + return WebViewEncoder.encodeAsJSONForJavascript(itemsData) } /// Bibliography happens once for selected item(s). Appropriate style and locale xmls are loaded, webView is initialized and loaded with index.html. When everything is loaded, @@ -266,7 +266,7 @@ class CitationController: NSObject { let localeData = try Data(contentsOf: localeUrl) let styleData = try self.fileStorage.read(Files.style(filename: styleFilename)) - subscriber(.success((WKWebView.encodeForJavascript(styleData), WKWebView.encodeForJavascript(localeData)))) + subscriber(.success((WebViewEncoder.encodeForJavascript(styleData), WebViewEncoder.encodeForJavascript(localeData)))) } catch let error { DDLogError("CitationController: can't read locale or style - \(error)") subscriber(.failure(Error.styleOrLocaleMissing)) @@ -302,7 +302,7 @@ class CitationController: NSObject { return Disposables.create() } - subscriber(.success(WKWebView.encodeForJavascript(schemaData))) + subscriber(.success(WebViewEncoder.encodeForJavascript(schemaData))) return Disposables.create() } @@ -323,7 +323,7 @@ class CitationController: NSObject { items.first?.realm?.invalidate() - subscriber(.success(WKWebView.encodeAsJSONForJavascript(data))) + subscriber(.success(WebViewEncoder.encodeAsJSONForJavascript(data))) } catch let error { DDLogError("CitationController: can't read items - \(error)") subscriber(.failure(error)) @@ -498,7 +498,7 @@ extension CitationController: WKScriptMessageHandler { case .csl: if let csl = jsResult as? [[String: Any]] { - result = .success(WKWebView.encodeAsJSONForJavascript(csl)) + result = .success(WebViewEncoder.encodeAsJSONForJavascript(csl)) } else { DDLogError("CitationController: CSL got unknown response - \(jsResult)") result = .failure(Error.missingResponse) diff --git a/Zotero/Controllers/Database/Requests/CreateAnnotationsDbRequest.swift b/Zotero/Controllers/Database/Requests/CreatePDFAnnotationsDbRequest.swift similarity index 90% rename from Zotero/Controllers/Database/Requests/CreateAnnotationsDbRequest.swift rename to Zotero/Controllers/Database/Requests/CreatePDFAnnotationsDbRequest.swift index 7d44c072d..e78f22fc8 100644 --- a/Zotero/Controllers/Database/Requests/CreateAnnotationsDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/CreatePDFAnnotationsDbRequest.swift @@ -1,5 +1,5 @@ // -// CreateAnnotationsDbRequest.swift +// CreatePDFAnnotationsDbRequest.swift // Zotero // // Created by Michal Rentka on 31.08.2022. @@ -10,10 +10,10 @@ import UIKit import RealmSwift -struct CreateAnnotationsDbRequest: DbRequest { +struct CreatePDFAnnotationsDbRequest: DbRequest { let attachmentKey: String let libraryId: LibraryIdentifier - let annotations: [DocumentAnnotation] + let annotations: [PDFDocumentAnnotation] let userId: Int unowned let schemaController: SchemaController @@ -29,7 +29,7 @@ struct CreateAnnotationsDbRequest: DbRequest { } } - private func create(annotation: DocumentAnnotation, parent: RItem, in database: Realm) { + 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 { @@ -70,8 +70,8 @@ struct CreateAnnotationsDbRequest: DbRequest { item.changes.append(RObjectChange.create(changes: changes)) } - private func addFields(for annotation: Annotation, to item: RItem, database: Realm) { - for field in FieldKeys.Item.Annotation.allFields(for: annotation.type) { + private func addFields(for annotation: PDFDocumentAnnotation, to item: RItem, database: Realm) { + for field in FieldKeys.Item.Annotation.allPDFFields(for: annotation.type) { let rField = RItemField() rField.key = field.key rField.baseKey = field.baseKey @@ -110,7 +110,7 @@ struct CreateAnnotationsDbRequest: DbRequest { private func add(rects: [CGRect], to item: RItem, changes: inout RItemChanges, database: Realm) { guard !rects.isEmpty else { return } - let page = UInt(DatabaseAnnotation(item: item).page) + let page = UInt(PDFDatabaseAnnotation(item: item).page) for rect in rects { let dbRect = self.boundingBoxConverter.convertToDb(rect: rect, page: page) ?? rect @@ -128,7 +128,7 @@ struct CreateAnnotationsDbRequest: DbRequest { private func add(paths: [[CGPoint]], to item: RItem, changes: inout RItemChanges, database: Realm) { guard !paths.isEmpty else { return } - let page = UInt(DatabaseAnnotation(item: item).page) + let page = UInt(PDFDatabaseAnnotation(item: item).page) for (idx, path) in paths.enumerated() { let rPath = RPath() diff --git a/Zotero/Controllers/Database/Requests/EditAnnotationPathsDbRequest.swift b/Zotero/Controllers/Database/Requests/EditAnnotationPathsDbRequest.swift index 7997f690c..e1993b17d 100644 --- a/Zotero/Controllers/Database/Requests/EditAnnotationPathsDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/EditAnnotationPathsDbRequest.swift @@ -20,7 +20,7 @@ struct EditAnnotationPathsDbRequest: DbRequest { 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(DatabaseAnnotation(item: item).page) + 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 }) } diff --git a/Zotero/Controllers/Database/Requests/EditAnnotationRectsDbRequest.swift b/Zotero/Controllers/Database/Requests/EditAnnotationRectsDbRequest.swift index 5db9d12c4..c5883f988 100644 --- a/Zotero/Controllers/Database/Requests/EditAnnotationRectsDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/EditAnnotationRectsDbRequest.swift @@ -20,7 +20,7 @@ struct EditAnnotationRectsDbRequest: DbRequest { 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(DatabaseAnnotation(item: item).page) + 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) diff --git a/Zotero/Controllers/Database/Requests/EditItemFieldsDbRequest.swift b/Zotero/Controllers/Database/Requests/EditItemFieldsDbRequest.swift index 8d0ef0243..2c1dfaece 100644 --- a/Zotero/Controllers/Database/Requests/EditItemFieldsDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/EditItemFieldsDbRequest.swift @@ -59,6 +59,7 @@ struct EditItemFieldsDbRequest: DbRequest { if didChange { item.changes.append(RObjectChange.create(changes: RItemChanges.fields)) item.changeType = .user + item.dateModified = Date() } } } diff --git a/Zotero/Controllers/Database/Requests/EditTagsForItemDbRequest.swift b/Zotero/Controllers/Database/Requests/EditTagsForItemDbRequest.swift index 338cb6b67..ef9c16fa4 100644 --- a/Zotero/Controllers/Database/Requests/EditTagsForItemDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/EditTagsForItemDbRequest.swift @@ -62,6 +62,7 @@ struct EditTagsForItemDbRequest: DbRequest { item.rawType = item.rawType item.changeType = .user item.changes.append(RObjectChange.create(changes: RItemChanges.tags)) + item.dateModified = Date() } } } diff --git a/Zotero/Controllers/Database/Requests/SplitAnnotationsDbRequest.swift b/Zotero/Controllers/Database/Requests/SplitAnnotationsDbRequest.swift index 3e4bfc03a..4467c7ba5 100644 --- a/Zotero/Controllers/Database/Requests/SplitAnnotationsDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/SplitAnnotationsDbRequest.swift @@ -31,11 +31,9 @@ struct SplitAnnotationsDbRequest: DbRequest { } } - /// Splits annotation if it exceedes position limit. If it is within limit, it returs original annotation. - /// - parameter annotation: Annotation to split - /// - parameter activeColor: Currently active color - /// - parameter viewModel: View model - /// - returns: Array with original annotation if limit was not exceeded. Otherwise array of new split annotations. + /// Splits database annotation if it exceedes position limit. + /// - parameter item: Database annotation to split + /// - parameter database: Database private func split(item: RItem, database: Realm) { guard let annotationType = item.fields.filter(.key(FieldKeys.Item.Annotation.type)).first.flatMap({ AnnotationType(rawValue: $0.value) }) else { return } diff --git a/Zotero/Controllers/Formatter.swift b/Zotero/Controllers/Formatter.swift index c6caead02..d022d0dc2 100644 --- a/Zotero/Controllers/Formatter.swift +++ b/Zotero/Controllers/Formatter.swift @@ -15,6 +15,12 @@ extension Formatter { return formatter }() + static let iso8601WithFractionalSeconds: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter + }() + static let dateAndTime: DateFormatter = { let formatter = DateFormatter() formatter.locale = Locale.autoupdatingCurrent diff --git a/Zotero/Controllers/Web View Handling/LookupWebViewHandler.swift b/Zotero/Controllers/Web View Handling/LookupWebViewHandler.swift index a32f5bee5..85f8e1643 100644 --- a/Zotero/Controllers/Web View Handling/LookupWebViewHandler.swift +++ b/Zotero/Controllers/Web View Handling/LookupWebViewHandler.swift @@ -123,7 +123,7 @@ final class LookupWebViewHandler { private func _lookUp(identifier: String) { DDLogInfo("LookupWebViewHandler: call translate js") - let encodedIdentifiers = WKWebView.encodeForJavascript(identifier.data(using: .utf8)) + let encodedIdentifiers = WebViewEncoder.encodeForJavascript(identifier.data(using: .utf8)) return self.webViewHandler.call(javascript: "lookup(\(encodedIdentifiers));") .subscribe(on: MainScheduler.instance) .observe(on: MainScheduler.instance) @@ -151,7 +151,7 @@ final class LookupWebViewHandler { } .flatMap { translators -> Single in DDLogInfo("LookupWebViewHandler: encode translators") - let encodedTranslators = WKWebView.encodeAsJSONForJavascript(translators) + let encodedTranslators = WebViewEncoder.encodeAsJSONForJavascript(translators) return self.webViewHandler.call(javascript: "initTranslators(\(encodedTranslators));") } } @@ -179,8 +179,8 @@ final class LookupWebViewHandler { return Disposables.create() } - let encodedSchema = WKWebView.encodeForJavascript(schemaData) - let encodedFormats = WKWebView.encodeForJavascript(dateFormatData) + let encodedSchema = WebViewEncoder.encodeForJavascript(schemaData) + let encodedFormats = WebViewEncoder.encodeForJavascript(dateFormatData) DDLogInfo("WebViewHandler: loaded bundled files") diff --git a/Zotero/Controllers/Web View Handling/WebViewHandler.swift b/Zotero/Controllers/Web View Handling/WebViewHandler.swift index e2fa21088..1d4b83b4e 100644 --- a/Zotero/Controllers/Web View Handling/WebViewHandler.swift +++ b/Zotero/Controllers/Web View Handling/WebViewHandler.swift @@ -30,7 +30,7 @@ final class WebViewHandler: NSObject { // MARK: - Lifecycle - init(webView: WKWebView, javascriptHandlers: [String]?) { + init(webView: WKWebView, javascriptHandlers: [String]?, userAgent: String? = nil) { let storage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: AppGroup.identifier) storage.cookieAcceptPolicy = .always @@ -45,6 +45,7 @@ final class WebViewHandler: NSObject { super.init() webView.navigationDelegate = self + webView.customUserAgent = "Zotero_iOS/\(DeviceInfoProvider.versionString ?? "")-\(DeviceInfoProvider.buildString ?? "")" if let handlers = javascriptHandlers { handlers.forEach { handler in @@ -92,7 +93,7 @@ final class WebViewHandler: NSObject { } func sendMessaging(response payload: [String: Any], for messageId: Int) { - let script = "Zotero.Messaging.receiveResponse('\(messageId)', \(WKWebView.encodeAsJSONForJavascript(payload)));" + let script = "Zotero.Messaging.receiveResponse('\(messageId)', \(WebViewEncoder.encodeAsJSONForJavascript(payload)));" inMainThread { [weak self] in self?.webView?.evaluateJavaScript(script, completionHandler: nil) } diff --git a/Zotero/Controllers/WebViewEncoder.swift b/Zotero/Controllers/WebViewEncoder.swift new file mode 100644 index 000000000..1f9db1aef --- /dev/null +++ b/Zotero/Controllers/WebViewEncoder.swift @@ -0,0 +1,26 @@ +// +// WebViewEncoder.swift +// Zotero +// +// Created by Michal Rentka on 15.11.2023. +// Copyright © 2023 Corporation for Digital Scholarship. All rights reserved. +// + +import Foundation + +struct WebViewEncoder { + static func optionalToJs(_ value: String?) -> String { + return value.flatMap({ "'" + $0 + "'" }) ?? "null" + } + + /// Encodes data which need to be sent to `webView`. All data that is passed to JS is Base64 encoded so that it can be sent as a simple `String`. + static func encodeForJavascript(_ data: Data?) -> String { + return data.flatMap({ "'" + $0.base64EncodedString(options: .endLineWithLineFeed) + "'" }) ?? "null" + } + + /// Encodes as JSON payload so that it can be sent to `webView`. + static func encodeAsJSONForJavascript(_ payload: Any) -> String { + let data = try? JSONSerialization.data(withJSONObject: payload, options: .prettyPrinted) + return self.encodeForJavascript(data) + } +} diff --git a/Zotero/Extensions/Localizable.swift b/Zotero/Extensions/Localizable.swift index f981e5def..1091d6b47 100644 --- a/Zotero/Extensions/Localizable.swift +++ b/Zotero/Extensions/Localizable.swift @@ -512,6 +512,8 @@ internal enum L10n { internal static let cantDeleteAnnotations = L10n.tr("Localizable", "errors.pdf.cant_delete_annotations", fallback: "Can't delete annotations.") /// Can't update annotation. internal static let cantUpdateAnnotation = L10n.tr("Localizable", "errors.pdf.cant_update_annotation", fallback: "Can't update annotation.") + /// This document is not supported. + internal static let incompatibleDocument = L10n.tr("Localizable", "errors.pdf.incompatible_document", fallback: "This document is not supported.") /// The combined annotation would be too large. internal static let mergeTooBig = L10n.tr("Localizable", "errors.pdf.merge_too_big", fallback: "The combined annotation would be too large.") /// Unable to merge annotations diff --git a/Zotero/Extensions/WKWebView+Extensions.swift b/Zotero/Extensions/WKWebView+Extensions.swift index 205af350d..a70b1bfcb 100644 --- a/Zotero/Extensions/WKWebView+Extensions.swift +++ b/Zotero/Extensions/WKWebView+Extensions.swift @@ -47,19 +47,4 @@ extension WKWebView { return Disposables.create() } } - - static func optionalToJs(_ value: String?) -> String { - return value.flatMap({ "'" + $0 + "'" }) ?? "null" - } - - /// Encodes data which need to be sent to `webView`. All data that is passed to JS is Base64 encoded so that it can be sent as a simple `String`. - static func encodeForJavascript(_ data: Data?) -> String { - return data.flatMap({ "'" + $0.base64EncodedString(options: .endLineWithLineFeed) + "'" }) ?? "null" - } - - /// Encodes as JSON payload so that it can be sent to `webView`. - static func encodeAsJSONForJavascript(_ payload: Any) -> String { - let data = try? JSONSerialization.data(withJSONObject: payload, options: .prettyPrinted) - return self.encodeForJavascript(data) - } } diff --git a/Zotero/Models/AnnotationTool.swift b/Zotero/Models/AnnotationTool.swift new file mode 100644 index 000000000..1bbf5f4c7 --- /dev/null +++ b/Zotero/Models/AnnotationTool.swift @@ -0,0 +1,17 @@ +// +// AnnotationTool.swift +// Zotero +// +// Created by Michal Rentka on 14.09.2023. +// Copyright © 2023 Corporation for Digital Scholarship. All rights reserved. +// + +import Foundation + +enum AnnotationTool { + case ink + case image + case note + case highlight + case eraser +} diff --git a/Zotero/Models/Defaults.swift b/Zotero/Models/Defaults.swift index e4a462121..24ff7959f 100644 --- a/Zotero/Models/Defaults.swift +++ b/Zotero/Models/Defaults.swift @@ -74,23 +74,19 @@ final class Defaults { @CodableUserDefault(key: "SelectedRawCollectionKey", defaultValue: CollectionIdentifier.custom(.all), encoder: Defaults.jsonEncoder, decoder: Defaults.jsonDecoder) var selectedCollectionId: CollectionIdentifier - // MARK: - Items Settings - #if MAINAPP + // MARK: - Items Settings + @CodableUserDefault(key: "RawItemsSortType", defaultValue: ItemsSortType.default, encoder: Defaults.jsonEncoder, decoder: Defaults.jsonDecoder, defaults: .standard) var itemsSortType: ItemsSortType - #endif // MARK: - Item Detail - #if MAINAPP @CodableUserDefault(key: "LastUsedCreatorNamePresentation", defaultValue: .separate, encoder: Defaults.jsonEncoder, decoder: Defaults.jsonDecoder) var creatorNamePresentation: ItemDetailState.Creator.NamePresentation - #endif // MARK: - PDF Settings - #if MAINAPP @UserDefault(key: "PdfReaderLineWidth", defaultValue: 2) var activeLineWidth: Float diff --git a/Zotero/Models/FieldKeys.swift b/Zotero/Models/FieldKeys.swift index f48692d39..875d966d2 100644 --- a/Zotero/Models/FieldKeys.swift +++ b/Zotero/Models/FieldKeys.swift @@ -97,7 +97,7 @@ struct FieldKeys { } } - static func allFields(for type: AnnotationType) -> [KeyBaseKeyPair] { + static func allPDFFields(for type: AnnotationType) -> [KeyBaseKeyPair] { switch type { case .highlight: return [KeyBaseKeyPair(key: Annotation.type, baseKey: nil), diff --git a/Zotero/Scenes/Detail/Annotation Popover/AnnotationPopoverCoordinator.swift b/Zotero/Scenes/Detail/Annotation Popover/AnnotationPopoverCoordinator.swift index 51455e182..8e2f87243 100644 --- a/Zotero/Scenes/Detail/Annotation Popover/AnnotationPopoverCoordinator.swift +++ b/Zotero/Scenes/Detail/Annotation Popover/AnnotationPopoverCoordinator.swift @@ -10,13 +10,11 @@ import UIKit import RxSwift -protocol AnnotationPopover: AnyObject { - var annotationKey: PDFReaderState.AnnotationKey? { get } -} +protocol AnnotationPopover: AnyObject {} protocol AnnotationPopoverAnnotationCoordinatorDelegate: AnyObject { func createShareAnnotationMenu(sender: UIButton) -> UIMenu? - func showEdit(annotation: Annotation, userId: Int, library: Library, saveAction: @escaping AnnotationEditSaveAction, deleteAction: @escaping AnnotationEditDeleteAction) + func showEdit(state: AnnotationPopoverState, saveAction: @escaping AnnotationEditSaveAction, deleteAction: @escaping AnnotationEditDeleteAction) func showTagPicker(libraryId: LibraryIdentifier, selected: Set, picked: @escaping ([Tag]) -> Void) func didFinish() } @@ -30,14 +28,18 @@ final class AnnotationPopoverCoordinator: NSObject, Coordinator { var childCoordinators: [Coordinator] weak var navigationController: UINavigationController? - private unowned let viewModel: ViewModel + var viewModelObservable: PublishSubject? { + return (navigationController?.viewControllers.first as? AnnotationPopoverViewController)?.viewModel.stateObservable + } + + private let data: AnnotationPopoverState.Data private unowned let controllers: Controllers private let disposeBag: DisposeBag - init(navigationController: NavigationViewController, controllers: Controllers, viewModel: ViewModel) { + init(data: AnnotationPopoverState.Data, navigationController: NavigationViewController, controllers: Controllers) { + self.data = data self.navigationController = navigationController self.controllers = controllers - self.viewModel = viewModel self.childCoordinators = [] self.disposeBag = DisposeBag() @@ -50,7 +52,9 @@ final class AnnotationPopoverCoordinator: NSObject, Coordinator { } func start(animated: Bool) { - let controller = AnnotationViewController(viewModel: self.viewModel, attributedStringConverter: self.controllers.htmlAttributedStringConverter) + let state = AnnotationPopoverState(data: data) + let handler = AnnotationPopoverActionHandler() + let controller = AnnotationPopoverViewController(viewModel: ViewModel(initialState: state, handler: handler)) controller.coordinatorDelegate = self self.navigationController?.isNavigationBarHidden = true self.navigationController?.setViewControllers([controller], animated: animated) @@ -59,18 +63,19 @@ final class AnnotationPopoverCoordinator: NSObject, Coordinator { extension AnnotationPopoverCoordinator: AnnotationPopoverAnnotationCoordinatorDelegate { func createShareAnnotationMenu(sender: UIButton) -> UIMenu? { - guard let pdfCoordinator = parentCoordinator as? PDFCoordinator, - let annotation = viewModel.state.selectedAnnotation - else { return nil } - return pdfCoordinator.createShareAnnotationMenu( - state: viewModel.state, - annotation: annotation, - sender: sender - ) + return (parentCoordinator as? PDFCoordinator)?.createShareAnnotationMenuForSelectedAnnotation(sender: sender) } - func showEdit(annotation: Annotation, userId: Int, library: Library, saveAction: @escaping AnnotationEditSaveAction, deleteAction: @escaping AnnotationEditDeleteAction) { - let state = AnnotationEditState(annotation: annotation, userId: userId, library: library) + func showEdit(state: AnnotationPopoverState, saveAction: @escaping AnnotationEditSaveAction, deleteAction: @escaping AnnotationEditDeleteAction) { + let data = AnnotationEditState.Data( + type: state.type, + isEditable: state.isEditable, + color: state.color, + lineWidth: state.lineWidth, + pageLabel: state.pageLabel, + highlightText: state.highlightText + ) + let state = AnnotationEditState(data: data) let handler = AnnotationEditActionHandler() let viewModel = ViewModel(initialState: state, handler: handler) let controller = AnnotationEditViewController(viewModel: viewModel, includeColorPicker: false, saveAction: saveAction, deleteAction: deleteAction) @@ -107,6 +112,6 @@ extension AnnotationPopoverCoordinator: AnnotationEditCoordinatorDelegate { extension AnnotationPopoverCoordinator: UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { - navigationController.setNavigationBarHidden((viewController is AnnotationViewController), animated: animated) + navigationController.setNavigationBarHidden((viewController is AnnotationPopoverViewController), animated: animated) } } diff --git a/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationEditState.swift b/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationEditState.swift index 650d935e8..ff6fa56ff 100644 --- a/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationEditState.swift +++ b/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationEditState.swift @@ -9,6 +9,15 @@ import UIKit struct AnnotationEditState: ViewModelState { + struct Data { + let type: AnnotationType + let isEditable: Bool + let color: String + let lineWidth: CGFloat + let pageLabel: String + let highlightText: String + } + struct Changes: OptionSet { typealias RawValue = UInt8 @@ -18,7 +27,6 @@ struct AnnotationEditState: ViewModelState { static let pageLabel = Changes(rawValue: 1 << 1) } - let key: PDFReaderState.AnnotationKey let type: AnnotationType let isEditable: Bool @@ -29,14 +37,13 @@ struct AnnotationEditState: ViewModelState { var updateSubsequentLabels: Bool var changes: Changes - init(annotation: Annotation, userId: Int, library: Library) { - self.key = annotation.readerKey - self.type = annotation.type - self.isEditable = annotation.editability(currentUserId: userId, library: library) == .editable - self.color = annotation.color - self.lineWidth = annotation.lineWidth ?? 0 - self.pageLabel = annotation.pageLabel - self.highlightText = annotation.text ?? "" + init(data: Data) { + self.type = data.type + self.isEditable = data.isEditable + self.color = data.color + self.lineWidth = data.lineWidth + self.pageLabel = data.pageLabel + self.highlightText = data.highlightText self.updateSubsequentLabels = false self.changes = [] } diff --git a/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationPopoverAction.swift b/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationPopoverAction.swift new file mode 100644 index 000000000..aebf6e2ba --- /dev/null +++ b/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationPopoverAction.swift @@ -0,0 +1,20 @@ +// +// AnnotationPopoverAction.swift +// Zotero +// +// Created by Michal Rentka on 30.11.2020. +// Copyright © 2020 Corporation for Digital Scholarship. All rights reserved. +// + +import UIKit + +enum AnnotationPopoverAction { + case setColor(String) + case setLineWidth(CGFloat) + case setPageLabel(String, Bool) + case setHighlight(String) + case setTags([Tag]) + case setComment(NSAttributedString) + case delete + case setProperties(pageLabel: String, updateSubsequentLabels: Bool, highlightText: String) +} diff --git a/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationPopoverState.swift b/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationPopoverState.swift new file mode 100644 index 000000000..cbaa72ce5 --- /dev/null +++ b/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationPopoverState.swift @@ -0,0 +1,74 @@ +// +// AnnotationPopoverState.swift +// Zotero +// +// Created by Michal Rentka on 30.11.2020. +// Copyright © 2020 Corporation for Digital Scholarship. All rights reserved. +// + +import UIKit + +struct AnnotationPopoverState: ViewModelState { + struct Data { + let libraryId: LibraryIdentifier + let type: AnnotationType + let isEditable: Bool + let author: String + let comment: NSAttributedString + let color: String + let lineWidth: CGFloat + let pageLabel: String + let highlightText: String + let tags: [Tag] + let showsDeleteButton: Bool + } + + struct Changes: OptionSet { + typealias RawValue = UInt8 + + let rawValue: UInt8 + + static let comment = Changes(rawValue: 1 << 0) + static let color = Changes(rawValue: 1 << 1) + static let lineWidth = Changes(rawValue: 1 << 2) + static let pageLabel = Changes(rawValue: 1 << 3) + static let highlight = Changes(rawValue: 1 << 4) + static let tags = Changes(rawValue: 1 << 5) + static let deletion = Changes(rawValue: 1 << 6) + } + + let libraryId: LibraryIdentifier + let type: AnnotationType + let isEditable: Bool + let author: String + let showsDeleteButton: Bool + + var comment: NSAttributedString + var color: String + var lineWidth: CGFloat + var pageLabel: String + var highlightText: String + var updateSubsequentLabels: Bool + var tags: [Tag] + var changes: Changes + + init(data: Data) { + self.libraryId = data.libraryId + self.type = data.type + self.isEditable = data.isEditable + self.author = data.author + self.comment = data.comment + self.color = data.color + self.lineWidth = data.lineWidth + self.pageLabel = data.pageLabel + self.highlightText = data.highlightText + self.tags = data.tags + self.showsDeleteButton = data.showsDeleteButton + self.updateSubsequentLabels = false + self.changes = [] + } + + mutating func cleanup() { + self.changes = [] + } +} diff --git a/Zotero/Scenes/Detail/Annotation Popover/ViewModels/AnnotationPopoverActionHandler.swift b/Zotero/Scenes/Detail/Annotation Popover/ViewModels/AnnotationPopoverActionHandler.swift new file mode 100644 index 000000000..6b544b498 --- /dev/null +++ b/Zotero/Scenes/Detail/Annotation Popover/ViewModels/AnnotationPopoverActionHandler.swift @@ -0,0 +1,74 @@ +// +// AnnotationPopoverActionHandler.swift +// Zotero +// +// Created by Michal Rentka on 25.10.2023. +// Copyright © 2023 Corporation for Digital Scholarship. All rights reserved. +// + +import Foundation + +struct AnnotationPopoverActionHandler: ViewModelActionHandler { + typealias Action = AnnotationPopoverAction + typealias State = AnnotationPopoverState + + func process(action: AnnotationPopoverAction, in viewModel: ViewModel) { + switch action { + case .setColor(let hexString): + update(viewModel: viewModel) { state in + state.color = hexString + state.changes = .color + } + + case .setLineWidth(let width): + update(viewModel: viewModel) { state in + state.lineWidth = width + state.changes = .lineWidth + } + + case .setPageLabel(let label, let updateSubsequentPages): + update(viewModel: viewModel) { state in + state.pageLabel = label + state.updateSubsequentLabels = updateSubsequentPages + state.changes = .pageLabel + } + + case .setHighlight(let text): + update(viewModel: viewModel) { state in + state.highlightText = text + state.changes = .highlight + } + + case .setComment(let comment): + update(viewModel: viewModel) { state in + state.comment = comment + state.changes = .comment + } + + case .setTags(let tags): + update(viewModel: viewModel) { state in + state.tags = tags + state.changes = .tags + } + + case .delete: + update(viewModel: viewModel) { state in + state.changes = .deletion + } + + case .setProperties(pageLabel: let pageLabel, updateSubsequentLabels: let updateSubsequentLabels, highlightText: let highlightText): + update(viewModel: viewModel) { state in + if state.pageLabel != pageLabel { + state.pageLabel = pageLabel + state.updateSubsequentLabels = updateSubsequentLabels + state.changes = .pageLabel + } + + if state.highlightText != highlightText { + state.highlightText = highlightText + state.changes.insert(.highlight) + } + } + } + } +} diff --git a/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationEditViewController.swift b/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationEditViewController.swift index 0e1d5179b..9136d17c9 100644 --- a/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationEditViewController.swift +++ b/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationEditViewController.swift @@ -10,8 +10,8 @@ import UIKit import RxSwift -typealias AnnotationEditSaveAction = (PDFReaderState.AnnotationKey, String, CGFloat, String, Bool, String) -> Void // key, color, lineWidth, pageLabel, updateSubsequentLabels, highlightText -typealias AnnotationEditDeleteAction = (PDFReaderState.AnnotationKey) -> Void +typealias AnnotationEditSaveAction = (String, CGFloat, String, Bool, String) -> Void // key, color, lineWidth, pageLabel, updateSubsequentLabels, highlightText +typealias AnnotationEditDeleteAction = () -> Void final class AnnotationEditViewController: UIViewController { private enum Section { @@ -167,7 +167,7 @@ final class AnnotationEditViewController: UIViewController { .subscribe(onNext: { [weak self] in guard let self = self else { return } let state = self.viewModel.state - self.saveAction(state.key, state.color, state.lineWidth, state.pageLabel, state.updateSubsequentLabels, state.highlightText) + self.saveAction(state.color, state.lineWidth, state.pageLabel, state.updateSubsequentLabels, state.highlightText) self.cancel() }) .disposed(by: self.disposeBag) @@ -259,7 +259,7 @@ extension AnnotationEditViewController: UITableViewDelegate { case .properties, .highlight: break case .actions: - self.deleteAction(self.viewModel.state.key) + self.deleteAction() case .pageLabel: guard self.viewModel.state.isEditable else { return } @@ -272,8 +272,4 @@ extension AnnotationEditViewController: UITableViewDelegate { } } -extension AnnotationEditViewController: AnnotationPopover { - var annotationKey: PDFReaderState.AnnotationKey? { - return self.viewModel.state.key - } -} +extension AnnotationEditViewController: AnnotationPopover {} diff --git a/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationPopoverViewController.swift b/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationPopoverViewController.swift new file mode 100644 index 000000000..809407816 --- /dev/null +++ b/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationPopoverViewController.swift @@ -0,0 +1,309 @@ +// +// AnnotationPopoverViewController.swift +// Zotero +// +// Created by Michal Rentka on 13/10/2020. +// Copyright © 2020 Corporation for Digital Scholarship. All rights reserved. +// + +import UIKit + +import RxSwift +import CocoaLumberjackSwift + +final class AnnotationPopoverViewController: UIViewController { + let viewModel: ViewModel + private let disposeBag: DisposeBag + + @IBOutlet private weak var scrollView: UIScrollView! + @IBOutlet private weak var containerStackView: UIStackView! + private weak var header: AnnotationViewHeader! + private weak var comment: AnnotationViewTextView? + private weak var colorPicker: ColorPickerStackView! + private weak var tagsButton: AnnotationViewButton! + private weak var tags: AnnotationViewText! + private weak var deleteButton: UIButton! + + weak var coordinatorDelegate: AnnotationPopoverAnnotationCoordinatorDelegate? + + private var commentPlaceholder: String { + return self.viewModel.state.isEditable ? L10n.Pdf.AnnotationsSidebar.addComment : L10n.Pdf.AnnotationPopover.noComment + } + + // MARK: - Lifecycle + + init(viewModel: ViewModel) { + self.viewModel = viewModel + self.disposeBag = DisposeBag() + super.init(nibName: "AnnotationPopoverViewController", bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupViews() + view.layoutSubviews() + + viewModel.stateObservable + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] state in + self?.update(state: state) + }) + .disposed(by: disposeBag) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + navigationController?.setNavigationBarHidden(true, animated: animated) + updatePreferredContentSize() + } + + deinit { + DDLogInfo("AnnotationPopoverViewController: deinitialized") + coordinatorDelegate?.didFinish() + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + updatePreferredContentSize() + } + + // MARK: - Actions + + private func updatePreferredContentSize() { + guard var size = containerStackView?.systemLayoutSizeFitting(CGSize(width: AnnotationPopoverLayout.width, height: .greatestFiniteMagnitude)) else { return } + size.width = AnnotationPopoverLayout.width + preferredContentSize = size + navigationController?.preferredContentSize = size + } + + private func update(state: AnnotationPopoverState) { + // Update header + header.setup( + type: state.type, + authorName: state.author, + pageLabel: state.pageLabel, + colorHex: state.color, + shareMenuProvider: { [weak coordinatorDelegate] button in + coordinatorDelegate?.createShareAnnotationMenu(sender: button) + }, + isEditable: state.isEditable, + showsLock: !state.isEditable, + accessibilityType: .view + ) + + // Update selected color + colorPicker.setSelected(hexColor: state.color) + + // Update tags + if !state.tags.isEmpty { + tags.setup(with: AnnotationView.attributedString(from: state.tags, layout: AnnotationPopoverLayout.annotationLayout)) + } + tags.isHidden = state.tags.isEmpty + tagsButton?.isHidden = !state.tags.isEmpty + } + + private func set(comment: NSAttributedString, heightDidChange: Bool) { + viewModel.process(action: .setComment(comment)) + guard heightDidChange else { return } + updatePreferredContentSize() + scrollToCursorIfNeeded() + } + + private func name(for color: String, isSelected: Bool) -> String { + let colorName = AnnotationsConfig.colorNames[color] ?? L10n.unknown + return !isSelected ? colorName : L10n.Accessibility.Pdf.selected + ": " + colorName + } + + private func showSettings() { + coordinatorDelegate?.showEdit( + state: viewModel.state, + saveAction: { [weak self] _, _, pageLabel, updateSubsequentLabels, highlightText in + self?.viewModel.process(action: .setProperties(pageLabel: pageLabel, updateSubsequentLabels: updateSubsequentLabels, highlightText: highlightText)) + }, + deleteAction: { [weak self] in + self?.viewModel.process(action: .delete) + } + ) + } + + private func showTagPicker() { + guard viewModel.state.isEditable else { return } + let selected = Set(viewModel.state.tags.map({ $0.name })) + coordinatorDelegate?.showTagPicker(libraryId: viewModel.state.libraryId, selected: selected, picked: { [weak self] tags in + self?.viewModel.process(action: .setTags(tags)) + }) + } + + private func scrollToCursorIfNeeded() { + guard let comment, comment.textView.isFirstResponder, let selectedPosition = comment.textView.selectedTextRange?.start else { return } + let caretRect = comment.textView.caretRect(for: selectedPosition) + guard (comment.frame.origin.y + caretRect.origin.y) > scrollView.frame.height else { return } + let rect = CGRect(x: caretRect.origin.x, y: (comment.frame.origin.y + caretRect.origin.y) + 10, width: caretRect.size.width, height: caretRect.size.height) + scrollView.scrollRectToVisible(rect, animated: true) + } + + // MARK: - Setups + + private func setupViews() { + let layout = AnnotationPopoverLayout.annotationLayout + + // Setup header + let header = AnnotationViewHeader(layout: layout) + header.setup( + type: viewModel.state.type, + authorName: viewModel.state.author, + pageLabel: viewModel.state.pageLabel, + colorHex: viewModel.state.color, + shareMenuProvider: { [weak coordinatorDelegate] button in + coordinatorDelegate?.createShareAnnotationMenu(sender: button) + }, + isEditable: viewModel.state.isEditable, + showsLock: !viewModel.state.isEditable, + accessibilityType: .view + ) + header.menuTap + .subscribe(with: self, onNext: { `self`, _ in + self.showSettings() + }) + .disposed(by: disposeBag) + if let tap = header.doneTap { + tap.subscribe(with: self, onNext: { `self`, _ in + self.presentingViewController?.dismiss(animated: true, completion: nil) + }) + .disposed(by: disposeBag) + } + self.header = header + + containerStackView.addArrangedSubview(header) + containerStackView.addArrangedSubview(AnnotationViewSeparator()) + + // Setup comment + if viewModel.state.type != .ink { + let commentView = AnnotationViewTextView(layout: layout, placeholder: commentPlaceholder) + commentView.setup(text: viewModel.state.comment) + commentView.isUserInteractionEnabled = viewModel.state.isEditable + commentView.textObservable + .debounce(.milliseconds(500), scheduler: MainScheduler.instance) + .subscribe(onNext: { [weak self] data in + guard let self, let data else { return } + set(comment: data.0, heightDidChange: data.1) + }) + .disposed(by: disposeBag) + comment = commentView + + containerStackView.addArrangedSubview(commentView) + containerStackView.addArrangedSubview(AnnotationViewSeparator()) + } + + // Setup color picker + if viewModel.state.isEditable { + let colorPickerContainer = UIView() + colorPickerContainer.backgroundColor = Asset.Colors.defaultCellBackground.color + colorPickerContainer.accessibilityLabel = L10n.Accessibility.Pdf.colorPicker + + let hexColors = AnnotationsConfig.colors(for: viewModel.state.type) + let colorPicker = ColorPickerStackView( + hexColors: hexColors, + columnsDistribution: .fixed(numberOfColumns: hexColors.count), + allowsMultipleSelection: false, + circleBackgroundColor: Asset.Colors.defaultCellBackground.color, + circleContentInsets: UIEdgeInsets(top: 11, left: 11, bottom: 11, right: 11), + accessibilityLabelProvider: { [weak self] hexColor, isSelected in + self?.name(for: hexColor, isSelected: isSelected) + }, + hexColorToggled: { [weak self] hexColor in + self?.viewModel.process(action: .setColor(hexColor)) + } + ) + colorPicker.setSelected(hexColor: viewModel.state.color) + colorPicker.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) + colorPicker.translatesAutoresizingMaskIntoConstraints = false + self.colorPicker = colorPicker + colorPickerContainer.addSubview(colorPicker) + + NSLayoutConstraint.activate([ + colorPicker.topAnchor.constraint(equalTo: colorPickerContainer.topAnchor), + colorPicker.bottomAnchor.constraint(equalTo: colorPickerContainer.bottomAnchor), + colorPicker.leadingAnchor.constraint(equalTo: colorPickerContainer.leadingAnchor, constant: 5), + colorPicker.trailingAnchor.constraint(lessThanOrEqualTo: colorPickerContainer.trailingAnchor) + ]) + + containerStackView.addArrangedSubview(colorPickerContainer) + containerStackView.addArrangedSubview(AnnotationViewSeparator()) + + if viewModel.state.type == .ink { + // Setup line width slider + let lineView = LineWidthView(title: L10n.Pdf.AnnotationPopover.lineWidth, settings: .lineWidth, contentInsets: UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)) + lineView.value = Float(viewModel.state.lineWidth) + lineView.valueObservable + .subscribe(with: self, onNext: { `self`, value in + self.viewModel.process(action: .setLineWidth(CGFloat(value))) + }) + .disposed(by: disposeBag) + containerStackView.addArrangedSubview(lineView) + containerStackView.addArrangedSubview(AnnotationViewSeparator()) + } + } + + // Setup tags + let tags = AnnotationViewText(layout: layout) + if !viewModel.state.tags.isEmpty { + tags.setup(with: AnnotationView.attributedString(from: viewModel.state.tags, layout: layout)) + } + tags.isHidden = viewModel.state.tags.isEmpty + tags.isEnabled = viewModel.state.isEditable + tags.tap + .subscribe(with: self, onNext: { `self`, _ in + self.showTagPicker() + }) + .disposed(by: disposeBag) + tags.button.accessibilityLabel = L10n.Accessibility.Pdf.tags + ": " + (self.tags?.textLabel.text ?? "") + tags.textLabel.isAccessibilityElement = false + self.tags = tags + + containerStackView.addArrangedSubview(tags) + + if viewModel.state.isEditable { + let tagButton = AnnotationViewButton(layout: layout) + tagButton.setTitle(L10n.Pdf.AnnotationsSidebar.addTags, for: .normal) + tagButton.isHidden = !viewModel.state.tags.isEmpty + tagButton.rx.tap + .subscribe(with: self, onNext: { `self`, _ in + self.showTagPicker() + }) + .disposed(by: disposeBag) + tagButton.accessibilityLabel = L10n.Pdf.AnnotationsSidebar.addTags + tagsButton = tagButton + + containerStackView.addArrangedSubview(tagButton) + containerStackView.addArrangedSubview(AnnotationViewSeparator()) + } + + if viewModel.state.showsDeleteButton { + var configuration = UIButton.Configuration.plain() + let attributes: [NSAttributedString.Key: Any] = [.font: UIFont.preferredFont(forTextStyle: .body), .foregroundColor: UIColor.red] + configuration.attributedTitle = AttributedString(L10n.Pdf.AnnotationPopover.delete, attributes: AttributeContainer(attributes)) + configuration.contentInsets = NSDirectionalEdgeInsets(top: 11, leading: 0, bottom: 12, trailing: 0) + let button = UIButton() + button.configuration = configuration + button + .rx + .tap + .subscribe(with: self, onNext: { `self`, _ in + self.viewModel.process(action: .delete) + }) + .disposed(by: disposeBag) + deleteButton = button + + containerStackView.addArrangedSubview(button) + } + } +} + +extension AnnotationPopoverViewController: AnnotationPopover {} diff --git a/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationViewController.xib b/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationPopoverViewController.xib similarity index 93% rename from Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationViewController.xib rename to Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationPopoverViewController.xib index c2759a835..8cf6c31e6 100644 --- a/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationViewController.xib +++ b/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationPopoverViewController.xib @@ -1,15 +1,15 @@ - + - + - + @@ -22,7 +22,7 @@ - + diff --git a/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationViewController.swift b/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationViewController.swift index 22e6ab8ad..68ad7a6a1 100644 --- a/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationViewController.swift +++ b/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationViewController.swift @@ -94,16 +94,17 @@ final class AnnotationViewController: UIViewController { // Update header let editability = annotation.editability(currentUserId: state.userId, library: state.library) self.header.setup( - with: annotation, + type: annotation.type, + authorName: annotation.author(displayName: state.displayName, username: state.username), + pageLabel: annotation.pageLabel, + colorHex: annotation.color, libraryId: state.library.identifier, shareMenuProvider: { [weak self] button in self?.createShareAnnotationMenu(sender: button) }, isEditable: (editability == .editable), showsLock: (editability != .editable), - accessibilityType: .view, - displayName: state.displayName, - username: state.username + accessibilityType: .view ) // Update selected color @@ -133,14 +134,27 @@ final class AnnotationViewController: UIViewController { private func showSettings() { guard let annotation = self.viewModel.state.selectedAnnotation else { return } - self.coordinatorDelegate?.showEdit(annotation: annotation, userId: self.viewModel.state.userId, library: self.viewModel.state.library, - saveAction: { [weak self] key, color, lineWidth, pageLabel, updateSubsequentLabels, highlightText in - self?.viewModel.process(action: .updateAnnotationProperties(key: key.key, color: color, lineWidth: lineWidth, pageLabel: pageLabel, - updateSubsequentLabels: updateSubsequentLabels, highlightText: highlightText)) - }, - deleteAction: { [weak self] key in - self?.viewModel.process(action: .removeAnnotation(key)) - }) + let key = annotation.readerKey + self.coordinatorDelegate?.showEdit( + annotation: annotation, + userId: self.viewModel.state.userId, + library: self.viewModel.state.library, + saveAction: { [weak self] color, lineWidth, pageLabel, updateSubsequentLabels, highlightText in + self?.viewModel.process( + action: .updateAnnotationProperties( + key: key.key, + color: color, + lineWidth: lineWidth, + pageLabel: pageLabel, + updateSubsequentLabels: updateSubsequentLabels, + highlightText: highlightText + ) + ) + }, + deleteAction: { [weak self] in + self?.viewModel.process(action: .removeAnnotation(key)) + } + ) } private func set(color: String) { @@ -177,16 +191,17 @@ final class AnnotationViewController: UIViewController { let header = AnnotationViewHeader(layout: layout) let editability = annotation.editability(currentUserId: self.viewModel.state.userId, library: self.viewModel.state.library) header.setup( - with: annotation, + type: annotation.type, + authorName: annotation.author(displayName: self.viewModel.state.displayName, username: self.viewModel.state.username), + pageLabel: annotation.pageLabel, + colorHex: annotation.color, libraryId: self.viewModel.state.library.identifier, shareMenuProvider: { [weak self] button in self?.createShareAnnotationMenu(sender: button) }, isEditable: (editability == .editable), showsLock: (editability != .editable), - accessibilityType: .view, - displayName: self.viewModel.state.displayName, - username: self.viewModel.state.username + accessibilityType: .view ) header.menuTap .subscribe(with: self, onNext: { `self`, _ in diff --git a/Zotero/Scenes/Detail/DetailCoordinator.swift b/Zotero/Scenes/Detail/DetailCoordinator.swift index 302e6595b..687e298c7 100644 --- a/Zotero/Scenes/Detail/DetailCoordinator.swift +++ b/Zotero/Scenes/Detail/DetailCoordinator.swift @@ -301,7 +301,7 @@ final class DetailCoordinator: Coordinator { let controller = createPDFController(key: key, parentKey: parentKey, library: library, url: url) navigationController?.present(controller, animated: true, completion: nil) } - + private func showWebView(for url: URL) { guard let currentNavigationController = self.navigationController else { return } let controller = WebViewController(url: url) diff --git a/Zotero/Scenes/Detail/PDF/AnnotationEditCoordinator.swift b/Zotero/Scenes/Detail/PDF/AnnotationEditCoordinator.swift index 10aa821a4..bf0d43a73 100644 --- a/Zotero/Scenes/Detail/PDF/AnnotationEditCoordinator.swift +++ b/Zotero/Scenes/Detail/PDF/AnnotationEditCoordinator.swift @@ -16,19 +16,20 @@ final class AnnotationEditCoordinator: Coordinator { var childCoordinators: [Coordinator] weak var navigationController: UINavigationController? - private let annotation: Annotation - private let userId: Int - private let library: Library + private let data: AnnotationEditState.Data private let saveAction: AnnotationEditSaveAction private let deleteAction: AnnotationEditDeleteAction private unowned let controllers: Controllers private let disposeBag: DisposeBag - init(annotation: Annotation, userId: Int, library: Library, saveAction: @escaping AnnotationEditSaveAction, deleteAction: @escaping AnnotationEditDeleteAction, - navigationController: NavigationViewController, controllers: Controllers) { - self.annotation = annotation - self.userId = userId - self.library = library + init( + data: AnnotationEditState.Data, + saveAction: @escaping AnnotationEditSaveAction, + deleteAction: @escaping AnnotationEditDeleteAction, + navigationController: NavigationViewController, + controllers: Controllers + ) { + self.data = data self.saveAction = saveAction self.deleteAction = deleteAction self.navigationController = navigationController @@ -46,7 +47,7 @@ final class AnnotationEditCoordinator: Coordinator { } func start(animated: Bool) { - let state = AnnotationEditState(annotation: self.annotation, userId: self.userId, library: self.library) + let state = AnnotationEditState(data: data) let handler = AnnotationEditActionHandler() let viewModel = ViewModel(initialState: state, handler: handler) let controller = AnnotationEditViewController(viewModel: viewModel, includeColorPicker: true, saveAction: self.saveAction, deleteAction: self.deleteAction) diff --git a/Zotero/Scenes/Detail/PDF/Models/AnnotationToolOptionsState.swift b/Zotero/Scenes/Detail/PDF/Models/AnnotationToolOptionsState.swift index ed71bb176..6221ef7fe 100644 --- a/Zotero/Scenes/Detail/PDF/Models/AnnotationToolOptionsState.swift +++ b/Zotero/Scenes/Detail/PDF/Models/AnnotationToolOptionsState.swift @@ -20,13 +20,13 @@ struct AnnotationToolOptionsState: ViewModelState { static let size = Changes(rawValue: 1 << 1) } - let tool: PSPDFKit.Annotation.Tool + let tool: AnnotationTool var colorHex: String? var size: Float? var changes: Changes - init(tool: PSPDFKit.Annotation.Tool, colorHex: String?, size: Float?) { + init(tool: AnnotationTool, colorHex: String?, size: Float?) { self.tool = tool self.colorHex = colorHex self.size = size diff --git a/Zotero/Scenes/Detail/PDF/Models/Annotation.swift b/Zotero/Scenes/Detail/PDF/Models/PDFAnnotation.swift similarity index 96% rename from Zotero/Scenes/Detail/PDF/Models/Annotation.swift rename to Zotero/Scenes/Detail/PDF/Models/PDFAnnotation.swift index f15e9a886..b1ac1ec61 100644 --- a/Zotero/Scenes/Detail/PDF/Models/Annotation.swift +++ b/Zotero/Scenes/Detail/PDF/Models/PDFAnnotation.swift @@ -1,5 +1,5 @@ // -// Annotation.swift +// PDFAnnotation.swift // Zotero // // Created by Michal Rentka on 29/04/2020. @@ -8,7 +8,7 @@ import UIKit -protocol Annotation { +protocol PDFAnnotation { var key: String { get } var readerKey: PDFReaderState.AnnotationKey { get } var type: AnnotationType { get } @@ -30,7 +30,7 @@ protocol Annotation { func paths(boundingBoxConverter: AnnotationBoundingBoxConverter) -> [[CGPoint]] } -extension Annotation { +extension PDFAnnotation { func previewBoundingBox(boundingBoxConverter: AnnotationBoundingBoxConverter) -> CGRect { let boundingBox = self.boundingBox(boundingBoxConverter: boundingBoxConverter) switch self.type { diff --git a/Zotero/Scenes/Detail/PDF/Models/DatabaseAnnotation.swift b/Zotero/Scenes/Detail/PDF/Models/PDFDatabaseAnnotation.swift similarity index 98% rename from Zotero/Scenes/Detail/PDF/Models/DatabaseAnnotation.swift rename to Zotero/Scenes/Detail/PDF/Models/PDFDatabaseAnnotation.swift index 0c4cf06bd..42c7f5ae7 100644 --- a/Zotero/Scenes/Detail/PDF/Models/DatabaseAnnotation.swift +++ b/Zotero/Scenes/Detail/PDF/Models/PDFDatabaseAnnotation.swift @@ -1,5 +1,5 @@ // -// DatabaseAnnotation.swift +// PDFDatabaseAnnotation.swift // Zotero // // Created by Michal Rentka on 26.08.2022. @@ -12,7 +12,7 @@ import CocoaLumberjackSwift import PSPDFKit import RxSwift -struct DatabaseAnnotation { +struct PDFDatabaseAnnotation { let item: RItem var key: String { @@ -154,7 +154,7 @@ struct DatabaseAnnotation { } } -extension DatabaseAnnotation: Annotation { +extension PDFDatabaseAnnotation: PDFAnnotation { var readerKey: PDFReaderState.AnnotationKey { return .init(key: self.key, type: .database) } diff --git a/Zotero/Scenes/Detail/PDF/Models/DocumentAnnotation.swift b/Zotero/Scenes/Detail/PDF/Models/PDFDocumentAnnotation.swift similarity index 93% rename from Zotero/Scenes/Detail/PDF/Models/DocumentAnnotation.swift rename to Zotero/Scenes/Detail/PDF/Models/PDFDocumentAnnotation.swift index ef676ffda..c103cc8dd 100644 --- a/Zotero/Scenes/Detail/PDF/Models/DocumentAnnotation.swift +++ b/Zotero/Scenes/Detail/PDF/Models/PDFDocumentAnnotation.swift @@ -1,5 +1,5 @@ // -// DocumentAnnotation.swift +// PDFDocumentAnnotation.swift // Zotero // // Created by Michal Rentka on 26.08.2022. @@ -8,7 +8,7 @@ import UIKit -struct DocumentAnnotation { +struct PDFDocumentAnnotation { let key: String let type: AnnotationType let page: Int @@ -25,7 +25,7 @@ struct DocumentAnnotation { let dateModified: Date } -extension DocumentAnnotation: Annotation { +extension PDFDocumentAnnotation: PDFAnnotation { var readerKey: PDFReaderState.AnnotationKey { return .init(key: self.key, type: .document) } diff --git a/Zotero/Scenes/Detail/PDF/Models/PDFReaderAction.swift b/Zotero/Scenes/Detail/PDF/Models/PDFReaderAction.swift index a6cf02883..558ba824a 100644 --- a/Zotero/Scenes/Detail/PDF/Models/PDFReaderAction.swift +++ b/Zotero/Scenes/Detail/PDF/Models/PDFReaderAction.swift @@ -43,7 +43,7 @@ enum PDFReaderAction { case export(includeAnnotations: Bool) case clearTmpData case setSidebarEditingEnabled(Bool) - case setSettings(settings: PDFSettings, currentUserInterfaceStyle: UIUserInterfaceStyle) + case setSettings(settings: PDFSettings, parentUserInterfaceStyle: UIUserInterfaceStyle) case changeIdleTimerDisabled(Bool) case changeFilter(AnnotationsFilter?) case submitPendingPage(Int) diff --git a/Zotero/Scenes/Detail/PDF/Models/PDFReaderState.swift b/Zotero/Scenes/Detail/PDF/Models/PDFReaderState.swift index f3b555fe6..518036ea9 100644 --- a/Zotero/Scenes/Detail/PDF/Models/PDFReaderState.swift +++ b/Zotero/Scenes/Detail/PDF/Models/PDFReaderState.swift @@ -50,20 +50,6 @@ struct PDFReaderState: ViewModelState { static let visiblePageFromThumbnailList = Changes(rawValue: 1 << 13) } - enum AppearanceMode: UInt { - case light - case dark - case automatic - - func userInterfaceStyle(currentUserInterfaceStyle: UIUserInterfaceStyle) -> UIUserInterfaceStyle { - switch self { - case .automatic: return currentUserInterfaceStyle - case .dark: return .dark - case .light: return .light - } - } - } - enum Error: Swift.Error { case cantDeleteAnnotation case cantAddAnnotations @@ -87,7 +73,7 @@ struct PDFReaderState: ViewModelState { var snapshotKeys: [AnnotationKey]? var token: NotificationToken? var databaseAnnotations: Results! - var documentAnnotations: [String: DocumentAnnotation] + var documentAnnotations: [String: PDFDocumentAnnotation] var comments: [String: NSAttributedString] var searchTerm: String? var filter: AnnotationsFilter? @@ -100,7 +86,7 @@ struct PDFReaderState: ViewModelState { /// Selected annotation when annotations are not being edited in sidebar var selectedAnnotationKey: AnnotationKey? - var selectedAnnotation: Annotation? { + var selectedAnnotation: PDFAnnotation? { return self.selectedAnnotationKey.flatMap({ self.annotation(for: $0) }) } var selectedAnnotationCommentActive: Bool @@ -166,10 +152,12 @@ struct PDFReaderState: ViewModelState { self.selectedAnnotationsDuringEditing = [] self.interfaceStyle = interfaceStyle self.sidebarEditingEnabled = false - self.toolColors = [.highlight: UIColor(hex: Defaults.shared.highlightColorHex), - .square: UIColor(hex: Defaults.shared.squareColorHex), - .note: UIColor(hex: Defaults.shared.noteColorHex), - .ink: UIColor(hex: Defaults.shared.inkColorHex)] + self.toolColors = [ + .highlight: UIColor(hex: Defaults.shared.highlightColorHex), + .square: UIColor(hex: Defaults.shared.squareColorHex), + .note: UIColor(hex: Defaults.shared.noteColorHex), + .ink: UIColor(hex: Defaults.shared.inkColorHex) + ] self.activeLineWidth = CGFloat(Defaults.shared.activeLineWidth) self.activeEraserSize = CGFloat(Defaults.shared.activeEraserSize) self.deletionEnabled = false @@ -179,10 +167,10 @@ struct PDFReaderState: ViewModelState { self.previewCache.totalCostLimit = 1024 * 1024 * 10 // Cache object limit - 10 MB } - func annotation(for key: AnnotationKey) -> Annotation? { + func annotation(for key: AnnotationKey) -> PDFAnnotation? { switch key.type { case .database: - return self.databaseAnnotations.filter(.key(key.key)).first.flatMap({ DatabaseAnnotation(item: $0) }) + return self.databaseAnnotations.filter(.key(key.key)).first.flatMap({ PDFDatabaseAnnotation(item: $0) }) case .document: return self.documentAnnotations[key.key] diff --git a/Zotero/Scenes/Detail/PDF/Models/PDFSettings.swift b/Zotero/Scenes/Detail/PDF/Models/PDFSettings.swift index e78e77950..94d7931a3 100644 --- a/Zotero/Scenes/Detail/PDF/Models/PDFSettings.swift +++ b/Zotero/Scenes/Detail/PDF/Models/PDFSettings.swift @@ -15,7 +15,7 @@ struct PDFSettings { var pageMode: PageMode var direction: ScrollDirection var pageFitting: PDFConfiguration.SpreadFitting - var appearanceMode: PDFReaderState.AppearanceMode + var appearanceMode: ReaderSettingsState.Appearance var idleTimerDisabled: Bool static var `default`: PDFSettings { @@ -38,7 +38,7 @@ extension PDFSettings: Codable { self.direction = ScrollDirection(rawValue: directionRaw) ?? .horizontal self.transition = PageTransition(rawValue: transitionRaw) ?? .scrollPerSpread - self.appearanceMode = PDFReaderState.AppearanceMode(rawValue: appearanceRaw) ?? .automatic + self.appearanceMode = ReaderSettingsState.Appearance(rawValue: appearanceRaw) ?? .automatic self.pageMode = PageMode(rawValue: modeRaw) ?? .automatic self.pageFitting = PDFConfiguration.SpreadFitting(rawValue: fittingRaw) ?? .adaptive // This setting is not persisted, always defaults to false diff --git a/Zotero/Scenes/Detail/PDF/Models/PDFSettingsAction.swift b/Zotero/Scenes/Detail/PDF/Models/PDFSettingsAction.swift deleted file mode 100644 index b7676228d..000000000 --- a/Zotero/Scenes/Detail/PDF/Models/PDFSettingsAction.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// PDFSettingsAction.swift -// Zotero -// -// Created by Michal Rentka on 02.03.2022. -// Copyright © 2022 Corporation for Digital Scholarship. All rights reserved. -// - -import Foundation - -import PSPDFKitUI - -enum PDFSettingsAction { - case setTransition(PageTransition) - case setPageMode(PageMode) - case setDirection(ScrollDirection) - case setPageFitting(PDFConfiguration.SpreadFitting) - case setAppearanceMode(PDFReaderState.AppearanceMode) - case setIdleTimerDisabled(Bool) -} diff --git a/Zotero/Scenes/Detail/PDF/Models/PDFSettingsState.swift b/Zotero/Scenes/Detail/PDF/Models/PDFSettingsState.swift deleted file mode 100644 index 91054fc1f..000000000 --- a/Zotero/Scenes/Detail/PDF/Models/PDFSettingsState.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// PDFSettingsState.swift -// Zotero -// -// Created by Michal Rentka on 01.03.2022. -// Copyright © 2022 Corporation for Digital Scholarship. All rights reserved. -// - -import UIKit - -import PSPDFKit - -struct PDFSettingsState: ViewModelState { - var settings: PDFSettings - - init(settings: PDFSettings) { - self.settings = settings - } - - func cleanup() {} -} diff --git a/Zotero/Scenes/Detail/PDF/PDFCoordinator.swift b/Zotero/Scenes/Detail/PDF/PDFCoordinator.swift index 4fb7e448b..ea9302fa5 100644 --- a/Zotero/Scenes/Detail/PDF/PDFCoordinator.swift +++ b/Zotero/Scenes/Detail/PDF/PDFCoordinator.swift @@ -15,27 +15,47 @@ import PSPDFKitUI import RxSwift protocol PdfReaderCoordinatorDelegate: AnyObject { - func showToolSettings(tool: PSPDFKit.Annotation.Tool, colorHex: String?, sizeValue: Float?, sender: SourceView, userInterfaceStyle: UIUserInterfaceStyle, valueChanged: @escaping (String?, Float?) -> Void) + func showToolSettings(tool: AnnotationTool, colorHex: String?, sizeValue: Float?, sender: SourceView, userInterfaceStyle: UIUserInterfaceStyle, valueChanged: @escaping (String?, Float?) -> Void) func showSearch(pdfController: PDFViewController, text: String?, sender: UIBarButtonItem, userInterfaceStyle: UIUserInterfaceStyle, delegate: PDFSearchDelegate) - func showAnnotationPopover(viewModel: ViewModel, sourceRect: CGRect, popoverDelegate: UIPopoverPresentationControllerDelegate, userInterfaceStyle: UIUserInterfaceStyle) + func showAnnotationPopover( + viewModel: ViewModel, + sourceRect: CGRect, + popoverDelegate: UIPopoverPresentationControllerDelegate, + userInterfaceStyle: UIUserInterfaceStyle + ) -> PublishSubject? func show(error: PDFReaderState.Error) func show(error: PDFDocumentExporter.Error) func share(url: URL, barButton: UIBarButtonItem) func share(text: String, rect: CGRect, view: UIView) func lookup(text: String, rect: CGRect, view: UIView, userInterfaceStyle: UIUserInterfaceStyle) func showDeletedAlertForPdf(completion: @escaping (Bool) -> Void) - func showSettings(with settings: PDFSettings, sender: UIBarButtonItem, userInterfaceStyle: UIUserInterfaceStyle, completion: @escaping (PDFSettings) -> Void) + func showSettings(with settings: PDFSettings, sender: UIBarButtonItem) -> ViewModel func showReader(document: Document, userInterfaceStyle: UIUserInterfaceStyle) func showCitation(for itemId: String, libraryId: LibraryIdentifier) func copyBibliography(using presenter: UIViewController, for itemId: String, libraryId: LibraryIdentifier) } protocol PdfAnnotationsCoordinatorDelegate: AnyObject { - func createShareAnnotationMenu(state: PDFReaderState, annotation: Annotation, sender: UIButton) -> UIMenu? - func shareAnnotationImage(state: PDFReaderState, annotation: Annotation, scale: CGFloat, sender: UIButton ) + func createShareAnnotationMenu(state: PDFReaderState, annotation: PDFAnnotation, sender: UIButton) -> UIMenu? + func shareAnnotationImage(state: PDFReaderState, annotation: PDFAnnotation, scale: CGFloat, sender: UIButton ) func showTagPicker(libraryId: LibraryIdentifier, selected: Set, userInterfaceStyle: UIUserInterfaceStyle?, picked: @escaping ([Tag]) -> Void) - func showCellOptions(for annotation: Annotation, userId: Int, library: Library, sender: UIButton, userInterfaceStyle: UIUserInterfaceStyle, saveAction: @escaping AnnotationEditSaveAction, deleteAction: @escaping AnnotationEditDeleteAction) - func showFilterPopup(from barButton: UIBarButtonItem, filter: AnnotationsFilter?, availableColors: [String], availableTags: [Tag], userInterfaceStyle: UIUserInterfaceStyle, completed: @escaping (AnnotationsFilter?) -> Void) + func showCellOptions( + for annotation: PDFAnnotation, + userId: Int, + library: Library, + sender: UIButton, + userInterfaceStyle: UIUserInterfaceStyle, + saveAction: @escaping AnnotationEditSaveAction, + deleteAction: @escaping AnnotationEditDeleteAction + ) + func showFilterPopup( + from barButton: UIBarButtonItem, + filter: AnnotationsFilter?, + availableColors: [String], + availableTags: [Tag], + userInterfaceStyle: UIUserInterfaceStyle, + completed: @escaping (AnnotationsFilter?) -> Void + ) } final class PDFCoordinator: Coordinator { @@ -105,7 +125,7 @@ final class PDFCoordinator: Coordinator { userId: userId, username: username, displayName: Defaults.shared.displayName, - interfaceStyle: settings.appearanceMode.userInterfaceStyle(currentUserInterfaceStyle: parentNavigationController.view.traitCollection.userInterfaceStyle) + interfaceStyle: settings.appearanceMode.userInterfaceStyle ) let controller = PDFReaderViewController( viewModel: ViewModel(initialState: state, handler: handler), @@ -119,14 +139,7 @@ final class PDFCoordinator: Coordinator { } extension PDFCoordinator: PdfReaderCoordinatorDelegate { - func showToolSettings( - tool: PSPDFKit.Annotation.Tool, - colorHex: String?, - sizeValue: Float?, - sender: SourceView, - userInterfaceStyle: UIUserInterfaceStyle, - valueChanged: @escaping (String?, Float?) -> Void - ) { + func showToolSettings(tool: AnnotationTool, colorHex: String?, sizeValue: Float?, sender: SourceView, userInterfaceStyle: UIUserInterfaceStyle, valueChanged: @escaping (String?, Float?) -> Void) { DDLogInfo("PDFCoordinator: show tool settings for \(tool)") let state = AnnotationToolOptionsState(tool: tool, colorHex: colorHex, size: sizeValue) let handler = AnnotationToolOptionsActionHandler() @@ -153,21 +166,43 @@ extension PDFCoordinator: PdfReaderCoordinatorDelegate { } } - func showAnnotationPopover(viewModel: ViewModel, sourceRect: CGRect, popoverDelegate: UIPopoverPresentationControllerDelegate, userInterfaceStyle: UIUserInterfaceStyle) { - guard let currentNavigationController = self.navigationController else { return } + func showAnnotationPopover( + viewModel: ViewModel, + sourceRect: CGRect, + popoverDelegate: UIPopoverPresentationControllerDelegate, + userInterfaceStyle: UIUserInterfaceStyle + ) -> PublishSubject? { + guard let currentNavigationController = navigationController, let annotation = viewModel.state.selectedAnnotation else { return nil } DDLogInfo("PDFCoordinator: show annotation popover") - if let coordinator = self.childCoordinators.last, coordinator is AnnotationPopoverCoordinator { - return + if let coordinator = childCoordinators.last, coordinator is AnnotationPopoverCoordinator { + return nil } let navigationController = NavigationViewController() navigationController.overrideUserInterfaceStyle = userInterfaceStyle - let coordinator = AnnotationPopoverCoordinator(navigationController: navigationController, controllers: self.controllers, viewModel: viewModel) + let author = viewModel.state.library.identifier == .custom(.myLibrary) ? "" : annotation.author(displayName: viewModel.state.displayName, username: viewModel.state.username) + let comment = viewModel.state.comments[annotation.key] ?? NSAttributedString() + let editability = annotation.editability(currentUserId: viewModel.state.userId, library: viewModel.state.library) + + let data = AnnotationPopoverState.Data( + libraryId: viewModel.state.library.identifier, + type: annotation.type, + isEditable: editability == .editable, + author: author, + comment: comment, + color: annotation.color, + lineWidth: annotation.lineWidth ?? 0, + pageLabel: annotation.pageLabel, + highlightText: annotation.text ?? "", + tags: annotation.tags, + showsDeleteButton: editability != .notEditable + ) + let coordinator = AnnotationPopoverCoordinator(data: data, navigationController: navigationController, controllers: controllers) coordinator.parentCoordinator = self - self.childCoordinators.append(coordinator) + childCoordinators.append(coordinator) coordinator.start(animated: false) if UIDevice.current.userInterfaceIdiom == .pad { @@ -179,6 +214,8 @@ extension PDFCoordinator: PdfReaderCoordinatorDelegate { } currentNavigationController.present(navigationController, animated: true, completion: nil) + + return coordinator.viewModelObservable } func showSearch(pdfController: PDFViewController, text: String?, sender: UIBarButtonItem, userInterfaceStyle: UIUserInterfaceStyle, delegate: PDFSearchDelegate) { @@ -311,29 +348,27 @@ extension PDFCoordinator: PdfReaderCoordinatorDelegate { self.navigationController?.present(controller, animated: true, completion: nil) } - func showSettings(with settings: PDFSettings, sender: UIBarButtonItem, userInterfaceStyle: UIUserInterfaceStyle, completion: @escaping (PDFSettings) -> Void) { + func showSettings(with settings: PDFSettings, sender: UIBarButtonItem) -> ViewModel { DDLogInfo("PDFCoordinator: show settings") - let state = PDFSettingsState(settings: settings) - let viewModel = ViewModel(initialState: state, handler: PDFSettingsActionHandler()) + let state = ReaderSettingsState(settings: settings) + let viewModel = ViewModel(initialState: state, handler: ReaderSettingsActionHandler()) + let baseController = ReaderSettingsViewController(rows: [.pageTransition, .pageMode, .scrollDirection, .pageFitting, .appearance, .sleep], viewModel: viewModel) let controller: UIViewController - if UIDevice.current.userInterfaceIdiom == .pad { - let _controller = PDFSettingsViewController(viewModel: viewModel) - _controller.changeHandler = completion - controller = _controller + controller = baseController } else { - let _controller = PDFSettingsViewController(viewModel: viewModel) - _controller.changeHandler = completion - controller = UINavigationController(rootViewController: _controller) + controller = UINavigationController(rootViewController: baseController) } controller.modalPresentationStyle = UIDevice.current.userInterfaceIdiom == .pad ? .popover : .formSheet controller.popoverPresentationController?.barButtonItem = sender controller.preferredContentSize = CGSize(width: 480, height: 306) - controller.overrideUserInterfaceStyle = userInterfaceStyle + controller.overrideUserInterfaceStyle = settings.appearanceMode.userInterfaceStyle self.navigationController?.present(controller, animated: true, completion: nil) + + return viewModel } func showReader(document: Document, userInterfaceStyle: UIUserInterfaceStyle) { @@ -357,7 +392,7 @@ extension PDFCoordinator: PdfReaderCoordinatorDelegate { extension PDFCoordinator: PdfAnnotationsCoordinatorDelegate { private func deferredShareImageMenuElement( state: PDFReaderState, - annotation: Annotation, + annotation: PDFAnnotation, sender: UIButton, boundingBoxConverter: AnnotationBoundingBoxConverter, scale: CGFloat, @@ -410,8 +445,13 @@ extension PDFCoordinator: PdfAnnotationsCoordinatorDelegate { .disposed(by: self.disposeBag) } } - - func createShareAnnotationMenu(state: PDFReaderState, annotation: Annotation, sender: UIButton) -> UIMenu? { + + func createShareAnnotationMenuForSelectedAnnotation(sender: UIButton) -> UIMenu? { + guard let pdfController = self.navigationController?.viewControllers.first as? PDFReaderViewController, let annotation = pdfController.state.selectedAnnotation else { return nil } + return createShareAnnotationMenu(state: pdfController.state, annotation: annotation, sender: sender) + } + + func createShareAnnotationMenu(state: PDFReaderState, annotation: PDFAnnotation, sender: UIButton) -> UIMenu? { guard annotation.type == .image, let boundingBoxConverter = self.navigationController?.viewControllers.last as? AnnotationBoundingBoxConverter else { return nil } var children: [UIMenuElement] = [] var shareImageMenuChildren: [UIMenuElement] = [] @@ -427,7 +467,7 @@ extension PDFCoordinator: PdfAnnotationsCoordinatorDelegate { return UIMenu(children: children) } - func shareAnnotationImage(state: PDFReaderState, annotation: Annotation, scale: CGFloat = 1.0, sender: UIButton) { + func shareAnnotationImage(state: PDFReaderState, annotation: PDFAnnotation, scale: CGFloat = 1.0, sender: UIButton) { guard annotation.type == .image, let pdfReaderViewController = navigationController?.viewControllers.last as? PDFReaderViewController else { return } let annotationPreviewController = controllers.annotationPreviewController let pageIndex: PageIndex = UInt(annotation.page) @@ -506,7 +546,7 @@ extension PDFCoordinator: PdfAnnotationsCoordinatorDelegate { } func showCellOptions( - for annotation: Annotation, + for annotation: PDFAnnotation, userId: Int, library: Library, sender: UIButton, @@ -518,9 +558,14 @@ extension PDFCoordinator: PdfAnnotationsCoordinatorDelegate { navigationController.overrideUserInterfaceStyle = userInterfaceStyle let coordinator = AnnotationEditCoordinator( - annotation: annotation, - userId: userId, - library: library, + data: AnnotationEditState.Data( + type: annotation.type, + isEditable: annotation.editability(currentUserId: userId, library: library) == .editable, + color: annotation.color, + lineWidth: annotation.lineWidth ?? 0, + pageLabel: annotation.pageLabel, + highlightText: annotation.text ?? "" + ), saveAction: saveAction, deleteAction: deleteAction, navigationController: navigationController, diff --git a/Zotero/Scenes/Detail/PDF/ViewModels/PDFReaderActionHandler.swift b/Zotero/Scenes/Detail/PDF/ViewModels/PDFReaderActionHandler.swift index 48cbe7606..cdaf137b2 100644 --- a/Zotero/Scenes/Detail/PDF/ViewModels/PDFReaderActionHandler.swift +++ b/Zotero/Scenes/Detail/PDF/ViewModels/PDFReaderActionHandler.swift @@ -222,13 +222,10 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi self.pdfThumbnailController.deleteAll(forKey: viewModel.state.key, libraryId: viewModel.state.library.identifier) case .setSettings(let settings, let userInterfaceStyle): - self.update(settings: settings, currentInterfaceStyle: userInterfaceStyle, in: viewModel) + self.update(settings: settings, parentInterfaceStyle: userInterfaceStyle, in: viewModel) case .changeIdleTimerDisabled(let disabled): - guard viewModel.state.settings.idleTimerDisabled != disabled else { return } - var settings = viewModel.state.settings - settings.idleTimerDisabled = disabled - self.update(settings: settings, currentInterfaceStyle: nil, in: viewModel) + changeIdleTimer(disabled: disabled, in: viewModel) case .setSidebarEditingEnabled(let enabled): self.setSidebar(editing: enabled, in: viewModel) @@ -353,7 +350,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi var color: String? // var rects: [CGRect]? - let hasSameProperties: (Annotation) -> Bool = { annotation in + let hasSameProperties: (PDFAnnotation) -> Bool = { annotation in // Check whether annotations of one type are selected if let type = type { if type != annotation.type { @@ -446,7 +443,24 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi } } - private func update(settings: PDFSettings, currentInterfaceStyle: UIUserInterfaceStyle?, in viewModel: ViewModel) { + private func changeIdleTimer(disabled: Bool, in viewModel: ViewModel) { + guard viewModel.state.settings.idleTimerDisabled != disabled else { return } + var settings = viewModel.state.settings + settings.idleTimerDisabled = disabled + + update(viewModel: viewModel) { state in + state.settings = settings + // Don't need to assign `changes` or update Defaults.shared.pdfSettings, this setting is not stored and doesn't change anything else + } + + if settings.idleTimerDisabled { + self.idleTimerController.disable() + } else { + self.idleTimerController.enable() + } + } + + private func update(settings: PDFSettings, parentInterfaceStyle: UIUserInterfaceStyle, in viewModel: ViewModel) { if viewModel.state.settings.idleTimerDisabled != settings.idleTimerDisabled { if settings.idleTimerDisabled { self.idleTimerController.disable() @@ -464,9 +478,20 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi Defaults.shared.pdfSettings = settings // Check whether interfaceStyle changed and update if needed - guard let newUserInterfaceStyle = currentInterfaceStyle.flatMap({ settings.appearanceMode.userInterfaceStyle(currentUserInterfaceStyle: $0) }), - newUserInterfaceStyle != viewModel.state.interfaceStyle else { return } - self.userInterfaceChanged(interfaceStyle: newUserInterfaceStyle, in: viewModel) + let settingsInterfaceStyle: UIUserInterfaceStyle + switch settings.appearanceMode { + case .dark: + settingsInterfaceStyle = .dark + + case .light: + settingsInterfaceStyle = .light + + case .automatic: + settingsInterfaceStyle = parentInterfaceStyle + } + + guard settingsInterfaceStyle != viewModel.state.interfaceStyle else { return } + self.userInterfaceChanged(interfaceStyle: settingsInterfaceStyle, in: viewModel) } private func set(page: Int, userActionFromDocument: Bool, fromThumbnailList: Bool, in viewModel: ViewModel) { @@ -628,9 +653,9 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi } } - typealias InkAnnotatationsData = (oldestAnnotation: Annotation, oldestDocumentAnnotation: PSPDFKit.InkAnnotation, lines: [[DrawingPoint]], lineWidth: CGFloat, tags: [Tag]) + typealias InkAnnotatationsData = (oldestAnnotation: PDFAnnotation, oldestDocumentAnnotation: PSPDFKit.InkAnnotation, lines: [[DrawingPoint]], lineWidth: CGFloat, tags: [Tag]) - private func merge(inkAnnotations annotations: [(Annotation, PSPDFKit.Annotation)], in viewModel: ViewModel) throws { + private func merge(inkAnnotations annotations: [(PDFAnnotation, PSPDFKit.Annotation)], in viewModel: ViewModel) throws { guard let (oldestAnnotation, oldestInkAnnotation, lines, lineWidth, tags) = self.collectInkAnnotationData(from: annotations, in: viewModel) else { return } if AnnotationSplitter.splitPathsIfNeeded(paths: lines) != nil { @@ -661,7 +686,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi self.set(tags: tags, key: oldestAnnotation.key, viewModel: viewModel) } - private func collectInkAnnotationData(from annotations: [(Annotation, PSPDFKit.Annotation)], in viewModel: ViewModel) -> InkAnnotatationsData? { + private func collectInkAnnotationData(from annotations: [(PDFAnnotation, PSPDFKit.Annotation)], in viewModel: ViewModel) -> InkAnnotatationsData? { guard let (oldestAnnotation, oldestDocumentAnnotation) = annotations.first, let oldestInkAnnotation = oldestDocumentAnnotation as? PSPDFKit.InkAnnotation else { return nil } var lines: [[DrawingPoint]] = oldestInkAnnotation.lines ?? [] @@ -807,8 +832,8 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi // } // } - private func groupedAnnotationsByPage(from keys: Set, state: PDFReaderState) -> [Int: [Annotation]] { - var groupedAnnotations: [Int: [Annotation]] = [:] + private func groupedAnnotationsByPage(from keys: Set, state: PDFReaderState) -> [Int: [PDFAnnotation]] { + var groupedAnnotations: [Int: [PDFAnnotation]] = [:] for key in keys { guard let annotation = state.annotation(for: key) else { continue } @@ -822,8 +847,8 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi return groupedAnnotations } - private func sortedSyncableAnnotationsAndDocumentAnnotations(from selected: Set, state: PDFReaderState) -> [(Annotation, PSPDFKit.Annotation)] { - var tuples: [(Annotation, PSPDFKit.Annotation)] = [] + private func sortedSyncableAnnotationsAndDocumentAnnotations(from selected: Set, state: PDFReaderState) -> [(PDFAnnotation, PSPDFKit.Annotation)] { + var tuples: [(PDFAnnotation, PSPDFKit.Annotation)] = [] for (page, annotations) in self.groupedAnnotationsByPage(from: selected, state: state) { let documentAnnotations = state.document.annotations(at: UInt(page)) @@ -923,7 +948,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi }) } - private func filter(annotation: Annotation, with term: String?, displayName: String, username: String) -> Bool { + private func filter(annotation: PDFAnnotation, with term: String?, displayName: String, username: String) -> Bool { guard let term = term else { return true } return annotation.key.lowercased() == term.lowercased() || annotation.author(displayName: displayName, username: username).localizedCaseInsensitiveContains(term) || @@ -932,7 +957,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi annotation.tags.contains(where: { $0.name.localizedCaseInsensitiveContains(term) }) } - private func filter(annotation: Annotation, with filter: AnnotationsFilter?) -> Bool { + private func filter(annotation: PDFAnnotation, with filter: AnnotationsFilter?) -> Bool { guard let filter = filter else { return true } let hasTag = filter.tags.isEmpty ? true : annotation.tags.contains(where: { filter.tags.contains($0.name) }) let hasColor = filter.colors.isEmpty ? true : filter.colors.contains(annotation.color) @@ -1235,7 +1260,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi } } - private func update(annotation: Annotation, color: (String, UIUserInterfaceStyle)? = nil, lineWidth: CGFloat? = nil, contents: String? = nil, in document: PSPDFKit.Document) { + private func update(annotation: PDFAnnotation, color: (String, UIUserInterfaceStyle)? = nil, lineWidth: CGFloat? = nil, contents: String? = nil, in document: PSPDFKit.Document) { guard let pdfAnnotation = document.annotations(at: PageIndex(annotation.page)).first(where: { $0.key == annotation.key }) else { return } var changes: PdfAnnotationChanges = [] @@ -1292,7 +1317,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi guard !finalAnnotations.isEmpty else { return } - let request = CreateAnnotationsDbRequest( + let request = CreatePDFAnnotationsDbRequest( attachmentKey: viewModel.state.key, libraryId: viewModel.state.library.identifier, annotations: finalAnnotations, @@ -1393,10 +1418,10 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi } } - private func splitIfNeededAndProcess(annotations: [PSPDFKit.Annotation], state: PDFReaderState) -> [DocumentAnnotation] { + private func splitIfNeededAndProcess(annotations: [PSPDFKit.Annotation], state: PDFReaderState) -> [PDFDocumentAnnotation] { var toRemove: [PSPDFKit.Annotation] = [] var toAdd: [PSPDFKit.Annotation] = [] - var documentAnnotations: [DocumentAnnotation] = [] + var documentAnnotations: [PDFDocumentAnnotation] = [] for annotation in annotations { guard let tool = self.tool(from: annotation), let activeColor = state.toolColors[tool] else { continue } @@ -1561,7 +1586,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi in viewModel: ViewModel ) -> (Int, (PDFReaderState.AnnotationKey, AnnotationDocumentLocation)?) { if let key = viewModel.state.selectedAnnotationKey, let item = databaseAnnotations.filter(.key(key.key)).first { - let annotation = DatabaseAnnotation(item: item) + let annotation = PDFDatabaseAnnotation(item: item) let page = annotation._page ?? storedPage let boundingBox = annotation.boundingBox(boundingBoxConverter: boundingBoxConverter) return (page, (key, (page, boundingBox))) @@ -1643,10 +1668,10 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi .disposed(by: self.pdfDisposeBag) } - private func createSortedKeys(fromDatabaseAnnotations databaseAnnotations: Results, documentAnnotations: [String: DocumentAnnotation]) -> [PDFReaderState.AnnotationKey] { + private func createSortedKeys(fromDatabaseAnnotations databaseAnnotations: Results, documentAnnotations: [String: PDFDocumentAnnotation]) -> [PDFReaderState.AnnotationKey] { var keys: [(PDFReaderState.AnnotationKey, String)] = [] for item in databaseAnnotations { - guard self.validate(databaseAnnotation: DatabaseAnnotation(item: item)) else { continue } + guard self.validate(databaseAnnotation: PDFDatabaseAnnotation(item: item)) else { continue } keys.append((PDFReaderState.AnnotationKey(key: item.key, type: .database), item.annotationSortIndex)) } for annotation in documentAnnotations.values { @@ -1659,7 +1684,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi return keys.map({ $0.0 }) } - private func validate(databaseAnnotation: DatabaseAnnotation) -> Bool { + private func validate(databaseAnnotation: PDFDatabaseAnnotation) -> Bool { if databaseAnnotation._page == nil { return false } @@ -1723,8 +1748,8 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi } } - private func loadAnnotations(from document: PSPDFKit.Document, library: Library, username: String, displayName: String) -> [String: DocumentAnnotation] { - var annotations: [String: DocumentAnnotation] = [:] + private func loadAnnotations(from document: PSPDFKit.Document, library: Library, username: String, displayName: String) -> [String: PDFDocumentAnnotation] { + var annotations: [String: PDFDocumentAnnotation] = [:] for (_, pdfAnnotations) in document.allAnnotations(of: AnnotationsConfig.supported) { for pdfAnnotation in pdfAnnotations { // Check whether square annotation was previously created by Zotero. If it's just "normal" square (instead of our image) annotation, don't convert it to Zotero annotation. @@ -1776,7 +1801,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi // Update database keys based on realm notification var updatedKeys: [PDFReaderState.AnnotationKey] = [] // Collect modified, deleted and inserted annotations to update the `Document` - var updatedPdfAnnotations: [(PSPDFKit.Annotation, DatabaseAnnotation)] = [] + var updatedPdfAnnotations: [(PSPDFKit.Annotation, PDFDatabaseAnnotation)] = [] var deletedPdfAnnotations: [PSPDFKit.Annotation] = [] var insertedPdfAnnotations: [PSPDFKit.Annotation] = [] @@ -1789,9 +1814,9 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi let key = keys[index] guard let item = objects.filter(.key(key.key)).first else { continue } - let annotation = DatabaseAnnotation(item: item) + let annotation = PDFDatabaseAnnotation(item: item) - if self.canUpdate(key: key, item: item, at: index, viewModel: viewModel) { + if canUpdate(key: key, item: item, at: index, viewModel: viewModel) { DDLogInfo("PDFReaderActionHandler: update key \(key)") updatedKeys.append(key) @@ -1827,7 +1852,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi selectionDeleted = true } - let oldAnnotation = DatabaseAnnotation(item: viewModel.state.databaseAnnotations[index]) + 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 } DDLogInfo("PDFReaderActionHandler: delete PDF annotation") deletedPdfAnnotations.append(pdfAnnotation) @@ -1849,7 +1874,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi keys.insert(PDFReaderState.AnnotationKey(key: item.key, type: .database), at: index) DDLogInfo("PDFReaderActionHandler: insert key \(item.key)") - let annotation = DatabaseAnnotation(item: item) + let annotation = PDFDatabaseAnnotation(item: item) switch item.changeType { case .user: @@ -1965,31 +1990,32 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi state.changes.insert(.sidebarEditing) } } - } - private func canUpdate(key: PDFReaderState.AnnotationKey, item: RItem, at index: Int, viewModel: ViewModel) -> Bool { - // If there was a sync type change, always update item - switch item.changeType { - case .sync: - // If sync happened and this item changed, always update item - return true + func canUpdate(key: PDFReaderState.AnnotationKey, item: RItem, at index: Int, viewModel: ViewModel) -> Bool { + // If there was a sync type change, always update item + switch item.changeType { + case .sync: + // If sync happened and this item changed, always update item + return true - case .syncResponse: - // This is a response to local changes being synced to backend, can be ignored - return false - case .user: break - } + case .syncResponse: + // This is a response to local changes being synced to backend, can be ignored + return false + + case .user: break + } - // Check whether selected annotation's comment is being edited. - guard viewModel.state.selectedAnnotationCommentActive && viewModel.state.selectedAnnotationKey == key else { return true } + // Check whether selected annotation's comment is being edited. + guard viewModel.state.selectedAnnotationCommentActive && viewModel.state.selectedAnnotationKey == key else { return true } - // Check whether the comment actually changed. - let newComment = item.fields.filter(.key(FieldKeys.Item.Annotation.comment)).first?.value - let oldComment = viewModel.state.databaseAnnotations[index].fields.filter(.key(FieldKeys.Item.Annotation.comment)).first?.value - return oldComment == newComment + // Check whether the comment actually changed. + let newComment = item.fields.filter(.key(FieldKeys.Item.Annotation.comment)).first?.value + let oldComment = viewModel.state.databaseAnnotations[index].fields.filter(.key(FieldKeys.Item.Annotation.comment)).first?.value + return oldComment == newComment + } } - private func update(pdfAnnotation: PSPDFKit.Annotation, with annotation: DatabaseAnnotation, parentKey: String, libraryId: LibraryIdentifier, interfaceStyle: UIUserInterfaceStyle) { + private func update(pdfAnnotation: PSPDFKit.Annotation, with annotation: PDFDatabaseAnnotation, parentKey: String, libraryId: LibraryIdentifier, interfaceStyle: UIUserInterfaceStyle) { guard let boundingBoxConverter = self.delegate else { return } var changes: PdfAnnotationChanges = [] diff --git a/Zotero/Scenes/Detail/PDF/Views/Annotation View/AnnotationView.swift b/Zotero/Scenes/Detail/PDF/Views/Annotation View/AnnotationView.swift index 15c9d20c1..31e30023e 100644 --- a/Zotero/Scenes/Detail/PDF/Views/Annotation View/AnnotationView.swift +++ b/Zotero/Scenes/Detail/PDF/Views/Annotation View/AnnotationView.swift @@ -57,13 +57,13 @@ final class AnnotationView: UIView { init(layout: AnnotationViewLayout, commentPlaceholder: String) { self.layout = layout - self.actionPublisher = PublishSubject() + actionPublisher = PublishSubject() super.init(frame: CGRect()) - self.backgroundColor = layout.backgroundColor - self.translatesAutoresizingMaskIntoConstraints = false - self.setupView(commentPlaceholder: commentPlaceholder) + backgroundColor = layout.backgroundColor + translatesAutoresizingMaskIntoConstraints = false + setupView(commentPlaceholder: commentPlaceholder) } required init?(coder: NSCoder) { @@ -74,16 +74,16 @@ final class AnnotationView: UIView { @discardableResult override func resignFirstResponder() -> Bool { - self.commentTextView.resignFirstResponder() + commentTextView.resignFirstResponder() } func updatePreview(image: UIImage?) { - guard let imageContent = self.imageContent, !imageContent.isHidden else { return } + guard let imageContent, !imageContent.isHidden else { return } imageContent.setup(with: image) } private func scrollToBottomIfNeeded() { - guard let scrollView = self.scrollView, let contentView = self.scrollViewContent, contentView.frame.height > scrollView.frame.height else { return } + guard let scrollView, let scrollViewContent, scrollViewContent.frame.height > scrollView.frame.height else { return } let yOffset = scrollView.contentSize.height - scrollView.bounds.height + scrollView.contentInset.bottom scrollView.setContentOffset(CGPoint(x: 0, y: yOffset), animated: true) } @@ -99,42 +99,89 @@ final class AnnotationView: UIView { /// - parameter library: Library of given annotation /// - parameter pdfAnnotationsCoordinatorDelegate: Delegate for getting share menu. /// - parameter state: State required for setting up share menu. - func setup(with annotation: Annotation, comment: Comment?, preview: UIImage?, selected: Bool, availableWidth: CGFloat, library: Library, currentUserId: Int, displayName: String, username: String, - boundingBoxConverter: AnnotationBoundingBoxConverter, pdfAnnotationsCoordinatorDelegate: PdfAnnotationsCoordinatorDelegate, state: PDFReaderState) { + func setup( + with annotation: PDFAnnotation, + comment: Comment?, + preview: UIImage?, + selected: Bool, + availableWidth: CGFloat, + library: Library, + currentUserId: Int, + displayName: String, + username: String, + boundingBoxConverter: AnnotationBoundingBoxConverter, + pdfAnnotationsCoordinatorDelegate: PdfAnnotationsCoordinatorDelegate, + state: PDFReaderState + ) { let editability = annotation.editability(currentUserId: currentUserId, library: library) let color = UIColor(hex: annotation.color) let canEdit = editability == .editable && selected + let author = library.identifier == .custom(.myLibrary) ? "" : annotation.author(displayName: displayName, username: username) - self.header.setup( - with: annotation, - libraryId: library.identifier, + header.setup( + type: annotation.type, + authorName: author, + pageLabel: annotation.pageLabel, + colorHex: annotation.color, shareMenuProvider: { button in pdfAnnotationsCoordinatorDelegate.createShareAnnotationMenu(state: state, annotation: annotation, sender: button) }, isEditable: (editability != .notEditable && selected), showsLock: editability != .editable, + accessibilityType: .cell + ) + setupContent( + for: annotation, + preview: preview, + color: color, + canEdit: canEdit, + selected: selected, + availableWidth: availableWidth, accessibilityType: .cell, - displayName: displayName, - username: username + boundingBoxConverter: boundingBoxConverter ) - self.setupContent(for: annotation, preview: preview, color: color, canEdit: canEdit, selected: selected, availableWidth: availableWidth, accessibilityType: .cell, boundingBoxConverter: boundingBoxConverter) - self.setup(comment: comment, canEdit: canEdit) - self.setupTags(for: annotation, canEdit: canEdit, accessibilityEnabled: selected) - self.setupObserving() + setup(comment: comment, canEdit: canEdit) + setup(tags: annotation.tags, canEdit: canEdit, accessibilityEnabled: selected) + setupObserving() - let commentButtonIsHidden = self.commentTextView.isHidden - let highlightContentIsHidden = self.highlightContent?.isHidden ?? true - let imageContentIsHidden = self.imageContent?.isHidden ?? true + let commentButtonIsHidden = commentTextView.isHidden + let highlightContentIsHidden = highlightContent?.isHidden ?? true + let imageContentIsHidden = imageContent?.isHidden ?? true // Top separator is hidden only if there is only header visible and nothing else - self.topSeparator.isHidden = self.commentTextView.isHidden && commentButtonIsHidden && highlightContentIsHidden && imageContentIsHidden && self.tags.isHidden && self.tagsButton.isHidden + topSeparator.isHidden = commentTextView.isHidden && commentButtonIsHidden && highlightContentIsHidden && imageContentIsHidden && tags.isHidden && tagsButton.isHidden // Bottom separator is visible, when tags are showing (either actual tags or tags button) and there is something visible above them (other than header, either content or comments/comments button) - self.bottomSeparator.isHidden = (self.tags.isHidden && self.tagsButton.isHidden) || (self.commentTextView.isHidden && commentButtonIsHidden && highlightContentIsHidden && imageContentIsHidden) + bottomSeparator.isHidden = (tags.isHidden && tagsButton.isHidden) || (commentTextView.isHidden && commentButtonIsHidden && highlightContentIsHidden && imageContentIsHidden) + } + + private func setupContent(type: AnnotationType, comment: String, text: String?, color: UIColor, canEdit: Bool, selected: Bool, availableWidth: CGFloat, accessibilityType: AccessibilityType) { + guard let highlightContent else { return } + + highlightContent.isUserInteractionEnabled = false + highlightContent.isHidden = type != .highlight + imageContent?.isHidden = true + + switch type { + case .highlight: + let bottomInset = inset(from: layout.highlightLineVerticalInsets, hasComment: !comment.isEmpty, selected: selected, canEdit: canEdit) + highlightContent.setup(with: color, text: (text ?? ""), bottomInset: bottomInset, accessibilityType: accessibilityType) + + case .image, .ink, .note: + break + } } - private func setupContent(for annotation: Annotation, preview: UIImage?, color: UIColor, canEdit: Bool, selected: Bool, availableWidth: CGFloat, accessibilityType: AccessibilityType, - boundingBoxConverter: AnnotationBoundingBoxConverter) { - guard let highlightContent = self.highlightContent, let imageContent = self.imageContent else { return } + private func setupContent( + for annotation: PDFAnnotation, + preview: UIImage?, + color: UIColor, + canEdit: Bool, + selected: Bool, + availableWidth: CGFloat, + accessibilityType: AccessibilityType, + boundingBoxConverter: AnnotationBoundingBoxConverter + ) { + guard let highlightContent, let imageContent else { return } highlightContent.isUserInteractionEnabled = false highlightContent.isHidden = annotation.type != .highlight @@ -144,19 +191,19 @@ final class AnnotationView: UIView { case .note: break case .highlight: - let bottomInset = self.inset(from: self.layout.highlightLineVerticalInsets, hasComment: !annotation.comment.isEmpty, selected: selected, canEdit: canEdit) + let bottomInset = inset(from: layout.highlightLineVerticalInsets, hasComment: !annotation.comment.isEmpty, selected: selected, canEdit: canEdit) highlightContent.setup(with: color, text: (annotation.text ?? ""), bottomInset: bottomInset, accessibilityType: accessibilityType) case .image, .ink: let size = annotation.previewBoundingBox(boundingBoxConverter: boundingBoxConverter).size - let maxWidth = availableWidth - (self.layout.horizontalInset * 2) + let maxWidth = availableWidth - (layout.horizontalInset * 2) var maxHeight = ceil((size.height / size.width) * maxWidth) if maxHeight.isNaN || maxHeight.isInfinite { maxHeight = maxWidth * 2 } else { maxHeight = min((maxWidth * 2), maxHeight) } - let bottomInset = self.inset(from: self.layout.verticalSpacerHeight, hasComment: !annotation.comment.isEmpty, selected: selected, canEdit: canEdit) + let bottomInset = inset(from: layout.verticalSpacerHeight, hasComment: !annotation.comment.isEmpty, selected: selected, canEdit: canEdit) imageContent.setup(with: preview, height: maxHeight, bottomInset: bottomInset) } } @@ -174,43 +221,43 @@ final class AnnotationView: UIView { private func setup(comment: Comment?, canEdit: Bool) { let isEmptyComment = (comment?.attributedString?.string ?? "").isEmpty guard let comment = comment, !isEmptyComment || canEdit else { - self.commentTextView.isHidden = true + commentTextView.isHidden = true return } - self.commentTextView.isHidden = false - self.commentTextView.isUserInteractionEnabled = canEdit + commentTextView.isHidden = false + commentTextView.isUserInteractionEnabled = canEdit // If comment is empty and not active, the input acts as a button. if isEmptyComment && !comment.isActive { - self.commentTextView.set(placeholderColor: Asset.Colors.zoteroBlue.color) - self.commentTextView.setup(text: nil) + commentTextView.set(placeholderColor: Asset.Colors.zoteroBlue.color) + commentTextView.setup(text: nil) return } // If there is any comment or the comment is active, the input acts as a text view with a placeholder. - let attributedString = comment.attributedString.flatMap({ AnnotationView.attributedString(from: $0, layout: self.layout) }) - self.commentTextView.set(placeholderColor: .placeholderText) - self.commentTextView.setup(text: attributedString) + let attributedString = comment.attributedString.flatMap({ AnnotationView.attributedString(from: $0, layout: layout) }) + commentTextView.set(placeholderColor: .placeholderText) + commentTextView.setup(text: attributedString) if canEdit && comment.isActive { - self.commentTextView.becomeFirstResponder() + commentTextView.becomeFirstResponder() } } - private func setupTags(for annotation: Annotation, canEdit: Bool, accessibilityEnabled: Bool) { - guard !annotation.tags.isEmpty else { - self.tagsButton.isHidden = !canEdit - self.tagsButton.accessibilityLabel = L10n.Pdf.AnnotationsSidebar.addTags - self.tagsButton.isAccessibilityElement = true + private func setup(tags: [Tag], canEdit: Bool, accessibilityEnabled: Bool) { + guard !tags.isEmpty else { + tagsButton.isHidden = !canEdit + tagsButton.accessibilityLabel = L10n.Pdf.AnnotationsSidebar.addTags + tagsButton.isAccessibilityElement = true self.tags.isHidden = true return } - let tagString = AnnotationView.attributedString(from: annotation.tags, layout: self.layout) + let tagString = AnnotationView.attributedString(from: tags, layout: layout) self.tags.setup(with: tagString) - self.tagsButton.isHidden = true + tagsButton.isHidden = true self.tags.isHidden = false self.tags.isUserInteractionEnabled = canEdit self.tags.button.isAccessibilityElement = true @@ -227,7 +274,7 @@ final class AnnotationView: UIView { @DisposeBag.DisposableBuilder private func buildDisposables() -> [Disposable] { - self.commentTextView.didBecomeActive.subscribe(onNext: { [weak self] _ in + commentTextView.didBecomeActive.subscribe(onNext: { [weak self] _ in self?.actionPublisher.on(.next(.setCommentActive(true))) }) commentTextView.textObservable @@ -240,62 +287,62 @@ final class AnnotationView: UIView { scrollToBottomIfNeeded() } }) - self.tags.tap.flatMap({ _ in Observable.just(Action.tags) }).bind(to: self.actionPublisher) - self.tagsButton.rx.tap.flatMap({ Observable.just(Action.tags) }).bind(to: self.actionPublisher) - self.header.menuTap.flatMap({ Observable.just(Action.options($0)) }).bind(to: self.actionPublisher) + tags.tap.flatMap({ _ in Observable.just(Action.tags) }).bind(to: actionPublisher) + tagsButton.rx.tap.flatMap({ Observable.just(Action.tags) }).bind(to: actionPublisher) + header.menuTap.flatMap({ Observable.just(Action.options($0)) }).bind(to: actionPublisher) } private func setupObserving() { var disposables: [Disposable] = buildDisposables() - if let doneTap = self.header.doneTap { - disposables.append(doneTap.flatMap({ Observable.just(Action.done) }).bind(to: self.actionPublisher)) + if let doneTap = header.doneTap { + disposables.append(doneTap.flatMap({ Observable.just(Action.done) }).bind(to: actionPublisher)) } disposeBag = CompositeDisposable(disposables: disposables) } private func setupView(commentPlaceholder: String) { - self.header = AnnotationViewHeader(layout: self.layout) - self.topSeparator = AnnotationViewSeparator() - self.commentTextView = AnnotationViewTextView(layout: self.layout, placeholder: commentPlaceholder) - self.commentTextView.accessibilityLabelPrefix = L10n.Accessibility.Pdf.comment + ": " - self.bottomSeparator = AnnotationViewSeparator() - self.tagsButton = AnnotationViewButton(layout: self.layout) - self.tagsButton.setTitle(L10n.Pdf.AnnotationsSidebar.addTags, for: .normal) - self.tags = AnnotationViewText(layout: self.layout) - - if self.layout.showsContent { - self.highlightContent = AnnotationViewHighlightContent(layout: self.layout) - self.imageContent = AnnotationViewImageContent(layout: self.layout) + header = AnnotationViewHeader(layout: layout) + topSeparator = AnnotationViewSeparator() + commentTextView = AnnotationViewTextView(layout: layout, placeholder: commentPlaceholder) + commentTextView.accessibilityLabelPrefix = L10n.Accessibility.Pdf.comment + ": " + bottomSeparator = AnnotationViewSeparator() + tagsButton = AnnotationViewButton(layout: layout) + tagsButton.setTitle(L10n.Pdf.AnnotationsSidebar.addTags, for: .normal) + tags = AnnotationViewText(layout: layout) + + if layout.showsContent { + highlightContent = AnnotationViewHighlightContent(layout: layout) + imageContent = AnnotationViewImageContent(layout: layout) } - let view = self.layout.scrollableBody ? self.createScrollableBodyView() : self.createStaticBodyView() - self.addSubview(view) + let view = layout.scrollableBody ? createScrollableBodyView() : createStaticBodyView() + addSubview(view) NSLayoutConstraint.activate([ - view.topAnchor.constraint(equalTo: self.topAnchor), - view.bottomAnchor.constraint(equalTo: self.bottomAnchor), - view.leadingAnchor.constraint(equalTo: self.leadingAnchor), - view.trailingAnchor.constraint(equalTo: self.trailingAnchor) + view.topAnchor.constraint(equalTo: topAnchor), + view.bottomAnchor.constraint(equalTo: bottomAnchor), + view.leadingAnchor.constraint(equalTo: leadingAnchor), + view.trailingAnchor.constraint(equalTo: trailingAnchor) ]) } private func createStaticBodyView() -> UIView { - var views: [UIView] = [self.header, self.topSeparator] - if self.layout.showsContent { - if let highlightContent = self.highlightContent { + var views: [UIView] = [header, topSeparator] + if layout.showsContent { + if let highlightContent { views.append(highlightContent) } - if let imageContent = self.imageContent { + if let imageContent { views.append(imageContent) } } - views.append(contentsOf: [self.commentTextView, self.bottomSeparator, self.tagsButton, self.tags]) - return self.createStackView(with: views) + views.append(contentsOf: [commentTextView, bottomSeparator, tagsButton, tags]) + return createStackView(with: views) } private func createScrollableBodyView() -> UIView { - let stackView = self.createStackView(with: [self.commentTextView, self.bottomSeparator, self.tagsButton, self.tags]) - self.scrollViewContent = stackView + let stackView = createStackView(with: [commentTextView, bottomSeparator, tagsButton, tags]) + scrollViewContent = stackView let scrollView = UIScrollView() scrollView.addSubview(stackView) @@ -304,8 +351,8 @@ final class AnnotationView: UIView { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(self.header) - view.addSubview(self.topSeparator) + view.addSubview(header) + view.addSubview(topSeparator) view.addSubview(scrollView) let height = stackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor) @@ -313,18 +360,18 @@ final class AnnotationView: UIView { NSLayoutConstraint.activate([ // Vertical - self.header.topAnchor.constraint(equalTo: view.topAnchor), - self.header.bottomAnchor.constraint(equalTo: self.topSeparator.topAnchor), - self.topSeparator.bottomAnchor.constraint(equalTo: scrollView.topAnchor), + header.topAnchor.constraint(equalTo: view.topAnchor), + header.bottomAnchor.constraint(equalTo: topSeparator.topAnchor), + topSeparator.bottomAnchor.constraint(equalTo: scrollView.topAnchor), scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), stackView.topAnchor.constraint(equalTo: scrollView.topAnchor), stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), height, // Horizontal - self.header.leadingAnchor.constraint(equalTo: view.leadingAnchor), - self.header.trailingAnchor.constraint(equalTo: view.trailingAnchor), - self.topSeparator.leadingAnchor.constraint(equalTo: view.leadingAnchor), - self.topSeparator.trailingAnchor.constraint(equalTo: view.trailingAnchor), + header.leadingAnchor.constraint(equalTo: view.leadingAnchor), + header.trailingAnchor.constraint(equalTo: view.trailingAnchor), + topSeparator.leadingAnchor.constraint(equalTo: view.leadingAnchor), + topSeparator.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), @@ -361,7 +408,7 @@ final class AnnotationView: UIView { static func attributedString(from tags: [Tag], layout: AnnotationViewLayout) -> NSAttributedString { let string = AttributedTagStringGenerator.attributedString(from: tags) - string.addAttribute(.paragraphStyle, value: self.paragraphStyle(for: layout), range: NSRange(location: 0, length: string.length)) + string.addAttribute(.paragraphStyle, value: paragraphStyle(for: layout), range: NSRange(location: 0, length: string.length)) return string } } diff --git a/Zotero/Scenes/Detail/PDF/Views/Annotation View/AnnotationViewHeader.swift b/Zotero/Scenes/Detail/PDF/Views/Annotation View/AnnotationViewHeader.swift index fe4c70bee..50a914427 100644 --- a/Zotero/Scenes/Detail/PDF/Views/Annotation View/AnnotationViewHeader.swift +++ b/Zotero/Scenes/Detail/PDF/Views/Annotation View/AnnotationViewHeader.swift @@ -112,22 +112,21 @@ final class AnnotationViewHeader: UIView { } func setup( - with annotation: Annotation, - libraryId: LibraryIdentifier, + type: AnnotationType, + authorName: String, + pageLabel: String, + colorHex: String, shareMenuProvider: @escaping ((UIButton) -> UIMenu?), isEditable: Bool, showsLock: Bool, - accessibilityType: AnnotationView.AccessibilityType, - displayName: String, - username: String + accessibilityType: AnnotationView.AccessibilityType ) { - let color = UIColor(hex: annotation.color) - let author = libraryId == .custom(.myLibrary) ? "" : annotation.author(displayName: displayName, username: username) + let color = UIColor(hex: colorHex) self.setup( - type: annotation.type, + type: type, color: color, - pageLabel: annotation.pageLabel, - author: author, + pageLabel: pageLabel, + author: authorName, shareMenu: shareMenuProvider(shareButton), showsMenuButton: isEditable, showsLock: showsLock, diff --git a/Zotero/Scenes/Detail/PDF/Views/AnnotationCell.swift b/Zotero/Scenes/Detail/PDF/Views/AnnotationCell.swift index b37b7ec92..252727aac 100644 --- a/Zotero/Scenes/Detail/PDF/Views/AnnotationCell.swift +++ b/Zotero/Scenes/Detail/PDF/Views/AnnotationCell.swift @@ -16,40 +16,40 @@ final class AnnotationCell: UITableViewCell { private weak var selectionView: UIView! var actionPublisher: PublishSubject { - return self.annotationView.actionPublisher + return annotationView.actionPublisher } var disposeBag: CompositeDisposable? { - return self.annotationView.disposeBag + return annotationView.disposeBag } // MARK: - Lifecycle override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - self.setupView() + setupView() } required init?(coder: NSCoder) { super.init(coder: coder) - self.setupView() + setupView() } override func prepareForReuse() { super.prepareForReuse() - self.key = "" + key = "" disposeBag?.dispose() } // MARK: - Actions func updatePreview(image: UIImage?) { - self.annotationView?.updatePreview(image: image) + annotationView?.updatePreview(image: image) } // MARK: - Setups private func setupView() { - self.backgroundColor = .systemGray6 + backgroundColor = .systemGray6 let selectionView = UIView() selectionView.backgroundColor = .systemGray6 @@ -64,66 +64,95 @@ final class AnnotationCell: UITableViewCell { annotationView.layer.masksToBounds = true self.annotationView = annotationView - self.contentView.addSubview(selectionView) - self.contentView.addSubview(annotationView) + contentView.addSubview(selectionView) + contentView.addSubview(annotationView) let selectionViewHorizontal = PDFReaderLayout.annotationLayout.horizontalInset - PDFReaderLayout.cellSelectionLineWidth let selectionViewBottom = PDFReaderLayout.cellSeparatorHeight - (PDFReaderLayout.cellSelectionLineWidth * 2) let annotationViewBottom = selectionViewBottom + PDFReaderLayout.cellSelectionLineWidth NSLayoutConstraint.activate([ - selectionView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: selectionViewHorizontal), - self.contentView.trailingAnchor.constraint(equalTo: selectionView.trailingAnchor, constant: selectionViewHorizontal), - selectionView.topAnchor.constraint(equalTo: self.contentView.topAnchor), - self.contentView.bottomAnchor.constraint(equalTo: selectionView.bottomAnchor, constant: selectionViewBottom), - annotationView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: PDFReaderLayout.annotationLayout.horizontalInset), - self.contentView.trailingAnchor.constraint(equalTo: annotationView.trailingAnchor, constant: PDFReaderLayout.annotationLayout.horizontalInset), - annotationView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: PDFReaderLayout.cellSelectionLineWidth), - self.contentView.bottomAnchor.constraint(equalTo: annotationView.bottomAnchor, constant: annotationViewBottom) + selectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: selectionViewHorizontal), + contentView.trailingAnchor.constraint(equalTo: selectionView.trailingAnchor, constant: selectionViewHorizontal), + selectionView.topAnchor.constraint(equalTo: contentView.topAnchor), + contentView.bottomAnchor.constraint(equalTo: selectionView.bottomAnchor, constant: selectionViewBottom), + annotationView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: PDFReaderLayout.annotationLayout.horizontalInset), + contentView.trailingAnchor.constraint(equalTo: annotationView.trailingAnchor, constant: PDFReaderLayout.annotationLayout.horizontalInset), + annotationView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: PDFReaderLayout.cellSelectionLineWidth), + contentView.bottomAnchor.constraint(equalTo: annotationView.bottomAnchor, constant: annotationViewBottom) ]) } - func setup(with annotation: Annotation, comment: AnnotationView.Comment?, preview: UIImage?, selected: Bool, availableWidth: CGFloat, library: Library, isEditing: Bool, currentUserId: Int, - displayName: String, username: String, boundingBoxConverter: AnnotationBoundingBoxConverter, pdfAnnotationsCoordinatorDelegate: PdfAnnotationsCoordinatorDelegate, state: PDFReaderState) { + func setup( + with annotation: PDFAnnotation, + comment: AnnotationView.Comment?, + preview: UIImage?, + selected: Bool, + availableWidth: CGFloat, + library: Library, + isEditing: Bool, + currentUserId: Int, + displayName: String, + username: String, + boundingBoxConverter: AnnotationBoundingBoxConverter, + pdfAnnotationsCoordinatorDelegate: PdfAnnotationsCoordinatorDelegate, + state: PDFReaderState + ) { if !selected { - self.annotationView.resignFirstResponder() + annotationView.resignFirstResponder() } - self.key = annotation.key - self.selectionView.layer.borderWidth = selected ? PDFReaderLayout.cellSelectionLineWidth : 0 + key = annotation.key + selectionView.layer.borderWidth = selected ? PDFReaderLayout.cellSelectionLineWidth : 0 let availableWidth = availableWidth - (PDFReaderLayout.annotationLayout.horizontalInset * 2) - self.annotationView.setup(with: annotation, comment: comment, preview: preview, selected: selected, availableWidth: availableWidth, library: library, currentUserId: currentUserId, - displayName: displayName, username: username, boundingBoxConverter: boundingBoxConverter, - pdfAnnotationsCoordinatorDelegate: pdfAnnotationsCoordinatorDelegate, state: state) - - self.setupAccessibility(for: annotation, selected: selected, currentUserId: currentUserId, displayName: displayName, username: username) + annotationView.setup( + with: annotation, + comment: comment, + preview: preview, + selected: selected, + availableWidth: availableWidth, + library: library, + currentUserId: currentUserId, + displayName: displayName, + username: username, + boundingBoxConverter: boundingBoxConverter, + pdfAnnotationsCoordinatorDelegate: pdfAnnotationsCoordinatorDelegate, + state: state + ) + + setupAccessibility( + isAuthor: annotation.isAuthor(currentUserId: currentUserId), + authorName: annotation.author(displayName: displayName, username: username), + type: annotation.type, + pageLabel: annotation.pageLabel, + text: annotation.text, + comment: annotation.comment, + selected: selected + ) } - private func setupAccessibility(for annotation: Annotation, selected: Bool, currentUserId: Int, displayName: String, username: String) { - let author = annotation.isAuthor(currentUserId: currentUserId) ? nil : annotation.author(displayName: displayName, username: username) - - var label = self.accessibilityLabel(for: annotation.type, pageLabel: annotation.pageLabel, author: author) - - if let text = annotation.text { + private func setupAccessibility(isAuthor: Bool, authorName: String, type: AnnotationType, pageLabel: String, text: String?, comment: String, selected: Bool) { + let author = isAuthor ? nil : authorName + var label = accessibilityLabel(for: type, pageLabel: pageLabel, author: author) + if let text { label += ", " + L10n.Accessibility.Pdf.highlightedText + ": " + text } - if !selected { - if !annotation.comment.isEmpty { - label += ", " + L10n.Accessibility.Pdf.comment + ": " + annotation.comment + if !comment.isEmpty { + label += ", " + L10n.Accessibility.Pdf.comment + ": " + comment } - if let tags = self.annotationView.tagString, !tags.isEmpty { + if let tags = annotationView.tagString, !tags.isEmpty { label += ", " + L10n.Accessibility.Pdf.tags + ": " + tags } } - self.isAccessibilityElement = false - self.accessibilityLabel = label - self.accessibilityTraits = .button + isAccessibilityElement = false + accessibilityLabel = label + accessibilityTraits = .button if selected { - self.accessibilityHint = nil + accessibilityHint = nil } else { - self.accessibilityHint = L10n.Accessibility.Pdf.annotationHint + accessibilityHint = L10n.Accessibility.Pdf.annotationHint } } diff --git a/Zotero/Scenes/Detail/PDF/Views/AnnotationToolOptionsViewController.swift b/Zotero/Scenes/Detail/PDF/Views/AnnotationToolOptionsViewController.swift index d1f265217..13ad0c7dc 100644 --- a/Zotero/Scenes/Detail/PDF/Views/AnnotationToolOptionsViewController.swift +++ b/Zotero/Scenes/Detail/PDF/Views/AnnotationToolOptionsViewController.swift @@ -120,12 +120,12 @@ class AnnotationToolOptionsViewController: UIViewController { view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: Self.horizontalInset) ]) - func colors(for tool: PSPDFKit.Annotation.Tool) -> [String] { + func colors(for tool: AnnotationTool) -> [String] { switch tool { case .ink: return AnnotationsConfig.colors(for: .ink) case .note: return AnnotationsConfig.colors(for: .note) case .highlight: return AnnotationsConfig.colors(for: .highlight) - case .square: return AnnotationsConfig.colors(for: .image) + case .image: return AnnotationsConfig.colors(for: .image) default: return [] } } diff --git a/Zotero/Scenes/Detail/PDF/Views/AnnotationToolbarHandler.swift b/Zotero/Scenes/Detail/PDF/Views/AnnotationToolbarHandler.swift new file mode 100644 index 000000000..33b7d686d --- /dev/null +++ b/Zotero/Scenes/Detail/PDF/Views/AnnotationToolbarHandler.swift @@ -0,0 +1,811 @@ +// +// AnnotationToolbarHandler.swift +// Zotero +// +// Created by Michal Rentka on 06.09.2023. +// Copyright © 2023 Corporation for Digital Scholarship. All rights reserved. +// + +import UIKit + +import RxSwift + +protocol AnnotationToolbarHandlerDelegate: AnyObject { + var toolbarState: AnnotationToolbarHandler.State { get set } + var statusBarVisible: Bool { get set } + var statusBarHeight: CGFloat { get set } + var isNavigationBarHidden: Bool { get } + var navigationBarHeight: CGFloat { get } + var isSidebarHidden: Bool { get } + var isCompactWidth: Bool { get } + var containerView: UIView { get } + var documentView: UIView { get } + var toolbarLeadingAnchor: NSLayoutXAxisAnchor { get } + var toolbarLeadingSafeAreaAnchor: NSLayoutXAxisAnchor { get } + + func layoutIfNeeded() + func setNeedsLayout() + func hideSidebarIfNeeded(forPosition position: AnnotationToolbarHandler.State.Position, isToolbarSmallerThanMinWidth: Bool, animated: Bool) + func setNavigationBar(hidden: Bool, animated: Bool) + func setNavigationBar(alpha: CGFloat) + func setDocumentInterface(hidden: Bool) + func topDidChange(forToolbarState state: AnnotationToolbarHandler.State) + func updateStatusBar() +} + +final class AnnotationToolbarHandler: NSObject { + struct State: Codable { + enum Position: Int, Codable { + case leading = 0 + case trailing = 1 + case top = 2 + case pinned = 3 + } + + let position: Position + let visible: Bool + } + + private unowned let controller: AnnotationToolbarViewController + private unowned let delegate: AnnotationToolbarHandlerDelegate + static let toolbarCompactInset: CGFloat = 12 + static let toolbarFullInsetInset: CGFloat = 20 + static let minToolbarWidth: CGFloat = 300 + static let annotationToolbarDragHandleHeight: CGFloat = 50 + private let previewBackgroundColor: UIColor + private let previewDashColor: UIColor + private let previewSelectedBackgroundColor: UIColor + private let previewSelectedDashColor: UIColor + private let disposeBag: DisposeBag + + private var toolbarInitialFrame: CGRect? + private weak var toolbarTop: NSLayoutConstraint! + private var toolbarLeading: NSLayoutConstraint! + private var toolbarLeadingSafeArea: NSLayoutConstraint! + private var toolbarTrailing: NSLayoutConstraint! + private var toolbarTrailingSafeArea: NSLayoutConstraint! + weak var dragHandleLongPressRecognizer: UILongPressGestureRecognizer! + private weak var toolbarPreviewsOverlay: UIView! + private weak var toolbarLeadingPreview: DashedView! + private weak var inbetweenTopDashedView: DashedView! + private weak var toolbarLeadingPreviewHeight: NSLayoutConstraint! + private weak var toolbarTrailingPreview: DashedView! + private weak var toolbarTrailingPreviewHeight: NSLayoutConstraint! + private weak var toolbarTopPreview: DashedView! + private weak var toolbarPinnedPreview: DashedView! + private weak var toolbarPinnedPreviewHeight: NSLayoutConstraint! + + var stateDidChange: ((State) -> Void)? + var didHide: (() -> Void)? + + init(controller: AnnotationToolbarViewController, delegate: AnnotationToolbarHandlerDelegate) { + self.controller = controller + self.delegate = delegate + previewDashColor = Asset.Colors.zoteroBlueWithDarkMode.color.withAlphaComponent(0.5) + previewBackgroundColor = Asset.Colors.zoteroBlueWithDarkMode.color.withAlphaComponent(0.15) + previewSelectedDashColor = Asset.Colors.zoteroBlueWithDarkMode.color + previewSelectedBackgroundColor = Asset.Colors.zoteroBlueWithDarkMode.color.withAlphaComponent(0.5) + disposeBag = DisposeBag() + + super.init() + + setupController() + createViewsWithConstraints() + createGestureRecognizers() + + func createGestureRecognizers() { + let panRecognizer = UIPanGestureRecognizer() + panRecognizer.delegate = self + panRecognizer.rx.event + .subscribe(with: self, onNext: { `self`, recognizer in + self.toolbarDidPan(recognizer: recognizer) + }) + .disposed(by: disposeBag) + controller.view.addGestureRecognizer(panRecognizer) + + let longPressRecognizer = UILongPressGestureRecognizer() + longPressRecognizer.delegate = self + longPressRecognizer.rx.event + .subscribe(with: self, onNext: { `self`, recognizer in + self.didTapToolbar(recognizer: recognizer) + }) + .disposed(by: disposeBag) + dragHandleLongPressRecognizer = longPressRecognizer + controller.view.addGestureRecognizer(longPressRecognizer) + } + + func createViewsWithConstraints() { + let previewsOverlay = UIView() + previewsOverlay.translatesAutoresizingMaskIntoConstraints = false + previewsOverlay.backgroundColor = .clear + previewsOverlay.isHidden = true + + let topPreview = DashedView(type: .partialStraight(sides: [.left, .right, .bottom])) + setup(toolbarPositionView: topPreview) + let inbetweenTopDash = DashedView(type: .partialStraight(sides: .bottom)) + setup(toolbarPositionView: inbetweenTopDash) + let pinnedPreview = DashedView(type: .partialStraight(sides: [.left, .right, .top])) + setup(toolbarPositionView: pinnedPreview) + let leadingPreview = DashedView(type: .rounded(cornerRadius: 8)) + leadingPreview.translatesAutoresizingMaskIntoConstraints = false + setup(toolbarPositionView: leadingPreview) + let trailingPreview = DashedView(type: .rounded(cornerRadius: 8)) + trailingPreview.translatesAutoresizingMaskIntoConstraints = false + setup(toolbarPositionView: trailingPreview) + + let topPreviewContainer = UIStackView(arrangedSubviews: [pinnedPreview, inbetweenTopDash, topPreview]) + topPreviewContainer.translatesAutoresizingMaskIntoConstraints = false + topPreviewContainer.axis = .vertical + + delegate.documentView.insertSubview(previewsOverlay, belowSubview: controller.view) + previewsOverlay.addSubview(topPreviewContainer) + previewsOverlay.addSubview(leadingPreview) + previewsOverlay.addSubview(trailingPreview) + + toolbarLeading = controller.view.leadingAnchor.constraint(equalTo: delegate.toolbarLeadingAnchor, constant: Self.toolbarFullInsetInset) + toolbarLeading.priority = .init(999) + toolbarLeadingSafeArea = controller.view.leadingAnchor.constraint(equalTo: delegate.toolbarLeadingSafeAreaAnchor) + toolbarTrailing = delegate.containerView.trailingAnchor.constraint(equalTo: controller.view.trailingAnchor, constant: Self.toolbarFullInsetInset) + toolbarTrailingSafeArea = delegate.containerView.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: controller.view.trailingAnchor, constant: Self.toolbarFullInsetInset) + let _toolbarTop = controller.view.topAnchor.constraint(equalTo: delegate.containerView.topAnchor, constant: Self.toolbarCompactInset) + let leadingPreviewHeight = leadingPreview.heightAnchor.constraint(equalToConstant: 50) + let trailingPreviewHeight = trailingPreview.heightAnchor.constraint(equalToConstant: 50) + let pinnedPreviewHeight = pinnedPreview.heightAnchor.constraint(equalToConstant: controller.size) + + NSLayoutConstraint.activate([ + _toolbarTop, + toolbarLeadingSafeArea, + previewsOverlay.topAnchor.constraint(equalTo: delegate.containerView.topAnchor), + previewsOverlay.bottomAnchor.constraint(equalTo: delegate.containerView.safeAreaLayoutGuide.bottomAnchor), + previewsOverlay.leadingAnchor.constraint(equalTo: delegate.documentView.leadingAnchor), + previewsOverlay.trailingAnchor.constraint(equalTo: delegate.containerView.trailingAnchor), + topPreviewContainer.topAnchor.constraint(equalTo: previewsOverlay.topAnchor), + topPreviewContainer.leadingAnchor.constraint(equalTo: previewsOverlay.leadingAnchor), + previewsOverlay.trailingAnchor.constraint(equalTo: topPreviewContainer.trailingAnchor), + pinnedPreviewHeight, + topPreview.heightAnchor.constraint(equalToConstant: controller.size), + leadingPreview.leadingAnchor.constraint(equalTo: previewsOverlay.safeAreaLayoutGuide.leadingAnchor, constant: Self.toolbarFullInsetInset), + leadingPreview.topAnchor.constraint(equalTo: topPreviewContainer.bottomAnchor, constant: Self.toolbarCompactInset), + leadingPreviewHeight, + leadingPreview.widthAnchor.constraint(equalToConstant: controller.size), + previewsOverlay.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: trailingPreview.trailingAnchor, constant: Self.toolbarFullInsetInset), + trailingPreview.topAnchor.constraint(equalTo: topPreviewContainer.bottomAnchor, constant: Self.toolbarCompactInset), + trailingPreviewHeight, + trailingPreview.widthAnchor.constraint(equalToConstant: controller.size), + inbetweenTopDash.heightAnchor.constraint(equalToConstant: 2 / UIScreen.main.scale) + ]) + + toolbarTop = _toolbarTop + toolbarPreviewsOverlay = previewsOverlay + toolbarTopPreview = topPreview + toolbarPinnedPreview = pinnedPreview + toolbarLeadingPreview = leadingPreview + toolbarLeadingPreviewHeight = leadingPreviewHeight + toolbarTrailingPreview = trailingPreview + toolbarTrailingPreviewHeight = trailingPreviewHeight + toolbarPinnedPreviewHeight = pinnedPreviewHeight + inbetweenTopDashedView = inbetweenTopDash + } + + func setupController() { + controller.view.translatesAutoresizingMaskIntoConstraints = false + controller.view.setContentHuggingPriority(.required, for: .horizontal) + controller.view.setContentHuggingPriority(.required, for: .vertical) + } + + func setup(toolbarPositionView view: DashedView) { + view.backgroundColor = previewBackgroundColor + view.dashColor = previewDashColor + view.layer.masksToBounds = true + } + } + + // MARK: - Layout + + func performInitialLayout() { + var rotation: AnnotationToolbarViewController.Rotation + switch delegate.toolbarState.position { + case .leading, .trailing: + rotation = .vertical + + case .top, .pinned: + rotation = .horizontal + } + controller.set(rotation: rotation, isCompactSize: isCompactSize(for: rotation)) + delegate.layoutIfNeeded() + } + + func viewIsAppearing(documentIsLocked: Bool) { + self.setConstraints(for: self.delegate.toolbarState.position, statusBarVisible: self.delegate.statusBarVisible) + self.delegate.topDidChange(forToolbarState: self.delegate.toolbarState) + self.setAnnotationToolbarHandleMinimumLongPressDuration(forPosition: self.delegate.toolbarState.position) + if self.delegate.toolbarState.visible && !documentIsLocked { + self.showAnnotationToolbar(state: self.delegate.toolbarState, statusBarVisible: self.delegate.statusBarVisible, animated: false) + } else { + self.hideAnnotationToolbar(newState: self.delegate.toolbarState, statusBarVisible: self.delegate.statusBarVisible, animated: false) + } + } + + func viewWillTransitionToNewSize() { + self.controller.prepareForSizeChange() + self.controller.updateAdditionalButtons() + self.setConstraints(for: self.delegate.toolbarState.position, statusBarVisible: self.delegate.statusBarVisible) + self.delegate.topDidChange(forToolbarState: self.delegate.toolbarState) + self.delegate.layoutIfNeeded() + self.controller.sizeDidChange() + } + + func interfaceVisibilityDidChange() { + self.delegate.topDidChange(forToolbarState: self.delegate.toolbarState) + self.setConstraints(for: self.delegate.toolbarState.position, statusBarVisible: self.delegate.statusBarVisible) + } + + private func isCompactSize(for rotation: AnnotationToolbarViewController.Rotation) -> Bool { + switch rotation { + case .horizontal: + return self.delegate.isCompactWidth + + case .vertical: + return self.delegate.containerView.frame.height <= 400 + } + } + + func topOffsets(statusBarVisible: Bool) -> (statusBarHeight: CGFloat, navigationBarHeight: CGFloat, total: CGFloat) { + let statusBarOffset = statusBarVisible || UIDevice.current.userInterfaceIdiom != .pad ? self.delegate.statusBarHeight : 0 + let navigationBarOffset = statusBarVisible ? self.delegate.navigationBarHeight : 0 + return (statusBarOffset, navigationBarOffset, statusBarOffset + navigationBarOffset) + } + + // MARK: - Actions + + func enableLeadingSafeConstraint() { + self.toolbarLeading.isActive = false + self.toolbarLeadingSafeArea.isActive = true + } + + func disableLeadingSafeConstraint() { + self.toolbarLeadingSafeArea.isActive = false + self.toolbarLeading.isActive = true + } + + func set(hidden: Bool, animated: Bool) { + self.delegate.toolbarState = State(position: self.delegate.toolbarState.position, visible: !hidden) + + if hidden { + self.hideAnnotationToolbar(newState: self.delegate.toolbarState, statusBarVisible: self.delegate.statusBarVisible, animated: animated) + } else { + self.showAnnotationToolbar(state: self.delegate.toolbarState, statusBarVisible: self.delegate.statusBarVisible, animated: animated) + } + } + + private func showAnnotationToolbar(state: State, statusBarVisible: Bool, animated: Bool) { + self.controller.prepareForSizeChange() + self.setConstraints(for: state.position, statusBarVisible: statusBarVisible) + self.controller.view.isHidden = false + self.delegate.layoutIfNeeded() + self.controller.sizeDidChange() + self.delegate.layoutIfNeeded() + self.delegate.topDidChange(forToolbarState: state) + + self.delegate.hideSidebarIfNeeded(forPosition: state.position, isToolbarSmallerThanMinWidth: self.controller.view.frame.width < Self.minToolbarWidth, animated: animated) + + let navigationBarHidden = !statusBarVisible || state.position == .pinned + + if !animated { + self.controller.view.alpha = 1 + self.delegate.setNavigationBar(hidden: navigationBarHidden, animated: false) + self.delegate.setNavigationBar(alpha: navigationBarHidden ? 0 : 1) + self.delegate.layoutIfNeeded() + return + } + + if !navigationBarHidden && self.delegate.isNavigationBarHidden { + self.delegate.setNavigationBar(hidden: false, animated: false) + self.delegate.setNavigationBar(alpha: 0) + } + + UIView.animate(withDuration: 0.2, animations: { + self.controller.view.alpha = 1 + self.delegate.setNavigationBar(alpha: navigationBarHidden ? 0 : 1) + self.delegate.layoutIfNeeded() + }, completion: { finished in + guard finished && navigationBarHidden else { return } + self.delegate.setNavigationBar(hidden: true, animated: false) + }) + } + + private func hideAnnotationToolbar(newState: State, statusBarVisible: Bool, animated: Bool) { + self.delegate.topDidChange(forToolbarState: newState) + + if !animated { + self.delegate.layoutIfNeeded() + self.controller.view.alpha = 0 + self.controller.view.isHidden = true + self.delegate.setNavigationBar(alpha: statusBarVisible ? 1 : 0) + self.delegate.setNavigationBar(hidden: !statusBarVisible, animated: false) + return + } + + if statusBarVisible && self.delegate.isNavigationBarHidden { + self.delegate.setNavigationBar(hidden: false, animated: false) + self.delegate.setNavigationBar(alpha: 0) + } + + UIView.animate(withDuration: 0.2, animations: { + self.delegate.layoutIfNeeded() + self.controller.view.alpha = 0 + self.delegate.setNavigationBar(alpha: statusBarVisible ? 1 : 0) + }, completion: { finished in + guard finished else { return } + self.controller.view.isHidden = true + self.didHide?() + if !statusBarVisible { + self.delegate.setNavigationBar(hidden: true, animated: false) + } + }) + } + + private func setConstraints(for position: State.Position, statusBarVisible: Bool) { + let rotation: AnnotationToolbarViewController.Rotation = (position == .top || position == .pinned) ? .horizontal : .vertical + if self.isCompactSize(for: rotation) { + self.setCompactConstraints(for: position, statusBarVisible: statusBarVisible) + } else { + self.setFullConstraints(for: position, statusBarVisible: statusBarVisible) + } + } + + private func setFullConstraints(for position: State.Position, statusBarVisible: Bool) { + switch position { + case .leading: + self.toolbarTrailing.isActive = false + self.toolbarTrailingSafeArea.isActive = false + if !self.delegate.isSidebarHidden { + self.toolbarLeadingSafeArea.isActive = false + self.toolbarLeading.isActive = true + self.toolbarLeading.constant = Self.toolbarFullInsetInset + } else { + self.toolbarLeading.isActive = false + self.toolbarLeadingSafeArea.isActive = true + self.toolbarLeadingSafeArea.constant = Self.toolbarFullInsetInset + } + self.toolbarTop.constant = Self.toolbarFullInsetInset + self.topOffsets(statusBarVisible: statusBarVisible).total + self.controller.set(rotation: .vertical, isCompactSize: false) + + case .trailing: + self.toolbarLeading.isActive = false + self.toolbarLeadingSafeArea.isActive = false + self.toolbarTrailing.isActive = false + self.toolbarTrailingSafeArea.isActive = true + self.toolbarTrailingSafeArea.constant = Self.toolbarFullInsetInset + self.toolbarTop.constant = Self.toolbarFullInsetInset + self.topOffsets(statusBarVisible: statusBarVisible).total + self.controller.set(rotation: .vertical, isCompactSize: false) + + case .top: + self.setupTopConstraints(isCompact: false, isPinned: false, statusBarVisible: statusBarVisible) + + case .pinned: + self.setupTopConstraints(isCompact: false, isPinned: true, statusBarVisible: statusBarVisible) + } + } + + private func setCompactConstraints(for position: State.Position, statusBarVisible: Bool) { + switch position { + case .leading: + self.toolbarTrailing.isActive = false + self.toolbarTrailingSafeArea.isActive = false + if !self.delegate.isSidebarHidden { + self.toolbarLeadingSafeArea.isActive = false + self.toolbarLeading.isActive = true + self.toolbarLeading.constant = Self.toolbarCompactInset + } else { + self.toolbarLeading.isActive = false + self.toolbarLeadingSafeArea.isActive = true + self.toolbarLeadingSafeArea.constant = Self.toolbarCompactInset + } + self.toolbarTop.constant = Self.toolbarCompactInset + self.topOffsets(statusBarVisible: statusBarVisible).total + self.controller.set(rotation: .vertical, isCompactSize: true) + + case .trailing: + self.toolbarLeading.isActive = false + self.toolbarLeadingSafeArea.isActive = false + self.toolbarTrailing.isActive = false + self.toolbarTrailingSafeArea.isActive = true + self.toolbarTrailingSafeArea.constant = Self.toolbarCompactInset + self.toolbarTop.constant = Self.toolbarCompactInset + self.topOffsets(statusBarVisible: statusBarVisible).total + self.controller.set(rotation: .vertical, isCompactSize: true) + + case .top: + self.setupTopConstraints(isCompact: true, isPinned: false, statusBarVisible: statusBarVisible) + + case .pinned: + self.setupTopConstraints(isCompact: true, isPinned: true, statusBarVisible: statusBarVisible) + } + } + + private func setupTopConstraints(isCompact: Bool, isPinned: Bool, statusBarVisible: Bool) { + self.toolbarLeadingSafeArea.isActive = false + self.toolbarTrailingSafeArea.isActive = false + self.toolbarTrailing.isActive = true + self.toolbarTrailing.constant = 0 + self.toolbarLeading.isActive = true + self.toolbarLeading.constant = 0 + self.toolbarTop.constant = isPinned ? self.topOffsets(statusBarVisible: statusBarVisible).statusBarHeight : self.topOffsets(statusBarVisible: statusBarVisible).total + self.controller.set(rotation: .horizontal, isCompactSize: isCompact) + } + + // MARK: - Gesture recognizers + + private func isSwipe(fromVelocity velocity: CGPoint) -> Bool { + return velocity.y <= -1500 || abs(velocity.x) >= 1500 + } + + /// Return new position for given touch point and velocity of toolbar. The user can pan up/left/right to move the toolbar. If velocity > 1500, it's considered a swipe and the toolbar is moved + /// in swipe direction. Otherwise the toolbar is pinned to closest point from touch. + private func position(fromTouch point: CGPoint, frame: CGRect, containerFrame: CGRect, velocity: CGPoint, statusBarVisible: Bool) -> State.Position { + if self.isSwipe(fromVelocity: velocity) { + // Move in direction of swipe + if abs(velocity.y) > abs(velocity.x) && containerFrame.size.width >= Self.minToolbarWidth { + return .top + } + return velocity.x < 0 ? .leading : .trailing + } + + let topViewBottomRightPoint = self.toolbarTopPreview.convert(CGPoint(x: self.toolbarTopPreview.bounds.maxX, y: self.toolbarTopPreview.bounds.maxY), to: self.delegate.containerView) + + if point.y < topViewBottomRightPoint.y { + let pinnedViewBottomRightPoint = self.toolbarPinnedPreview.convert(CGPoint(x: self.toolbarPinnedPreview.frame.maxX, y: self.toolbarPinnedPreview.frame.maxY), to: self.delegate.containerView) + return point.y < pinnedViewBottomRightPoint.y ? .pinned : .top + } + + let xPos = point.x - containerFrame.minX + + if point.y < (topViewBottomRightPoint.y + 150) { + if xPos > 150 && xPos < (containerFrame.size.width - 150) { + return .top + } + return xPos <= 150 ? .leading : .trailing + } + + return xPos > containerFrame.size.width / 2 ? .trailing : .leading + } + + private func velocity(from panVelocity: CGPoint, newPosition: State.Position) -> CGFloat { + let currentPosition: CGFloat + let endPosition: CGFloat + let velocity: CGFloat + + switch newPosition { + case .top: + velocity = panVelocity.y + currentPosition = self.controller.view.frame.minY + endPosition = self.delegate.containerView.safeAreaInsets.top + + case .leading: + velocity = panVelocity.x + currentPosition = self.controller.view.frame.minX + endPosition = 0 + + case .trailing: + velocity = panVelocity.x + currentPosition = self.controller.view.frame.maxX + endPosition = self.delegate.containerView.frame.width + + case .pinned: + velocity = panVelocity.y + currentPosition = self.controller.view.frame.minY + endPosition = self.delegate.containerView.safeAreaInsets.top + } + + return abs(velocity / (endPosition - currentPosition)) + } + + func didTapToolbar(recognizer: UILongPressGestureRecognizer) { + switch recognizer.state { + case .began: + self.setHighlightSelected(at: self.delegate.toolbarState.position) + self.showPreviews() + + case .ended, .failed: + self.hidePreviewsIfNeeded() + + default: break + } + } + + func toolbarDidPan(recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.toolbarInitialFrame = self.controller.view.frame + + case .changed: + guard let originalFrame = self.toolbarInitialFrame else { return } + let translation = recognizer.translation(in: self.controller.view) + let location = recognizer.location(in: self.delegate.containerView) + let position = self.position( + fromTouch: location, + frame: self.controller.view.frame, + containerFrame: self.delegate.documentView.frame, + velocity: CGPoint(), + statusBarVisible: self.delegate.statusBarVisible + ) + + self.controller.view.frame = originalFrame.offsetBy(dx: translation.x, dy: translation.y) + + self.showPreviewsOnDragIfNeeded(translation: translation, velocity: recognizer.velocity(in: self.delegate.containerView), currentPosition: self.delegate.toolbarState.position) + + if !self.toolbarPreviewsOverlay.isHidden { + self.setHighlightSelected(at: position) + } + + case .ended, .failed: + let velocity = recognizer.velocity(in: self.delegate.containerView) + let location = recognizer.location(in: self.delegate.containerView) + let position = self.position( + fromTouch: location, + frame: self.controller.view.frame, + containerFrame: self.delegate.documentView.frame, + velocity: velocity, + statusBarVisible: self.delegate.statusBarVisible + ) + let newState = State(position: position, visible: true) + + if position == .top && self.delegate.toolbarState.position == .pinned { + self.delegate.statusBarVisible = true + } + self.set(toolbarPosition: position, oldPosition: self.delegate.toolbarState.position, velocity: velocity, statusBarVisible: self.delegate.statusBarVisible) + self.setAnnotationToolbarHandleMinimumLongPressDuration(forPosition: position) + self.delegate.toolbarState = newState + self.toolbarInitialFrame = nil + + default: break + } + } + + private func setAnnotationToolbarHandleMinimumLongPressDuration(forPosition position: State.Position) { + switch position { + case .leading, .trailing: + self.dragHandleLongPressRecognizer.minimumPressDuration = 0.3 + + case .top, .pinned: + self.dragHandleLongPressRecognizer.minimumPressDuration = 0 + } + } + + // MARK: - Previews + + private func showPreviewsOnDragIfNeeded(translation: CGPoint, velocity: CGPoint, currentPosition: State.Position) { + guard self.toolbarPreviewsOverlay.isHidden else { return } + + let distance = sqrt((translation.x * translation.x) + (translation.y * translation.y)) + let distanceThreshold: CGFloat = (currentPosition == .pinned || currentPosition == .top) ? 0 : 70 + + guard distance > distanceThreshold && !self.isSwipe(fromVelocity: velocity) else { return } + + self.showPreviews() + } + + private func showPreviews() { + self.updatePositionOverlayViews( + currentHeight: self.controller.view.frame.height, + containerSize: self.delegate.documentView.frame.size, + position: self.delegate.toolbarState.position, + statusBarVisible: self.delegate.statusBarVisible + ) + self.toolbarPreviewsOverlay.alpha = 0 + self.toolbarPreviewsOverlay.isHidden = false + + UIView.animate(withDuration: 0.2, animations: { + self.toolbarPreviewsOverlay.alpha = 1 + self.delegate.setNavigationBar(alpha: 0) + }) + } + + private func hidePreviewsIfNeeded() { + guard self.toolbarPreviewsOverlay.alpha == 1 else { return } + + UIView.animate(withDuration: 0.2, animations: { + self.delegate.setNavigationBar(alpha: 1) + self.toolbarPreviewsOverlay.alpha = 0 + }, completion: { finished in + guard finished else { return } + self.toolbarPreviewsOverlay.isHidden = true + }) + } + + private func updatePositionOverlayViews(currentHeight: CGFloat, containerSize: CGSize, position: State.Position, statusBarVisible: Bool) { + let topToolbarsAvailable = containerSize.width >= Self.minToolbarWidth + let verticalHeight: CGFloat + switch position { + case .leading, .trailing: + // Position the preview so that the bottom of preview matches actual bottom of toolbar, add offset for dashed border + let offset = self.controller.size + (statusBarVisible ? 0 : self.controller.size) + verticalHeight = currentHeight - offset + (DashedView.dashWidth * 2) + 1 + + case .top, .pinned: + verticalHeight = min(containerSize.height - currentHeight - (position == .pinned ? self.delegate.navigationBarHeight : 0), AnnotationToolbarViewController.estimatedVerticalHeight) + } + + self.toolbarPinnedPreview.isHidden = !topToolbarsAvailable || (position == .top && !statusBarVisible) + self.inbetweenTopDashedView.isHidden = self.toolbarPinnedPreview.isHidden + if !self.toolbarPinnedPreview.isHidden { + // Change height based on current position so that preview is shown around currently visible toolbar + let baseHeight = position == .pinned ? self.controller.size : self.delegate.navigationBarHeight + self.toolbarPinnedPreviewHeight.constant = baseHeight + self.topOffsets(statusBarVisible: statusBarVisible).statusBarHeight - (position == .top ? 1 : 0) + } + self.toolbarTopPreview.isHidden = !topToolbarsAvailable + self.toolbarLeadingPreviewHeight.constant = verticalHeight + self.toolbarTrailingPreviewHeight.constant = verticalHeight + self.toolbarPreviewsOverlay.layoutIfNeeded() + } + + private func set(toolbarPosition newPosition: State.Position, oldPosition: State.Position, velocity velocityPoint: CGPoint, statusBarVisible: Bool) { + let navigationBarHidden = newPosition == .pinned || !statusBarVisible + + switch (newPosition, oldPosition) { + case (.leading, .leading), (.trailing, .trailing), (.top, .top), (.pinned, .pinned): + // Position didn't change, move to initial frame + let frame = self.toolbarInitialFrame ?? CGRect() + let velocity = self.velocity(from: velocityPoint, newPosition: newPosition) + + if !navigationBarHidden && self.delegate.isNavigationBarHidden { + self.delegate.setNavigationBar(hidden: false, animated: false) + self.delegate.setNavigationBar(alpha: 0) + } + + UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity, options: [.curveEaseOut], animations: { + self.toolbarPreviewsOverlay.alpha = 0 + self.controller.view.frame = frame + self.delegate.setNavigationBar(alpha: navigationBarHidden ? 0 : 1) + self.delegate.setDocumentInterface(hidden: !statusBarVisible) + }, completion: { finished in + guard finished else { return } + + self.toolbarPreviewsOverlay.isHidden = true + + if navigationBarHidden { + self.delegate.setNavigationBar(hidden: true, animated: false) + } + }) + + case (.leading, .trailing), (.trailing, .leading), (.top, .pinned), (.pinned, .top): + // Move from side to side or vertically + let velocity = self.velocity(from: velocityPoint, newPosition: newPosition) + self.setConstraints(for: newPosition, statusBarVisible: statusBarVisible) + self.delegate.topDidChange(forToolbarState: State(position: newPosition, visible: true)) + self.delegate.setNeedsLayout() + + self.delegate.hideSidebarIfNeeded(forPosition: newPosition, isToolbarSmallerThanMinWidth: self.controller.view.frame.width < Self.minToolbarWidth, animated: true) + + if !navigationBarHidden && self.delegate.isNavigationBarHidden { + self.delegate.setNavigationBar(hidden: false, animated: false) + self.delegate.setNavigationBar(alpha: 0) + } + + UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity, options: [], animations: { + self.delegate.layoutIfNeeded() + self.toolbarPreviewsOverlay.alpha = 0 + self.delegate.setNavigationBar(alpha: navigationBarHidden ? 0 : 1) + self.delegate.setDocumentInterface(hidden: !statusBarVisible) + self.delegate.updateStatusBar() + }, completion: { finished in + guard finished else { return } + + self.toolbarPreviewsOverlay.isHidden = true + + if navigationBarHidden { + self.delegate.setNavigationBar(hidden: true, animated: false) + } + }) + + case (.top, .leading), (.top, .trailing), (.leading, .top), (.leading, .pinned), (.trailing, .top), (.trailing, .pinned), (.pinned, .leading), (.pinned, .trailing): + let velocity = self.velocity(from: velocityPoint, newPosition: newPosition) + UIView.animate(withDuration: 0.1, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity, options: [], animations: { + let newFrame = self.controller.view.frame.offsetBy(dx: velocityPoint.x / 10, dy: velocityPoint.y / 10) + self.controller.view.frame = newFrame + self.controller.view.alpha = 0 + }, completion: { finished in + guard finished else { return } + + if !navigationBarHidden && self.delegate.isNavigationBarHidden { + self.delegate.setNavigationBar(hidden: false, animated: false) + self.delegate.setNavigationBar(alpha: 0) + } + + self.controller.prepareForSizeChange() + self.setConstraints(for: newPosition, statusBarVisible: statusBarVisible) + self.delegate.layoutIfNeeded() + self.controller.sizeDidChange() + self.delegate.layoutIfNeeded() + self.delegate.topDidChange(forToolbarState: State(position: newPosition, visible: true)) + + self.delegate.hideSidebarIfNeeded(forPosition: newPosition, isToolbarSmallerThanMinWidth: self.controller.view.frame.width < Self.minToolbarWidth, animated: true) + + UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity, options: [], animations: { + self.controller.view.alpha = 1 + self.delegate.layoutIfNeeded() + self.toolbarPreviewsOverlay.alpha = 0 + self.delegate.setNavigationBar(alpha: navigationBarHidden ? 0 : 1) + self.delegate.setDocumentInterface(hidden: !statusBarVisible) + self.delegate.updateStatusBar() + }, completion: { finished in + guard finished else { return } + + self.toolbarPreviewsOverlay.isHidden = true + + if navigationBarHidden { + self.delegate.setNavigationBar(hidden: true, animated: false) + } + }) + }) + } + } + + private func setHighlightSelected(at position: State.Position) { + switch position { + case .top: + self.toolbarLeadingPreview.backgroundColor = self.previewBackgroundColor + self.toolbarLeadingPreview.dashColor = self.previewDashColor + self.toolbarTrailingPreview.backgroundColor = self.previewBackgroundColor + self.toolbarTrailingPreview.dashColor = self.previewDashColor + self.toolbarTopPreview.backgroundColor = self.previewSelectedBackgroundColor + self.toolbarTopPreview.dashColor = self.previewSelectedDashColor + self.toolbarPinnedPreview.backgroundColor = self.previewBackgroundColor + self.toolbarPinnedPreview.dashColor = self.previewDashColor + + case .leading: + self.toolbarLeadingPreview.backgroundColor = self.previewSelectedBackgroundColor + self.toolbarLeadingPreview.dashColor = self.previewSelectedDashColor + self.toolbarTrailingPreview.backgroundColor = self.previewBackgroundColor + self.toolbarTrailingPreview.dashColor = self.previewDashColor + self.toolbarTopPreview.backgroundColor = self.previewBackgroundColor + self.toolbarTopPreview.dashColor = self.previewDashColor + self.toolbarPinnedPreview.backgroundColor = self.previewBackgroundColor + self.toolbarPinnedPreview.dashColor = self.previewDashColor + + case .trailing: + self.toolbarLeadingPreview.backgroundColor = self.previewBackgroundColor + self.toolbarLeadingPreview.dashColor = self.previewDashColor + self.toolbarTrailingPreview.backgroundColor = self.previewSelectedBackgroundColor + self.toolbarTrailingPreview.dashColor = self.previewSelectedDashColor + self.toolbarTopPreview.backgroundColor = self.previewBackgroundColor + self.toolbarTopPreview.dashColor = self.previewDashColor + self.toolbarPinnedPreview.backgroundColor = self.previewBackgroundColor + self.toolbarPinnedPreview.dashColor = self.previewDashColor + + case .pinned: + self.toolbarLeadingPreview.backgroundColor = self.previewBackgroundColor + self.toolbarLeadingPreview.dashColor = self.previewDashColor + self.toolbarTrailingPreview.backgroundColor = self.previewBackgroundColor + self.toolbarTrailingPreview.dashColor = self.previewDashColor + self.toolbarTopPreview.backgroundColor = self.previewBackgroundColor + self.toolbarTopPreview.dashColor = self.previewDashColor + self.toolbarPinnedPreview.backgroundColor = self.previewSelectedBackgroundColor + self.toolbarPinnedPreview.dashColor = self.previewSelectedDashColor + } + } +} + +extension AnnotationToolbarHandler: UIGestureRecognizerDelegate { + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + guard let longPressRecognizer = gestureRecognizer as? UILongPressGestureRecognizer else { return true } + + let location = longPressRecognizer.location(in: self.controller.view) + let currentLocation: CGFloat + let border: CGFloat + + switch self.delegate.toolbarState.position { + case .pinned, .top: + currentLocation = location.x + border = self.controller.view.frame.width - Self.annotationToolbarDragHandleHeight + + case .leading, .trailing: + currentLocation = location.y + border = self.controller.view.frame.height - Self.annotationToolbarDragHandleHeight + } + return currentLocation >= border + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } +} diff --git a/Zotero/Scenes/Detail/PDF/Views/AnnotationToolbarViewController.swift b/Zotero/Scenes/Detail/PDF/Views/AnnotationToolbarViewController.swift index a21af98b0..dfbb6fc62 100644 --- a/Zotero/Scenes/Detail/PDF/Views/AnnotationToolbarViewController.swift +++ b/Zotero/Scenes/Detail/PDF/Views/AnnotationToolbarViewController.swift @@ -25,14 +25,12 @@ struct AnnotationToolOptions: OptionSet { } protocol AnnotationToolbarDelegate: AnyObject { - var rotation: AnnotationToolbarViewController.Rotation { get } - var activeAnnotationTool: PSPDFKit.Annotation.Tool? { get } + var activeAnnotationTool: AnnotationTool? { get } var canUndo: Bool { get } var canRedo: Bool { get } var maxAvailableToolbarSize: CGFloat { get } - func isCompactSize(for rotation: AnnotationToolbarViewController.Rotation) -> Bool - func toggle(tool: PSPDFKit.Annotation.Tool, options: AnnotationToolOptions) + func toggle(tool: AnnotationTool, options: AnnotationToolOptions) func showToolOptions(sender: SourceView) func closeAnnotationToolbar() func performUndo() @@ -44,15 +42,15 @@ class AnnotationToolbarViewController: UIViewController { case horizontal, vertical } - private struct Tool { - let type: PSPDFKit.Annotation.Tool + private struct ToolButton { + let type: AnnotationTool let title: String let accessibilityLabel: String let image: UIImage let isHidden: Bool - func copy(isHidden: Bool) -> Tool { - return Tool(type: self.type, title: self.title, accessibilityLabel: self.accessibilityLabel, image: self.image, isHidden: isHidden) + func copy(isHidden: Bool) -> ToolButton { + return ToolButton(type: self.type, title: self.title, accessibilityLabel: self.accessibilityLabel, image: self.image, isHidden: isHidden) } } @@ -65,6 +63,7 @@ class AnnotationToolbarViewController: UIViewController { private static let toolsToAdditionalFullOffset: CGFloat = 70 private static let toolsToAdditionalCompactOffset: CGFloat = 20 private let disposeBag: DisposeBag + private let undoRedoEnabled: Bool private var horizontalHeight: NSLayoutConstraint! private weak var stackView: UIStackView! @@ -89,13 +88,14 @@ class AnnotationToolbarViewController: UIViewController { private var containerToPickerVertical: NSLayoutConstraint! private var containerToPickerHorizontal: NSLayoutConstraint! private var hairlineView: UIView! - private var tools: [Tool] + private var toolButtons: [ToolButton] weak var delegate: AnnotationToolbarDelegate? private var lastGestureRecognizerTouch: UITouch? - init(size: CGFloat) { + init(tools: [AnnotationTool], undoRedoEnabled: Bool, size: CGFloat) { self.size = size - self.tools = AnnotationToolbarViewController.createTools() + self.toolButtons = Self.createButtons(from: tools) + self.undoRedoEnabled = undoRedoEnabled self.disposeBag = DisposeBag() super.init(nibName: nil, bundle: nil) } @@ -104,44 +104,57 @@ class AnnotationToolbarViewController: UIViewController { fatalError("init(coder:) has not been implemented") } - private static func createTools() -> [Tool] { - return [ - Tool( - type: .highlight, - title: L10n.Pdf.AnnotationToolbar.highlight, - accessibilityLabel: L10n.Accessibility.Pdf.highlightAnnotationTool, - image: Asset.Images.Annotations.highlighterLarge.image, - isHidden: false - ), - Tool( - type: .note, - title: L10n.Pdf.AnnotationToolbar.note, - accessibilityLabel: L10n.Accessibility.Pdf.noteAnnotationTool, - image: Asset.Images.Annotations.noteLarge.image, - isHidden: false - ), - Tool( - type: .square, - title: L10n.Pdf.AnnotationToolbar.image, - accessibilityLabel: L10n.Accessibility.Pdf.imageAnnotationTool, - image: Asset.Images.Annotations.areaLarge.image, - isHidden: false - ), - Tool( - type: .ink, - title: L10n.Pdf.AnnotationToolbar.ink, - accessibilityLabel: L10n.Accessibility.Pdf.inkAnnotationTool, - image: Asset.Images.Annotations.inkLarge.image, - isHidden: false - ), - Tool( - type: .eraser, - title: L10n.Pdf.AnnotationToolbar.eraser, - accessibilityLabel: L10n.Accessibility.Pdf.eraserAnnotationTool, - image: Asset.Images.Annotations.eraserLarge.image, - isHidden: false - ) - ] + private static func createButtons(from tools: [AnnotationTool]) -> [ToolButton] { + return tools.map({ button(from: $0) }) + + func button(from tool: AnnotationTool) -> ToolButton { + switch tool { + case .highlight: + ToolButton( + type: .highlight, + title: L10n.Pdf.AnnotationToolbar.highlight, + accessibilityLabel: L10n.Accessibility.Pdf.highlightAnnotationTool, + image: Asset.Images.Annotations.highlighterLarge.image, + isHidden: false + ) + + case .note: + ToolButton( + type: .note, + title: L10n.Pdf.AnnotationToolbar.note, + accessibilityLabel: L10n.Accessibility.Pdf.noteAnnotationTool, + image: Asset.Images.Annotations.noteLarge.image, + isHidden: false + ) + + case .image: + ToolButton( + type: .image, + title: L10n.Pdf.AnnotationToolbar.image, + accessibilityLabel: L10n.Accessibility.Pdf.imageAnnotationTool, + image: Asset.Images.Annotations.areaLarge.image, + isHidden: false + ) + + case .ink: + ToolButton( + type: .ink, + title: L10n.Pdf.AnnotationToolbar.ink, + accessibilityLabel: L10n.Accessibility.Pdf.inkAnnotationTool, + image: Asset.Images.Annotations.inkLarge.image, + isHidden: false + ) + + case .eraser: + ToolButton( + type: .eraser, + title: L10n.Pdf.AnnotationToolbar.eraser, + accessibilityLabel: L10n.Accessibility.Pdf.eraserAnnotationTool, + image: Asset.Images.Annotations.eraserLarge.image, + isHidden: false + ) + } + } } override func viewDidLoad() { @@ -151,11 +164,6 @@ class AnnotationToolbarViewController: UIViewController { self.view.addInteraction(UILargeContentViewerInteraction()) self.setupViews() - if let delegate = self.delegate { - let rotation = delegate.rotation - self.set(rotation: rotation, isCompactSize: delegate.isCompactSize(for: rotation)) - self.view.layoutIfNeeded() - } } // MARK: - Undo/Redo state @@ -180,8 +188,8 @@ class AnnotationToolbarViewController: UIViewController { } func sizeDidChange() { - guard self.stackView.arrangedSubviews.count == self.tools.count + 1 else { - DDLogError("AnnotationToolbarViewController: too many views in stack view! Stack view views: \(self.stackView.arrangedSubviews.count). Tools: \(self.tools.count)") + guard self.stackView.arrangedSubviews.count == self.toolButtons.count + 1 else { + DDLogError("AnnotationToolbarViewController: too many views in stack view! Stack view views: \(self.stackView.arrangedSubviews.count). Tools: \(self.toolButtons.count)") return } guard let button = self.stackView.arrangedSubviews.last, let maxAvailableSize = self.delegate?.maxAvailableToolbarSize, maxAvailableSize > 0 else { return } @@ -198,18 +206,18 @@ class AnnotationToolbarViewController: UIViewController { let pickerToAdditionalOffset = isHorizontal ? self.colorPickerToAdditionalHorizontal.constant : self.colorPickerToAdditionalVertical.constant let additionalOffset = isHorizontal ? self.additionalTrailing.constant : self.additionalBottom.constant let remainingSize = maxAvailableSize - stackViewOffset - containerToPickerOffset - pickerSize - pickerToAdditionalOffset - additionalSize - additionalOffset - let count = max(0, min(Int(floor(remainingSize / buttonSize)), self.tools.count)) + let count = max(0, min(Int(floor(remainingSize / buttonSize)), self.toolButtons.count)) for idx in 0.. UIMenu { - let children = self.tools.filter({ $0.isHidden }).map({ tool in + let children = self.toolButtons.filter({ $0.isHidden }).map({ tool in let isActive = self.delegate?.activeAnnotationTool == tool.type return UIAction(title: tool.title, image: tool.image.withRenderingMode(.alwaysTemplate), discoverabilityTitle: tool.accessibilityLabel, state: (isActive ? .on : .off), handler: { [weak self] _ in @@ -353,7 +371,7 @@ class AnnotationToolbarViewController: UIViewController { return UIMenu(children: children) } - private func createToolButtons(from tools: [Tool]) -> [UIView] { + private func createToolButtons(from tools: [ToolButton]) -> [UIView] { var showMoreConfig = UIButton.Configuration.plain() showMoreConfig.contentInsets = AnnotationToolbarViewController.buttonContentInsets showMoreConfig.image = UIImage(systemName: "ellipsis")?.withRenderingMode(.alwaysTemplate) @@ -395,39 +413,45 @@ class AnnotationToolbarViewController: UIViewController { } private func createAdditionalItems() -> [UIView] { - var undoConfig = UIButton.Configuration.plain() - undoConfig.contentInsets = AnnotationToolbarViewController.buttonContentInsets - undoConfig.image = UIImage(systemName: "arrow.uturn.left")?.applyingSymbolConfiguration(.init(scale: .large)) - let undo = UIButton(type: .custom) - undo.configuration = undoConfig - undo.isEnabled = self.delegate?.canUndo ?? false - undo.showsLargeContentViewer = true - undo.accessibilityLabel = L10n.Accessibility.Pdf.undo - undo.largeContentTitle = L10n.Accessibility.Pdf.undo - undo.rx.controlEvent(.touchUpInside) - .subscribe(with: self, onNext: { `self`, _ in - guard self.delegate?.canUndo == true else { return } - self.delegate?.performUndo() - }) - .disposed(by: self.disposeBag) - self.undoButton = undo - - var redoConfig = UIButton.Configuration.plain() - redoConfig.contentInsets = AnnotationToolbarViewController.buttonContentInsets - redoConfig.image = UIImage(systemName: "arrow.uturn.right")?.applyingSymbolConfiguration(.init(scale: .large)) - let redo = UIButton(type: .custom) - redo.configuration = redoConfig - redo.isEnabled = self.delegate?.canRedo ?? false - redo.showsLargeContentViewer = true - redo.accessibilityLabel = L10n.Accessibility.Pdf.redo - redo.largeContentTitle = L10n.Accessibility.Pdf.redo - redo.rx.controlEvent(.touchUpInside) - .subscribe(with: self, onNext: { `self`, _ in - guard self.delegate?.canRedo == true else { return } - self.delegate?.performRedo() - }) - .disposed(by: self.disposeBag) - self.redoButton = redo + var items: [UIView] = [] + + if self.undoRedoEnabled { + var undoConfig = UIButton.Configuration.plain() + undoConfig.contentInsets = AnnotationToolbarViewController.buttonContentInsets + undoConfig.image = UIImage(systemName: "arrow.uturn.left")?.applyingSymbolConfiguration(.init(scale: .large)) + let undo = UIButton(type: .custom) + undo.configuration = undoConfig + undo.isEnabled = self.delegate?.canUndo ?? false + undo.showsLargeContentViewer = true + undo.accessibilityLabel = L10n.Accessibility.Pdf.undo + undo.largeContentTitle = L10n.Accessibility.Pdf.undo + undo.rx.controlEvent(.touchUpInside) + .subscribe(with: self, onNext: { `self`, _ in + guard self.delegate?.canUndo == true else { return } + self.delegate?.performUndo() + }) + .disposed(by: self.disposeBag) + self.undoButton = undo + items.append(undo) + + var redoConfig = UIButton.Configuration.plain() + redoConfig.contentInsets = AnnotationToolbarViewController.buttonContentInsets + redoConfig.image = UIImage(systemName: "arrow.uturn.right")?.applyingSymbolConfiguration(.init(scale: .large)) + let redo = UIButton(type: .custom) + redo.configuration = redoConfig + redo.isEnabled = self.delegate?.canRedo ?? false + redo.showsLargeContentViewer = true + redo.accessibilityLabel = L10n.Accessibility.Pdf.redo + redo.largeContentTitle = L10n.Accessibility.Pdf.redo + redo.rx.controlEvent(.touchUpInside) + .subscribe(with: self, onNext: { `self`, _ in + guard self.delegate?.canRedo == true else { return } + self.delegate?.performRedo() + }) + .disposed(by: self.disposeBag) + self.redoButton = redo + items.append(redo) + } var closeConfig = UIButton.Configuration.plain() closeConfig.contentInsets = AnnotationToolbarViewController.buttonContentInsets @@ -442,12 +466,14 @@ class AnnotationToolbarViewController: UIViewController { self.delegate?.closeAnnotationToolbar() }) .disposed(by: self.disposeBag) + items.append(close) let handle = UIImageView(image: UIImage(systemName: "line.3.horizontal")?.applyingSymbolConfiguration(.init(scale: .large))) handle.showsLargeContentViewer = false handle.contentMode = .center + items.append(handle) - for view in [undo, redo, close, handle] { + for view in items { view.translatesAutoresizingMaskIntoConstraints = false view.tintColor = Asset.Colors.zoteroBlueWithDarkMode.color view.setContentCompressionResistancePriority(.required, for: .horizontal) @@ -455,7 +481,7 @@ class AnnotationToolbarViewController: UIViewController { view.widthAnchor.constraint(equalTo: view.heightAnchor).isActive = true } - return [undo, redo, close, handle] + return items } private func createColorPickerButton() -> UIButton { @@ -483,7 +509,7 @@ class AnnotationToolbarViewController: UIViewController { private func setupViews() { self.view.translatesAutoresizingMaskIntoConstraints = false - let stackView = UIStackView(arrangedSubviews: self.createToolButtons(from: self.tools)) + let stackView = UIStackView(arrangedSubviews: self.createToolButtons(from: self.toolButtons)) stackView.showsLargeContentViewer = true stackView.axis = .vertical stackView.spacing = 0 diff --git a/Zotero/Scenes/Detail/PDF/Views/AnnotationsViewController.swift b/Zotero/Scenes/Detail/PDF/Views/PDFAnnotationsViewController.swift similarity index 94% rename from Zotero/Scenes/Detail/PDF/Views/AnnotationsViewController.swift rename to Zotero/Scenes/Detail/PDF/Views/PDFAnnotationsViewController.swift index 63e440e11..df9a37109 100644 --- a/Zotero/Scenes/Detail/PDF/Views/AnnotationsViewController.swift +++ b/Zotero/Scenes/Detail/PDF/Views/PDFAnnotationsViewController.swift @@ -1,5 +1,5 @@ // -// AnnotationsViewController.swift +// PDFAnnotationsViewController.swift // Zotero // // Created by Michal Rentka on 24/04/2020. @@ -15,7 +15,7 @@ import RxSwift typealias AnnotationsViewControllerAction = (AnnotationView.Action, Annotation, UIButton) -> Void -final class AnnotationsViewController: UIViewController { +final class PDFAnnotationsViewController: UIViewController { private static let cellId = "AnnotationCell" private unowned let viewModel: ViewModel private let disposeBag: DisposeBag @@ -95,7 +95,7 @@ final class AnnotationsViewController: UIViewController { // MARK: - Actions - private func perform(action: AnnotationView.Action, annotation: Annotation) { + private func perform(action: AnnotationView.Action, annotation: PDFAnnotation) { let state = self.viewModel.state guard state.library.metadataEditable else { return } @@ -104,19 +104,25 @@ final class AnnotationsViewController: UIViewController { case .tags: guard annotation.isAuthor(currentUserId: self.viewModel.state.userId) else { return } let selected = Set(annotation.tags.map({ $0.name })) - self.coordinatorDelegate?.showTagPicker(libraryId: state.library.identifier, selected: selected, userInterfaceStyle: self.viewModel.state.interfaceStyle, picked: { [weak self] tags in - self?.viewModel.process(action: .setTags(key: annotation.key, tags: tags)) - }) + self.coordinatorDelegate?.showTagPicker( + libraryId: state.library.identifier, + selected: selected, + userInterfaceStyle: self.viewModel.state.settings.appearanceMode.userInterfaceStyle, + picked: { [weak self] tags in + self?.viewModel.process(action: .setTags(key: annotation.key, tags: tags)) + } + ) case .options(let sender): guard let sender else { return } + let key = annotation.readerKey self.coordinatorDelegate?.showCellOptions( for: annotation, userId: self.viewModel.state.userId, library: self.viewModel.state.library, sender: sender, - userInterfaceStyle: self.viewModel.state.interfaceStyle, - saveAction: { [weak self] key, color, lineWidth, pageLabel, updateSubsequentLabels, highlightText in + userInterfaceStyle: self.viewModel.state.settings.appearanceMode.userInterfaceStyle, + saveAction: { [weak self] color, lineWidth, pageLabel, updateSubsequentLabels, highlightText in self?.viewModel.process( action: .updateAnnotationProperties( key: key.key, @@ -128,7 +134,7 @@ final class AnnotationsViewController: UIViewController { ) ) }, - deleteAction: { [weak self] key in + deleteAction: { [weak self] in self?.viewModel.process(action: .removeAnnotation(key)) } ) @@ -275,7 +281,7 @@ final class AnnotationsViewController: UIViewController { } } - private func setup(cell: AnnotationCell, with annotation: Annotation, state: PDFReaderState) { + private func setup(cell: AnnotationCell, with annotation: PDFAnnotation, state: PDFReaderState) { let selected = annotation.key == state.selectedAnnotationKey?.key let loadPreview: () -> UIImage? = { @@ -325,7 +331,7 @@ final class AnnotationsViewController: UIViewController { _ = cell.disposeBag?.insert(actionSubscription) } - private func loadAttributedComment(for annotation: Annotation) -> NSAttributedString? { + private func loadAttributedComment(for annotation: PDFAnnotation) -> NSAttributedString? { let comment = annotation.comment guard !comment.isEmpty else { return nil } @@ -342,7 +348,7 @@ final class AnnotationsViewController: UIViewController { var colors: Set = [] var tags: Set = [] - let processAnnotation: (Annotation) -> Void = { annotation in + let processAnnotation: (PDFAnnotation) -> Void = { annotation in colors.insert(annotation.color) for tag in annotation.tags { tags.insert(tag) @@ -350,7 +356,7 @@ final class AnnotationsViewController: UIViewController { } for annotation in self.viewModel.state.databaseAnnotations { - processAnnotation(DatabaseAnnotation(item: annotation)) + processAnnotation(PDFDatabaseAnnotation(item: annotation)) } for annotation in self.viewModel.state.documentAnnotations.values { processAnnotation(annotation) @@ -377,7 +383,7 @@ final class AnnotationsViewController: UIViewController { filter: self.viewModel.state.filter, availableColors: sortedColors, availableTags: sortedTags, - userInterfaceStyle: self.viewModel.state.interfaceStyle, + userInterfaceStyle: self.viewModel.state.settings.appearanceMode.userInterfaceStyle, completed: { [weak self] filter in self?.viewModel.process(action: .changeFilter(filter)) } @@ -406,7 +412,7 @@ final class AnnotationsViewController: UIViewController { tableView.separatorStyle = .none tableView.backgroundColor = .systemGray6 tableView.backgroundView?.backgroundColor = .systemGray6 - tableView.register(AnnotationCell.self, forCellReuseIdentifier: AnnotationsViewController.cellId) + tableView.register(AnnotationCell.self, forCellReuseIdentifier: Self.cellId) tableView.setEditing(self.viewModel.state.sidebarEditingEnabled, animated: false) tableView.allowsMultipleSelectionDuringEditing = true self.view.addSubview(tableView) @@ -458,7 +464,7 @@ final class AnnotationsViewController: UIViewController { private func setupDataSource() { self.dataSource = TableViewDiffableDataSource(tableView: self.tableView, cellProvider: { [weak self] tableView, indexPath, key in - let cell = tableView.dequeueReusableCell(withIdentifier: AnnotationsViewController.cellId, for: indexPath) + let cell = tableView.dequeueReusableCell(withIdentifier: Self.cellId, for: indexPath) if let self, let cell = cell as? AnnotationCell, let annotation = self.viewModel.state.annotation(for: key) { cell.contentView.backgroundColor = self.view.backgroundColor @@ -581,7 +587,8 @@ final class AnnotationsViewController: UIViewController { let filterImageName = filterOn ? "line.horizontal.3.decrease.circle.fill" : "line.horizontal.3.decrease.circle" let filter = UIBarButtonItem(image: UIImage(systemName: filterImageName), style: .plain, target: nil, action: nil) filter.rx.tap - .subscribe(with: self, onNext: { `self`, _ in + .subscribe(with: self, onNext: { [weak filter] `self`, _ in + guard let filter else { return } self.showFilterPopup(from: filter) }) .disposed(by: self.disposeBag) @@ -600,7 +607,7 @@ final class AnnotationsViewController: UIViewController { } } -extension AnnotationsViewController: UITableViewDelegate, UITableViewDataSourcePrefetching { +extension PDFAnnotationsViewController: UITableViewDelegate, UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { let keys = indexPaths.compactMap({ self.dataSource.itemIdentifier(for: $0) }) .filter({ key in diff --git a/Zotero/Scenes/Detail/PDF/Views/PDFDocumentViewController.swift b/Zotero/Scenes/Detail/PDF/Views/PDFDocumentViewController.swift index b6ba10858..eb4a14b60 100644 --- a/Zotero/Scenes/Detail/PDF/Views/PDFDocumentViewController.swift +++ b/Zotero/Scenes/Detail/PDF/Views/PDFDocumentViewController.swift @@ -327,9 +327,46 @@ final class PDFDocumentViewController: UIViewController { let annotation = state.selectedAnnotation, let pageView = self.pdfController?.pageViewForPage(at: UInt(annotation.page)) else { return } - let frame = self.view.convert(annotation.boundingBox(boundingBoxConverter: self), from: pageView.pdfCoordinateSpace) + let key = annotation.readerKey + var frame = self.view.convert(annotation.boundingBox(boundingBoxConverter: self), from: pageView.pdfCoordinateSpace) + frame.origin.y += (self.parentDelegate?.statusBarHeight ?? 0) + (self.parentDelegate?.navigationBarHeight ?? 0) + let observable = self.coordinatorDelegate?.showAnnotationPopover( + viewModel: self.viewModel, + sourceRect: frame, + popoverDelegate: self, + userInterfaceStyle: self.viewModel.state.settings.appearanceMode.userInterfaceStyle + ) - self.coordinatorDelegate?.showAnnotationPopover(viewModel: self.viewModel, sourceRect: frame, popoverDelegate: self, userInterfaceStyle: self.viewModel.state.interfaceStyle) + guard let observable else { return } + observable.subscribe(with: self) { `self`, state in + if state.changes.contains(.color) { + self.viewModel.process(action: .setColor(key: key.key, color: state.color)) + } + if state.changes.contains(.comment) { + self.viewModel.process(action: .setComment(key: key.key, comment: state.comment)) + } + if state.changes.contains(.deletion) { + self.viewModel.process(action: .removeAnnotation(key)) + } + if state.changes.contains(.lineWidth) { + self.viewModel.process(action: .setLineWidth(key: key.key, width: state.lineWidth)) + } + if state.changes.contains(.tags) { + self.viewModel.process(action: .setTags(key: key.key, tags: state.tags)) + } + if state.changes.contains(.pageLabel) || state.changes.contains(.highlight) { + self.viewModel.process(action: + .updateAnnotationProperties( + key: key.key, + color: state.color, + lineWidth: state.lineWidth, + pageLabel: state.pageLabel, + updateSubsequentLabels: state.updateSubsequentLabels, + highlightText: state.highlightText) + ) + } + } + .disposed(by: disposeBag) } private func updatePencilSettingsIfNeeded() { @@ -383,7 +420,7 @@ final class PDFDocumentViewController: UIViewController { /// - parameter annotation: Annotation to select. Existing selection will be deselected if set to `nil`. /// - parameter pageIndex: Page index of page where (de)selection should happen. /// - parameter document: Active `Document` instance. - private func select(annotation: Annotation?, pageIndex: PageIndex, document: PSPDFKit.Document) { + private func select(annotation: PDFAnnotation?, pageIndex: PageIndex, document: PSPDFKit.Document) { guard let pageView = self.pdfController?.pageViewForPage(at: pageIndex) else { return } self.updateSelection(on: pageView, annotation: annotation) @@ -400,7 +437,7 @@ final class PDFDocumentViewController: UIViewController { } /// Focuses given annotation and selects it if it's not selected yet. - private func focus(annotation: Annotation, at location: AnnotationDocumentLocation, document: PSPDFKit.Document) { + private func focus(annotation: PDFAnnotation, at location: AnnotationDocumentLocation, document: PSPDFKit.Document) { let pageIndex = PageIndex(location.page) self.scrollIfNeeded(to: pageIndex, animated: true) { self.select(annotation: annotation, pageIndex: pageIndex, document: document) @@ -410,7 +447,7 @@ final class PDFDocumentViewController: UIViewController { /// Updates `SelectionView` for `PDFPageView` based on selected annotation. /// - parameter pageView: `PDFPageView` instance for given page. /// - parameter selectedAnnotation: Selected annotation or `nil` if there is no selection. - private func updateSelection(on pageView: PDFPageView, annotation: Annotation?) { + private func updateSelection(on pageView: PDFPageView, annotation: PDFAnnotation?) { // Delete existing custom highlight selection view if let view = self.selectionView { view.removeFromSuperview() @@ -639,7 +676,12 @@ extension PDFDocumentViewController: PDFViewControllerDelegate { case .PSPDFKit.define: return action.replacing(title: L10n.lookUp, handler: { [weak self] _ in guard let self, let view = self.pdfController?.view else { return } - self.coordinatorDelegate?.lookup(text: glyphs.text, rect: glyphs.boundingBox, view: view, userInterfaceStyle: self.viewModel.state.interfaceStyle) + self.coordinatorDelegate?.lookup( + text: glyphs.text, + rect: glyphs.boundingBox, + view: view, + userInterfaceStyle: self.viewModel.state.settings.appearanceMode.userInterfaceStyle + ) }) case .PSPDFKit.searchDocument: diff --git a/Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift b/Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift index 9865857b7..84a741f8e 100644 --- a/Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift +++ b/Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift @@ -15,6 +15,8 @@ import RxSwift protocol PDFReaderContainerDelegate: AnyObject { var isSidebarVisible: Bool { get } + var statusBarHeight: CGFloat { get } + var navigationBarHeight: CGFloat { get } func showSearch(pdfController: PDFViewController, text: String?) } @@ -40,26 +42,18 @@ class PDFReaderViewController: UIViewController { private static let toolbarCompactInset: CGFloat = 12 private static let toolbarFullInsetInset: CGFloat = 20 private static let minToolbarWidth: CGFloat = 300 - private static let annotationToolbarDragHandleHeight: CGFloat = 50 + private static let sidebarButtonTag = 7 private let viewModel: ViewModel private let disposeBag: DisposeBag - private let previewBackgroundColor: UIColor - private let previewDashColor: UIColor - private let previewSelectedBackgroundColor: UIColor - private let previewSelectedDashColor: UIColor + var state: PDFReaderState { return viewModel.state } private weak var sidebarController: PDFSidebarViewController! private weak var sidebarControllerLeft: NSLayoutConstraint! private weak var documentController: PDFDocumentViewController! private weak var documentControllerLeft: NSLayoutConstraint! private weak var annotationToolbarController: AnnotationToolbarViewController! - private weak var annotationToolbarDragHandleLongPressRecognizer: UILongPressGestureRecognizer! private var documentTop: NSLayoutConstraint! - private weak var toolbarTop: NSLayoutConstraint! - private var toolbarLeading: NSLayoutConstraint! - private var toolbarLeadingSafeArea: NSLayoutConstraint! - private var toolbarTrailing: NSLayoutConstraint! - private var toolbarTrailingSafeArea: NSLayoutConstraint! + private var annotationToolbarHandler: AnnotationToolbarHandler! private weak var toolbarPreviewsOverlay: UIView! private weak var toolbarLeadingPreview: DashedView! private weak var inbetweenTopDashedView: DashedView! @@ -70,21 +64,19 @@ class PDFReaderViewController: UIViewController { private weak var toolbarPinnedPreview: DashedView! private weak var toolbarPinnedPreviewHeight: NSLayoutConstraint! private(set) var isCompactWidth: Bool - @CodableUserDefault(key: "PDFReaderToolbarState", defaultValue: ToolbarState(position: .leading, visible: true), encoder: Defaults.jsonEncoder, decoder: Defaults.jsonDecoder) - private var toolbarState: ToolbarState - private var toolbarInitialFrame: CGRect? + @CodableUserDefault(key: "PDFReaderToolbarState", defaultValue: AnnotationToolbarHandler.State(position: .leading, visible: true), encoder: Defaults.jsonEncoder, decoder: Defaults.jsonDecoder) + var toolbarState: AnnotationToolbarHandler.State @UserDefault(key: "PDFReaderStatusBarVisible", defaultValue: true) - private var statusBarVisible: Bool { + var statusBarVisible: Bool { didSet { (self.navigationController as? NavigationViewController)?.statusBarVisible = self.statusBarVisible } } - private var didAppear: Bool private var previousTraitCollection: UITraitCollection? var isSidebarVisible: Bool { return self.sidebarControllerLeft?.constant == 0 } var key: String { return self.viewModel.state.key } - private var statusBarHeight: CGFloat - private var navigationBarHeight: CGFloat { + var statusBarHeight: CGFloat + var navigationBarHeight: CGFloat { return self.navigationController?.navigationBar.frame.height ?? 0.0 } @@ -97,7 +89,6 @@ class PDFReaderViewController: UIViewController { share.accessibilityLabel = L10n.Accessibility.Pdf.share share.title = L10n.Accessibility.Pdf.share share.tag = NavigationBarButton.share.rawValue - let deferredMenu = UIDeferredMenuElement.uncached { [weak self] elementProvider in var elements: [UIMenuElement] = [] defer { @@ -176,14 +167,7 @@ class PDFReaderViewController: UIViewController { .subscribe(onNext: { [weak self, weak checkbox] _ in guard let self, let checkbox else { return } checkbox.isSelected = !checkbox.isSelected - - self.toolbarState = ToolbarState(position: self.toolbarState.position, visible: checkbox.isSelected) - - if checkbox.isSelected { - self.showAnnotationToolbar(state: self.toolbarState, statusBarVisible: self.statusBarVisible, animated: true) - } else { - self.hideAnnotationToolbar(newState: self.toolbarState, statusBarVisible: self.statusBarVisible, animated: true) - } + self.annotationToolbarHandler.set(hidden: !checkbox.isSelected, animated: true) }) .disposed(by: self.disposeBag) let barButton = UIBarButtonItem(customView: checkbox) @@ -198,11 +182,6 @@ class PDFReaderViewController: UIViewController { self.viewModel = viewModel self.isCompactWidth = compactSize self.disposeBag = DisposeBag() - self.didAppear = false - self.previewDashColor = Asset.Colors.zoteroBlueWithDarkMode.color.withAlphaComponent(0.5) - self.previewBackgroundColor = Asset.Colors.zoteroBlueWithDarkMode.color.withAlphaComponent(0.15) - self.previewSelectedDashColor = Asset.Colors.zoteroBlueWithDarkMode.color - self.previewSelectedBackgroundColor = Asset.Colors.zoteroBlueWithDarkMode.color.withAlphaComponent(0.5) self.statusBarHeight = UIApplication .shared .connectedScenes @@ -230,7 +209,6 @@ class PDFReaderViewController: UIViewController { self.view.backgroundColor = .systemGray6 self.setupViews() self.setupNavigationBar() - self.setupGestureRecognizer() self.setupObserving() self.updateInterface(to: self.viewModel.state.settings) @@ -239,22 +217,13 @@ class PDFReaderViewController: UIViewController { } } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - if !self.didAppear { - self.setAnnotationToolbarHandleMinimumLongPressDuration(forPosition: self.toolbarState.position) - if self.toolbarState.visible && !self.viewModel.state.document.isLocked { - self.showAnnotationToolbar(state: self.toolbarState, statusBarVisible: self.statusBarVisible, animated: false) - } else { - self.hideAnnotationToolbar(newState: self.toolbarState, statusBarVisible: self.statusBarVisible, animated: false) - } - } + override func viewIsAppearing(_ animated: Bool) { + super.viewIsAppearing(animated) + self.annotationToolbarHandler.viewIsAppearing(documentIsLocked: self.viewModel.state.document.isLocked) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.didAppear = true } deinit { @@ -264,13 +233,7 @@ class PDFReaderViewController: UIViewController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - if !self.didAppear { - let state = self.toolbarState - self.setConstraints(for: state.position, statusBarVisible: self.statusBarVisible) - self.setDocumentTopConstraint(forToolbarState: state, statusBarVisible: self.statusBarVisible) - } - - if self.documentController.view.frame.width < PDFReaderViewController.minToolbarWidth && self.toolbarState.visible && self.toolbarState.position == .top { + if self.documentController.view.frame.width < AnnotationToolbarHandler.minToolbarWidth && self.toolbarState.visible && self.toolbarState.position == .top { self.closeAnnotationToolbar() } } @@ -293,12 +256,7 @@ class PDFReaderViewController: UIViewController { coordinator.animate(alongsideTransition: { _ in self.statusBarHeight = self.view.safeAreaInsets.top - (self.navigationController?.isNavigationBarHidden == true ? 0 : self.navigationBarHeight) - self.annotationToolbarController.prepareForSizeChange() - self.annotationToolbarController.updateAdditionalButtons() - self.setConstraints(for: self.toolbarState.position, statusBarVisible: self.statusBarVisible) - self.setDocumentTopConstraint(forToolbarState: self.toolbarState, statusBarVisible: self.statusBarVisible) - self.view.layoutIfNeeded() - self.annotationToolbarController.sizeDidChange() + self.annotationToolbarHandler.viewWillTransitionToNewSize() }, completion: nil) } @@ -325,8 +283,7 @@ class PDFReaderViewController: UIViewController { if state.changes.contains(.annotations) { // Hide popover if annotation has been deleted - if let controller = (self.presentedViewController as? UINavigationController)?.viewControllers.first as? AnnotationPopover, - let key = controller.annotationKey, !state.sortedKeys.contains(key) { + if (self.presentedViewController as? UINavigationController)?.viewControllers.first is AnnotationPopover, let key = state.selectedAnnotationKey, !state.sortedKeys.contains(key) { self.dismiss(animated: true, completion: nil) } } @@ -345,7 +302,7 @@ class PDFReaderViewController: UIViewController { } } - if let tool = state.changedColorForTool, self.activeAnnotationTool == tool, let color = state.toolColors[tool] { + if let tool = state.changedColorForTool, self.documentController.pdfController?.annotationStateManager.state == tool, let color = state.toolColors[tool] { self.annotationToolbarController.set(activeColor: color) } @@ -413,7 +370,7 @@ class PDFReaderViewController: UIViewController { } func showToolOptions(sender: SourceView) { - guard let tool = self.activeAnnotationTool else { return } + guard let tool = self.documentController.pdfController?.annotationStateManager.state, let toolbarTool = tool.toolbarTool else { return } let colorHex = self.viewModel.state.toolColors[tool]?.hexString let size: Float? @@ -429,32 +386,24 @@ class PDFReaderViewController: UIViewController { } self.coordinatorDelegate?.showToolSettings( - tool: tool, + tool: toolbarTool, colorHex: colorHex, sizeValue: size, sender: sender, - userInterfaceStyle: self.viewModel.state.interfaceStyle + userInterfaceStyle: self.viewModel.state.settings.appearanceMode.userInterfaceStyle ) { [weak self] newColor, newSize in self?.viewModel.process(action: .setToolOptions(color: newColor, size: newSize.flatMap(CGFloat.init), tool: tool)) } } - private func hideSidebarIfNeeded(forPosition position: ToolbarState.Position, animated: Bool) { - guard self.isSidebarVisible && - (position == .pinned || (position == .top && self.annotationToolbarController.view.frame.width < PDFReaderViewController.minToolbarWidth)) else { return } - self.toggleSidebar(animated: animated) - } - private func toggleSidebar(animated: Bool) { let shouldShow = !self.isSidebarVisible if self.toolbarState.position == .leading { if shouldShow { - self.toolbarLeadingSafeArea.isActive = false - self.toolbarLeading.isActive = true + self.annotationToolbarHandler.disableLeadingSafeConstraint() } else { - self.toolbarLeading.isActive = false - self.toolbarLeadingSafeArea.isActive = true + self.annotationToolbarHandler.enableLeadingSafeConstraint() } } // If the layout is compact, show annotation sidebar above pdf document. @@ -507,14 +456,33 @@ class PDFReaderViewController: UIViewController { } func showSearch(pdfController: PDFViewController, text: String?) { - self.coordinatorDelegate?.showSearch(pdfController: pdfController, text: text, sender: self.searchButton, userInterfaceStyle: self.viewModel.state.interfaceStyle, delegate: self) + self.coordinatorDelegate?.showSearch( + pdfController: pdfController, + text: text, + sender: self.searchButton, + userInterfaceStyle: self.viewModel.state.settings.appearanceMode.userInterfaceStyle, + delegate: self + ) } private func showSettings(sender: UIBarButtonItem) { - self.coordinatorDelegate?.showSettings(with: self.viewModel.state.settings, sender: sender, userInterfaceStyle: self.viewModel.state.interfaceStyle, completion: { [weak self] settings in - guard let self, let interfaceStyle = self.presentingViewController?.traitCollection.userInterfaceStyle else { return } - self.viewModel.process(action: .setSettings(settings: settings, currentUserInterfaceStyle: interfaceStyle)) - }) + let viewModel = self.coordinatorDelegate?.showSettings(with: self.viewModel.state.settings, sender: sender) + + viewModel?.stateObservable + .observe(on: MainScheduler.instance) + .subscribe(with: self, onNext: { `self`, state in + guard let interfaceStyle = self.presentingViewController?.traitCollection.userInterfaceStyle else { return } + let settings = PDFSettings( + transition: state.transition, + pageMode: state.pageMode, + direction: state.scrollDirection, + pageFitting: state.pageFitting, + appearanceMode: state.appearance, + idleTimerDisabled: state.idleTimerDisabled + ) + self.viewModel.process(action: .setSettings(settings: settings, parentUserInterfaceStyle: interfaceStyle)) + }) + .disposed(by: disposeBag) } private func close() { @@ -526,561 +494,8 @@ class PDFReaderViewController: UIViewController { self.navigationController?.presentingViewController?.dismiss(animated: true, completion: nil) } - // MARK: - Annotation Bar - - private func isSwipe(fromVelocity velocity: CGPoint) -> Bool { - return velocity.y <= -1500 || abs(velocity.x) >= 1500 - } - - /// Return new position for given touch point and velocity of toolbar. The user can pan up/left/right to move the toolbar. If velocity > 1500, it's considered a swipe and the toolbar is moved - /// in swipe direction. Otherwise the toolbar is pinned to closest point from touch. - private func position(fromTouch point: CGPoint, frame: CGRect, containerFrame: CGRect, velocity: CGPoint, statusBarVisible: Bool) -> ToolbarState.Position { - if self.isSwipe(fromVelocity: velocity) { - // Move in direction of swipe - if abs(velocity.y) > abs(velocity.x) && containerFrame.size.width >= PDFReaderViewController.minToolbarWidth { - return .top - } - return velocity.x < 0 ? .leading : .trailing - } - - let topViewBottomRightPoint = self.toolbarTopPreview.convert(CGPoint(x: self.toolbarTopPreview.bounds.maxX, y: self.toolbarTopPreview.bounds.maxY), to: self.view) - - if point.y < topViewBottomRightPoint.y { - let pinnedViewBottomRightPoint = self.toolbarPinnedPreview.convert(CGPoint(x: self.toolbarPinnedPreview.frame.maxX, y: self.toolbarPinnedPreview.frame.maxY), to: self.view) - return point.y < pinnedViewBottomRightPoint.y ? .pinned : .top - } - - let xPos = point.x - containerFrame.minX - - if point.y < (topViewBottomRightPoint.y + 150) { - if xPos > 150 && xPos < (containerFrame.size.width - 150) { - return .top - } - return xPos <= 150 ? .leading : .trailing - } - - return xPos > containerFrame.size.width / 2 ? .trailing : .leading - } - - private func velocity(from panVelocity: CGPoint, newPosition: ToolbarState.Position) -> CGFloat { - let currentPosition: CGFloat - let endPosition: CGFloat - let velocity: CGFloat - - switch newPosition { - case .top: - velocity = panVelocity.y - currentPosition = self.annotationToolbarController.view.frame.minY - endPosition = self.view.safeAreaInsets.top - - case .leading: - velocity = panVelocity.x - currentPosition = self.annotationToolbarController.view.frame.minX - endPosition = 0 - - case .trailing: - velocity = panVelocity.x - currentPosition = self.annotationToolbarController.view.frame.maxX - endPosition = self.view.frame.width - - case .pinned: - velocity = panVelocity.y - currentPosition = self.annotationToolbarController.view.frame.minY - endPosition = self.view.safeAreaInsets.top - } - - return abs(velocity / (endPosition - currentPosition)) - } - - private func didTapToolbar(recognizer: UILongPressGestureRecognizer) { - switch recognizer.state { - case .began: - self.setHighlightSelected(at: self.toolbarState.position) - self.showPreviews() - - case .ended, .failed: - self.hidePreviewsIfNeeded() - - default: break - } - } - - private func toolbarDidPan(recognizer: UIPanGestureRecognizer) { - switch recognizer.state { - case .began: - self.toolbarInitialFrame = self.annotationToolbarController.view.frame - - case .changed: - guard let originalFrame = self.toolbarInitialFrame else { return } - let translation = recognizer.translation(in: self.annotationToolbarController.view) - let location = recognizer.location(in: self.view) - let position = self.position( - fromTouch: location, - frame: self.annotationToolbarController.view.frame, - containerFrame: self.documentController.view.frame, - velocity: CGPoint(), - statusBarVisible: self.statusBarVisible - ) - - self.annotationToolbarController.view.frame = originalFrame.offsetBy(dx: translation.x, dy: translation.y) - - self.showPreviewsOnDragIfNeeded(translation: translation, velocity: recognizer.velocity(in: self.view), currentPosition: self.toolbarState.position) - - if !self.toolbarPreviewsOverlay.isHidden { - self.setHighlightSelected(at: position) - } - - case .ended, .failed: - let velocity = recognizer.velocity(in: self.view) - let location = recognizer.location(in: self.view) - let position = self.position( - fromTouch: location, - frame: self.annotationToolbarController.view.frame, - containerFrame: self.documentController.view.frame, - velocity: velocity, - statusBarVisible: self.statusBarVisible - ) - let newState = ToolbarState(position: position, visible: true) - - if position == .top && self.toolbarState.position == .pinned { - self.statusBarVisible = true - } - self.set(toolbarPosition: position, oldPosition: self.toolbarState.position, velocity: velocity, statusBarVisible: self.statusBarVisible) - self.setAnnotationToolbarHandleMinimumLongPressDuration(forPosition: position) - self.toolbarState = newState - self.toolbarInitialFrame = nil - - default: break - } - } - - private func showPreviewsOnDragIfNeeded(translation: CGPoint, velocity: CGPoint, currentPosition: ToolbarState.Position) { - guard self.toolbarPreviewsOverlay.isHidden else { return } - - let distance = sqrt((translation.x * translation.x) + (translation.y * translation.y)) - let distanceThreshold: CGFloat = (currentPosition == .pinned || currentPosition == .top) ? 0 : 70 - - guard distance > distanceThreshold && !self.isSwipe(fromVelocity: velocity) else { return } - - self.showPreviews() - } - - private func showPreviews() { - self.updatePositionOverlayViews( - currentHeight: self.annotationToolbarController.view.frame.height, - containerSize: self.documentController.view.frame.size, - position: self.toolbarState.position, - statusBarVisible: self.statusBarVisible - ) - self.toolbarPreviewsOverlay.alpha = 0 - self.toolbarPreviewsOverlay.isHidden = false - - UIView.animate(withDuration: 0.2, animations: { - self.toolbarPreviewsOverlay.alpha = 1 - self.navigationController?.navigationBar.alpha = 0 - }) - } - - private func hidePreviewsIfNeeded() { - guard self.toolbarPreviewsOverlay.alpha == 1 else { return } - - UIView.animate(withDuration: 0.2, animations: { - self.navigationController?.navigationBar.alpha = 1 - self.toolbarPreviewsOverlay.alpha = 0 - }, completion: { finished in - guard finished else { return } - self.toolbarPreviewsOverlay.isHidden = true - }) - } - - private func updatePositionOverlayViews(currentHeight: CGFloat, containerSize: CGSize, position: ToolbarState.Position, statusBarVisible: Bool) { - let topToolbarsAvailable = containerSize.width >= PDFReaderViewController.minToolbarWidth - let verticalHeight: CGFloat - switch position { - case .leading, .trailing: - // Position the preview so that the bottom of preview matches actual bottom of toolbar, add offset for dashed border - let offset = self.annotationToolbarController.size + (statusBarVisible ? 0 : self.annotationToolbarController.size) - verticalHeight = currentHeight - offset + (DashedView.dashWidth * 2) + 1 - - case .top, .pinned: - verticalHeight = min(containerSize.height - currentHeight - (position == .pinned ? self.navigationBarHeight : 0), AnnotationToolbarViewController.estimatedVerticalHeight) - } - - self.toolbarPinnedPreview.isHidden = !topToolbarsAvailable || (position == .top && !statusBarVisible) - self.inbetweenTopDashedView.isHidden = self.toolbarPinnedPreview.isHidden - if !self.toolbarPinnedPreview.isHidden { - // Change height based on current position so that preview is shown around currently visible toolbar - let baseHeight = position == .pinned ? self.annotationToolbarController.size : self.navigationBarHeight - self.toolbarPinnedPreviewHeight.constant = baseHeight + self.topOffsets(statusBarVisible: statusBarVisible).statusBarHeight - (position == .top ? 1 : 0) - } - self.toolbarTopPreview.isHidden = !topToolbarsAvailable - self.toolbarLeadingPreviewHeight.constant = verticalHeight - self.toolbarTrailingPreviewHeight.constant = verticalHeight - self.toolbarPreviewsOverlay.layoutIfNeeded() - } - - private func set(toolbarPosition newPosition: ToolbarState.Position, oldPosition: ToolbarState.Position, velocity velocityPoint: CGPoint, statusBarVisible: Bool) { - let navigationBarHidden = newPosition == .pinned || !statusBarVisible - - switch (newPosition, oldPosition) { - case (.leading, .leading), (.trailing, .trailing), (.top, .top), (.pinned, .pinned): - // Position didn't change, move to initial frame - let frame = self.toolbarInitialFrame ?? CGRect() - let velocity = self.velocity(from: velocityPoint, newPosition: newPosition) - - if !navigationBarHidden && self.navigationController?.navigationBar.isHidden == true { - self.navigationController?.setNavigationBarHidden(false, animated: false) - self.navigationController?.navigationBar.alpha = 0 - } - - UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity, options: [.curveEaseOut], animations: { - self.toolbarPreviewsOverlay.alpha = 0 - self.annotationToolbarController.view.frame = frame - self.navigationController?.navigationBar.alpha = navigationBarHidden ? 0 : 1 - self.documentController.setInterface(hidden: !statusBarVisible) - }, completion: { finished in - guard finished else { return } - - self.toolbarPreviewsOverlay.isHidden = true - - if navigationBarHidden { - self.navigationController?.setNavigationBarHidden(true, animated: false) - } - }) - - case (.leading, .trailing), (.trailing, .leading), (.top, .pinned), (.pinned, .top): - // Move from side to side or vertically - let velocity = self.velocity(from: velocityPoint, newPosition: newPosition) - self.setConstraints(for: newPosition, statusBarVisible: statusBarVisible) - self.setDocumentTopConstraint(forToolbarState: ToolbarState(position: newPosition, visible: true), statusBarVisible: statusBarVisible) - self.view.setNeedsLayout() - - self.hideSidebarIfNeeded(forPosition: newPosition, animated: true) - - if !navigationBarHidden && self.navigationController?.navigationBar.isHidden == true { - self.navigationController?.setNavigationBarHidden(false, animated: false) - self.navigationController?.navigationBar.alpha = 0 - } - - UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity, options: [], animations: { - self.view.layoutIfNeeded() - self.toolbarPreviewsOverlay.alpha = 0 - self.navigationController?.navigationBar.alpha = navigationBarHidden ? 0 : 1 - self.documentController.setInterface(hidden: !statusBarVisible) - self.navigationController?.setNeedsStatusBarAppearanceUpdate() - self.setNeedsStatusBarAppearanceUpdate() - }, completion: { finished in - guard finished else { return } - - self.toolbarPreviewsOverlay.isHidden = true - - if navigationBarHidden { - self.navigationController?.setNavigationBarHidden(true, animated: false) - } - }) - - case (.top, .leading), (.top, .trailing), (.leading, .top), (.leading, .pinned), (.trailing, .top), (.trailing, .pinned), (.pinned, .leading), (.pinned, .trailing): - let velocity = self.velocity(from: velocityPoint, newPosition: newPosition) - UIView.animate(withDuration: 0.1, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity, options: [], animations: { - let newFrame = self.annotationToolbarController.view.frame.offsetBy(dx: velocityPoint.x / 10, dy: velocityPoint.y / 10) - self.annotationToolbarController.view.frame = newFrame - self.annotationToolbarController.view.alpha = 0 - }, completion: { finished in - guard finished else { return } - - if !navigationBarHidden && self.navigationController?.navigationBar.isHidden == true { - self.navigationController?.setNavigationBarHidden(false, animated: false) - self.navigationController?.navigationBar.alpha = 0 - } - - self.annotationToolbarController.prepareForSizeChange() - self.setConstraints(for: newPosition, statusBarVisible: statusBarVisible) - self.view.layoutIfNeeded() - self.annotationToolbarController.sizeDidChange() - self.view.layoutIfNeeded() - self.setDocumentTopConstraint(forToolbarState: ToolbarState(position: newPosition, visible: true), statusBarVisible: statusBarVisible) - - self.hideSidebarIfNeeded(forPosition: newPosition, animated: true) - - UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity, options: [], animations: { - self.annotationToolbarController.view.alpha = 1 - self.view.layoutIfNeeded() - self.toolbarPreviewsOverlay.alpha = 0 - self.navigationController?.navigationBar.alpha = navigationBarHidden ? 0 : 1 - self.documentController.setInterface(hidden: !statusBarVisible) - self.navigationController?.setNeedsStatusBarAppearanceUpdate() - self.setNeedsStatusBarAppearanceUpdate() - }, completion: { finished in - guard finished else { return } - - self.toolbarPreviewsOverlay.isHidden = true - - if navigationBarHidden { - self.navigationController?.setNavigationBarHidden(true, animated: false) - } - }) - }) - } - } - - private func setConstraints(for position: ToolbarState.Position, statusBarVisible: Bool) { - let rotation: AnnotationToolbarViewController.Rotation = (position == .top || position == .pinned) ? .horizontal : .vertical - if self.isCompactSize(for: rotation) { - self.setCompactConstraints(for: position, statusBarVisible: statusBarVisible) - } else { - self.setFullConstraints(for: position, statusBarVisible: statusBarVisible) - } - } - - func topOffsets(statusBarVisible: Bool) -> (statusBarHeight: CGFloat, navigationBarHeight: CGFloat, total: CGFloat) { - let statusBarOffset = statusBarVisible || UIDevice.current.userInterfaceIdiom != .pad ? self.statusBarHeight : 0 - let navigationBarOffset = statusBarVisible ? self.navigationBarHeight : 0 - return (statusBarOffset, navigationBarOffset, statusBarOffset + navigationBarOffset) - } - - private func setDocumentTopConstraint(forToolbarState state: ToolbarState, statusBarVisible: Bool) { - let (statusBarOffset, _, totalOffset) = self.topOffsets(statusBarVisible: statusBarVisible) - - if !state.visible { - self.documentTop.constant = totalOffset - return - } - - switch state.position { - case .pinned: - self.documentTop.constant = statusBarOffset + self.annotationToolbarController.size - - case .top: - self.documentTop.constant = totalOffset + self.annotationToolbarController.size - - case .trailing, .leading: - self.documentTop.constant = totalOffset - } - } - - private func setFullConstraints(for position: ToolbarState.Position, statusBarVisible: Bool) { - switch position { - case .leading: - self.toolbarTrailing.isActive = false - self.toolbarTrailingSafeArea.isActive = false - if self.isSidebarVisible { - self.toolbarLeadingSafeArea.isActive = false - self.toolbarLeading.isActive = true - self.toolbarLeading.constant = PDFReaderViewController.toolbarFullInsetInset - } else { - self.toolbarLeading.isActive = false - self.toolbarLeadingSafeArea.isActive = true - self.toolbarLeadingSafeArea.constant = PDFReaderViewController.toolbarFullInsetInset - } - self.toolbarTop.constant = PDFReaderViewController.toolbarFullInsetInset + self.topOffsets(statusBarVisible: statusBarVisible).total - self.annotationToolbarController.set(rotation: .vertical, isCompactSize: false) - - case .trailing: - self.toolbarLeading.isActive = false - self.toolbarLeadingSafeArea.isActive = false - self.toolbarTrailing.isActive = false - self.toolbarTrailingSafeArea.isActive = true - self.toolbarTrailingSafeArea.constant = PDFReaderViewController.toolbarFullInsetInset - self.toolbarTop.constant = PDFReaderViewController.toolbarFullInsetInset + self.topOffsets(statusBarVisible: statusBarVisible).total - self.annotationToolbarController.set(rotation: .vertical, isCompactSize: false) - - case .top: - self.setupTopConstraints(isCompact: false, isPinned: false, statusBarVisible: statusBarVisible) - - case .pinned: - self.setupTopConstraints(isCompact: false, isPinned: true, statusBarVisible: statusBarVisible) - } - } - - private func setCompactConstraints(for position: ToolbarState.Position, statusBarVisible: Bool) { - switch position { - case .leading: - self.toolbarTrailing.isActive = false - self.toolbarTrailingSafeArea.isActive = false - if self.isSidebarVisible { - self.toolbarLeadingSafeArea.isActive = false - self.toolbarLeading.isActive = true - self.toolbarLeading.constant = PDFReaderViewController.toolbarCompactInset - } else { - self.toolbarLeading.isActive = false - self.toolbarLeadingSafeArea.isActive = true - self.toolbarLeadingSafeArea.constant = PDFReaderViewController.toolbarCompactInset - } - self.toolbarTop.constant = PDFReaderViewController.toolbarCompactInset + self.topOffsets(statusBarVisible: statusBarVisible).total - self.annotationToolbarController.set(rotation: .vertical, isCompactSize: true) - - case .trailing: - self.toolbarLeading.isActive = false - self.toolbarLeadingSafeArea.isActive = false - self.toolbarTrailing.isActive = false - self.toolbarTrailingSafeArea.isActive = true - self.toolbarTrailingSafeArea.constant = PDFReaderViewController.toolbarCompactInset - self.toolbarTop.constant = PDFReaderViewController.toolbarCompactInset + self.topOffsets(statusBarVisible: statusBarVisible).total - self.annotationToolbarController.set(rotation: .vertical, isCompactSize: true) - - case .top: - self.setupTopConstraints(isCompact: true, isPinned: false, statusBarVisible: statusBarVisible) - - case .pinned: - self.setupTopConstraints(isCompact: true, isPinned: true, statusBarVisible: statusBarVisible) - } - } - - private func setupTopConstraints(isCompact: Bool, isPinned: Bool, statusBarVisible: Bool) { - self.toolbarLeadingSafeArea.isActive = false - self.toolbarTrailingSafeArea.isActive = false - self.toolbarTrailing.isActive = true - self.toolbarTrailing.constant = 0 - self.toolbarLeading.isActive = true - self.toolbarLeading.constant = 0 - self.toolbarTop.constant = isPinned ? self.topOffsets(statusBarVisible: statusBarVisible).statusBarHeight : self.topOffsets(statusBarVisible: statusBarVisible).total - self.annotationToolbarController.set(rotation: .horizontal, isCompactSize: isCompact) - } - - private func showAnnotationToolbar(state: ToolbarState, statusBarVisible: Bool, animated: Bool) { - self.annotationToolbarController.prepareForSizeChange() - self.setConstraints(for: state.position, statusBarVisible: statusBarVisible) - self.annotationToolbarController.view.isHidden = false - self.view.layoutIfNeeded() - self.annotationToolbarController.sizeDidChange() - self.view.layoutIfNeeded() - self.setDocumentTopConstraint(forToolbarState: state, statusBarVisible: statusBarVisible) - - self.hideSidebarIfNeeded(forPosition: state.position, animated: animated) - - let navigationBarHidden = !statusBarVisible || state.position == .pinned - - if !animated { - self.annotationToolbarController.view.alpha = 1 - self.navigationController?.setNavigationBarHidden(navigationBarHidden, animated: false) - self.navigationController?.navigationBar.alpha = navigationBarHidden ? 0 : 1 - self.view.layoutIfNeeded() - return - } - - if !navigationBarHidden && self.navigationController?.navigationBar.isHidden == true { - self.navigationController?.setNavigationBarHidden(false, animated: false) - self.navigationController?.navigationBar.alpha = 0 - } - - UIView.animate(withDuration: 0.2, animations: { - self.annotationToolbarController.view.alpha = 1 - self.navigationController?.navigationBar.alpha = navigationBarHidden ? 0 : 1 - self.view.layoutIfNeeded() - }, completion: { finished in - guard finished && navigationBarHidden else { return } - self.navigationController?.setNavigationBarHidden(true, animated: false) - }) - } - - private func hideAnnotationToolbar(newState: ToolbarState, statusBarVisible: Bool, animated: Bool) { - self.setDocumentTopConstraint(forToolbarState: newState, statusBarVisible: statusBarVisible) - - if !animated { - self.view.layoutIfNeeded() - self.annotationToolbarController.view.alpha = 0 - self.annotationToolbarController.view.isHidden = true - self.navigationController?.navigationBar.alpha = statusBarVisible ? 1 : 0 - self.navigationController?.setNavigationBarHidden(!statusBarVisible, animated: false) - return - } - - if statusBarVisible && self.navigationController?.navigationBar.isHidden == true { - self.navigationController?.setNavigationBarHidden(false, animated: false) - self.navigationController?.navigationBar.alpha = 0 - } - - UIView.animate(withDuration: 0.2, animations: { - self.view.layoutIfNeeded() - self.annotationToolbarController.view.alpha = 0 - self.navigationController?.navigationBar.alpha = statusBarVisible ? 1 : 0 - }, completion: { finished in - guard finished else { return } - self.annotationToolbarController.view.isHidden = true - self.documentController.disableAnnotationTools() - if !statusBarVisible { - self.navigationController?.setNavigationBarHidden(true, animated: false) - } - }) - } - - private func setAnnotationToolbarHandleMinimumLongPressDuration(forPosition position: ToolbarState.Position) { - switch position { - case .leading, .trailing: - self.annotationToolbarDragHandleLongPressRecognizer.minimumPressDuration = 0.3 - - case .top, .pinned: - self.annotationToolbarDragHandleLongPressRecognizer.minimumPressDuration = 0 - } - } - - private func setHighlightSelected(at position: ToolbarState.Position) { - switch position { - case .top: - self.toolbarLeadingPreview.backgroundColor = self.previewBackgroundColor - self.toolbarLeadingPreview.dashColor = self.previewDashColor - self.toolbarTrailingPreview.backgroundColor = self.previewBackgroundColor - self.toolbarTrailingPreview.dashColor = self.previewDashColor - self.toolbarTopPreview.backgroundColor = self.previewSelectedBackgroundColor - self.toolbarTopPreview.dashColor = self.previewSelectedDashColor - self.toolbarPinnedPreview.backgroundColor = self.previewBackgroundColor - self.toolbarPinnedPreview.dashColor = self.previewDashColor - - case .leading: - self.toolbarLeadingPreview.backgroundColor = self.previewSelectedBackgroundColor - self.toolbarLeadingPreview.dashColor = self.previewSelectedDashColor - self.toolbarTrailingPreview.backgroundColor = self.previewBackgroundColor - self.toolbarTrailingPreview.dashColor = self.previewDashColor - self.toolbarTopPreview.backgroundColor = self.previewBackgroundColor - self.toolbarTopPreview.dashColor = self.previewDashColor - self.toolbarPinnedPreview.backgroundColor = self.previewBackgroundColor - self.toolbarPinnedPreview.dashColor = self.previewDashColor - - case .trailing: - self.toolbarLeadingPreview.backgroundColor = self.previewBackgroundColor - self.toolbarLeadingPreview.dashColor = self.previewDashColor - self.toolbarTrailingPreview.backgroundColor = self.previewSelectedBackgroundColor - self.toolbarTrailingPreview.dashColor = self.previewSelectedDashColor - self.toolbarTopPreview.backgroundColor = self.previewBackgroundColor - self.toolbarTopPreview.dashColor = self.previewDashColor - self.toolbarPinnedPreview.backgroundColor = self.previewBackgroundColor - self.toolbarPinnedPreview.dashColor = self.previewDashColor - - case .pinned: - self.toolbarLeadingPreview.backgroundColor = self.previewBackgroundColor - self.toolbarLeadingPreview.dashColor = self.previewDashColor - self.toolbarTrailingPreview.backgroundColor = self.previewBackgroundColor - self.toolbarTrailingPreview.dashColor = self.previewDashColor - self.toolbarTopPreview.backgroundColor = self.previewBackgroundColor - self.toolbarTopPreview.dashColor = self.previewDashColor - self.toolbarPinnedPreview.backgroundColor = self.previewSelectedBackgroundColor - self.toolbarPinnedPreview.dashColor = self.previewSelectedDashColor - } - } - // MARK: - Setups - private func setupGestureRecognizer() { - let panRecognizer = UIPanGestureRecognizer() - panRecognizer.delegate = self - panRecognizer.rx.event - .subscribe(with: self, onNext: { `self`, recognizer in - self.toolbarDidPan(recognizer: recognizer) - }) - .disposed(by: self.disposeBag) - self.annotationToolbarController.view.addGestureRecognizer(panRecognizer) - - let longPressRecognizer = UILongPressGestureRecognizer() - longPressRecognizer.delegate = self - longPressRecognizer.rx.event - .subscribe(with: self, onNext: { `self`, recognizer in - self.didTapToolbar(recognizer: recognizer) - }) - .disposed(by: self.disposeBag) - self.annotationToolbarDragHandleLongPressRecognizer = longPressRecognizer - self.annotationToolbarController.view.addGestureRecognizer(longPressRecognizer) - } - private func add(controller: UIViewController) { controller.willMove(toParent: self) self.addChild(controller) @@ -1088,6 +503,10 @@ class PDFReaderViewController: UIViewController { } private func setupViews() { + let topSafeAreaSpacer = UIView() + topSafeAreaSpacer.translatesAutoresizingMaskIntoConstraints = false + topSafeAreaSpacer.backgroundColor = Asset.Colors.navbarBackground.color + let documentController = PDFDocumentViewController(viewModel: self.viewModel, compactSize: self.isCompactWidth, initialUIHidden: !self.statusBarVisible) documentController.parentDelegate = self documentController.coordinatorDelegate = self.coordinatorDelegate @@ -1103,63 +522,21 @@ class PDFReaderViewController: UIViewController { separator.translatesAutoresizingMaskIntoConstraints = false separator.backgroundColor = Asset.Colors.annotationSidebarBorderColor.color - let annotationToolbar = AnnotationToolbarViewController(size: self.navigationBarHeight) + let annotationToolbar = AnnotationToolbarViewController(tools: [.highlight, .note, .image, .ink, .eraser], undoRedoEnabled: true, size: self.navigationBarHeight) annotationToolbar.delegate = self - annotationToolbar.view.translatesAutoresizingMaskIntoConstraints = false - annotationToolbar.view.setContentHuggingPriority(.required, for: .horizontal) - annotationToolbar.view.setContentHuggingPriority(.required, for: .vertical) - - let previewsOverlay = UIView() - previewsOverlay.translatesAutoresizingMaskIntoConstraints = false - previewsOverlay.backgroundColor = .clear - previewsOverlay.isHidden = true - - let topSafeAreaSpacer = UIView() - topSafeAreaSpacer.translatesAutoresizingMaskIntoConstraints = false - topSafeAreaSpacer.backgroundColor = Asset.Colors.navbarBackground.color - self.view.addSubview(topSafeAreaSpacer) - - let topPreview = DashedView(type: .partialStraight(sides: [.left, .right, .bottom])) - self.setup(toolbarPositionView: topPreview) - let inbetweenTopDash = DashedView(type: .partialStraight(sides: .bottom)) - self.setup(toolbarPositionView: inbetweenTopDash) - let pinnedPreview = DashedView(type: .partialStraight(sides: [.left, .right, .top])) - self.setup(toolbarPositionView: pinnedPreview) - let leadingPreview = DashedView(type: .rounded(cornerRadius: 8)) - leadingPreview.translatesAutoresizingMaskIntoConstraints = false - self.setup(toolbarPositionView: leadingPreview) - let trailingPreview = DashedView(type: .rounded(cornerRadius: 8)) - trailingPreview.translatesAutoresizingMaskIntoConstraints = false - self.setup(toolbarPositionView: trailingPreview) - - let topPreviewContainer = UIStackView(arrangedSubviews: [pinnedPreview, inbetweenTopDash, topPreview]) - topPreviewContainer.translatesAutoresizingMaskIntoConstraints = false - topPreviewContainer.axis = .vertical self.add(controller: documentController) self.add(controller: sidebarController) self.add(controller: annotationToolbar) + self.view.addSubview(topSafeAreaSpacer) self.view.addSubview(documentController.view) self.view.addSubview(sidebarController.view) self.view.addSubview(separator) self.view.addSubview(annotationToolbar.view) - self.view.insertSubview(previewsOverlay, belowSubview: annotationToolbar.view) - previewsOverlay.addSubview(topPreviewContainer) - previewsOverlay.addSubview(leadingPreview) - previewsOverlay.addSubview(trailingPreview) let documentLeftConstraint = documentController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor) let sidebarLeftConstraint = sidebarController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: -PDFReaderLayout.sidebarWidth) self.documentTop = documentController.view.topAnchor.constraint(equalTo: self.view.topAnchor) - self.toolbarLeading = annotationToolbar.view.leadingAnchor.constraint(equalTo: sidebarController.view.trailingAnchor, constant: PDFReaderViewController.toolbarFullInsetInset) - self.toolbarLeading.priority = .init(999) - self.toolbarLeadingSafeArea = annotationToolbar.view.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor) - self.toolbarTrailing = self.view.trailingAnchor.constraint(equalTo: annotationToolbar.view.trailingAnchor, constant: PDFReaderViewController.toolbarFullInsetInset) - self.toolbarTrailingSafeArea = self.view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: annotationToolbar.view.trailingAnchor, constant: PDFReaderViewController.toolbarFullInsetInset) - let toolbarTop = annotationToolbar.view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: PDFReaderViewController.toolbarCompactInset) - let leadingPreviewHeight = leadingPreview.heightAnchor.constraint(equalToConstant: 50) - let trailingPreviewHeight = trailingPreview.heightAnchor.constraint(equalToConstant: 50) - let pinnedPreviewHeight = pinnedPreview.heightAnchor.constraint(equalToConstant: annotationToolbar.size) NSLayoutConstraint.activate([ topSafeAreaSpacer.topAnchor.constraint(equalTo: self.view.topAnchor), @@ -1177,27 +554,7 @@ class PDFReaderViewController: UIViewController { documentController.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), self.documentTop, documentController.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor), - documentLeftConstraint, - toolbarTop, - self.toolbarLeadingSafeArea, - previewsOverlay.topAnchor.constraint(equalTo: self.view.topAnchor), - previewsOverlay.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor), - previewsOverlay.leadingAnchor.constraint(equalTo: documentController.view.leadingAnchor), - previewsOverlay.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), - topPreviewContainer.topAnchor.constraint(equalTo: previewsOverlay.topAnchor), - topPreviewContainer.leadingAnchor.constraint(equalTo: previewsOverlay.leadingAnchor), - previewsOverlay.trailingAnchor.constraint(equalTo: topPreviewContainer.trailingAnchor), - pinnedPreviewHeight, - topPreview.heightAnchor.constraint(equalToConstant: annotationToolbar.size), - leadingPreview.leadingAnchor.constraint(equalTo: previewsOverlay.safeAreaLayoutGuide.leadingAnchor, constant: PDFReaderViewController.toolbarFullInsetInset), - leadingPreview.topAnchor.constraint(equalTo: topPreviewContainer.bottomAnchor, constant: PDFReaderViewController.toolbarCompactInset), - leadingPreviewHeight, - leadingPreview.widthAnchor.constraint(equalToConstant: annotationToolbar.size), - previewsOverlay.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: trailingPreview.trailingAnchor, constant: PDFReaderViewController.toolbarFullInsetInset), - trailingPreview.topAnchor.constraint(equalTo: topPreviewContainer.bottomAnchor, constant: PDFReaderViewController.toolbarCompactInset), - trailingPreviewHeight, - trailingPreview.widthAnchor.constraint(equalToConstant: annotationToolbar.size), - inbetweenTopDash.heightAnchor.constraint(equalToConstant: 2 / UIScreen.main.scale) + documentLeftConstraint ]) self.documentController = documentController @@ -1205,22 +562,12 @@ class PDFReaderViewController: UIViewController { self.sidebarController = sidebarController self.sidebarControllerLeft = sidebarLeftConstraint self.annotationToolbarController = annotationToolbar - self.toolbarTop = toolbarTop - self.toolbarPreviewsOverlay = previewsOverlay - self.toolbarTopPreview = topPreview - self.toolbarPinnedPreview = pinnedPreview - self.toolbarLeadingPreview = leadingPreview - self.toolbarLeadingPreviewHeight = leadingPreviewHeight - self.toolbarTrailingPreview = trailingPreview - self.toolbarTrailingPreviewHeight = trailingPreviewHeight - self.toolbarPinnedPreviewHeight = pinnedPreviewHeight - self.inbetweenTopDashedView = inbetweenTopDash - } - private func setup(toolbarPositionView view: DashedView) { - view.backgroundColor = self.previewBackgroundColor - view.dashColor = self.previewDashColor - view.layer.masksToBounds = true + self.annotationToolbarHandler = AnnotationToolbarHandler(controller: annotationToolbar, delegate: self) + self.annotationToolbarHandler.didHide = { [weak self] in + self?.documentController.disableAnnotationTools() + } + self.annotationToolbarHandler.performInitialLayout() } private func setupAccessibility(forSidebarButton button: UIBarButtonItem) { @@ -1245,11 +592,11 @@ class PDFReaderViewController: UIViewController { readerButton.accessibilityLabel = L10n.Accessibility.Pdf.openReader readerButton.title = L10n.Accessibility.Pdf.openReader readerButton.rx - .tap - .subscribe(with: self, onNext: { `self`, _ in - self.coordinatorDelegate?.showReader(document: self.viewModel.state.document, userInterfaceStyle: self.viewModel.state.interfaceStyle) - }) - .disposed(by: self.disposeBag) + .tap + .subscribe(with: self, onNext: { `self`, _ in + self.coordinatorDelegate?.showReader(document: self.viewModel.state.document, userInterfaceStyle: self.viewModel.state.settings.appearanceMode.userInterfaceStyle) + }) + .disposed(by: self.disposeBag) self.navigationItem.leftBarButtonItems = [closeButton, sidebarButton, readerButton] self.navigationItem.rightBarButtonItems = self.rightBarButtonItems @@ -1300,6 +647,131 @@ class PDFReaderViewController: UIViewController { extension PDFReaderViewController: PDFReaderContainerDelegate {} +extension PDFReaderViewController: AnnotationToolbarHandlerDelegate { + var isNavigationBarHidden: Bool { + self.navigationController?.navigationBar.isHidden ?? false + } + + var isSidebarHidden: Bool { + !self.isSidebarVisible + } + + var toolbarLeadingAnchor: NSLayoutXAxisAnchor { + return self.sidebarController.view.trailingAnchor + } + + var toolbarLeadingSafeAreaAnchor: NSLayoutXAxisAnchor { + return self.view.safeAreaLayoutGuide.leadingAnchor + } + + var containerView: UIView { + return self.view + } + + var documentView: UIView { + return self.documentController.view + } + + func layoutIfNeeded() { + self.view.layoutIfNeeded() + } + + func setNeedsLayout() { + self.view.setNeedsLayout() + } + + func setNavigationBar(hidden: Bool, animated: Bool) { + self.navigationController?.setNavigationBarHidden(hidden, animated: animated) + } + + func setNavigationBar(alpha: CGFloat) { + self.navigationController?.navigationBar.alpha = alpha + } + + func topDidChange(forToolbarState state: AnnotationToolbarHandler.State) { + let (statusBarOffset, _, totalOffset) = self.annotationToolbarHandler.topOffsets(statusBarVisible: self.statusBarVisible) + + if !state.visible { + self.documentTop.constant = totalOffset + return + } + + switch state.position { + case .pinned: + self.documentTop.constant = statusBarOffset + self.annotationToolbarController.size + + case .top: + self.documentTop.constant = totalOffset + self.annotationToolbarController.size + + case .trailing, .leading: + self.documentTop.constant = totalOffset + } + } + + func hideSidebarIfNeeded(forPosition position: AnnotationToolbarHandler.State.Position, isToolbarSmallerThanMinWidth: Bool, animated: Bool) { + guard self.isSidebarVisible && (position == .pinned || (position == .top && isToolbarSmallerThanMinWidth)) else { return } + self.toggleSidebar(animated: animated) + } + + func setDocumentInterface(hidden: Bool) { + self.documentController.setInterface(hidden: hidden) + } + + func updateStatusBar() { + self.navigationController?.setNeedsStatusBarAppearanceUpdate() + self.setNeedsStatusBarAppearanceUpdate() + } +} + +extension PDFReaderViewController: AnnotationToolbarDelegate { + func closeAnnotationToolbar() { + (self.toolbarButton.customView as? CheckboxButton)?.isSelected = false + self.annotationToolbarHandler.set(hidden: true, animated: true) + } + + var activeAnnotationTool: AnnotationTool? { + return self.documentController.pdfController?.annotationStateManager.state?.toolbarTool + } + + var maxAvailableToolbarSize: CGFloat { + guard self.toolbarState.visible, let documentController = self.documentController else { return 0 } + + switch self.toolbarState.position { + case .top, .pinned: + return self.isCompactWidth ? documentController.view.frame.size.width : (documentController.view.frame.size.width - (2 * AnnotationToolbarHandler.toolbarFullInsetInset)) + + case .trailing, .leading: + let window = (view.scene as? UIWindowScene)?.windows.first(where: \.isKeyWindow) + let topInset = window?.safeAreaInsets.top ?? 0 + let bottomInset = window?.safeAreaInsets.bottom ?? 0 + let interfaceIsHidden = self.navigationController?.isNavigationBarHidden ?? false + return self.view.frame.size.height - (2 * AnnotationToolbarHandler.toolbarCompactInset) - (interfaceIsHidden ? 0 : (topInset + documentController.scrubberBarHeight)) - bottomInset + } + } + + func toggle(tool: AnnotationTool, options: AnnotationToolOptions) { + let pspdfkitTool = tool.pspdfkitTool + let color = self.viewModel.state.toolColors[pspdfkitTool] + self.documentController.toggle(annotationTool: pspdfkitTool, color: color, tappedWithStylus: (options == .stylus)) + } + + var canUndo: Bool { + return self.viewModel.state.document.undoController.undoManager.canUndo + } + + func performUndo() { + self.viewModel.state.document.undoController.undoManager.undo() + } + + var canRedo: Bool { + return self.viewModel.state.document.undoController.undoManager.canRedo + } + + func performRedo() { + self.viewModel.state.document.undoController.undoManager.redo() + } +} + extension PDFReaderViewController: SidebarDelegate { func tableOfContentsSelected(page: UInt) { self.documentController.focus(page: page) @@ -1316,13 +788,15 @@ extension PDFReaderViewController: PDFDocumentDelegate { variantFrom oldVariant: PSPDFKit.Annotation.Variant?, to newVariant: PSPDFKit.Annotation.Variant? ) { - if let state = oldState { + if let state = oldState?.toolbarTool { self.annotationToolbarController.set(selected: false, to: state, color: nil) } if let state = newState { let color = self.viewModel.state.toolColors[state] - self.annotationToolbarController.set(selected: true, to: state, color: color) + if let tool = state.toolbarTool { + self.annotationToolbarController.set(selected: true, to: tool, color: color) + } } } @@ -1331,8 +805,7 @@ extension PDFReaderViewController: PDFDocumentDelegate { } func interfaceVisibilityDidChange(to isHidden: Bool) { - let state = self.toolbarState - let shouldChangeNavigationBarVisibility = !state.visible || state.position != .pinned + let shouldChangeNavigationBarVisibility = !self.toolbarState.visible || self.toolbarState.position != .pinned if !isHidden && shouldChangeNavigationBarVisibility && self.navigationController?.navigationBar.isHidden == true { self.navigationController?.setNavigationBarHidden(false, animated: false) @@ -1340,8 +813,7 @@ extension PDFReaderViewController: PDFDocumentDelegate { } self.statusBarVisible = !isHidden - self.setDocumentTopConstraint(forToolbarState: state, statusBarVisible: self.statusBarVisible) - self.setConstraints(for: state.position, statusBarVisible: self.statusBarVisible) + self.annotationToolbarHandler.interfaceVisibilityDidChange() UIView.animate(withDuration: 0.15, animations: { self.navigationController?.setNeedsStatusBarAppearanceUpdate() @@ -1398,72 +870,6 @@ extension PDFReaderViewController: AnnotationBoundingBoxConverter { } } -extension PDFReaderViewController: AnnotationToolbarDelegate { - var rotation: AnnotationToolbarViewController.Rotation { - switch self.toolbarState.position { - case .leading, .trailing: return .vertical - case .top, .pinned: return .horizontal - } - } - - func closeAnnotationToolbar() { - (self.toolbarButton.customView as? CheckboxButton)?.isSelected = false - self.toolbarState = ToolbarState(position: self.toolbarState.position, visible: false) - self.hideAnnotationToolbar(newState: self.toolbarState, statusBarVisible: self.statusBarVisible, animated: true) - } - - var activeAnnotationTool: PSPDFKit.Annotation.Tool? { - return self.documentController.pdfController?.annotationStateManager.state - } - - var maxAvailableToolbarSize: CGFloat { - guard self.toolbarState.visible, let documentController = self.documentController else { return 0 } - - switch self.toolbarState.position { - case .top, .pinned: - return self.isCompactWidth ? documentController.view.frame.size.width : (documentController.view.frame.size.width - (2 * PDFReaderViewController.toolbarFullInsetInset)) - - case .trailing, .leading: - let window = (view.scene as? UIWindowScene)?.windows.first(where: \.isKeyWindow) - let topInset = window?.safeAreaInsets.top ?? 0 - let bottomInset = window?.safeAreaInsets.bottom ?? 0 - let interfaceIsHidden = self.navigationController?.isNavigationBarHidden ?? false - return self.view.frame.size.height - (2 * PDFReaderViewController.toolbarCompactInset) - (interfaceIsHidden ? 0 : (topInset + documentController.scrubberBarHeight)) - bottomInset - } - } - - func isCompactSize(for rotation: AnnotationToolbarViewController.Rotation) -> Bool { - switch rotation { - case .horizontal: - return self.isCompactWidth - - case .vertical: - return self.view.frame.height <= 400 - } - } - - func toggle(tool: PSPDFKit.Annotation.Tool, options: AnnotationToolOptions) { - let color = self.viewModel.state.toolColors[tool] - self.documentController.toggle(annotationTool: tool, color: color, tappedWithStylus: (options == .stylus)) - } - - var canUndo: Bool { - return self.viewModel.state.document.undoController.undoManager.canUndo - } - - func performUndo() { - self.viewModel.state.document.undoController.undoManager.undo() - } - - var canRedo: Bool { - return self.viewModel.state.document.undoController.undoManager.canRedo - } - - func performRedo() { - self.viewModel.state.document.undoController.undoManager.redo() - } -} - extension PDFReaderViewController: UIGestureRecognizerDelegate { func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { guard let longPressRecognizer = gestureRecognizer as? UILongPressGestureRecognizer else { return true } @@ -1475,11 +881,11 @@ extension PDFReaderViewController: UIGestureRecognizerDelegate { switch self.toolbarState.position { case .pinned, .top: currentLocation = location.x - border = self.annotationToolbarController.view.frame.width - PDFReaderViewController.annotationToolbarDragHandleHeight + border = self.annotationToolbarController.view.frame.width - AnnotationToolbarHandler.annotationToolbarDragHandleHeight case .leading, .trailing: currentLocation = location.y - border = self.annotationToolbarController.view.frame.height - PDFReaderViewController.annotationToolbarDragHandleHeight + border = self.annotationToolbarController.view.frame.height - AnnotationToolbarHandler.annotationToolbarDragHandleHeight } return currentLocation >= border } @@ -1493,8 +899,53 @@ extension PDFReaderViewController: PDFSearchDelegate { func didFinishSearch(with results: [SearchResult], for text: String?) { documentController.highlightSearchResults(results) } - + func didSelectSearchResult(_ result: SearchResult) { 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 + + 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 + } + } +} diff --git a/Zotero/Scenes/Detail/PDF/Views/PDFSidebarViewController.swift b/Zotero/Scenes/Detail/PDF/Views/PDFSidebarViewController.swift index cdd4c0854..3afb31e59 100644 --- a/Zotero/Scenes/Detail/PDF/Views/PDFSidebarViewController.swift +++ b/Zotero/Scenes/Detail/PDF/Views/PDFSidebarViewController.swift @@ -45,7 +45,7 @@ class PDFSidebarViewController: UIViewController { private weak var picker: UISegmentedControl! private weak var thumbnailsController: PDFThumbnailsViewController! - private weak var annotationsController: AnnotationsViewController! + private weak var annotationsController: PDFAnnotationsViewController! private weak var outlineController: TableOfContentsViewController! weak var parentDelegate: (PDFReaderContainerDelegate & SidebarDelegate)? weak var coordinatorDelegate: PdfAnnotationsCoordinatorDelegate? @@ -144,7 +144,7 @@ class PDFSidebarViewController: UIViewController { } private func setupControllers() { - let annotationsController = AnnotationsViewController(viewModel: viewModel) + let annotationsController = PDFAnnotationsViewController(viewModel: viewModel) annotationsController.parentDelegate = parentDelegate annotationsController.coordinatorDelegate = coordinatorDelegate annotationsController.boundingBoxConverter = boundingBoxConverter diff --git a/Zotero/Scenes/General/Models/ReaderSettingsAction.swift b/Zotero/Scenes/General/Models/ReaderSettingsAction.swift new file mode 100644 index 000000000..78f4aa91a --- /dev/null +++ b/Zotero/Scenes/General/Models/ReaderSettingsAction.swift @@ -0,0 +1,22 @@ +// +// ReaderSettingsAction.swift +// Zotero +// +// Created by Michal Rentka on 02.03.2022. +// Copyright © 2022 Corporation for Digital Scholarship. All rights reserved. +// + +import Foundation + +import PSPDFKitUI + +enum ReaderSettingsAction { + // PDF only + case setTransition(PSPDFKitUI.PageTransition) + case setPageMode(PSPDFKitUI.PageMode) + case setDirection(PSPDFKitUI.ScrollDirection) + case setPageFitting(PSPDFKitUI.PDFConfiguration.SpreadFitting) + // General + case setAppearance(ReaderSettingsState.Appearance) + case setIdleTimerDisabled(Bool) +} diff --git a/Zotero/Scenes/General/Models/ReaderSettingsState.swift b/Zotero/Scenes/General/Models/ReaderSettingsState.swift new file mode 100644 index 000000000..da627c132 --- /dev/null +++ b/Zotero/Scenes/General/Models/ReaderSettingsState.swift @@ -0,0 +1,45 @@ +// +// ReaderSettingsState.swift +// Zotero +// +// Created by Michal Rentka on 01.03.2022. +// Copyright © 2022 Corporation for Digital Scholarship. All rights reserved. +// + +import UIKit + +import PSPDFKitUI + +struct ReaderSettingsState: ViewModelState { + enum Appearance: UInt { + case light + case dark + case automatic + + var userInterfaceStyle: UIUserInterfaceStyle { + switch self { + case .automatic: return .unspecified + case .dark: return .dark + case .light: return .light + } + } + } + + var transition: PSPDFKitUI.PageTransition + var pageMode: PSPDFKitUI.PageMode + var scrollDirection: PSPDFKitUI.ScrollDirection + var pageFitting: PSPDFKitUI.PDFConfiguration.SpreadFitting + var appearance: ReaderSettingsState.Appearance + var idleTimerDisabled: Bool + + init(settings: PDFSettings) { + self.transition = settings.transition + self.pageMode = settings.pageMode + self.scrollDirection = settings.direction + self.pageFitting = settings.pageFitting + self.appearance = settings.appearanceMode + self.idleTimerDisabled = settings.idleTimerDisabled + } + + func cleanup() {} +} diff --git a/Zotero/Scenes/Detail/PDF/ViewModels/PDFSettingsActionHandler.swift b/Zotero/Scenes/General/ViewModels/ReaderSettingsActionHandler.swift similarity index 57% rename from Zotero/Scenes/Detail/PDF/ViewModels/PDFSettingsActionHandler.swift rename to Zotero/Scenes/General/ViewModels/ReaderSettingsActionHandler.swift index ad3d0f16a..dc35b494f 100644 --- a/Zotero/Scenes/Detail/PDF/ViewModels/PDFSettingsActionHandler.swift +++ b/Zotero/Scenes/General/ViewModels/ReaderSettingsActionHandler.swift @@ -1,5 +1,5 @@ // -// PDFSettingsActionHandler.swift +// ReaderSettingsActionHandler.swift // Zotero // // Created by Michal Rentka on 02.03.2022. @@ -10,40 +10,40 @@ import Foundation import PSPDFKit -struct PDFSettingsActionHandler: ViewModelActionHandler { - typealias Action = PDFSettingsAction - typealias State = PDFSettingsState +struct ReaderSettingsActionHandler: ViewModelActionHandler { + typealias Action = ReaderSettingsAction + typealias State = ReaderSettingsState - func process(action: PDFSettingsAction, in viewModel: ViewModel) { + func process(action: ReaderSettingsAction, in viewModel: ViewModel) { switch action { case .setTransition(let pageTransition): self.update(viewModel: viewModel) { state in - state.settings.transition = pageTransition + state.transition = pageTransition } case .setPageMode(let pageMode): self.update(viewModel: viewModel) { state in - state.settings.pageMode = pageMode + state.pageMode = pageMode } case .setDirection(let direction): self.update(viewModel: viewModel) { state in - state.settings.direction = direction + state.scrollDirection = direction } case .setPageFitting(let fitting): self.update(viewModel: viewModel) { state in - state.settings.pageFitting = fitting + state.pageFitting = fitting } - case .setAppearanceMode(let appearanceMode): + case .setAppearance(let appearance): self.update(viewModel: viewModel) { state in - state.settings.appearanceMode = appearanceMode + state.appearance = appearance } case .setIdleTimerDisabled(let disabled): self.update(viewModel: viewModel) { state in - state.settings.idleTimerDisabled = disabled + state.idleTimerDisabled = disabled } } } diff --git a/Zotero/Scenes/Detail/PDF/Views/PDFSettingsSegmentedCell.swift b/Zotero/Scenes/General/Views/ReaderSettingsSegmentedCell.swift similarity index 80% rename from Zotero/Scenes/Detail/PDF/Views/PDFSettingsSegmentedCell.swift rename to Zotero/Scenes/General/Views/ReaderSettingsSegmentedCell.swift index 8c4c621c0..63fd04f6a 100644 --- a/Zotero/Scenes/Detail/PDF/Views/PDFSettingsSegmentedCell.swift +++ b/Zotero/Scenes/General/Views/ReaderSettingsSegmentedCell.swift @@ -1,5 +1,5 @@ // -// PDFSettingsSegmentedCell.swift +// ReaderSettingsSegmentedCell.swift // Zotero // // Created by Michal Rentka on 03.03.2022. @@ -8,7 +8,7 @@ import UIKit -final class PDFSettingsSegmentedCell: UICollectionViewListCell { +final class ReaderSettingsSegmentedCell: UICollectionViewListCell { struct ContentConfiguration: UIContentConfiguration { let title: String let actions: [UIAction] @@ -31,14 +31,14 @@ final class PDFSettingsSegmentedCell: UICollectionViewListCell { } } - fileprivate weak var contentView: PDFSettingsSegmentedCellContentView? + fileprivate weak var contentView: ReaderSettingsSegmentedCellContentView? init(configuration: ContentConfiguration) { self.configuration = configuration super.init(frame: .zero) - guard let view = UINib.init(nibName: "PDFSettingsSegmentedCellContentView", bundle: nil).instantiate(withOwner: self)[0] as? PDFSettingsSegmentedCellContentView else { return } + guard let view = UINib.init(nibName: "ReaderSettingsSegmentedCellContentView", bundle: nil).instantiate(withOwner: self)[0] as? ReaderSettingsSegmentedCellContentView else { return } self.setup(view: view) self.apply(configuration: configuration) } @@ -51,7 +51,7 @@ final class PDFSettingsSegmentedCell: UICollectionViewListCell { self.contentView?.setup(title: configuration.title, actions: configuration.actions, selectedIndex: configuration.getSelectedIndex()) } - private func setup(view: PDFSettingsSegmentedCellContentView) { + private func setup(view: ReaderSettingsSegmentedCellContentView) { view.translatesAutoresizingMaskIntoConstraints = false self.addSubview(view) self.contentView = view diff --git a/Zotero/Scenes/Detail/PDF/Views/PDFSettingsSegmentedCellContentView.swift b/Zotero/Scenes/General/Views/ReaderSettingsSegmentedCellContentView.swift similarity index 86% rename from Zotero/Scenes/Detail/PDF/Views/PDFSettingsSegmentedCellContentView.swift rename to Zotero/Scenes/General/Views/ReaderSettingsSegmentedCellContentView.swift index f871fe792..a93ef8bb6 100644 --- a/Zotero/Scenes/Detail/PDF/Views/PDFSettingsSegmentedCellContentView.swift +++ b/Zotero/Scenes/General/Views/ReaderSettingsSegmentedCellContentView.swift @@ -1,5 +1,5 @@ // -// PDFSettingsSegmentedCellContentView.swift +// ReaderSettingsSegmentedCellContentView.swift // Zotero // // Created by Michal Rentka on 03.03.2022. @@ -8,7 +8,7 @@ import UIKit -class PDFSettingsSegmentedCellContentView: UIView { +class ReaderSettingsSegmentedCellContentView: UIView { @IBOutlet private weak var titleLabel: UILabel! @IBOutlet private weak var segmentedControl: UISegmentedControl! diff --git a/Zotero/Scenes/Detail/PDF/Views/PDFSettingsSegmentedCellContentView.xib b/Zotero/Scenes/General/Views/ReaderSettingsSegmentedCellContentView.xib similarity index 94% rename from Zotero/Scenes/Detail/PDF/Views/PDFSettingsSegmentedCellContentView.xib rename to Zotero/Scenes/General/Views/ReaderSettingsSegmentedCellContentView.xib index 8aa547b49..4fd94538b 100644 --- a/Zotero/Scenes/Detail/PDF/Views/PDFSettingsSegmentedCellContentView.xib +++ b/Zotero/Scenes/General/Views/ReaderSettingsSegmentedCellContentView.xib @@ -1,9 +1,9 @@ - + - + @@ -11,18 +11,18 @@ - + - + diff --git a/Zotero/Scenes/Detail/PDF/Views/PDFSettingsViewController.swift b/Zotero/Scenes/General/Views/ReaderSettingsViewController.swift similarity index 78% rename from Zotero/Scenes/Detail/PDF/Views/PDFSettingsViewController.swift rename to Zotero/Scenes/General/Views/ReaderSettingsViewController.swift index 23b620a69..3500efcff 100644 --- a/Zotero/Scenes/Detail/PDF/Views/PDFSettingsViewController.swift +++ b/Zotero/Scenes/General/Views/ReaderSettingsViewController.swift @@ -1,5 +1,5 @@ // -// PDFSettingsViewController.swift +// ReaderSettingsViewController.swift // Zotero // // Created by Michal Rentka on 02.03.2022. @@ -11,8 +11,8 @@ import UIKit import PSPDFKitUI import RxSwift -final class PDFSettingsViewController: UICollectionViewController { - private enum Row { +final class ReaderSettingsViewController: UICollectionViewController { + enum Row { case pageTransition case pageMode case scrollDirection @@ -21,13 +21,14 @@ final class PDFSettingsViewController: UICollectionViewController { case appearance } - private let viewModel: ViewModel + let viewModel: ViewModel + private let rows: [Row] private let disposeBag: DisposeBag private var dataSource: UICollectionViewDiffableDataSource! - var changeHandler: ((PDFSettings) -> Void)? - init(viewModel: ViewModel) { + init(rows: [Row], viewModel: ViewModel) { + self.rows = rows self.viewModel = viewModel disposeBag = DisposeBag() @@ -55,31 +56,10 @@ final class PDFSettingsViewController: UICollectionViewController { .disposed(by: disposeBag) } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - if UIDevice.current.userInterfaceIdiom == .phone { - changeHandler?(viewModel.state.settings) - } - } - // MARK: - Actions - private func update(state: PDFSettingsState) { - switch state.settings.appearanceMode { - case .automatic: - overrideUserInterfaceStyle = .unspecified - - case .light: - overrideUserInterfaceStyle = .light - - case .dark: - overrideUserInterfaceStyle = .dark - } - - if UIDevice.current.userInterfaceIdiom == .pad { - changeHandler?(state.settings) - } + private func update(state: ReaderSettingsState) { + overrideUserInterfaceStyle = state.appearance.userInterfaceStyle } @objc private func done() { @@ -105,7 +85,7 @@ final class PDFSettingsViewController: UICollectionViewController { private func applySnapshot() { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([0]) - snapshot.appendItems([.pageTransition, .pageMode, .scrollDirection, .pageFitting, .appearance, .sleep], toSection: 0) + snapshot.appendItems(rows, toSection: 0) dataSource.apply(snapshot, animatingDifferences: false) } @@ -120,7 +100,7 @@ final class PDFSettingsViewController: UICollectionViewController { cell.contentConfiguration = configuration let toggle = UISwitch() - toggle.setOn(!viewModel.state.settings.idleTimerDisabled, animated: false) + toggle.setOn(!viewModel.state.idleTimerDisabled, animated: false) toggle.addAction(UIAction(handler: { [weak self, weak toggle] _ in self?.viewModel.process(action: .setIdleTimerDisabled(!(toggle?.isOn ?? false))) }), for: .valueChanged) let customConfiguration = UICellAccessory.CustomViewConfiguration(customView: toggle, placement: .trailing(displayed: .always)) @@ -128,8 +108,8 @@ final class PDFSettingsViewController: UICollectionViewController { } }() - private lazy var segmentedRegistration: UICollectionView.CellRegistration = { - return UICollectionView.CellRegistration { [weak self] cell, _, row in + private lazy var segmentedRegistration: UICollectionView.CellRegistration = { + return UICollectionView.CellRegistration { [weak self] cell, _, row in guard let self else { return } let title: String @@ -141,7 +121,7 @@ final class PDFSettingsViewController: UICollectionViewController { title = L10n.Pdf.Settings.PageTransition.title getSelectedIndex = { [weak self] in guard let self else { return 0 } - return Int(viewModel.state.settings.transition.rawValue) + return Int(viewModel.state.transition.rawValue) } actions = [UIAction(title: L10n.Pdf.Settings.PageTransition.jump, handler: { [weak self] _ in self?.viewModel.process(action: .setTransition(.scrollPerSpread)) }), UIAction(title: L10n.Pdf.Settings.PageTransition.continuous, handler: { [weak self] _ in self?.viewModel.process(action: .setTransition(.scrollContinuous)) })] @@ -150,7 +130,7 @@ final class PDFSettingsViewController: UICollectionViewController { title = L10n.Pdf.Settings.PageMode.title getSelectedIndex = { [weak self] in guard let self else { return 0 } - return Int(viewModel.state.settings.pageMode.rawValue) + return Int(viewModel.state.pageMode.rawValue) } actions = [UIAction(title: L10n.Pdf.Settings.PageMode.single, handler: { [weak self] _ in self?.viewModel.process(action: .setPageMode(.single)) }), UIAction(title: L10n.Pdf.Settings.PageMode.double, handler: { [weak self] _ in self?.viewModel.process(action: .setPageMode(.double)) }), @@ -160,7 +140,7 @@ final class PDFSettingsViewController: UICollectionViewController { title = L10n.Pdf.Settings.ScrollDirection.title getSelectedIndex = { [weak self] in guard let self else { return 0 } - return Int(viewModel.state.settings.direction.rawValue) + return Int(viewModel.state.scrollDirection.rawValue) } actions = [UIAction(title: L10n.Pdf.Settings.ScrollDirection.horizontal, handler: { [weak self] _ in self?.viewModel.process(action: .setDirection(.horizontal)) }), UIAction(title: L10n.Pdf.Settings.ScrollDirection.vertical, handler: { [weak self] _ in self?.viewModel.process(action: .setDirection(.vertical)) })] @@ -169,7 +149,7 @@ final class PDFSettingsViewController: UICollectionViewController { title = L10n.Pdf.Settings.PageFitting.title getSelectedIndex = { [weak self] in guard let self else { return 0 } - return viewModel.state.settings.pageFitting.rawValue + return viewModel.state.pageFitting.rawValue } actions = [UIAction(title: L10n.Pdf.Settings.PageFitting.fit, handler: { [weak self] _ in self?.viewModel.process(action: .setPageFitting(.fit)) }), UIAction(title: L10n.Pdf.Settings.PageFitting.fill, handler: { [weak self] _ in self?.viewModel.process(action: .setPageFitting(.fill)) }), @@ -179,17 +159,17 @@ final class PDFSettingsViewController: UICollectionViewController { title = L10n.Pdf.Settings.Appearance.title getSelectedIndex = { [weak self] in guard let self else { return 0 } - return Int(viewModel.state.settings.appearanceMode.rawValue) + return Int(viewModel.state.appearance.rawValue) } - actions = [UIAction(title: L10n.Pdf.Settings.Appearance.lightMode, handler: { [weak self] _ in self?.viewModel.process(action: .setAppearanceMode(.light)) }), - UIAction(title: L10n.Pdf.Settings.Appearance.darkMode, handler: { [weak self] _ in self?.viewModel.process(action: .setAppearanceMode(.dark)) }), - UIAction(title: L10n.Pdf.Settings.Appearance.auto, handler: { [weak self] _ in self?.viewModel.process(action: .setAppearanceMode(.automatic)) })] + actions = [UIAction(title: L10n.Pdf.Settings.Appearance.lightMode, handler: { [weak self] _ in self?.viewModel.process(action: .setAppearance(.light)) }), + UIAction(title: L10n.Pdf.Settings.Appearance.darkMode, handler: { [weak self] _ in self?.viewModel.process(action: .setAppearance(.dark)) }), + UIAction(title: L10n.Pdf.Settings.Appearance.auto, handler: { [weak self] _ in self?.viewModel.process(action: .setAppearance(.automatic)) })] case .sleep: return } - let configuration = PDFSettingsSegmentedCell.ContentConfiguration(title: title, actions: actions, getSelectedIndex: getSelectedIndex) + let configuration = ReaderSettingsSegmentedCell.ContentConfiguration(title: title, actions: actions, getSelectedIndex: getSelectedIndex) cell.contentConfiguration = configuration } }() @@ -205,8 +185,7 @@ final class PDFSettingsViewController: UICollectionViewController { private func setupNavigationBarIfNeeded() { guard UIDevice.current.userInterfaceIdiom == .phone else { return } - - let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(PDFSettingsViewController.done)) + let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(Self.done)) navigationItem.rightBarButtonItem = button } } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..4e3475736 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "zotero-ios", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}