Skip to content

Commit

Permalink
Add support for multiple open items
Browse files Browse the repository at this point in the history
Improve AppCoordinator code
Keep track of open items count in items navigation bar
Add ability to restore most recently opened item
Implement state restoration for open items
Allow switch to another open item via bar button menu
Use instant presenter to switch between open items
Maintain user order of open items
Add support for notes in open items
Validate open items when set on app launch
Observe open items for deletions
Add support for different open items per session
Improve DetailCoordinator presented item replacement for multiple items
Set user activity when new note is actually created
Show actions submenu for current item
Improve ItemsViewController right bar button items
Add icons item type icons to open items menu
Add close all action to open items menu
Simplify NoteEditorViewController open items button creation
Add PDFReaderViewController open items observer
Add close other items action to submenu for current item
Add getSessionIdentifier convenience property to UIViewController
Save user activity when open items change w/o current item change
Open items in their respective scene, if already open
Update copy
Improve NSUserActivity extension
Simplify DetailCoordinator
Improve open items bar button image creation
Improve open items bar button creation
Properly close PDF Reader when switching current open item
Properly close Note Editor when switching current open item
Improve note editor save callback for new notes
Improve note editor activity title update
Improve NoteEditorActionHandler
Refactor PDFReaderState
Improve PDFReaderActionHandler
Pass note title when showing note editor
Clarify NoteEditorState parameter names
  • Loading branch information
mvasilak committed Aug 19, 2024
1 parent 842e867 commit 8131f0b
Show file tree
Hide file tree
Showing 28 changed files with 872 additions and 145 deletions.
8 changes: 8 additions & 0 deletions Zotero/Assets/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
"items.generating_bib" = "Generating Bibliography";
"items.creator_summary.and" = "%@ and %@";
"items.creator_summary.etal" = "%@ et al.";
"items.restore_open" = "Restore Open Items";

"lookup.title" = "Enter ISBNs, DOls, PMIDs, arXiv IDs, or ADS Bibcodes to add to your library:";

