diff --git a/Zotero.xcodeproj/project.pbxproj b/Zotero.xcodeproj/project.pbxproj index 4827f85bd..6d8699ea3 100644 --- a/Zotero.xcodeproj/project.pbxproj +++ b/Zotero.xcodeproj/project.pbxproj @@ -339,7 +339,6 @@ B305CEBB29E6E67600B9E2B4 /* AssignItemsToTagDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B305CEBA29E6E67600B9E2B4 /* AssignItemsToTagDbRequest.swift */; }; B307A2732704A87D005986B3 /* IdleTimerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B307A2722704A87D005986B3 /* IdleTimerController.swift */; }; B30A44A629B8799600332B4E /* MasterContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30A44A529B8799600332B4E /* MasterContainerViewController.swift */; }; - B30A44A829B882EB00332B4E /* MasterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30A44A729B882EB00332B4E /* MasterCoordinator.swift */; }; B30A44AE29B88E7200332B4E /* TagFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30A44AD29B88E7200332B4E /* TagFilterViewController.swift */; }; B30A8C672582690900EC56FB /* HighlightAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30A8C662582690900EC56FB /* HighlightAnnotation.swift */; }; B30B405F2490CAFC00FAAF6D /* ItemCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B30B405E2490CAFC00FAAF6D /* ItemCell.xib */; }; @@ -641,7 +640,7 @@ B3593F56241A61C700760E20 /* CollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3593F11241A61C700760E20 /* CollectionCell.swift */; }; B3593F59241A61C700760E20 /* CollectionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3593F14241A61C700760E20 /* CollectionsViewController.swift */; }; B3593F62241A62DD00760E20 /* DetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3593F61241A62DD00760E20 /* DetailCoordinator.swift */; }; - B3593F64241A62E400760E20 /* MasterTopCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3593F63241A62E400760E20 /* MasterTopCoordinator.swift */; }; + B3593F64241A62E400760E20 /* MasterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3593F63241A62E400760E20 /* MasterCoordinator.swift */; }; B3593F77241A76E600760E20 /* CollectionEditActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3593F6F241A76E600760E20 /* CollectionEditActionHandler.swift */; }; B3593F78241A76E600760E20 /* CollectionEditError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3593F71241A76E600760E20 /* CollectionEditError.swift */; }; B3593F79241A76E600760E20 /* CollectionEditAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3593F72241A76E600760E20 /* CollectionEditAction.swift */; }; @@ -1367,7 +1366,6 @@ B307A2722704A87D005986B3 /* IdleTimerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdleTimerController.swift; sourceTree = ""; }; B307E55D22D4A87B00592B3C /* SyncActionsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncActionsSpec.swift; sourceTree = ""; }; B30A44A529B8799600332B4E /* MasterContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterContainerViewController.swift; sourceTree = ""; }; - B30A44A729B882EB00332B4E /* MasterCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterCoordinator.swift; sourceTree = ""; }; B30A44AD29B88E7200332B4E /* TagFilterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagFilterViewController.swift; sourceTree = ""; }; B30A8C662582690900EC56FB /* HighlightAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightAnnotation.swift; sourceTree = ""; }; B30B405E2490CAFC00FAAF6D /* ItemCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ItemCell.xib; sourceTree = ""; }; @@ -1619,7 +1617,7 @@ B3593F172668E6A700FA4BB2 /* StyleParserDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyleParserDelegate.swift; sourceTree = ""; }; B3593F1B2668E86100FA4BB2 /* SyncStylesDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncStylesDbRequest.swift; sourceTree = ""; }; B3593F61241A62DD00760E20 /* DetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailCoordinator.swift; sourceTree = ""; }; - B3593F63241A62E400760E20 /* MasterTopCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTopCoordinator.swift; sourceTree = ""; }; + B3593F63241A62E400760E20 /* MasterCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterCoordinator.swift; sourceTree = ""; }; B3593F6F241A76E600760E20 /* CollectionEditActionHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionEditActionHandler.swift; sourceTree = ""; }; B3593F71241A76E600760E20 /* CollectionEditError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionEditError.swift; sourceTree = ""; }; B3593F72241A76E600760E20 /* CollectionEditAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionEditAction.swift; sourceTree = ""; }; @@ -3125,8 +3123,7 @@ B374911F2488D7040042FF85 /* Settings */, B30A44A929B88E3200332B4E /* TagFiltering */, B30A44A529B8799600332B4E /* MasterContainerViewController.swift */, - B30A44A729B882EB00332B4E /* MasterCoordinator.swift */, - B3593F63241A62E400760E20 /* MasterTopCoordinator.swift */, + B3593F63241A62E400760E20 /* MasterCoordinator.swift */, ); path = Master; sourceTree = ""; @@ -4879,7 +4876,7 @@ B3343079282A563B00FB41BC /* NSUserActivity+Activities.swift in Sources */, B305661C23FC051E003304F2 /* LoadUploadDataSyncAction.swift in Sources */, B31EEE5A24EBD18200E3B3AD /* ReadAnnotationsDbRequest.swift in Sources */, - B3593F64241A62E400760E20 /* MasterTopCoordinator.swift in Sources */, + B3593F64241A62E400760E20 /* MasterCoordinator.swift in Sources */, B3597D0227DA5C280069F019 /* AnnotationSplitter.swift in Sources */, B3422F48289BB65900C53DD2 /* ItemDetailFieldMultilineEditContentView.swift in Sources */, B3E8FE182714297200F51458 /* CiteState.swift in Sources */, @@ -5121,7 +5118,6 @@ B35FC1F02628490C00858772 /* MarkFileAsDownloadedDbRequest.swift in Sources */, B3593F7C241A76E600760E20 /* CollectionEditView.swift in Sources */, B3981B76258399AA00F8D15A /* NoteAnnotation.swift in Sources */, - B30A44A829B882EB00332B4E /* MasterCoordinator.swift in Sources */, B30565E223FC051E003304F2 /* StoreCollectionsDbRequest.swift in Sources */, B30565FA23FC051E003304F2 /* KeyGenerator.swift in Sources */, B30566B923FC051F003304F2 /* SchemaResponse.swift in Sources */, diff --git a/Zotero/Scenes/AppCoordinator.swift b/Zotero/Scenes/AppCoordinator.swift index e7c0f8cf6..292140963 100644 --- a/Zotero/Scenes/AppCoordinator.swift +++ b/Zotero/Scenes/AppCoordinator.swift @@ -119,8 +119,9 @@ final class AppCoordinator: NSObject { } DDLogInfo("AppCoordinator: show main screen logged \(isLogged ? "in" : "out"); animated=\(animated)") - self.show(viewController: viewController, in: window, animated: animated) - process(urlContext: urlContext, data: data) + show(viewController: viewController, in: window, animated: animated) { [weak self] in + self?.process(urlContext: urlContext, data: data) + } } private func preprocess(connectionOptions: UIScene.ConnectionOptions?, session: UISceneSession?) -> (UIOpenURLContext?, RestoredStateData?) { @@ -162,7 +163,8 @@ final class AppCoordinator: NSObject { self.showItemDetail(key: key, library: library, selectChildKey: preselectedChildKey, animated: animated) case .pdfReader(let attachment, let library, let page, let annotation, let parentKey, let isAvailable): - DDLogInfo("AppCoordinator: show custom url - pdf reader; key=\(attachment.key); library=\(library.identifier); page=\(page.flatMap(String.init) ?? "nil"); annotation=\(annotation ?? "nil"); parentKey=\(parentKey ?? "nil")") + DDLogInfo("AppCoordinator: show custom url - pdf reader; key=\(attachment.key); library=\(library.identifier); page=\(page.flatMap(String.init) ?? "nil");" + + " annotation=\(annotation ?? "nil"); parentKey=\(parentKey ?? "nil")") if isAvailable { self.open(attachment: attachment, library: library, on: page, annotation: annotation, parentKey: parentKey, animated: animated) return @@ -194,9 +196,9 @@ final class AppCoordinator: NSObject { guard let mainController = self.window?.rootViewController as? MainViewController else { return } // Show "All" collection in given library/group - if mainController.masterCoordinator?.topCoordinator.visibleLibraryId != library.identifier || - (mainController.masterCoordinator?.topCoordinator.navigationController?.visibleViewController as? CollectionsViewController)?.selectedIdentifier != .custom(.all) { - mainController.masterCoordinator?.topCoordinator.showCollections(for: library.identifier, preselectedCollection: .custom(.all), animated: animated) + if mainController.masterCoordinator?.visibleLibraryId != library.identifier || + (mainController.masterCoordinator?.navigationController?.visibleViewController as? CollectionsViewController)?.selectedIdentifier != .custom(.all) { + mainController.masterCoordinator?.showCollections(for: library.identifier, preselectedCollection: .custom(.all), animated: animated) } // Show item detail of given key @@ -383,12 +385,15 @@ final class AppCoordinator: NSObject { self.window?.rootViewController?.present(controller, animated: true, completion: nil) } - private func show(viewController: UIViewController?, in window: UIWindow, animated: Bool = false) { + private func show(viewController: UIViewController?, in window: UIWindow, animated: Bool = false, completion: @escaping () -> Void) { window.rootViewController = viewController - guard animated else { return } + guard animated else { + completion() + return + } - UIView.transition(with: window, duration: 0.2, options: .transitionCrossDissolve, animations: {}, completion: { _ in }) + UIView.transition(with: window, duration: 0.2, options: .transitionCrossDissolve, animations: {}, completion: { _ in completion() }) } private func presentActivityViewController(with items: [Any], completed: @escaping () -> Void) { @@ -449,7 +454,7 @@ final class AppCoordinator: NSObject { extension AppCoordinator: AppDelegateCoordinatorDelegate { func showMainScreen(isLoggedIn: Bool) { - self.showMainScreen(isLogged: isLoggedIn, options: self.tmpConnectionOptions, session: self.tmpSession, animated: true) + self.showMainScreen(isLogged: isLoggedIn, options: self.tmpConnectionOptions, session: self.tmpSession, animated: false) self.tmpConnectionOptions = nil self.tmpSession = nil } diff --git a/Zotero/Scenes/Main/Views/MainViewController.swift b/Zotero/Scenes/Main/Views/MainViewController.swift index 69e1b9e26..f647ac3d6 100644 --- a/Zotero/Scenes/Main/Views/MainViewController.swift +++ b/Zotero/Scenes/Main/Views/MainViewController.swift @@ -93,9 +93,16 @@ final class MainViewController: UISplitViewController { private func showItems(for collection: Collection, in library: Library, searchItemKeys: [String]?) { let navigationController = UINavigationController() - let tagFilterController = (self.viewControllers.first as? MasterContainerViewController)?.bottomController as? TagFilterViewController - - let coordinator = DetailCoordinator(library: library, collection: collection, searchItemKeys: searchItemKeys, navigationController: navigationController, itemsTagFilterDelegate: tagFilterController, controllers: self.controllers) + let tagFilterController = (self.viewControllers.first as? MasterContainerViewController)?.bottomController as? ItemsTagFilterDelegate + + let coordinator = DetailCoordinator( + library: library, + collection: collection, + searchItemKeys: searchItemKeys, + navigationController: navigationController, + itemsTagFilterDelegate: tagFilterController, + controllers: self.controllers + ) coordinator.start(animated: false) self.detailCoordinator = coordinator @@ -105,14 +112,15 @@ final class MainViewController: UISplitViewController { // MARK: - Setups private func setupControllers() { - let masterCoordinator = MasterCoordinator(mainController: self, controllers: self.controllers) - masterCoordinator.start() + let masterController = MasterContainerViewController() + let masterCoordinator = MasterCoordinator(navigationController: masterController, mainCoordinatorDelegate: self, controllers: self.controllers) + masterController.coordinatorDelegate = masterCoordinator + masterCoordinator.start(animated: false) + self.viewControllers = [masterController] self.masterCoordinator = masterCoordinator - if let progressObservable = self.controllers.userControllers?.syncScheduler.syncController.progressObservable, - let dbStorage = self.controllers.userControllers?.dbStorage, - let navigationController = masterCoordinator.topCoordinator.navigationController { - self.syncToolbarController = SyncToolbarController(parent: navigationController, progressObservable: progressObservable, dbStorage: dbStorage) + if let progressObservable = self.controllers.userControllers?.syncScheduler.syncController.progressObservable, let dbStorage = self.controllers.userControllers?.dbStorage { + self.syncToolbarController = SyncToolbarController(parent: masterController, progressObservable: progressObservable, dbStorage: dbStorage) self.syncToolbarController?.coordinatorDelegate = self } } @@ -148,7 +156,7 @@ extension MainViewController: MainCoordinatorSyncToolbarDelegate { guard let library = library, let collectionType = collectionType else { return } - self.masterCoordinator?.topCoordinator.showCollections(for: libraryId, preselectedCollection: .custom(collectionType), animated: true) + self.masterCoordinator?.showCollections(for: libraryId, preselectedCollection: .custom(collectionType), animated: true) self.showItems(for: Collection(custom: collectionType), in: library, searchItemKeys: keys) } catch let error { DDLogError("MainViewController: can't load searched keys - \(error)") diff --git a/Zotero/Scenes/Master/MasterContainerViewController.swift b/Zotero/Scenes/Master/MasterContainerViewController.swift index 32e244f44..1c2c563ee 100644 --- a/Zotero/Scenes/Master/MasterContainerViewController.swift +++ b/Zotero/Scenes/Master/MasterContainerViewController.swift @@ -25,10 +25,17 @@ final class MasterContainerViewController: UINavigationController { func topOffset(availableHeight: CGFloat) -> CGFloat { switch self { - case .mostlyVisible: return 202 - case .default: return availableHeight * 0.6 - case .hidden: return availableHeight - MasterContainerViewController.bottomControllerHandleHeight - case .custom(let offset): return offset + case .mostlyVisible: + return 202 + + case .default: + return availableHeight * 0.6 + + case .hidden: + return availableHeight - MasterContainerViewController.bottomControllerHandleHeight + + case .custom(let offset): + return availableHeight - offset < MasterContainerViewController.minVisibleBottomHeight ? MasterContainerViewController.minVisibleBottomHeight : offset } } } @@ -40,54 +47,24 @@ final class MasterContainerViewController: UINavigationController { private static let minVisibleBottomHeight: CGFloat = 200 private let disposeBag: DisposeBag - private var bottomContainer: UIView! - private var bottomYConstraint: NSLayoutConstraint! - private var bottomContainerBottomConstraint: NSLayoutConstraint! + lazy var bottomController: DraggableViewController? = { + return coordinatorDelegate?.createBottomController() + }() + private weak var bottomContainer: UIView? + private weak var bottomYConstraint: NSLayoutConstraint? + private weak var bottomContainerBottomConstraint: NSLayoutConstraint? // Current position of bottom container private var bottomPosition: BottomPosition // Previous position of bottom container. Used to return to previous position when drag handle is tapped. private var previousBottomPosition: BottomPosition? - private var didAppear: Bool // Used to calculate position and velocity when dragging private var initialBottomMinY: CGFloat? private var keyboardHeight: CGFloat = 0 - - private var _bottomController: DraggableViewController? - var bottomController: DraggableViewController? { - if _bottomController != nil { - return _bottomController - } - guard let bottomController = coordinatorDelegate?.bottomController() else { return nil } - setupBottomController() - _bottomController = bottomController - return _bottomController - - func setupBottomController() { - bottomController.view.translatesAutoresizingMaskIntoConstraints = false - // Since the instance keeps a strong reference to the bottomController, its view is simply added as a subview. - // Adding bottomController as a child view controller, would mess up the navigation stack. - bottomContainer.addSubview(bottomController.view) - let bottomControllerHeight = bottomController.view.heightAnchor.constraint(greaterThanOrEqualToConstant: 100) - bottomControllerHeight.priority = .required - let bottomControllerBottom = bottomController.view.bottomAnchor.constraint(equalTo: bottomContainer.bottomAnchor) - bottomControllerBottom.priority = UILayoutPriority(999) - NSLayoutConstraint.activate([ - // bottom controller view - bottomController.view.topAnchor.constraint(equalTo: bottomContainer.topAnchor, constant: Self.bottomControllerHandleHeight), - bottomController.view.leadingAnchor.constraint(equalTo: bottomContainer.leadingAnchor), - bottomController.view.trailingAnchor.constraint(equalTo: bottomContainer.trailingAnchor), - bottomControllerHeight, - bottomControllerBottom - ]) - } - } - private weak var coordinatorDelegate: MasterContainerCoordinatorDelegate? + weak var coordinatorDelegate: MasterContainerCoordinatorDelegate? - init(coordinatorDelegate: MasterContainerCoordinatorDelegate) { - self.coordinatorDelegate = coordinatorDelegate + init() { self.bottomPosition = .default - self.didAppear = false self.disposeBag = DisposeBag() super.init(nibName: nil, bundle: nil) } @@ -103,34 +80,16 @@ final class MasterContainerViewController: UINavigationController { view.backgroundColor = .clear setupView() setupKeyboardObserving() - showBottomSheet(false) + setBottomSheet(hidden: true) func setupView() { - let bottomPanRecognizer = UIPanGestureRecognizer() - bottomPanRecognizer.delegate = self - bottomPanRecognizer.rx.event - .subscribe(with: self, onNext: { _, recognizer in - toolbarDidPan(recognizer: recognizer) - }) - .disposed(by: disposeBag) - - let tapRecognizer = UITapGestureRecognizer() - tapRecognizer.delegate = self - tapRecognizer.require(toFail: bottomPanRecognizer) - tapRecognizer.rx.event - .subscribe(with: self, onNext: { _, _ in - toggleBottomPosition() - }) - .disposed(by: disposeBag) + guard let bottomController else { return } let bottomContainer = UIView() bottomContainer.translatesAutoresizingMaskIntoConstraints = false bottomContainer.layer.masksToBounds = true bottomContainer.backgroundColor = .systemBackground - bottomContainer.addGestureRecognizer(bottomPanRecognizer) - bottomContainer.addGestureRecognizer(tapRecognizer) view.addSubview(bottomContainer) - self.bottomContainer = bottomContainer let handleBackground = UIView() handleBackground.translatesAutoresizingMaskIntoConstraints = false @@ -147,6 +106,15 @@ final class MasterContainerViewController: UINavigationController { separator.backgroundColor = .opaqueSeparator bottomContainer.addSubview(separator) + bottomController.view.translatesAutoresizingMaskIntoConstraints = false + // Since the instance keeps a strong reference to the bottomController, its view is simply added as a subview. + // Adding bottomController as a child view controller, would mess up the navigation stack. + bottomContainer.addSubview(bottomController.view) + + let bottomControllerHeight = bottomController.view.heightAnchor.constraint(greaterThanOrEqualToConstant: 100) + bottomControllerHeight.priority = .required + let bottomControllerBottom = bottomController.view.bottomAnchor.constraint(equalTo: bottomContainer.bottomAnchor) + bottomControllerBottom.priority = UILayoutPriority(999) let bottomYConstraint = bottomContainer.topAnchor.constraint(equalTo: view.topAnchor) let bottomContainerBottomConstraint = view.bottomAnchor.constraint(equalTo: bottomContainer.bottomAnchor) @@ -170,17 +138,44 @@ final class MasterContainerViewController: UINavigationController { separator.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale), separator.topAnchor.constraint(equalTo: bottomContainer.topAnchor), separator.leadingAnchor.constraint(equalTo: bottomContainer.leadingAnchor), - separator.trailingAnchor.constraint(equalTo: bottomContainer.trailingAnchor) + separator.trailingAnchor.constraint(equalTo: bottomContainer.trailingAnchor), + // bottom controller view + bottomController.view.topAnchor.constraint(equalTo: bottomContainer.topAnchor, constant: Self.bottomControllerHandleHeight), + bottomController.view.leadingAnchor.constraint(equalTo: bottomContainer.leadingAnchor), + bottomController.view.trailingAnchor.constraint(equalTo: bottomContainer.trailingAnchor), + bottomControllerHeight, + bottomControllerBottom ]) + self.bottomContainer = bottomContainer self.bottomYConstraint = bottomYConstraint self.bottomContainerBottomConstraint = bottomContainerBottomConstraint - + + let bottomPanRecognizer = UIPanGestureRecognizer() + bottomPanRecognizer.delegate = self + bottomPanRecognizer.rx.event + .subscribe(with: self, onNext: { _, recognizer in + toolbarDidPan(recognizer: recognizer) + }) + .disposed(by: disposeBag) + + let tapRecognizer = UITapGestureRecognizer() + tapRecognizer.delegate = self + tapRecognizer.require(toFail: bottomPanRecognizer) + tapRecognizer.rx.event + .subscribe(with: self, onNext: { _, _ in + toggleBottomPosition() + }) + .disposed(by: disposeBag) + + bottomContainer.addGestureRecognizer(bottomPanRecognizer) + bottomContainer.addGestureRecognizer(tapRecognizer) + func toolbarDidPan(recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: - initialBottomMinY = self.bottomContainer.frame.minY - bottomController?.disablePanning() + initialBottomMinY = bottomContainer.frame.minY + bottomController.disablePanning() case .changed: guard let initialBottomMinY else { return } @@ -196,14 +191,14 @@ final class MasterContainerViewController: UINavigationController { minY = hiddenTopOffset } - self.bottomYConstraint.constant = minY + bottomYConstraint.constant = minY view.layoutIfNeeded() case .ended, .failed: let availableHeight = view.frame.height - keyboardHeight let dragVelocity = recognizer.velocity(in: view) - let newPosition = position(fromYPos: self.bottomYConstraint.constant, containerHeight: availableHeight, velocity: dragVelocity) - let velocity = velocity(from: dragVelocity, currentYPos: self.bottomYConstraint.constant, position: newPosition, availableHeight: availableHeight) + let newPosition = position(fromYPos: bottomYConstraint.constant, containerHeight: availableHeight, velocity: dragVelocity) + let velocity = velocity(from: dragVelocity, currentYPos: bottomYConstraint.constant, position: newPosition, availableHeight: availableHeight) set(bottomPosition: newPosition, containerHeight: availableHeight) @@ -218,7 +213,7 @@ final class MasterContainerViewController: UINavigationController { } initialBottomMinY = nil - bottomController?.enablePanning() + bottomController.enablePanning() case .cancelled, .possible: break @@ -239,7 +234,7 @@ final class MasterContainerViewController: UINavigationController { } } - if yPos > (containerHeight - MasterContainerViewController.minVisibleBottomHeight) { + if yPos > (containerHeight - Self.minVisibleBottomHeight) { return velocity.y > 0 ? .hidden : .default } @@ -279,30 +274,30 @@ final class MasterContainerViewController: UINavigationController { func setupKeyboardObserving() { NotificationCenter.default - .keyboardWillShow - .observe(on: MainScheduler.instance) - .subscribe(onNext: { notification in - if let data = notification.keyboardData { - setupKeyboard(with: data) - } - }) - .disposed(by: disposeBag) + .keyboardWillShow + .observe(on: MainScheduler.instance) + .subscribe(onNext: { notification in + if let data = notification.keyboardData { + setupKeyboard(with: data) + } + }) + .disposed(by: disposeBag) NotificationCenter.default - .keyboardWillHide - .observe(on: MainScheduler.instance) - .subscribe(onNext: { notification in - if let data = notification.keyboardData { - setupKeyboard(with: data) - } - }) - .disposed(by: disposeBag) + .keyboardWillHide + .observe(on: MainScheduler.instance) + .subscribe(onNext: { notification in + if let data = notification.keyboardData { + setupKeyboard(with: data) + } + }) + .disposed(by: disposeBag) func setupKeyboard(with keyboardData: KeyboardData) { keyboardHeight = keyboardData.visibleHeight updateBottomPosition() - bottomContainerBottomConstraint.constant = keyboardData.visibleHeight + bottomContainerBottomConstraint?.constant = keyboardData.visibleHeight UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: { self.view.layoutIfNeeded() }) @@ -310,24 +305,17 @@ final class MasterContainerViewController: UINavigationController { } } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - guard !didAppear else { return } - didAppear = true + override func viewIsAppearing(_ animated: Bool) { + super.viewIsAppearing(animated) + + updateBottomPosition() if let splitViewController { // Split view controller collapsed status when the app launches is correct here, so it's used to show/hide bottom sheet for the first appearance. // The app may be launched in collapsed mode, if it was in such mode the last time it was moved to background. - showBottomSheet(!splitViewController.isCollapsed) + setBottomSheet(hidden: splitViewController.isCollapsed) } } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - guard !didAppear else { return } - updateBottomPosition() - } - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) @@ -340,7 +328,7 @@ final class MasterContainerViewController: UINavigationController { } override func collapseSecondaryViewController(_ secondaryViewController: UIViewController, for splitViewController: UISplitViewController) { - showBottomSheet(false) + setBottomSheet(hidden: true) super.collapseSecondaryViewController(secondaryViewController, for: splitViewController) // The search bar is hidden when the app goes to background for unknown reason. This is a workaround to reset it if needed when // the app returns to active state. @@ -350,7 +338,7 @@ final class MasterContainerViewController: UINavigationController { } override func separateSecondaryViewController(for splitViewController: UISplitViewController) -> UIViewController? { - showBottomSheet(true) + setBottomSheet(hidden: false) guard topViewController?.isKind(of: UINavigationController.self) == true else { // When separating from an initially collapsed split view controller, the detail view controller is not yet set. coordinatorDelegate?.showDefaultCollection() @@ -360,18 +348,15 @@ final class MasterContainerViewController: UINavigationController { } // MARK: - Bottom panning - private func showBottomSheet(_ show: Bool) { - if show, bottomController != nil { - bottomContainer.isHidden = false - additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 16, right: 0) - } else { - bottomContainer.isHidden = true - additionalSafeAreaInsets = .zero - } + + private func setBottomSheet(hidden: Bool) { + guard let container = bottomContainer else { return } + container.isHidden = hidden + additionalSafeAreaInsets = hidden ? .zero : UIEdgeInsets(top: 0, left: 0, bottom: 16, right: 0) } private func set(bottomPosition: BottomPosition, containerHeight: CGFloat) { - bottomYConstraint.constant = bottomPosition.topOffset(availableHeight: containerHeight) + bottomYConstraint?.constant = bottomPosition.topOffset(availableHeight: containerHeight) self.bottomPosition = bottomPosition } @@ -387,6 +372,8 @@ final class MasterContainerViewController: UINavigationController { extension MasterContainerViewController: UIGestureRecognizerDelegate { func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + guard let bottomContainer else { return false } + let location = gestureRecognizer.location(in: bottomContainer) if gestureRecognizer is UITapGestureRecognizer { diff --git a/Zotero/Scenes/Master/MasterCoordinator.swift b/Zotero/Scenes/Master/MasterCoordinator.swift index e2ebd074f..5e05ea7e2 100644 --- a/Zotero/Scenes/Master/MasterCoordinator.swift +++ b/Zotero/Scenes/Master/MasterCoordinator.swift @@ -2,51 +2,281 @@ // MasterCoordinator.swift // Zotero // -// Created by Michal Rentka on 08.03.2023. -// Copyright © 2023 Corporation for Digital Scholarship. All rights reserved. +// Created by Michal Rentka on 12/03/2020. +// Copyright © 2020 Corporation for Digital Scholarship. All rights reserved. // import UIKit +import SwiftUI import CocoaLumberjackSwift +import RxSwift + +protocol MasterLibrariesCoordinatorDelegate: AnyObject { + func showCollections(for libraryId: LibraryIdentifier) + func showSettings() + func show(error: LibrariesError) + func showDeleteGroupQuestion(id: Int, name: String, viewModel: ViewModel) + func showDefaultLibrary() + + var visibleLibraryId: LibraryIdentifier { get } +} + +protocol MasterCollectionsCoordinatorDelegate: MainCoordinatorDelegate { + func showEditView(for data: CollectionStateEditingData, library: Library) + func showCiteExport(for itemIds: Set, libraryId: LibraryIdentifier) + func showCiteExportError() + func showSearch(for state: CollectionsState, in controller: UIViewController, selectAction: @escaping (Collection) -> Void) + func showDefaultCollection() +} protocol MasterContainerCoordinatorDelegate: AnyObject { func showDefaultCollection() - func bottomController() -> DraggableViewController? + func createBottomController() -> DraggableViewController? } -final class MasterCoordinator { - private let controllers: Controllers - private unowned let mainController: MainViewController +final class MasterCoordinator: NSObject, Coordinator { + weak var parentCoordinator: Coordinator? + var childCoordinators: [Coordinator] + private(set) var visibleLibraryId: LibraryIdentifier + weak var navigationController: UINavigationController? - private(set) var topCoordinator: MasterTopCoordinator! + private unowned let controllers: Controllers + private unowned let mainCoordinatorDelegate: MainCoordinatorDelegate - init(mainController: MainViewController, controllers: Controllers) { - self.mainController = mainController + init(navigationController: UINavigationController, mainCoordinatorDelegate: MainCoordinatorDelegate, controllers: Controllers) { + self.navigationController = navigationController + self.mainCoordinatorDelegate = mainCoordinatorDelegate self.controllers = controllers + self.childCoordinators = [] + self.visibleLibraryId = Defaults.shared.selectedLibrary + + super.init() + } + + func start(animated: Bool) { + guard let userControllers = self.controllers.userControllers else { return } + let librariesController = self.createLibrariesViewController(dbStorage: userControllers.dbStorage) + let collectionsController = self.createCollectionsViewController( + libraryId: self.visibleLibraryId, + selectedCollectionId: Defaults.shared.selectedCollectionId, + dbStorage: userControllers.dbStorage, + attachmentDownloader: userControllers.fileDownloader + ) + self.navigationController?.setViewControllers([librariesController, collectionsController], animated: animated) + } + + private func createLibrariesViewController(dbStorage: DbStorage) -> UIViewController { + let viewModel = ViewModel(initialState: LibrariesState(), handler: LibrariesActionHandler(dbStorage: dbStorage)) + let controller = LibrariesViewController(viewModel: viewModel) + controller.coordinatorDelegate = self + return controller } - deinit { - DDLogInfo("MasterCoordinator: deinitialized") + private func createCollectionsViewController( + libraryId: LibraryIdentifier, + selectedCollectionId: CollectionIdentifier, + dbStorage: DbStorage, + attachmentDownloader: AttachmentDownloader + ) -> CollectionsViewController { + DDLogInfo("MasterTopCoordinator: show collections for \(selectedCollectionId.id); \(libraryId)") + let handler = CollectionsActionHandler(dbStorage: dbStorage, fileStorage: self.controllers.fileStorage, attachmentDownloader: attachmentDownloader) + let state = CollectionsState(libraryId: libraryId, selectedCollectionId: selectedCollectionId) + return CollectionsViewController(viewModel: ViewModel(initialState: state, handler: handler), dragDropController: self.controllers.dragDropController, coordinatorDelegate: self) } - func start() { - let masterController = MasterContainerViewController(coordinatorDelegate: self) - let masterCoordinator = MasterTopCoordinator(navigationController: masterController, mainCoordinatorDelegate: self.mainController, controllers: self.controllers) - masterCoordinator.start(animated: false) - self.topCoordinator = masterCoordinator - self.mainController.viewControllers = [masterController] + private func storeIfNeeded(libraryId: LibraryIdentifier, preselectedCollection collectionId: CollectionIdentifier? = nil) -> CollectionIdentifier { + if Defaults.shared.selectedLibrary == libraryId { + if let collectionId = collectionId { + Defaults.shared.selectedCollectionId = collectionId + return collectionId + } + return Defaults.shared.selectedCollectionId + } + + let collectionId = collectionId ?? .custom(.all) + Defaults.shared.selectedLibrary = libraryId + Defaults.shared.selectedCollectionId = collectionId + return collectionId } } -extension MasterCoordinator: MasterContainerCoordinatorDelegate { - func showDefaultCollection() { - topCoordinator?.showDefaultCollection() +extension MasterCoordinator: MasterLibrariesCoordinatorDelegate { + func showDefaultLibrary() { + guard let userControllers = self.controllers.userControllers else { return } + + let libraryId = LibraryIdentifier.custom(.myLibrary) + let collectionId = self.storeIfNeeded(libraryId: libraryId) + + let controller = self.createCollectionsViewController( + libraryId: libraryId, + selectedCollectionId: collectionId, + dbStorage: userControllers.dbStorage, + attachmentDownloader: userControllers.fileDownloader + ) + + let animated: Bool + var viewControllers = self.navigationController?.viewControllers ?? [] + + if let index = viewControllers.firstIndex(where: { $0 is CollectionsViewController }) { + // If `CollectionsViewController` is visible, replace it with new controller without animation + viewControllers[index] = controller + animated = false + } else { + // If `CollectionsViewController` is not visible, just push it with animation + viewControllers.append(controller) + animated = true + } + + self.navigationController?.setViewControllers(viewControllers, animated: animated) + } + + func show(error: LibrariesError) { + let title: String + let message: String + + switch error { + case .cantLoadData: + title = L10n.error + message = L10n.Errors.Libraries.cantLoad + } + + let controller = UIAlertController(title: title, message: message, preferredStyle: .alert) + controller.addAction(UIAlertAction(title: L10n.ok, style: .cancel, handler: nil)) + self.navigationController?.present(controller, animated: true, completion: nil) + } + + func showDeleteGroupQuestion(id: Int, name: String, viewModel: ViewModel) { + let controller = UIAlertController(title: L10n.delete, message: L10n.Libraries.deleteQuestion(name), preferredStyle: .alert) + controller.addAction(UIAlertAction(title: L10n.yes, style: .destructive, handler: { [weak viewModel] _ in + viewModel?.process(action: .deleteGroup(id)) + })) + controller.addAction(UIAlertAction(title: L10n.no, style: .cancel, handler: nil)) + self.navigationController?.present(controller, animated: true, completion: nil) + } + + func showCollections(for libraryId: LibraryIdentifier) { + guard let userControllers = self.controllers.userControllers else { return } + + let collectionId = self.storeIfNeeded(libraryId: libraryId) + + let controller = self.createCollectionsViewController( + libraryId: libraryId, + selectedCollectionId: collectionId, + dbStorage: userControllers.dbStorage, + attachmentDownloader: userControllers.fileDownloader + ) + self.navigationController?.pushViewController(controller, animated: true) + } + + func showCollections(for libraryId: LibraryIdentifier, preselectedCollection collectionId: CollectionIdentifier, animated: Bool) { + guard let navigationController, let userControllers = self.controllers.userControllers else { return } + + let collectionId = self.storeIfNeeded(libraryId: libraryId, preselectedCollection: collectionId) + + if navigationController.viewControllers.count == 1 { + // If only "Libraries" screen is visible, push collections + let controller = self.createCollectionsViewController( + libraryId: libraryId, + selectedCollectionId: collectionId, + dbStorage: userControllers.dbStorage, + attachmentDownloader: userControllers.fileDownloader + ) + navigationController.pushViewController(controller, animated: animated) + } else if libraryId != self.visibleLibraryId { + // If Collections screen is visible, but for different library, switch controllers + let controller = self.createCollectionsViewController( + libraryId: libraryId, + selectedCollectionId: collectionId, + dbStorage: userControllers.dbStorage, + attachmentDownloader: userControllers.fileDownloader + ) + + var viewControllers = navigationController.viewControllers + _ = viewControllers.popLast() + viewControllers.append(controller) + + navigationController.setViewControllers(viewControllers, animated: animated) + } else if let controller = navigationController.visibleViewController as? CollectionsViewController, controller.selectedIdentifier != .custom(.all) { + // Correct Collections screen is visible, just select proper collection + controller.viewModel.process(action: .select(.custom(.all))) + } + } + + func showSettings() { + let navigationController = NavigationViewController() + let containerController = ContainerViewController(rootViewController: navigationController) + let coordinator = SettingsCoordinator(startsWithExport: false, navigationController: navigationController, controllers: self.controllers) + coordinator.parentCoordinator = self + self.childCoordinators.append(coordinator) + coordinator.start(animated: false) + + self.navigationController?.present(containerController, animated: true, completion: nil) + } +} + +extension MasterCoordinator: MasterCollectionsCoordinatorDelegate { + func showEditView(for data: CollectionStateEditingData, library: Library) { + let navigationController = UINavigationController() + navigationController.isModalInPresentation = true + navigationController.modalPresentationStyle = .formSheet + + let coordinator = CollectionEditingCoordinator(data: data, library: library, navigationController: navigationController, controllers: self.controllers) + coordinator.parentCoordinator = self + self.childCoordinators.append(coordinator) + coordinator.start(animated: false) + + self.navigationController?.present(navigationController, animated: true, completion: nil) + } + + func showItems(for collection: Collection, in library: Library, saveCollectionToDefaults: Bool) { + self.visibleLibraryId = library.identifier + self.mainCoordinatorDelegate.showItems(for: collection, in: library, saveCollectionToDefaults: saveCollectionToDefaults) + } + + var isSplit: Bool { + return self.mainCoordinatorDelegate.isSplit + } + + func showCiteExport(for itemIds: Set, libraryId: LibraryIdentifier) { + let navigationController = NavigationViewController() + let containerController = ContainerViewController(rootViewController: navigationController) + let coordinator = CitationBibliographyExportCoordinator(itemIds: itemIds, libraryId: libraryId, navigationController: navigationController, controllers: self.controllers) + coordinator.parentCoordinator = self + self.childCoordinators.append(coordinator) + coordinator.start(animated: false) + + self.navigationController?.present(containerController, animated: true, completion: nil) + } + + func showCiteExportError() { + let controller = UIAlertController(title: L10n.error, message: L10n.Errors.Collections.bibliographyFailed, preferredStyle: .alert) + controller.addAction(UIAlertAction(title: L10n.ok, style: .cancel, handler: nil)) + self.navigationController?.present(controller, animated: true, completion: nil) + } + + func showSearch(for state: CollectionsState, in controller: UIViewController, selectAction: @escaping (Collection) -> Void) { + let searchState = CollectionsSearchState(collectionsTree: state.collectionTree) + let viewModel = ViewModel(initialState: searchState, handler: CollectionsSearchActionHandler()) + + let searchController = CollectionsSearchViewController(viewModel: viewModel, selectAction: selectAction) + searchController.modalPresentationStyle = .overCurrentContext + searchController.modalTransitionStyle = .crossDissolve + searchController.isModalInPresentation = true + + controller.present(searchController, animated: true, completion: nil) } - func bottomController() -> DraggableViewController? { - guard UIDevice.current.userInterfaceIdiom == .pad else { return nil } - guard let dbStorage = controllers.userControllers?.dbStorage else { return nil } + func showDefaultCollection() { + let library = Library(identifier: visibleLibraryId, name: "", metadataEditable: true, filesEditable: true) + let collection = Collection(custom: .all) + showItems(for: collection, in: library, saveCollectionToDefaults: false) + } +} + +extension MasterCoordinator: MasterContainerCoordinatorDelegate { + func createBottomController() -> DraggableViewController? { + guard UIDevice.current.userInterfaceIdiom == .pad, let dbStorage = controllers.userControllers?.dbStorage else { return nil } let state = TagFilterState(selectedTags: [], showAutomatic: Defaults.shared.tagPickerShowAutomaticTags, displayAll: Defaults.shared.tagPickerDisplayAllTags) let handler = TagFilterActionHandler(dbStorage: dbStorage) let viewModel = ViewModel(initialState: state, handler: handler) diff --git a/Zotero/Scenes/Master/MasterTopCoordinator.swift b/Zotero/Scenes/Master/MasterTopCoordinator.swift deleted file mode 100644 index a7d042df2..000000000 --- a/Zotero/Scenes/Master/MasterTopCoordinator.swift +++ /dev/null @@ -1,270 +0,0 @@ -// -// MasterTopCoordinator.swift -// Zotero -// -// Created by Michal Rentka on 12/03/2020. -// Copyright © 2020 Corporation for Digital Scholarship. All rights reserved. -// - -import UIKit -import SwiftUI - -import CocoaLumberjackSwift -import RxSwift - -protocol MasterLibrariesCoordinatorDelegate: AnyObject { - func showCollections(for libraryId: LibraryIdentifier) - func showSettings() - func show(error: LibrariesError) - func showDeleteGroupQuestion(id: Int, name: String, viewModel: ViewModel) - func showDefaultLibrary() - - var visibleLibraryId: LibraryIdentifier { get } -} - -protocol MasterCollectionsCoordinatorDelegate: MainCoordinatorDelegate { - func showEditView(for data: CollectionStateEditingData, library: Library) - func showCiteExport(for itemIds: Set, libraryId: LibraryIdentifier) - func showCiteExportError() - func showSearch(for state: CollectionsState, in controller: UIViewController, selectAction: @escaping (Collection) -> Void) - func showDefaultCollection() -} - -final class MasterTopCoordinator: NSObject, Coordinator { - weak var parentCoordinator: Coordinator? - var childCoordinators: [Coordinator] - private(set) var visibleLibraryId: LibraryIdentifier - weak var navigationController: UINavigationController? - - private unowned let controllers: Controllers - private unowned let mainCoordinatorDelegate: MainCoordinatorDelegate - - init(navigationController: UINavigationController, mainCoordinatorDelegate: MainCoordinatorDelegate, controllers: Controllers) { - self.navigationController = navigationController - self.mainCoordinatorDelegate = mainCoordinatorDelegate - self.controllers = controllers - self.childCoordinators = [] - self.visibleLibraryId = Defaults.shared.selectedLibrary - - super.init() - } - - func start(animated: Bool) { - guard let userControllers = self.controllers.userControllers else { return } - let librariesController = self.createLibrariesViewController(dbStorage: userControllers.dbStorage) - let collectionsController = self.createCollectionsViewController( - libraryId: self.visibleLibraryId, - selectedCollectionId: Defaults.shared.selectedCollectionId, - dbStorage: userControllers.dbStorage, - attachmentDownloader: userControllers.fileDownloader - ) - self.navigationController?.setViewControllers([librariesController, collectionsController], animated: animated) - } - - private func createLibrariesViewController(dbStorage: DbStorage) -> UIViewController { - let viewModel = ViewModel(initialState: LibrariesState(), handler: LibrariesActionHandler(dbStorage: dbStorage)) - let controller = LibrariesViewController(viewModel: viewModel) - controller.coordinatorDelegate = self - return controller - } - - private func createCollectionsViewController( - libraryId: LibraryIdentifier, - selectedCollectionId: CollectionIdentifier, - dbStorage: DbStorage, - attachmentDownloader: AttachmentDownloader - ) -> CollectionsViewController { - DDLogInfo("MasterTopCoordinator: show collections for \(selectedCollectionId.id); \(libraryId)") - let handler = CollectionsActionHandler(dbStorage: dbStorage, fileStorage: self.controllers.fileStorage, attachmentDownloader: attachmentDownloader) - let state = CollectionsState(libraryId: libraryId, selectedCollectionId: selectedCollectionId) - return CollectionsViewController(viewModel: ViewModel(initialState: state, handler: handler), dragDropController: self.controllers.dragDropController, coordinatorDelegate: self) - } - - private func storeIfNeeded(libraryId: LibraryIdentifier, preselectedCollection collectionId: CollectionIdentifier? = nil) -> CollectionIdentifier { - if Defaults.shared.selectedLibrary == libraryId { - if let collectionId = collectionId { - Defaults.shared.selectedCollectionId = collectionId - return collectionId - } - return Defaults.shared.selectedCollectionId - } - - let collectionId = collectionId ?? .custom(.all) - Defaults.shared.selectedLibrary = libraryId - Defaults.shared.selectedCollectionId = collectionId - return collectionId - } -} - -extension MasterTopCoordinator: MasterLibrariesCoordinatorDelegate { - func showDefaultLibrary() { - guard let userControllers = self.controllers.userControllers else { return } - - let libraryId = LibraryIdentifier.custom(.myLibrary) - let collectionId = self.storeIfNeeded(libraryId: libraryId) - - let controller = self.createCollectionsViewController( - libraryId: libraryId, - selectedCollectionId: collectionId, - dbStorage: userControllers.dbStorage, - attachmentDownloader: userControllers.fileDownloader - ) - - let animated: Bool - var viewControllers = self.navigationController?.viewControllers ?? [] - - if let index = viewControllers.firstIndex(where: { $0 is CollectionsViewController }) { - // If `CollectionsViewController` is visible, replace it with new controller without animation - viewControllers[index] = controller - animated = false - } else { - // If `CollectionsViewController` is not visible, just push it with animation - viewControllers.append(controller) - animated = true - } - - self.navigationController?.setViewControllers(viewControllers, animated: animated) - } - - func show(error: LibrariesError) { - let title: String - let message: String - - switch error { - case .cantLoadData: - title = L10n.error - message = L10n.Errors.Libraries.cantLoad - } - - let controller = UIAlertController(title: title, message: message, preferredStyle: .alert) - controller.addAction(UIAlertAction(title: L10n.ok, style: .cancel, handler: nil)) - self.navigationController?.present(controller, animated: true, completion: nil) - } - - func showDeleteGroupQuestion(id: Int, name: String, viewModel: ViewModel) { - let controller = UIAlertController(title: L10n.delete, message: L10n.Libraries.deleteQuestion(name), preferredStyle: .alert) - controller.addAction(UIAlertAction(title: L10n.yes, style: .destructive, handler: { [weak viewModel] _ in - viewModel?.process(action: .deleteGroup(id)) - })) - controller.addAction(UIAlertAction(title: L10n.no, style: .cancel, handler: nil)) - self.navigationController?.present(controller, animated: true, completion: nil) - } - - func showCollections(for libraryId: LibraryIdentifier) { - guard let userControllers = self.controllers.userControllers else { return } - - let collectionId = self.storeIfNeeded(libraryId: libraryId) - - let controller = self.createCollectionsViewController( - libraryId: libraryId, - selectedCollectionId: collectionId, - dbStorage: userControllers.dbStorage, - attachmentDownloader: userControllers.fileDownloader - ) - self.navigationController?.pushViewController(controller, animated: true) - } - - func showCollections(for libraryId: LibraryIdentifier, preselectedCollection collectionId: CollectionIdentifier, animated: Bool) { - guard let navigationController, let userControllers = self.controllers.userControllers else { return } - - let collectionId = self.storeIfNeeded(libraryId: libraryId, preselectedCollection: collectionId) - - if navigationController.viewControllers.count == 1 { - // If only "Libraries" screen is visible, push collections - let controller = self.createCollectionsViewController( - libraryId: libraryId, - selectedCollectionId: collectionId, - dbStorage: userControllers.dbStorage, - attachmentDownloader: userControllers.fileDownloader - ) - navigationController.pushViewController(controller, animated: animated) - } else if libraryId != self.visibleLibraryId { - // If Collections screen is visible, but for different library, switch controllers - let controller = self.createCollectionsViewController( - libraryId: libraryId, - selectedCollectionId: collectionId, - dbStorage: userControllers.dbStorage, - attachmentDownloader: userControllers.fileDownloader - ) - - var viewControllers = navigationController.viewControllers - _ = viewControllers.popLast() - viewControllers.append(controller) - - navigationController.setViewControllers(viewControllers, animated: animated) - } else if let controller = navigationController.visibleViewController as? CollectionsViewController, controller.selectedIdentifier != .custom(.all) { - // Correct Collections screen is visible, just select proper collection - controller.viewModel.process(action: .select(.custom(.all))) - } - } - - func showSettings() { - let navigationController = NavigationViewController() - let containerController = ContainerViewController(rootViewController: navigationController) - let coordinator = SettingsCoordinator(startsWithExport: false, navigationController: navigationController, controllers: self.controllers) - coordinator.parentCoordinator = self - self.childCoordinators.append(coordinator) - coordinator.start(animated: false) - - self.navigationController?.present(containerController, animated: true, completion: nil) - } -} - -extension MasterTopCoordinator: MasterCollectionsCoordinatorDelegate { - func showEditView(for data: CollectionStateEditingData, library: Library) { - let navigationController = UINavigationController() - navigationController.isModalInPresentation = true - navigationController.modalPresentationStyle = .formSheet - - let coordinator = CollectionEditingCoordinator(data: data, library: library, navigationController: navigationController, controllers: self.controllers) - coordinator.parentCoordinator = self - self.childCoordinators.append(coordinator) - coordinator.start(animated: false) - - self.navigationController?.present(navigationController, animated: true, completion: nil) - } - - func showItems(for collection: Collection, in library: Library, saveCollectionToDefaults: Bool) { - self.visibleLibraryId = library.identifier - self.mainCoordinatorDelegate.showItems(for: collection, in: library, saveCollectionToDefaults: saveCollectionToDefaults) - } - - var isSplit: Bool { - return self.mainCoordinatorDelegate.isSplit - } - - func showCiteExport(for itemIds: Set, libraryId: LibraryIdentifier) { - let navigationController = NavigationViewController() - let containerController = ContainerViewController(rootViewController: navigationController) - let coordinator = CitationBibliographyExportCoordinator(itemIds: itemIds, libraryId: libraryId, navigationController: navigationController, controllers: self.controllers) - coordinator.parentCoordinator = self - self.childCoordinators.append(coordinator) - coordinator.start(animated: false) - - self.navigationController?.present(containerController, animated: true, completion: nil) - } - - func showCiteExportError() { - let controller = UIAlertController(title: L10n.error, message: L10n.Errors.Collections.bibliographyFailed, preferredStyle: .alert) - controller.addAction(UIAlertAction(title: L10n.ok, style: .cancel, handler: nil)) - self.navigationController?.present(controller, animated: true, completion: nil) - } - - func showSearch(for state: CollectionsState, in controller: UIViewController, selectAction: @escaping (Collection) -> Void) { - let searchState = CollectionsSearchState(collectionsTree: state.collectionTree) - let viewModel = ViewModel(initialState: searchState, handler: CollectionsSearchActionHandler()) - - let searchController = CollectionsSearchViewController(viewModel: viewModel, selectAction: selectAction) - searchController.modalPresentationStyle = .overCurrentContext - searchController.modalTransitionStyle = .crossDissolve - searchController.isModalInPresentation = true - - controller.present(searchController, animated: true, completion: nil) - } - - func showDefaultCollection() { - let library = Library(identifier: visibleLibraryId, name: "", metadataEditable: true, filesEditable: true) - let collection = Collection(custom: .all) - showItems(for: collection, in: library, saveCollectionToDefaults: false) - } -}