forked from zotero/zotero-ios
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathShareViewController.swift
More file actions
824 lines (685 loc) · 35.3 KB
/
ShareViewController.swift
File metadata and controls
824 lines (685 loc) · 35.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
//
// ShareViewController.swift
// ZShare
//
// Created by Michal Rentka on 21/11/2019.
// Copyright © 2019 Corporation for Digital Scholarship. All rights reserved.
//
import Combine
import Social
import SwiftUI
import UIKit
import WebKit
import CocoaLumberjackSwift
final class ShareViewController: UIViewController {
// Outlets
@IBOutlet private weak var webView: WKWebView!
@IBOutlet private weak var stackView: UIStackView!
// Title
@IBOutlet private weak var translationContainer: UIView!
@IBOutlet private weak var itemContainer: UIStackView!
@IBOutlet private weak var itemIcon: UIImageView!
@IBOutlet private weak var itemTitleLabel: UILabel!
@IBOutlet private weak var attachmentContainer: UIView!
@IBOutlet private weak var attachmentContainerLeft: NSLayoutConstraint!
@IBOutlet private weak var attachmentIcon: FileAttachmentView!
@IBOutlet private weak var attachmentTitleLabel: UILabel!
@IBOutlet private weak var attachmentProgressView: CircularProgressView!
@IBOutlet private weak var attachmentActivityIndicator: UIActivityIndicatorView!
// Collection picker
@IBOutlet private weak var collectionPickerStackContainer: UIView!
@IBOutlet private weak var collectionPickerTitleLabel: UILabel!
@IBOutlet private weak var collectionPickerContainer: UIView!
@IBOutlet private weak var collectionPickerStackView: UIStackView!
@IBOutlet private weak var collectionPickerLoadingContainer: UIView?
@IBOutlet private weak var collectionPickerLoadingLabel: UILabel!
@IBOutlet private weak var collectionPickerPickOtherButton: RightButton!
@IBOutlet private weak var collectionPickerFailureLabel: UILabel?
// Item picker
@IBOutlet private weak var itemPickerStackContainer: UIView!
@IBOutlet private weak var itemPickerTitleLabel: UILabel!
@IBOutlet private weak var itemPickerContainer: UIView!
@IBOutlet private weak var itemPickerLabel: UILabel!
@IBOutlet private weak var itemPickerChevron: UIImageView!
@IBOutlet private weak var itemPickerButton: UIButton!
// Tag picker
@IBOutlet private weak var tagPickerStackContainer: UIView!
@IBOutlet private weak var tagPickerTitleLabel: UILabel!
@IBOutlet private weak var tagPickerContainer: UIView!
@IBOutlet private weak var tagPickerStackView: UIStackView!
@IBOutlet private weak var tagPickerAddButton: RightButton!
// Progress
@IBOutlet private weak var bottomProgressContainer: UIView!
@IBOutlet private weak var bottomProgressActivityIndicator: UIActivityIndicatorView!
@IBOutlet private weak var bottomProgressLabel: UILabel!
@IBOutlet private weak var failureLabel: UILabel!
// Saving
@IBOutlet private weak var savingContainer: UIView!
@IBOutlet private weak var savingInnerContainer: UIView!
// Variables
private var translatorsController: TranslatorsAndStylesController!
private var dbStorage: DbStorage!
private var bundledDataStorage: DbStorage!
private var fileStorage: FileStorage!
private var debugLogging: DebugLogging!
private var schemaController: SchemaController!
private var documentWorkerController: DocumentWorkerController!
private var secureStorage: KeychainSecureStorage!
private var viewModel: ExtensionViewModel!
private var storeCancellable: AnyCancellable?
private var viewIsVisible: Bool = true
// Constants
private static let toolbarTitleIdx = 1
private static let childAttachmentLeftOffset: CGFloat = 16
private static let maxCollectionCount = 5
private static let width: CGFloat = 468
private static let pickerSize = CGSize(width: width, height: 500.0)
lazy private var cancelButton: UIBarButtonItem = {
.init(barButtonSystemItem: .cancel, target: self, action: #selector(ShareViewController.cancel))
}()
lazy private var doneButton: UIBarButtonItem = {
.init(title: L10n.Shareext.save, style: .done, target: self, action: #selector(ShareViewController.done))
}()
lazy private var continueButton: UIBarButtonItem = {
.init(title: L10n.Shareext.resolveChallenge, style: .done, target: self, action: #selector(ShareViewController.continueAfterChallenge))
}()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
DDLog.add(DDOSLogger.sharedInstance)
let fileStorage = FileStorageController()
let schemaController = SchemaController()
let apiClient = self.setupApiClient(schemaController: schemaController)
// Start logging as soon as possible
self.debugLogging = DebugLogging(apiClient: apiClient, fileStorage: fileStorage)
self.debugLogging.startLoggingOnLaunchIfNeeded()
DDLogInfo("View loaded")
let session = self.setupSession()
self.setupNavbar(loggedIn: (session != nil))
guard let session = session else {
self.showInitialError(message: L10n.Errors.Shareext.loggedOut)
return
}
self.setupControllers(with: session, apiClient: apiClient, fileStorage: fileStorage, schemaController: schemaController)
DDLogInfo("Controllers initialized")
// Setup UI
self.setupPickers()
self.setupSavingOverlay()
self.attachmentIcon.set(backgroundColor: .white)
// Setup observing
self.storeCancellable = self.viewModel?.$state.receive(on: DispatchQueue.main)
.sink { [weak self] state in
self?.update(to: state)
}
// Load initial data
if let context = self.extensionContext, let extensionItem = context.inputItems.first as? NSExtensionItem {
DDLogInfo("Load extension item (\(context.inputItems.count))")
self.viewModel?.start(with: extensionItem)
} else {
self.showInitialError(message: L10n.Errors.Shareext.cantLoadData)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.viewIsVisible = true
self.updatePreferredContentSize()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.viewIsVisible = false
}
deinit {
DDLogInfo("ShareViewController: deinitialize")
}
// MARK: - Actions
private func updatePreferredContentSize() {
var size = self.stackView.systemLayoutSizeFitting(CGSize(width: ShareViewController.width, height: .greatestFiniteMagnitude))
size.height += 32
self.preferredContentSize = size
self.navigationController?.preferredContentSize = size
}
private func showInitialError(message: String) {
self.navigationController?.view.alpha = 0.0
let controller = UIAlertController(title: L10n.error, message: message, preferredStyle: .alert)
controller.addAction(UIAlertAction(title: L10n.ok, style: .cancel, handler: { [weak self] _ in
self?.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}))
self.present(controller, animated: false, completion: nil)
}
@IBAction private func showTagPicker() {
guard let dbStorage = self.dbStorage else { return }
let state = TagPickerState(libraryId: self.viewModel.state.selectedLibraryId, selectedTags: Set(self.viewModel.state.tags.map({ $0.name })))
let handler = TagPickerActionHandler(dbStorage: dbStorage)
let viewModel = ViewModel(initialState: state, handler: handler)
let controller = TagPickerViewController(viewModel: viewModel, saveAction: { [weak self] tags in
self?.viewModel.set(tags: tags)
})
controller.preferredContentSize = ShareViewController.pickerSize
self.navigationController?.preferredContentSize = ShareViewController.pickerSize
self.navigationController?.pushViewController(controller, animated: true)
}
@IBAction private func showItemPicker() {
guard let items = self.viewModel.state.itemPickerState?.items else { return }
let view = ItemPickerView(data: items) { [weak self] picked in
self?.viewModel.pickItem(picked)
self?.navigationController?.popViewController(animated: true)
}
let controller = UIHostingController(rootView: view)
controller.preferredContentSize = ShareViewController.pickerSize
self.navigationController?.preferredContentSize = ShareViewController.pickerSize
self.navigationController?.pushViewController(controller, animated: true)
}
@IBAction private func showCollectionPicker() {
guard let dbStorage = self.dbStorage else { return }
let state = AllCollectionPickerState(selectedCollectionId: self.viewModel.state.selectedCollectionId, selectedLibraryId: self.viewModel.state.selectedLibraryId)
let handler = AllCollectionPickerActionHandler(dbStorage: dbStorage, queue: .main)
let controller = AllCollectionPickerViewController(viewModel: ViewModel(initialState: state, handler: handler))
controller.pickedAction = { [weak self] collection, library in
self?.viewModel?.set(collection: collection, library: library)
self?.navigationController?.popViewController(animated: true)
}
controller.preferredContentSize = ShareViewController.pickerSize
self.navigationController?.preferredContentSize = ShareViewController.pickerSize
self.navigationController?.pushViewController(controller, animated: true)
}
@objc private func done() {
self.viewModel?.submit()
}
@objc private func continueAfterChallenge() {
viewModel?.continueAfterChallenge()
}
@objc private func cancel() {
self.viewModel?.cancel()
self.debugLogging.storeLogs { [unowned self] in
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}
}
private func update(to state: ExtensionViewModel.State) {
if state.isDone {
DDLogInfo("State: done")
// Don't do anything for `.done`, the extension is supposed to just close at this point.
self.debugLogging.storeLogs { [unowned self] in
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}
return
}
if state.isSubmitting {
DDLogInfo("State: submitting")
}
let hasItem = state.processedAttachment != nil
self.log(attachmentState: state.attachmentState, itemState: state.itemPickerState)
update(
libraryIsFilesEditable: state.collectionPickerState.libraryIsFilesEditable,
item: state.expectedItem,
attachment: state.expectedAttachment,
attachmentState: state.attachmentState,
defaultTitle: state.title
)
self.update(attachmentState: state.attachmentState, itemState: state.itemPickerState, hasItem: hasItem, isSubmitting: state.isSubmitting)
self.update(collectionPicker: state.collectionPickerState, recents: state.recents)
self.update(itemPicker: state.itemPickerState, hasExpectedItem: (state.expectedItem != nil || state.expectedAttachment != nil))
switch state.processedAttachment {
case .none, .file:
tagPickerStackContainer.isHidden = true
case .item, .itemWithAttachment:
tagPickerStackContainer.isHidden = false
}
self.updateTagPicker(with: state.tags)
if self.viewIsVisible {
self.updatePreferredContentSize()
}
}
private func log(attachmentState: ExtensionViewModel.State.AttachmentState, itemState: ExtensionViewModel.State.ItemPickerState?) {
switch attachmentState {
case .decoding:
DDLogInfo("State: decoding")
case .downloading(let progress):
DDLogInfo("State: downloading \(progress)")
case .failed(let error):
DDLogInfo("State: failed with \(error)")
case .processed:
DDLogInfo("State: processed")
case .translating(let name):
DDLogInfo("State: translating with \(name)")
case .challengePending:
DDLogInfo("State: challenge pending")
}
if let state = itemState {
if let picked = state.picked {
DDLogInfo("State: picked item \(picked)")
} else {
DDLogInfo("State: loaded \(state.items.count) items")
}
}
}
private func update(attachmentState state: ExtensionViewModel.State.AttachmentState, itemState: ExtensionViewModel.State.ItemPickerState?, hasItem: Bool, isSubmitting: Bool) {
self.updateNavigationItems(for: state, isSubmitting: isSubmitting)
self.updateBottomProgress(for: state, itemState: itemState, hasItem: hasItem, isSubmitting: isSubmitting)
}
private func update(libraryIsFilesEditable: Bool, item: ItemResponse?, attachment: (String, File)?, attachmentState: ExtensionViewModel.State.AttachmentState, defaultTitle title: String?) {
self.translationContainer.isHidden = false
if item == nil && attachment == nil {
// If no item or attachment were found, either translation is in progress or there was a fatal error.
guard !attachmentState.translationInProgress, let title = title else {
// If translation is in progress, hide whole container, there is nothing to show yet.
self.translationContainer.isHidden = true
return
}
// If there was a fatal error, we can always at least save a webpage item, so show that in UI
self.itemContainer.isHidden = false
self.attachmentContainer.isHidden = true
self.setItem(title: title, type: ItemTypes.webpage)
return
}
let itemFound: Bool
if let item {
itemFound = true
// Item was found, show its metadata.
let itemTitle = itemTitle(for: item, schemaController: schemaController, defaultValue: title ?? "")
setItem(title: itemTitle, type: item.rawType)
itemContainer.isHidden = false
} else {
itemFound = false
itemContainer.isHidden = true
}
if libraryIsFilesEditable, let (attachmentTitle, file) = attachment {
// The picked library allows file editing, and either item with attachment, or only attachment (local/remote file), was found, show its metadata.
setAttachment(title: attachmentTitle, file: file, state: attachmentState)
attachmentContainerLeft.constant = itemFound ? Self.childAttachmentLeftOffset : 0
attachmentContainer.isHidden = false
} else {
attachmentContainer.isHidden = true
}
func setAttachment(title: String, file: File, state: ExtensionViewModel.State.AttachmentState) {
attachmentTitleLabel.text = title
let type: Attachment.Kind = .file(filename: "", contentType: file.mimeType, location: .local, linkType: .importedFile, compressed: false)
let iconState = FileAttachmentView.State.stateFrom(type: type, progress: nil, error: state.error)
attachmentIcon.set(state: iconState, style: .shareExtension)
switch state {
case .downloading(let progress):
attachmentProgressView.isHidden = progress == 0
attachmentActivityIndicator.isHidden = progress > 0
attachmentProgressView.progress = CGFloat(progress)
if progress == 0 && !attachmentActivityIndicator.isAnimating {
attachmentActivityIndicator.startAnimating()
}
attachmentIcon.alpha = 0.5
attachmentTitleLabel.alpha = 0.5
default:
attachmentProgressView.isHidden = true
if attachmentActivityIndicator.isAnimating {
attachmentActivityIndicator.stopAnimating()
}
attachmentActivityIndicator.isHidden = true
attachmentIcon.alpha = 1
attachmentTitleLabel.alpha = 1
}
}
}
private func itemTitle(for item: ItemResponse, schemaController: SchemaController, defaultValue: String) -> String {
return schemaController.titleKey(for: item.rawType).flatMap({ item.fields[KeyBaseKeyPair(key: $0, baseKey: nil)] }) ?? defaultValue
}
private func setItem(title: String, type: String) {
self.itemTitleLabel.text = title
self.itemIcon.image = UIImage(named: ItemTypes.iconName(for: type))
}
private func updateNavigationItems(for state: ExtensionViewModel.State.AttachmentState, isSubmitting: Bool) {
if case .challengePending = state {
navigationItem.leftBarButtonItem?.isEnabled = !isSubmitting
navigationItem.rightBarButtonItem = continueButton
return
}
if let error = state.error {
switch error {
case .quotaLimit, .webDavFailure, .webDavUnauthorized, .webDavForbidden, .apiFailure, .forbidden:
navigationItem.leftBarButtonItem = nil
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(ShareViewController.cancel))
return
default:
break
}
}
doneButton.isEnabled = !isSubmitting && state.isSubmittable
navigationItem.rightBarButtonItem = doneButton
navigationItem.leftBarButtonItem?.isEnabled = !isSubmitting
}
private func updateBottomProgress(for state: ExtensionViewModel.State.AttachmentState, itemState: ExtensionViewModel.State.ItemPickerState?, hasItem: Bool, isSubmitting: Bool) {
if let state = itemState, state.picked == nil {
// Don't show progress bar when waiting for item pick
self.bottomProgressContainer.isHidden = true
return
}
let message: String?
let showActivityIndicator: Bool
if isSubmitting {
message = nil
showActivityIndicator = false
self.showSavingOverlay()
} else {
switch state {
case .decoding:
message = L10n.Shareext.decodingAttachment
showActivityIndicator = true
case .processed, .challengePending:
message = nil
showActivityIndicator = false
case .translating(let _message):
message = _message
showActivityIndicator = true
case .downloading:
message = nil
showActivityIndicator = true
case .failed(let error):
message = nil
showActivityIndicator = false
let hidePickers = error.isFatalOrQuota
self.hideSavingOverlay()
self.collectionPickerStackContainer.isHidden = hidePickers
self.tagPickerStackContainer.isHidden = hidePickers
self.itemPickerStackContainer.isHidden = true
self.show(error: error, hasItem: hasItem)
}
}
self.bottomProgressContainer.isHidden = message == nil
if let text = message {
self.bottomProgressLabel.text = text.uppercased()
self.bottomProgressActivityIndicator.isHidden = !showActivityIndicator
}
}
private func show(error: ExtensionViewModel.State.AttachmentState.Error, hasItem: Bool) {
guard var message = self.errorMessage(for: error) else {
self.failureLabel.isHidden = true
return
}
if error.isFatal {
self.failureLabel.textColor = .red
self.failureLabel.textAlignment = .center
} else {
switch error {
case .downloadedFileNotPdf, .apiFailure:
self.failureLabel.textAlignment = .center
case .quotaLimit:
self.failureLabel.textAlignment = .left
default:
if !hasItem {
message += "\n" + L10n.Errors.Shareext.failedAdditional
}
self.failureLabel.textAlignment = .center
}
self.failureLabel.textColor = .darkGray
}
self.failureLabel.text = message
self.failureLabel.isHidden = false
}
private func errorMessage(for error: ExtensionViewModel.State.AttachmentState.Error) -> String? {
switch error {
case .webDavNotVerified:
return L10n.Errors.Shareext.webdavNotVerified
case .webDavUnauthorized:
return L10n.Errors.Settings.Webdav.unauthorized
case .webDavForbidden:
return L10n.Errors.Settings.Webdav.forbidden
case .cantLoadSchema:
return L10n.Errors.Shareext.cantLoadSchema
case .cantLoadWebData:
return L10n.Errors.Shareext.cantLoadData
case .downloadFailed:
return L10n.Errors.Shareext.downloadFailed
case .itemsNotFound:
return L10n.Errors.Shareext.itemsNotFound
case .parseError:
return "Error parsing translator response"
case .schemaError:
return L10n.Errors.Shareext.schemaError
case .webViewError(let error):
switch error {
case .incompatibleItem:
return "No data returned"
case .javascriptCallMissingResult:
return "JS call failed"
case .noSuccessfulTranslators:
return nil
case .cantFindFile, .webExtractionMissingJs: // should never happen
return "Translator missing"
case .webExtractionMissingData:
return L10n.Errors.Shareext.responseMissingData
}
case .unknown, .expired:
return L10n.Errors.Shareext.unknown
case .fileMissing:
return "Could not find file to upload"
case .apiFailure:
return L10n.Errors.Shareext.apiError
case .webDavFailure:
return L10n.Errors.Shareext.webdavError
case .quotaLimit(let libraryId):
switch libraryId {
case .custom:
return L10n.Errors.Shareext.personalQuotaReached
case .group(let groupId):
let group = try? self.dbStorage.perform(request: ReadGroupDbRequest(identifier: groupId), on: .main)
let groupName = group?.name ?? "\(groupId)"
return L10n.Errors.Shareext.groupQuotaReached(groupName)
}
case .forbidden(let libraryId):
switch libraryId {
case .custom:
return L10n.Errors.Shareext.forbidden(L10n.Libraries.myLibrary)
case .group(let groupId):
let group = try? self.dbStorage.perform(request: ReadGroupDbRequest(identifier: groupId), on: .main)
let groupName = group?.name ?? "\(groupId)"
return L10n.Errors.Shareext.forbidden(groupName)
}
case .requiresBrowser:
return L10n.Errors.Shareext.requiresBrowser
case .downloadedFileNotPdf, .md5Missing, .mtimeMissing:
return nil
}
}
private func update(itemPicker state: ExtensionViewModel.State.ItemPickerState?, hasExpectedItem: Bool) {
guard let state = state, !hasExpectedItem else {
self.itemPickerStackContainer.isHidden = true
return
}
self.itemPickerStackContainer.isHidden = false
if let text = state.picked {
self.itemPickerLabel.text = text
self.itemPickerLabel.textColor = UIColor(dynamicProvider: { traitCollection -> UIColor in
return traitCollection.userInterfaceStyle == .light ? .darkText : .white
})
self.itemPickerChevron.isHidden = true
self.itemPickerButton.isEnabled = false
} else {
self.itemPickerLabel.text = L10n.Shareext.Translation.itemSelection
self.itemPickerLabel.textColor = Asset.Colors.zoteroBlue.color
self.itemPickerChevron.tintColor = Asset.Colors.zoteroBlue.color
self.itemPickerChevron.isHidden = false
self.itemPickerButton.isEnabled = true
}
}
private func update(collectionPicker state: ExtensionViewModel.State.CollectionPickerState, recents: [RecentData]) {
switch state {
case .picked(let library, let collection):
if self.collectionPickerLoadingContainer != nil {
// These are unnecessary anymore
self.collectionPickerLoadingContainer?.removeFromSuperview()
self.collectionPickerFailureLabel?.removeFromSuperview()
// Show pick other button
self.collectionPickerPickOtherButton.isHidden = false
}
let count = min(ShareViewController.maxCollectionCount, recents.count)
self.updateRowCount(in: self.collectionPickerStackView, hasAddButton: true, to: count,
createRow: { Bundle.main.loadNibNamed("CollectionRowView", owner: nil, options: nil)?.first as? CollectionRowView })
self.updateCollections(to: recents, pickedCollection: collection, library: library)
case .loading:
self.collectionPickerLoadingContainer?.isHidden = false
self.collectionPickerFailureLabel?.isHidden = true
self.collectionPickerPickOtherButton.isHidden = true
case .failed:
self.collectionPickerLoadingContainer?.isHidden = true
self.collectionPickerFailureLabel?.isHidden = false
self.collectionPickerPickOtherButton.isHidden = true
}
}
private func updateCollections(to recents: [RecentData], pickedCollection collection: Collection?, library: Library) {
for (idx, view) in self.collectionPickerStackView.arrangedSubviews.enumerated() {
guard let row = view as? CollectionRowView else { continue }
let recent = recents[idx]
let selected = recent.collection?.identifier == collection?.identifier && recent.library.identifier == library.identifier
row.setup(with: (recent.collection?.name ?? recent.library.name), isSelected: selected)
row.tapAction = { [weak self] in
self?.viewModel.setFromRecent(collection: recent.collection, library: recent.library)
}
}
}
private func updateTagPicker(with tags: [Tag]) {
self.updateRowCount(in: self.tagPickerStackView, hasAddButton: true, to: tags.count, createRow: { Bundle.main.loadNibNamed("TagRow", owner: nil, options: nil)?.first as? TagRow })
for (idx, view) in self.tagPickerStackView.arrangedSubviews.enumerated() {
guard let row = view as? TagRow else { continue }
row.setup(with: tags[idx])
}
}
private func updateRowCount(in stackView: UIStackView, hasAddButton: Bool, to count: Int, createRow: () -> UIView?) {
let visibleCount = stackView.arrangedSubviews.count - (hasAddButton ? 1 : 0)
guard visibleCount != count else { return }
if visibleCount > count {
for _ in 0..<(visibleCount - count) {
guard let view = stackView.arrangedSubviews.first else { break }
view.removeFromSuperview()
}
return
}
for _ in 0..<(count - visibleCount) {
guard let row = createRow() else { continue }
stackView.insertArrangedSubview(row, at: 0)
}
}
private func showSavingOverlay() {
guard self.savingContainer.isHidden else { return }
self.savingContainer.alpha = 0
self.savingContainer.isHidden = false
UIView.animate(withDuration: 0.2) {
self.savingContainer.alpha = 1
}
}
private func hideSavingOverlay() {
guard !self.savingContainer.isHidden else { return }
UIView.animate(withDuration: 0.2, animations: {
self.savingContainer.alpha = 0
}, completion: { finished in
guard finished else { return }
self.savingContainer.isHidden = true
})
}
// MARK: - Setups
private func setupSavingOverlay() {
self.savingContainer.isHidden = true
self.savingInnerContainer.layer.cornerRadius = 8
self.savingInnerContainer.layer.masksToBounds = true
}
private func setupPickers() {
[self.translationContainer,
self.collectionPickerContainer,
self.itemPickerContainer,
self.tagPickerContainer].forEach { container in
container!.layer.cornerRadius = 8
container!.layer.masksToBounds = true
container?.backgroundColor = Asset.Colors.defaultCellBackground.color
}
self.collectionPickerTitleLabel.text = L10n.Shareext.collectionTitle.uppercased()
self.collectionPickerFailureLabel?.text = L10n.Shareext.syncError
self.collectionPickerLoadingLabel.text = L10n.Shareext.loadingCollections
self.collectionPickerPickOtherButton.setTitle(L10n.Shareext.collectionOther, for: .normal)
self.itemPickerTitleLabel.text = L10n.Shareext.itemTitle.uppercased()
self.tagPickerTitleLabel.text = L10n.Shareext.tagsTitle.uppercased()
self.tagPickerAddButton.setTitle(L10n.add, for: .normal)
}
private func setupNavbar(loggedIn: Bool) {
navigationController?.navigationBar.tintColor = Asset.Colors.zoteroBlue.color
navigationItem.leftBarButtonItem = cancelButton
if loggedIn {
doneButton.isEnabled = false
navigationItem.rightBarButtonItem = doneButton
}
}
private func setupSession() -> SessionData? {
let sessionController = SessionController(secureStorage: KeychainSecureStorage(), defaults: Defaults.shared)
try? sessionController.initializeSession()
return sessionController.sessionData
}
private func setupApiClient(schemaController: SchemaController) -> ApiClient {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = ["Zotero-API-Version": ApiConstants.version.description, "Zotero-Schema-Version": schemaController.version]
configuration.sharedContainerIdentifier = AppGroup.identifier
configuration.timeoutIntervalForRequest = ApiConstants.requestTimeout
configuration.timeoutIntervalForResource = ApiConstants.resourceTimeout
return ZoteroApiClient(baseUrl: ApiConstants.baseUrlString, configuration: configuration)
}
private func setupControllers(with session: SessionData, apiClient: ApiClient, fileStorage: FileStorage, schemaController: SchemaController) {
let dbUrl = Files.dbFile(for: session.userId, sessionId: session.sessionId).createUrl()
let dbStorage = RealmDbStorage(config: Database.mainConfiguration(url: dbUrl, fileStorage: fileStorage))
let configuration = Database.bundledDataConfiguration(fileStorage: fileStorage)
let bundledDataStorage = RealmDbStorage(config: configuration)
let translatorsController = TranslatorsAndStylesController(apiClient: apiClient, bundledDataStorage: bundledDataStorage, fileStorage: fileStorage)
let secureStorage = KeychainSecureStorage()
let webDavController = WebDavControllerImpl(dbStorage: dbStorage, fileStorage: fileStorage, sessionStorage: SecureWebDavSessionStorage(secureStorage: secureStorage))
let documentWorkerController = DocumentWorkerController(fileStorage: fileStorage)
apiClient.set(authToken: ("Bearer " + session.apiToken))
translatorsController.updateFromRepo(type: .shareExtension)
self.fileStorage = fileStorage
self.schemaController = schemaController
self.dbStorage = dbStorage
self.bundledDataStorage = bundledDataStorage
self.translatorsController = translatorsController
self.documentWorkerController = documentWorkerController
self.secureStorage = secureStorage
self.viewModel = self.createViewModel(for: session.userId, dbStorage: dbStorage, apiClient: apiClient, schemaController: schemaController, fileStorage: fileStorage,
webDavController: webDavController, translatorsController: translatorsController)
}
private func createViewModel(for userId: Int, dbStorage: DbStorage, apiClient: ApiClient, schemaController: SchemaController, fileStorage: FileStorage, webDavController: WebDavController,
translatorsController: TranslatorsAndStylesController) -> ExtensionViewModel {
let dateParser = DateParser()
let requestProvider = BackgroundUploaderRequestProvider(fileStorage: fileStorage)
let backgroundUploadContext = BackgroundUploaderContext()
let backgroundUploader = BackgroundUploader(context: backgroundUploadContext, requestProvider: requestProvider, schemaVersion: schemaController.version)
let backgroundProcessor = BackgroundUploadProcessor(apiClient: apiClient, dbStorage: dbStorage, fileStorage: fileStorage, webDavController: webDavController)
let backgroundTaskController = BackgroundTaskController()
let backgroundUploadObserver = BackgroundUploadObserver(context: backgroundUploadContext, processor: backgroundProcessor, backgroundTaskController: backgroundTaskController)
let attachmentDownloader = AttachmentDownloader(userId: userId, apiClient: apiClient, fileStorage: fileStorage, dbStorage: dbStorage, webDavController: webDavController)
let syncController = SyncController(userId: userId, apiClient: apiClient, dbStorage: dbStorage, fileStorage: fileStorage, schemaController: schemaController, dateParser: dateParser,
backgroundUploaderContext: backgroundUploadContext, webDavController: webDavController, attachmentDownloader: attachmentDownloader, syncDelayIntervals: DelayIntervals.sync, maxRetryCount: DelayIntervals.retry.count)
let recognizerController = RecognizerController(
documentWorkerController: documentWorkerController,
apiClient: apiClient,
translatorsController: translatorsController,
schemaController: schemaController,
dbStorage: dbStorage,
dateParser: dateParser,
fileStorage: fileStorage
)
recognizerController.webViewProvider = self
return ExtensionViewModel(
webView: webView,
apiClient: apiClient,
attachmentDownloader: attachmentDownloader,
backgroundUploader: backgroundUploader,
backgroundUploadObserver: backgroundUploadObserver,
dbStorage: dbStorage,
schemaController: schemaController,
webDavController: webDavController,
dateParser: dateParser,
fileStorage: fileStorage,
syncController: syncController,
translatorsController: translatorsController,
recognizerController: recognizerController
)
}
}
extension ShareViewController: WebViewProvider {
func addWebView(configuration: WKWebViewConfiguration?) -> WKWebView {
let webView: WKWebView = configuration.flatMap({ WKWebView(frame: .zero, configuration: $0) }) ?? WKWebView()
webView.isHidden = true
view.insertSubview(webView, at: 0)
return webView
}
}