Expand Down Expand Up @@ -576,3 +577,10 @@
"accessibility.pdf.undo" = "Undo";
"accessibility.pdf.toggle_annotation_toolbar" = "Toggle annotation toolbar";
"accessibility.pdf.show_more_tools" = "Show more";
"accessibility.pdf.open_items" = "Open Items";
"accessibility.pdf.current_item" = "Current Item";
"accessibility.pdf.current_item_close" = "Close";
"accessibility.pdf.current_item_move_to_start" = "Move to start";
"accessibility.pdf.current_item_move_to end" = "Move to end";
"accessibility.pdf.close_all_open_items" = "Close all";
"accessibility.pdf.close_other_open_items" = "Close other items";
2 changes: 1 addition & 1 deletion Zotero/Controllers/Architecture/Coordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ enum SourceView {
protocol Coordinator: AnyObject {
var parentCoordinator: Coordinator? { get }
var childCoordinators: [Coordinator] { get set }
var navigationController: UINavigationController? { get }
var navigationController: UINavigationController? { get set }

func start(animated: Bool)
func childDidFinish(_ child: Coordinator)
Expand Down
2 changes: 2 additions & 0 deletions Zotero/Controllers/Controllers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ final class UserControllers {
let webDavController: WebDavController
let customUrlController: CustomURLController
let fullSyncDebugger: FullSyncDebugger
let openItemsController: OpenItemsController
private let isFirstLaunch: Bool
private let lastBuildNumber: Int?
private unowned let translatorsAndStylesController: TranslatorsAndStylesController
Expand Down Expand Up @@ -403,6 +404,7 @@ final class UserControllers {
fullSyncDebugger = FullSyncDebugger(syncScheduler: syncScheduler, debugLogging: controllers.debugLogging, sessionController: controllers.sessionController)
self.idleTimerController = controllers.idleTimerController
self.customUrlController = CustomURLController(dbStorage: dbStorage, fileStorage: controllers.fileStorage)
openItemsController = OpenItemsController(dbStorage: dbStorage, fileStorage: controllers.fileStorage)
self.lastBuildNumber = controllers.lastBuildNumber
self.disposeBag = DisposeBag()
}
Expand Down
12 changes: 12 additions & 0 deletions Zotero/Controllers/Database/Requests/ReadItemsDbRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,15 @@ struct ReadItemsWithKeysDbRequest: DbResponseRequest {
return database.objects(RItem.self).filter(.keys(self.keys, in: self.libraryId))
}
}

struct ReadItemsWithKeysFromMultipleLibrariesDbRequest: DbResponseRequest {
typealias Response = Results<RItem>

let keysByLibraryIdentifier: [LibraryIdentifier: Set<String>]

var needsWrite: Bool { return false }

func process(in database: Realm) throws -> Results<RItem> {
database.objects(RItem.self).filter(.keysByLibraryIdentifier(keysByLibraryIdentifier))
}
}
384 changes: 384 additions & 0 deletions Zotero/Controllers/OpenItemsController.swift

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions Zotero/Extensions/Localizable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,22 @@ internal enum L10n {
internal static let annotationHint = L10n.tr("Localizable", "accessibility.pdf.annotation_hint", fallback: "Double tap to select and edit")
/// Author
internal static let author = L10n.tr("Localizable", "accessibility.pdf.author", fallback: "Author")
/// Close all
internal static let closeAllOpenItems = L10n.tr("Localizable", "accessibility.pdf.close_all_open_items", fallback: "Close all")
/// Close other items
internal static let closeOtherOpenItems = L10n.tr("Localizable", "accessibility.pdf.close_other_open_items", fallback: "Close other items")
/// Color picker
internal static let colorPicker = L10n.tr("Localizable", "accessibility.pdf.color_picker", fallback: "Color picker")
/// Comment
internal static let comment = L10n.tr("Localizable", "accessibility.pdf.comment", fallback: "Comment")
/// Current Item
internal static let currentItem = L10n.tr("Localizable", "accessibility.pdf.current_item", fallback: "Current Item")
/// Close
internal static let currentItemClose = L10n.tr("Localizable", "accessibility.pdf.current_item_close", fallback: "Close")
/// Move to end
internal static let currentItemMoveToEnd = L10n.tr("Localizable", "accessibility.pdf.current_item_move_to end", fallback: "Move to end")
/// Move to start
internal static let currentItemMoveToStart = L10n.tr("Localizable", "accessibility.pdf.current_item_move_to_start", fallback: "Move to start")
/// Edit annotation
internal static let editAnnotation = L10n.tr("Localizable", "accessibility.pdf.edit_annotation", fallback: "Edit annotation")
/// Eraser
Expand Down Expand Up @@ -230,6 +242,8 @@ internal enum L10n {
internal static let noteAnnotation = L10n.tr("Localizable", "accessibility.pdf.note_annotation", fallback: "Note annotation")
/// Create note annotation
internal static let noteAnnotationTool = L10n.tr("Localizable", "accessibility.pdf.note_annotation_tool", fallback: "Create note annotation")
/// Open Items
internal static let openItems = L10n.tr("Localizable", "accessibility.pdf.open_items", fallback: "Open Items")
/// Open text reader
internal static let openReader = L10n.tr("Localizable", "accessibility.pdf.open_reader", fallback: "Open text reader")
/// Redo
Expand Down Expand Up @@ -835,6 +849,8 @@ internal enum L10n {
}
/// Remove from Collection
internal static let removeFromCollectionTitle = L10n.tr("Localizable", "items.remove_from_collection_title", fallback: "Remove from Collection")
/// Restore Open Items
internal static let restoreOpen = L10n.tr("Localizable", "items.restore_open", fallback: "Restore Open Items")
/// Search Items
internal static let searchTitle = L10n.tr("Localizable", "items.search_title", fallback: "Search Items")
/// Select All
Expand Down
8 changes: 4 additions & 4 deletions Zotero/Extensions/NSUserActivity+Activities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ struct RestoredStateData {
let openItems: [OpenItem]
let restoreMostRecentlyOpenedItem: Bool

static func myLibrary() -> Self {
.init(libraryId: .custom(.myLibrary), collectionId: .custom(.all), openItems: [], restoreMostRecentlyOpenedItem: false)
static func myLibrary(openItems: [OpenItem] = []) -> Self {
.init(libraryId: .custom(.myLibrary), collectionId: .custom(.all), openItems: openItems, restoreMostRecentlyOpenedItem: false)
}
}

Expand All @@ -29,9 +29,9 @@ extension NSUserActivity {
private static let openItemsKey = "openItems"
private static let restoreMostRecentlyOpenedItemKey = "restoreMostRecentlyOpenedItem"

static func mainActivity() -> NSUserActivity {
static func mainActivity(with openItems: [OpenItem]) -> NSUserActivity {
return NSUserActivity(activityType: self.mainId)
.addUserInfoEntries(openItems: [])
.addUserInfoEntries(openItems: openItems)
.addUserInfoEntries(restoreMostRecentlyOpened: false)
}

Expand Down
4 changes: 4 additions & 0 deletions Zotero/Extensions/UIViewController+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,8 @@ extension UIViewController {
// Parent also didn't return a scene. Trying presenting view controller.
return presentingViewController?.scene
}

var sessionIdentifier: String? {
scene?.session.persistentIdentifier
}
}
4 changes: 4 additions & 0 deletions Zotero/Models/Predicates.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ extension NSPredicate {
.library(with: libraryId)])
}

static func keysByLibraryIdentifier(_ keysByLibraryIdentifier: [LibraryIdentifier: Set<String>]) -> NSPredicate {
NSCompoundPredicate(orPredicateWithSubpredicates: keysByLibraryIdentifier.map({ .keys($0.value, in: $0.key) }))
}

static func key(notIn keys: [String], in libraryId: LibraryIdentifier) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [.library(with: libraryId), .key(notIn: keys)])
}
Expand Down
7 changes: 6 additions & 1 deletion Zotero/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {

func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
if shortcutItem.type == NSUserActivity.mainId {
completionHandler(coordinator.showMainScreen(with: .myLibrary(), session: windowScene.session))
let openItems: [OpenItem] = windowScene.userActivity?.restoredStateData?.openItems ?? []
completionHandler(coordinator.showMainScreen(with: .myLibrary(openItems: openItems), session: windowScene.session))
}
completionHandler(false)
}
Expand Down Expand Up @@ -117,4 +118,8 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
return scene.userActivity
}

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
coordinator.continueUserActivity(userActivity, for: scene.session.persistentIdentifier)
}
}
85 changes: 20 additions & 65 deletions Zotero/Scenes/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ protocol AppDelegateCoordinatorDelegate: AnyObject {
func didRotate(to size: CGSize)
func show(customUrl: CustomURLController.Kind, animated: Bool)
func showMainScreen(with data: RestoredStateData, session: UISceneSession) -> Bool
func continueUserActivity(_ userActivity: NSUserActivity, for sessionIdentifier: String)
}

