Skip to content

Commit

Permalink
Indicate when an item is retrieving metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
mvasilak committed Feb 19, 2025
1 parent fe9adc2 commit 64b65a1
Show file tree
Hide file tree
Showing 12 changed files with 270 additions and 106 deletions.
4 changes: 2 additions & 2 deletions ZShare/ViewModels/ExtensionViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ final class ExtensionViewModel {
case .recognitionInProgress, .remoteRecognitionInProgress, .identifierLookupInProgress:
updateState(with: .decoding)

case .translated(item: let item):
case .translated(let item):
var state = self.state
state.expectedItem = item
state.expectedAttachment = (filename, tmpFile)
Expand All @@ -562,7 +562,7 @@ final class ExtensionViewModel {
state.processedAttachment = .itemWithAttachment(item: item, attachment: [:], attachmentFile: file)
self.state = state

case .createdParent:
case .enqueued, .createdParent:
break
}
}
Expand Down
38 changes: 31 additions & 7 deletions Zotero/Controllers/RecognizerController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ final class RecognizerController {
enum Kind {
case failed(Error)
case cancelled
case enqueued
case recognitionInProgress
case remoteRecognitionInProgress(data: [String: Any])
case identifierLookupInProgress(response: RemoteRecognizerResponse, identifier: String)
Expand Down Expand Up @@ -136,6 +137,7 @@ final class RecognizerController {
// Accessed only via accessQueue
private static let maxConcurrentRecognizerTasks: Int = 1
private var queue: OrderedDictionary<RecognizerTask, (state: RecognizerTaskState, observable: PublishSubject<Update>)> = [:]
private var latestUpdates: [LibraryIdentifier: [String: Update.Kind]] = [:]
private var lookupWebViewHandlersByRecognizerTask: [RecognizerTask: LookupWebViewHandler] = [:]

// MARK: Object Lifecycle
Expand Down Expand Up @@ -163,28 +165,39 @@ final class RecognizerController {
}

// MARK: Actions
func queue(task: RecognizerTask, completion: @escaping (_ observable: Observable<Update>?) -> Void) {
func queue(task: RecognizerTask, completion: ((_ observable: Observable<Update>?) -> Void)? = nil) {
accessQueue.async(flags: .barrier) { [weak self] in
guard let self else {
completion(nil)
completion?(nil)
return
}
if let (_, observable) = queue[task] {
completion(observable.asObservable())
completion?(observable.asObservable())
return
}
let state: RecognizerTaskState = .enqueued
let observable: PublishSubject<Update> = PublishSubject()
queue[task] = (state, observable)
completion(observable.asObservable())
completion?(observable.asObservable())
observable.subscribe(onNext: { [weak self] update in
self?.updatesSubject.on(.next(update))
}).disposed(by: disposeBag)

emmitUpdate(for: task, observable: observable, kind: .enqueued)
startRecognitionIfNeeded()
}
}

private func emmitUpdate(for task: RecognizerTask, observable: PublishSubject<Update>, kind: Update.Kind) {
let update = Update(task: task, kind: kind)
if case .createParentForItem(let libraryId, let key) = task.kind {
var libraryLatestUpdates = latestUpdates[libraryId, default: [:]]
libraryLatestUpdates[key] = kind
latestUpdates[libraryId] = libraryLatestUpdates
}
observable.on(.next(update))
}

private func startRecognitionIfNeeded() {
let runningRecognizerTasksCount = queue.filter({
switch $0.value.state {
Expand Down Expand Up @@ -212,7 +225,7 @@ final class RecognizerController {

func start(task: RecognizerTask, observable: PublishSubject<Update>) {
queue[task] = (.recognitionInProgress, observable)
observable.on(.next(Update(task: task, kind: .recognitionInProgress)))
emmitUpdate(for: task, observable: observable, kind: .recognitionInProgress)

pdfWorkerController.queue(work: PDFWorkerController.PDFWork(file: task.file, kind: .recognizer)) { [weak self] pdfWorkerObservable in
guard let self else { return }
Expand Down Expand Up @@ -266,7 +279,7 @@ final class RecognizerController {
return
}
queue[task] = (.remoteRecognitionInProgress(data: data), observable)
observable.on(.next(Update(task: task, kind: .remoteRecognitionInProgress(data: data))))
emmitUpdate(for: task, observable: observable, kind: .remoteRecognitionInProgress(data: data))

apiClient.send(request: RecognizerRequest(parameters: data)).subscribe(
onSuccess: { (response: (RemoteRecognizerResponse, HTTPURLResponse)) in
Expand Down Expand Up @@ -378,7 +391,7 @@ final class RecognizerController {
enqueueNextIdentifierLookup(for: task)
return
}
observable.on(.next(Update(task: task, kind: .identifierLookupInProgress(response: response, identifier: identifier))))
emmitUpdate(for: task, observable: observable, kind: .identifierLookupInProgress(response: response, identifier: identifier))
lookupWebViewHandler.lookUp(identifier: identifier)

func getLookupWebViewHandler(for task: RecognizerTask) -> LookupWebViewHandler? {
Expand Down Expand Up @@ -548,6 +561,10 @@ final class RecognizerController {

func cleanup(for task: RecognizerTask, completion: @escaping (_ observable: PublishSubject<Update>?) -> Void) {
let observable = queue.removeValue(forKey: task).flatMap({ $0.observable })
if case .createParentForItem(let libraryId, let key) = task.kind, var libraryLatestUpdates = latestUpdates[libraryId] {
libraryLatestUpdates[key] = nil
latestUpdates[libraryId] = libraryLatestUpdates
}
DDLogInfo("RecognizerController: \(task) - cleaned up")
if let webView = lookupWebViewHandlersByRecognizerTask.removeValue(forKey: task)?.webViewHandler.webView {
DispatchQueue.main.async {
Expand All @@ -558,4 +575,11 @@ final class RecognizerController {
startRecognitionIfNeeded()
}
}

func latestUpdate(for key: String, libraryId: LibraryIdentifier) -> Update.Kind? {
return accessQueue.sync { [weak self] in
guard let self, let libraryLatestUpdates = latestUpdates[libraryId] else { return nil }
return libraryLatestUpdates[key]
}
}
}
50 changes: 44 additions & 6 deletions Zotero/Scenes/Detail/Items/Models/ItemCellModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,30 @@ struct ItemCellModel {
case url
}

struct Subtitle {
let text: String
let animated: Bool
}

let key: String
let typeIconName: String
let iconRenderingMode: UIImage.RenderingMode
let typeName: String
let title: NSAttributedString
let subtitle: String
let subtitle: Subtitle?
let hasNote: Bool
let tagColors: [UIColor]
let tagEmojis: [String]
let accessory: Accessory?
let hasDetailButton: Bool

init(item: RItem, typeName: String, title: NSAttributedString, accessory: Accessory?) {
init(item: RItem, typeName: String, title: NSAttributedString, subtitle: Subtitle?, accessory: Accessory?) {
key = item.key
typeIconName = Self.typeIconName(for: item)
iconRenderingMode = .alwaysOriginal
self.typeName = typeName
self.title = title
subtitle = Self.creatorSummary(for: item)
self.subtitle = subtitle
hasNote = Self.hasNote(item: item)
self.accessory = accessory
let (colors, emojis) = Self.tagData(item: item)
Expand All @@ -44,8 +49,14 @@ struct ItemCellModel {
hasDetailButton = true
}

init(item: RItem, typeName: String, title: NSAttributedString, accessory: ItemAccessory?, fileDownloader: AttachmentDownloader?) {
self.init(item: item, typeName: typeName, title: title, accessory: Self.createAccessory(from: accessory, fileDownloader: fileDownloader))
init(item: RItem, typeName: String, title: NSAttributedString, accessory: ItemAccessory?, fileDownloader: AttachmentDownloader?, recognizerController: RecognizerController?) {
self.init(
item: item,
typeName: typeName,
title: title,
subtitle: Self.createSubtitle(for: item, recognizerController: recognizerController),
accessory: Self.createAccessory(from: accessory, fileDownloader: fileDownloader)
)
}

init(collectionWithKey key: String, title: NSAttributedString) {
Expand All @@ -54,7 +65,7 @@ struct ItemCellModel {
accessory = nil
typeIconName = Asset.Images.Cells.collection.name
iconRenderingMode = .alwaysTemplate
subtitle = ""
subtitle = nil
hasNote = false
tagColors = []
tagEmojis = []
Expand Down Expand Up @@ -118,4 +129,31 @@ struct ItemCellModel {
}
return result
}

static func createSubtitle(for item: RItem, update: RecognizerController.Update.Kind?) -> Subtitle? {
guard item.parent == nil, let update else {
return Subtitle(text: creatorSummary(for: item), animated: false)
}

let text: String
let animated: Bool
switch update {
case .failed, .cancelled, .createdParent:
text = creatorSummary(for: item)
animated = false

case .enqueued, .recognitionInProgress, .remoteRecognitionInProgress, .identifierLookupInProgress, .translated:
text = "Retrieving metadata"
animated = true
}
return Subtitle(text: text, animated: animated)
}

static func createSubtitle(for item: RItem, recognizerController: RecognizerController?) -> Subtitle? {
guard item.parent == nil, let recognizerController else {
return Subtitle(text: creatorSummary(for: item), animated: false)
}
let update = recognizerController.latestUpdate(for: item.key, libraryId: item.libraryIdentifier)
return createSubtitle(for: item, update: update)
}
}
1 change: 1 addition & 0 deletions Zotero/Scenes/Detail/Items/Models/ItemsAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ enum ItemsAction {
case updateDownload(update: AttachmentDownloader.Update, batchData: ItemsState.DownloadBatchData?)
case updateIdentifierLookup(update: IdentifierLookupController.Update, batchData: ItemsState.IdentifierLookupBatchData)
case updateRemoteDownload(update: RemoteAttachmentDownloader.Update, batchData: ItemsState.DownloadBatchData?)
case updateMetadataRetrieval(itemKey: String, update: RecognizerController.Update.Kind)
case openAttachment(attachment: Attachment, parentKey: String?)
case attachmentOpened(String)
case updateKeys(items: Results<RItem>, deletions: [Int], insertions: [Int], modifications: [Int])
Expand Down
16 changes: 13 additions & 3 deletions Zotero/Scenes/Detail/Items/Models/ItemsState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ struct ItemsState: ViewModelState {
}
}

struct ItemUpdate {
enum Kind {
case accessory
case subtitle(ItemCellModel.Subtitle?)
}

let key: String
let kind: Kind
}

let collection: Collection

var library: Library
Expand All @@ -102,8 +112,8 @@ struct ItemsState: ViewModelState {
var changes: Changes
var error: ItemsError?
var itemKeyToDuplicate: String?
// Used to indicate which row should update it's attachment view. The update is done directly to cell instead of tableView reload.
var updateItemKey: String?
// Used to indicate which row should update directly a cell element (e.g. attachment view). The update is done directly to cell instead of tableView reload.
var updateItem: ItemUpdate?
var attachmentToOpen: String?
var downloadBatchData: DownloadBatchData?
var remoteDownloadBatchData: DownloadBatchData?
Expand Down Expand Up @@ -152,6 +162,6 @@ struct ItemsState: ViewModelState {
error = nil
changes = []
itemKeyToDuplicate = nil
updateItemKey = nil
updateItem = nil
}
}
18 changes: 14 additions & 4 deletions Zotero/Scenes/Detail/Items/ViewModels/ItemsActionHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ final class ItemsActionHandler: BaseItemsActionHandler, ViewModelActionHandler {
case .updateRemoteDownload(let update, let batchData):
self.process(remoteDownloadUpdate: update, batchData: batchData, in: viewModel)

case .updateMetadataRetrieval(let itemKey, let update):
process(metadataRetrievalUpdate: update, itemKey: itemKey, in: viewModel)

case .openAttachment(let attachment, let parentKey):
self.open(attachment: attachment, parentKey: parentKey, in: viewModel)

Expand Down Expand Up @@ -266,7 +269,7 @@ final class ItemsActionHandler: BaseItemsActionHandler, ViewModelActionHandler {
let updatedAccessory = accessory.updatedAttachment(update: { attachment in attachment.changed(location: .remote, condition: { $0 == .local }) }) else { return }
self.update(viewModel: viewModel) { state in
state.itemAccessories[updateKey] = updatedAccessory
state.updateItemKey = updateKey
state.updateItem = ItemsState.ItemUpdate(key: updateKey, kind: .accessory)
}
}
}
Expand Down Expand Up @@ -310,7 +313,7 @@ final class ItemsActionHandler: BaseItemsActionHandler, ViewModelActionHandler {
guard let updatedAttachment = attachment.changed(location: .local, compressed: compressed) else { return }
updateViewModel { state in
state.itemAccessories[updateKey] = .attachment(attachment: updatedAttachment, parentKey: downloadUpdate.parentKey)
state.updateItemKey = updateKey
state.updateItem = ItemsState.ItemUpdate(key: updateKey, kind: .accessory)
}

case .progress:
Expand All @@ -319,13 +322,13 @@ final class ItemsActionHandler: BaseItemsActionHandler, ViewModelActionHandler {
guard let currentProgress = fileDownloader.data(for: downloadUpdate.key, parentKey: downloadUpdate.parentKey, libraryId: downloadUpdate.libraryId).progress, currentProgress < 1
else { return }
updateViewModel { state in
state.updateItemKey = updateKey
state.updateItem = ItemsState.ItemUpdate(key: updateKey, kind: .accessory)
}

case .cancelled, .failed:
DDLogInfo("ItemsActionHandler: download update \(attachment.key); \(attachment.libraryId); kind \(downloadUpdate.kind)")
updateViewModel { state in
state.updateItemKey = updateKey
state.updateItem = ItemsState.ItemUpdate(key: updateKey, kind: .accessory)
}
}

Expand Down Expand Up @@ -359,6 +362,13 @@ final class ItemsActionHandler: BaseItemsActionHandler, ViewModelActionHandler {
}
}

private func process(metadataRetrievalUpdate: RecognizerController.Update.Kind, itemKey: String, in viewModel: ViewModel<ItemsActionHandler>) {
guard let item = viewModel.state.results?.filter("key == %@", itemKey).first else { return }
update(viewModel: viewModel) { state in
state.updateItem = ItemsState.ItemUpdate(key: itemKey, kind: .subtitle(ItemCellModel.createSubtitle(for: item, update: metadataRetrievalUpdate)))
}
}

private func cacheItemAccessory(for item: RItem, in viewModel: ViewModel<ItemsActionHandler>) {
// Create cached accessory only if there is nothing in cache yet.
guard viewModel.state.itemAccessories[item.key] == nil, let accessory = ItemAccessory.create(from: item, fileStorage: fileStorage, urlDetector: urlDetector) else { return }
Expand Down
Loading

0 comments on commit 64b65a1

Please sign in to comment.