Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for notes in open items
Browse files Browse the repository at this point in the history
mvasilak committed Nov 1, 2023

Verified

This commit was signed with the committer’s verified signature. The key has expired.
renovate-bot Mend Renovate
1 parent 813870d commit f3a2e38
Showing 8 changed files with 172 additions and 33 deletions.
42 changes: 38 additions & 4 deletions Zotero/Controllers/OpenItemsController.swift
Original file line number Diff line number Diff line change
@@ -23,17 +23,22 @@ final class OpenItemsController {
struct Item: Hashable, Equatable, Codable {
enum Kind: Hashable, Equatable, Codable {
case pdf(libraryId: LibraryIdentifier, key: String)
case note(libraryId: LibraryIdentifier, key: String)

// MARK: Types
enum `Type`: String, Codable {
case pdf
case note
}

// MARK: Properties
var type: `Type` {
switch self {
case .pdf:
return .pdf

case .note:
return .note
}
}

@@ -48,7 +53,7 @@ final class OpenItemsController {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type, forKey: .type)
switch self {
case .pdf(let libraryId, let key):
case .pdf(let libraryId, let key), .note(let libraryId, let key):
try container.encode(libraryId, forKey: .libraryId)
try container.encode(key, forKey: .key)
}
@@ -63,6 +68,11 @@ final class OpenItemsController {
let libraryId = try container.decode(LibraryIdentifier.self, forKey: .libraryId)
let key = try container.decode(String.self, forKey: .key)
self = .pdf(libraryId: libraryId, key: key)

case .note:
let libraryId = try container.decode(LibraryIdentifier.self, forKey: .libraryId)
let key = try container.decode(String.self, forKey: .key)
self = .note(libraryId: libraryId, key: key)
}
}
}
@@ -80,6 +90,7 @@ final class OpenItemsController {

enum Presentation {
case pdf(library: Library, key: String, url: URL)
case note(library: Library, key: String, text: String, tags: [Tag], title: NoteEditorState.TitleData?)
}

// MARK: Properties
@@ -174,7 +185,7 @@ final class OpenItemsController {
try dbStorage.perform(on: .main) { coordinator in
for item in itemsSortedByUserOrder {
switch item.kind {
case .pdf(let libraryId, let key):
case .pdf(let libraryId, let key), .note(let libraryId, let key):
do {
let rItem = try coordinator.perform(request: ReadItemDbRequest(libraryId: libraryId, key: key))
itemTuples.append((item, rItem))
@@ -208,6 +219,9 @@ final class OpenItemsController {
switch item.kind {
case .pdf(let libraryId, let key):
return loadPDFPresentation(key: key, libraryId: libraryId)

case .note(let libraryId, let key):
return loadNotePresentation(key: key, libraryId: libraryId)
}

func loadPDFPresentation(key: String, libraryId: LibraryIdentifier) -> Presentation? {
@@ -224,11 +238,11 @@ final class OpenItemsController {
case .local, .localAndChangedRemotely:
let file = Files.attachmentFile(in: libraryId, key: key, filename: filename, contentType: contentType)
url = file.createUrl()

case .remote, .remoteMissing:
break
}

default:
break
}
@@ -239,6 +253,26 @@ final class OpenItemsController {
guard let library, let url else { return nil }
return .pdf(library: library, key: key, url: url)
}

func loadNotePresentation(key: String, libraryId: LibraryIdentifier) -> Presentation? {
var library: Library?
var note: Note?
var title: NoteEditorState.TitleData?
do {
try dbStorage.perform(on: .main) { coordinator in
library = try coordinator.perform(request: ReadLibraryDbRequest(libraryId: libraryId))
let rItem = try coordinator.perform(request: ReadItemDbRequest(libraryId: libraryId, key: key))
note = Note(item: rItem)
if let parent = rItem.parent {
title = NoteEditorState.TitleData(type: parent.rawType, title: parent.displayTitle)
}
}
} catch let error {
DDLogError("OpenItemsController: can't load item \(item) - \(error)")
}
guard let library, let note else { return nil }
return .note(library: library, key: note.key, text: note.text, tags: note.tags, title: title)
}
}

private func presentItem(with presentation: Presentation, using presenter: OpenItemsPresenter) {
21 changes: 12 additions & 9 deletions Zotero/Scenes/AppCoordinator.swift
Original file line number Diff line number Diff line change
@@ -816,18 +816,21 @@ extension AppCoordinator: SyncRequestReceiver {

extension AppCoordinator: OpenItemsPresenter {
func showItem(with presentation: ItemPresentation) {
switch presentation {
case .pdf(let library, let key, let url):
showPDF(at: url, key: key, library: library)
}
}

private func showPDF(at url: URL, key: String, library: Library) {
guard let window, let mainController = window.rootViewController as? MainViewController else { return }
mainController.getDetailCoordinator { [weak self] coordinator in
guard let self else { return }
let controller = coordinator.createPDFController(key: key, library: library, url: url)
self.show(controller: controller, by: mainController, in: window, animated: false)
var controller: UIViewController
switch presentation {
case .pdf(let library, let key, let url):
controller = coordinator.createPDFController(key: key, library: library, url: url)

case .note(let library, let key, let text, let tags, let title):
let kind: NoteEditorKind = library.metadataEditable ? .edit(key: key) : .readOnly(key: key)
// TODO: Check if a callback is required
controller = coordinator.createNoteController(library: library, kind: kind, text: text, tags: tags, title: title) { _ in
}
}
show(controller: controller, by: mainController, in: window, animated: false)
}
}
}
23 changes: 21 additions & 2 deletions Zotero/Scenes/Detail/DetailCoordinator.swift
Original file line number Diff line number Diff line change
@@ -888,15 +888,29 @@ extension DetailCoordinator: DetailNoteEditorCoordinatorDelegate {
saveCallback: @escaping NoteEditorSaveCallback = { _ in }
) {
guard let navigationController else { return }
let controller = createNoteController(library: library, kind: kind, text: text, tags: tags, title: title, saveCallback: saveCallback)
var amendedSaveCallback = saveCallback
switch kind {
case .itemCreation, .standaloneCreation:
DDLogInfo("DetailCoordinator: show note creation")

amendedSaveCallback = { [weak self] result in
switch result {
case .success(let note):
// If indeed a new note is created inform open items controller about it.
self?.controllers.userControllers?.openItemsController.open(.note(libraryId: library.identifier, key: note.key))

case .failure:
break
}

saveCallback(result)
}

case .edit(let key), .readOnly(let key):
DDLogInfo("DetailCoordinator: show note \(key)")
controllers.userControllers?.openItemsController.open(.note(libraryId: library.identifier, key: key))
}

let controller = createNoteController(library: library, kind: kind, text: text, tags: tags, title: title, saveCallback: amendedSaveCallback)
if let presentedViewController = navigationController.presentedViewController {
guard let window = presentedViewController.view.window else { return }
show(controller: controller, by: navigationController, in: window, animated: false)
@@ -933,6 +947,11 @@ extension DetailCoordinator: OpenItemsPresenter {
switch presentation {
case .pdf(let library, let key, let url):
showPDF(at: url, key: key, library: library)

case .note(let library, let key, let text, let tags, let title):
let kind: NoteEditorKind = library.metadataEditable ? .edit(key: key) : .readOnly(key: key)
// TODO: Check if a callback is required
showNote(library: library, kind: kind, text: text, tags: tags, title: title)
}
}
}
1 change: 1 addition & 0 deletions Zotero/Scenes/General/Models/NoteEditorAction.swift
Original file line number Diff line number Diff line change
@@ -12,4 +12,5 @@ enum NoteEditorAction {
case save
case setTags([Tag])
case setText(String)
case updateOpenItems(items: [OpenItem])
}
5 changes: 4 additions & 1 deletion Zotero/Scenes/General/Models/NoteEditorState.swift
Original file line number Diff line number Diff line change
@@ -35,6 +35,7 @@ struct NoteEditorState: ViewModelState {

static let tags = Changes(rawValue: 1 << 0)
static let save = Changes(rawValue: 1 << 1)
static let openItems = Changes(rawValue: 1 << 2)
}

struct TitleData {
@@ -53,14 +54,16 @@ struct NoteEditorState: ViewModelState {
var text: String
var tags: [Tag]
var changes: Changes
var openItemsCount: Int

init(kind: Kind, library: Library, title: TitleData?, text: String, tags: [Tag]) {
init(kind: Kind, library: Library, title: TitleData?, text: String, tags: [Tag], openItemsCount: Int) {
self.kind = kind
self.text = text
self.tags = tags
self.library = library
self.title = title
changes = []
self.openItemsCount = openItemsCount
}

mutating func cleanup() {
12 changes: 9 additions & 3 deletions Zotero/Scenes/General/NoteEditorCoordinator.swift
Original file line number Diff line number Diff line change
@@ -70,12 +70,12 @@ final class NoteEditorCoordinator: NSObject, Coordinator {
}

func start(animated: Bool) {
guard let dbStorage = controllers.userControllers?.dbStorage else { return }
guard let dbStorage = controllers.userControllers?.dbStorage, let openItemsController = controllers.userControllers?.openItemsController else { return }

let state = NoteEditorState(kind: kind, library: library, title: title, text: initialText, tags: initialTags)
let state = NoteEditorState(kind: kind, library: library, title: title, text: initialText, tags: initialTags, openItemsCount: openItemsController.items.count)
let handler = NoteEditorActionHandler(dbStorage: dbStorage, schemaController: controllers.schemaController, saveCallback: saveCallback)
let viewModel = ViewModel(initialState: state, handler: handler)
let controller = NoteEditorViewController(viewModel: viewModel)
let controller = NoteEditorViewController(viewModel: viewModel, openItemsController: openItemsController)
controller.coordinatorDelegate = self
navigationController?.setViewControllers([controller], animated: animated)
}
@@ -113,3 +113,9 @@ extension NoteEditorCoordinator: NoteEditorCoordinatorDelegate {
navigationController.present(controller, animated: true, completion: nil)
}
}

extension NoteEditorCoordinator: OpenItemsPresenter {
func showItem(with presentation: ItemPresentation) {
(parentCoordinator as? OpenItemsPresenter)?.showItem(with: presentation)
}
}
Original file line number Diff line number Diff line change
@@ -45,6 +45,14 @@ struct NoteEditorActionHandler: ViewModelActionHandler, BackgroundDbProcessingAc
state.tags = tags
state.changes = [.tags, .save]
}

case .updateOpenItems(let items):
update(viewModel: viewModel) { state in
if state.openItemsCount != items.count {
state.openItemsCount = items.count
state.changes = .openItems
}
}
}

func save(viewModel: ViewModel<NoteEditorActionHandler>) {
93 changes: 79 additions & 14 deletions Zotero/Scenes/General/Views/NoteEditorViewController.swift
Original file line number Diff line number Diff line change
@@ -13,6 +13,11 @@ import WebKit
import RxSwift

final class NoteEditorViewController: UIViewController {
private enum RightBarButtonItem: Int {
case done
case restoreOpenItems
}

@IBOutlet private weak var webView: WKWebView!
@IBOutlet private weak var tagsTitleLabel: UILabel!
@IBOutlet private weak var tagsLabel: UILabel!
@@ -22,7 +27,8 @@ final class NoteEditorViewController: UIViewController {
private let disposeBag: DisposeBag

private var debounceDisposeBag: DisposeBag?
weak var coordinatorDelegate: NoteEditorCoordinatorDelegate?
private unowned let openItemsController: OpenItemsController
weak var coordinatorDelegate: (NoteEditorCoordinatorDelegate & OpenItemsPresenter)?

private var htmlUrl: URL? {
if viewModel.state.kind.readOnly {
@@ -32,26 +38,30 @@ final class NoteEditorViewController: UIViewController {
}
}

init(viewModel: ViewModel<NoteEditorActionHandler>) {
init(viewModel: ViewModel<NoteEditorActionHandler>, openItemsController: OpenItemsController) {
self.viewModel = viewModel
self.openItemsController = openItemsController
disposeBag = DisposeBag()
super.init(nibName: "NoteEditorViewController", bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()

set(userActivity: .pdfActivity(with: openItemsController.items, libraryId: viewModel.state.library.identifier, collectionId: Defaults.shared.selectedCollectionId))

if let data = viewModel.state.title {
navigationItem.titleView = NoteEditorTitleView(type: data.type, title: data.title)
}

view.backgroundColor = .systemBackground
setupNavbarItems()
setupNavbarItems(for: viewModel.state)
setupWebView()
setupOpenItemsObserving()
update(tags: viewModel.state.tags)

viewModel.stateObservable
@@ -60,15 +70,58 @@ final class NoteEditorViewController: UIViewController {
})
.disposed(by: disposeBag)

func setupNavbarItems() {
let done = UIBarButtonItem(title: L10n.done, style: .done, target: nil, action: nil)
done.rx.tap
.subscribe(with: self, onNext: { `self`, _ in
forceSaveIfNeeded()
self.navigationController?.presentingViewController?.dismiss(animated: true, completion: nil)
})
.disposed(by: disposeBag)
navigationItem.rightBarButtonItem = done
func setupNavbarItems(for state: NoteEditorState) {
defer {
updateRestoreOpenItemsButton(withCount: state.openItemsCount)
}
let currentItems = (self.navigationItem.rightBarButtonItems ?? []).compactMap({ RightBarButtonItem(rawValue: $0.tag) })
let expectedItems = rightBarButtonItemTypes(for: state)
guard currentItems != expectedItems else { return }
navigationItem.rightBarButtonItems = expectedItems.map({ createRightBarButtonItem($0) }).reversed()

func rightBarButtonItemTypes(for state: NoteEditorState) -> [RightBarButtonItem] {
var items: [RightBarButtonItem] = [.done]
if state.openItemsCount > 0 {
items = [.restoreOpenItems] + items
}
return items
}

func createRightBarButtonItem(_ type: RightBarButtonItem) -> UIBarButtonItem {
let item: UIBarButtonItem
switch type {
case .done:
let done = UIBarButtonItem(title: L10n.done, style: .done, target: nil, action: nil)
done.rx.tap
.subscribe(with: self, onNext: { `self`, _ in
forceSaveIfNeeded()
self.navigationController?.presentingViewController?.dismiss(animated: true, completion: nil)
})
.disposed(by: disposeBag)
item = done

case .restoreOpenItems:
let openItems = UIBarButtonItem(image: UIImage(systemName: "\(openItemsController.items.count).square"), style: .plain, target: nil, action: nil)
openItems.isEnabled = true
openItems.accessibilityLabel = L10n.Accessibility.Pdf.openItems
openItems.title = L10n.Accessibility.Pdf.openItems
let deferredOpenItemsMenuElement = openItemsController.deferredOpenItemsMenuElement(disableOpenItem: true) { [weak self] item, _ in
guard let self, let coordinatorDelegate else { return }
openItemsController.restore(item, using: coordinatorDelegate)
}
let openItemsMenu = UIMenu(title: "Open Items", options: [.displayInline], children: [deferredOpenItemsMenuElement])
openItems.menu = UIMenu(children: [openItemsMenu])
item = openItems
}

item.tag = type.rawValue
return item
}

func updateRestoreOpenItemsButton(withCount count: Int) {
guard let item = navigationItem.rightBarButtonItems?.first(where: { button in RightBarButtonItem(rawValue: button.tag) == .restoreOpenItems }) else { return }
item.image = UIImage(systemName: "\(count).square")
}

func forceSaveIfNeeded() {
guard debounceDisposeBag != nil else { return }
@@ -86,13 +139,25 @@ final class NoteEditorViewController: UIViewController {
webView.loadHTMLString(data, baseURL: url)
}

func setupOpenItemsObserving() {
openItemsController.observable
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] items in
self?.viewModel.process(action: .updateOpenItems(items: items))
})
.disposed(by: disposeBag)
}

func process(state: NoteEditorState) {
if state.changes.contains(.tags) {
update(tags: state.tags)
}
if state.changes.contains(.save) {
debounceSave()
}
if state.changes.contains(.openItems) {
setupNavbarItems(for: state)
}

func debounceSave() {
debounceDisposeBag = nil

0 comments on commit f3a2e38

Please sign in to comment.