protocol AppOnboardingCoordinatorDelegate: AnyObject {
Expand Down Expand Up @@ -138,7 +139,7 @@ extension AppCoordinator: AppDelegateCoordinatorDelegate {

DDLogInfo("AppCoordinator: show main screen logged \(isLoggedIn ? "in" : "out"); animated=\(animated)")
show(viewController: viewController, in: window, animated: animated) {
process(urlContext: urlContext, data: data)
process(urlContext: urlContext, data: data, sessionIdentifier: session.persistentIdentifier)
}

func show(viewController: UIViewController?, in window: UIWindow, animated: Bool = false, completion: @escaping () -> Void) {
Expand All @@ -157,8 +158,9 @@ extension AppCoordinator: AppDelegateCoordinatorDelegate {
var userActivity: NSUserActivity?
var data: RestoredStateData?
if connectionOptions.shortcutItem?.type == NSUserActivity.mainId {
userActivity = .mainActivity()
data = .myLibrary()
let openItems: [OpenItem] = session.stateRestorationActivity?.restoredStateData?.openItems ?? []
userActivity = .mainActivity(with: openItems)
data = .myLibrary(openItems: openItems)
} else {
userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity
data = userActivity?.restoredStateData
Expand All @@ -168,11 +170,12 @@ extension AppCoordinator: AppDelegateCoordinatorDelegate {
DDLogInfo("AppCoordinator: Preprocessing restored state - \(data)")
Defaults.shared.selectedLibrary = data.libraryId
Defaults.shared.selectedCollectionId = data.collectionId
controllers.userControllers?.openItemsController.setItems(data.openItems, for: session.persistentIdentifier, validate: true)
}
return (urlContext, data)
}

func process(urlContext: UIOpenURLContext?, data: RestoredStateData?) {
func process(urlContext: UIOpenURLContext?, data: RestoredStateData?, sessionIdentifier: String) {
if let urlContext, let urlController = controllers.userControllers?.customUrlController {
// If scene was started from custom URL
let sourceApp = urlContext.options.sourceApplication ?? "unknown"
Expand All @@ -187,10 +190,11 @@ extension AppCoordinator: AppDelegateCoordinatorDelegate {
if let data {
DDLogInfo("AppCoordinator: Processing restored state - \(data)")
// If scene had state stored, restore state
showRestoredState(for: data)
showRestoredState(for: data, sessionIdentifier: sessionIdentifier)
}

func showRestoredState(for data: RestoredStateData) {
func showRestoredState(for data: RestoredStateData, sessionIdentifier: String) {
guard let openItemsController = controllers.userControllers?.openItemsController else { return }
DDLogInfo("AppCoordinator: show restored state")
guard let mainController = window.rootViewController as? MainViewController else {
DDLogWarn("AppCoordinator: show restored state aborted - invalid root view controller")
Expand All @@ -207,8 +211,8 @@ extension AppCoordinator: AppDelegateCoordinatorDelegate {
collection = Collection(custom: .all)
}
mainController.showItems(for: collection, in: data.libraryId)
guard data.restoreMostRecentlyOpenedItem, let item = data.openItems.first else { return }
restoreMostRecentlyOpenedItem(using: self, item: item)
guard data.restoreMostRecentlyOpenedItem else { return }
openItemsController.restoreMostRecentlyOpenedItem(using: self, sessionIdentifier: sessionIdentifier)

func loadRestoredStateData(libraryId: LibraryIdentifier, collectionId: CollectionIdentifier) -> Collection? {
guard let dbStorage = controllers.userControllers?.dbStorage else { return nil }
Expand All @@ -224,63 +228,6 @@ extension AppCoordinator: AppDelegateCoordinatorDelegate {

return collection
}

func restoreMostRecentlyOpenedItem(using presenter: OpenItemsPresenter, item: OpenItem) {
guard let presentation = loadPresentation(for: item) else { return }
presenter.showItem(with: presentation)

func loadPresentation(for item: OpenItem) -> ItemPresentation? {
guard let dbStorage = controllers.userControllers?.dbStorage else { return nil }
var presentation: ItemPresentation?
do {
try dbStorage.perform(on: .main) { coordinator in
switch item.kind {
case .pdf(let libraryId, let key):
presentation = try loadPDFPresentation(key: key, libraryId: libraryId, coordinator: coordinator)

case .note(let libraryId, let key):
presentation = try loadNotePresentation(key: key, libraryId: libraryId, coordinator: coordinator)
}
}
} catch let error {
DDLogError("OpenItemsController: can't load item \(item) - \(error)")
}
return presentation

func loadPDFPresentation(key: String, libraryId: LibraryIdentifier, coordinator: DbCoordinator) throws -> ItemPresentation? {
let library: Library = try coordinator.perform(request: ReadLibraryDbRequest(libraryId: libraryId))
let rItem = try coordinator.perform(request: ReadItemDbRequest(libraryId: libraryId, key: key))
let parentKey = rItem.parent?.key
guard let attachment = AttachmentCreator.attachment(for: rItem, fileStorage: controllers.fileStorage, urlDetector: nil) else { return nil }
var url: URL?
switch attachment.type {
case .file(let filename, let contentType, let location, _, _):
switch location {
case .local, .localAndChangedRemotely:
let file = Files.attachmentFile(in: libraryId, key: key, filename: filename, contentType: contentType)
url = file.createUrl()

case .remote, .remoteMissing:
break
}

case .url:
break
}
guard let url else { return nil }
return .pdf(library: library, key: key, parentKey: parentKey, url: url)
}

func loadNotePresentation(key: String, libraryId: LibraryIdentifier, coordinator: DbCoordinator) throws -> ItemPresentation? {
let library = try coordinator.perform(request: ReadLibraryDbRequest(libraryId: libraryId))
let rItem = try coordinator.perform(request: ReadItemDbRequest(libraryId: libraryId, key: key))
let note = Note(item: rItem)
let parentTitleData: NoteEditorState.TitleData? = rItem.parent.flatMap { .init(type: $0.rawType, title: $0.displayTitle) }
guard let note else { return nil }
return .note(library: library, key: note.key, text: note.text, tags: note.tags, parentTitleData: parentTitleData, title: note.title)
}
}
}
}
}
}
Expand Down Expand Up @@ -424,11 +371,19 @@ extension AppCoordinator: AppDelegateCoordinatorDelegate {

func showMainScreen(with data: RestoredStateData, session: UISceneSession) -> Bool {
guard let window, let mainController = window.rootViewController as? MainViewController else { return false }
controllers.userControllers?.openItemsController.setItems(data.openItems, for: session.persistentIdentifier, validate: true)
mainController.dismiss(animated: false) {
mainController.masterCoordinator?.showCollections(for: data.libraryId, preselectedCollection: data.collectionId, animated: false)
}
return true
}

func continueUserActivity(_ userActivity: NSUserActivity, for sessionIdentifier: String) {
guard userActivity.activityType == NSUserActivity.contentContainerId, let window, let mainController = window.rootViewController as? MainViewController else { return }
mainController.getDetailCoordinator { [weak self] coordinator in
self?.controllers.userControllers?.openItemsController.restoreMostRecentlyOpenedItem(using: coordinator, sessionIdentifier: sessionIdentifier)
}
}
}

extension AppCoordinator: MFMailComposeViewControllerDelegate {
Expand Down
Loading

0 comments on commit 8131f0b

Please sign in to comment.