Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple open items #829

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Zotero/Assets/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,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 @@ -579,3 +580,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 @@ -314,6 +314,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 @@ -407,6 +408,7 @@ final class UserControllers {
fullSyncDebugger = FullSyncDebugger(syncScheduler: syncScheduler, debugLogging: controllers.debugLogging, sessionController: controllers.sessionController)
idleTimerController = controllers.idleTimerController
customUrlController = CustomURLController(dbStorage: dbStorage, fileStorage: controllers.fileStorage)
openItemsController = OpenItemsController(dbStorage: dbStorage, fileStorage: controllers.fileStorage, attachmentDownloader: fileDownloader)
lastBuildNumber = controllers.lastBuildNumber
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))
}
}
427 changes: 427 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 @@ -204,10 +204,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 @@ -236,6 +248,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 @@ -839,6 +853,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)
}
}
97 changes: 32 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.set(items: 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,14 @@ 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) { item in
if let item {
DDLogInfo("AppCoordinator: restored open item - \(item)")
} else {
DDLogInfo("AppCoordinator: no open item to restore")
mvasilak marked this conversation as resolved.
Show resolved Hide resolved
}
}

func loadRestoredStateData(libraryId: LibraryIdentifier, collectionId: CollectionIdentifier) -> Collection? {
guard let dbStorage = controllers.userControllers?.dbStorage else { return nil }
Expand All @@ -224,63 +234,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 @@ -398,11 +351,25 @@ 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.set(items: 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) { item in
if let item {
DDLogInfo("AppCoordinator: restored open item for continued user activity - \(item)")
} else {
DDLogInfo("AppCoordinator: no open item to restore for continued user activity")
mvasilak marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
}

extension AppCoordinator: MFMailComposeViewControllerDelegate {
Expand Down
Loading