Skip to content

Commit a9dd452

Browse files
committed
Fix support for file editing (#1174)
* Fix add actions library editing permissions checks * In adding by identifier if library not file editable ignore attachments * In share extension if picked library not file editable ignore attachment * Remove convert to standalone attachment action from url attachment menu * Use first linked URL if any for item accessory without file attachments * Improve code
1 parent a155dfa commit a9dd452

File tree

8 files changed

+143
-82
lines changed

8 files changed

+143
-82
lines changed

ZShare/View Controllers/ShareViewController.swift

Lines changed: 53 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,13 @@ final class ShareViewController: UIViewController {
237237

238238
let hasItem = state.processedAttachment != nil
239239
self.log(attachmentState: state.attachmentState, itemState: state.itemPickerState)
240-
self.update(item: state.expectedItem, attachment: state.expectedAttachment, attachmentState: state.attachmentState, defaultTitle: state.title)
240+
update(
241+
libraryIsFilesEditable: state.collectionPickerState.libraryIsFilesEditable,
242+
item: state.expectedItem,
243+
attachment: state.expectedAttachment,
244+
attachmentState: state.attachmentState,
245+
defaultTitle: state.title
246+
)
241247
self.update(attachmentState: state.attachmentState, itemState: state.itemPickerState, hasItem: hasItem, isSubmitting: state.isSubmitting)
242248
self.update(collectionPicker: state.collectionPickerState, recents: state.recents)
243249
self.update(itemPicker: state.itemPickerState, hasExpectedItem: (state.expectedItem != nil || state.expectedAttachment != nil))
@@ -287,7 +293,7 @@ final class ShareViewController: UIViewController {
287293
self.updateBottomProgress(for: state, itemState: itemState, hasItem: hasItem, isSubmitting: isSubmitting)
288294
}
289295

290-
private func update(item: ItemResponse?, attachment: (String, File)?, attachmentState: ExtensionViewModel.State.AttachmentState, defaultTitle title: String?) {
296+
private func update(libraryIsFilesEditable: Bool, item: ItemResponse?, attachment: (String, File)?, attachmentState: ExtensionViewModel.State.AttachmentState, defaultTitle title: String?) {
291297
self.translationContainer.isHidden = false
292298

293299
if item == nil && attachment == nil {
@@ -306,23 +312,52 @@ final class ShareViewController: UIViewController {
306312
return
307313
}
308314

309-
self.itemContainer.isHidden = item == nil
310-
self.attachmentContainer.isHidden = attachment == nil
315+
let itemFound: Bool
316+
if let item {
317+
itemFound = true
318+
// Item was found, show its metadata.
319+
let itemTitle = itemTitle(for: item, schemaController: schemaController, defaultValue: title ?? "")
320+
setItem(title: itemTitle, type: item.rawType)
321+
itemContainer.isHidden = false
322+
} else {
323+
itemFound = false
324+
itemContainer.isHidden = true
325+
}
326+
if libraryIsFilesEditable, let (attachmentTitle, file) = attachment {
327+
// The picked library allows file editing, and either item with attachment, or only attachment (local/remote file), was found, show its metadata.
328+
setAttachment(title: attachmentTitle, file: file, state: attachmentState)
329+
attachmentContainerLeft.constant = itemFound ? Self.childAttachmentLeftOffset : 0
330+
attachmentContainer.isHidden = false
331+
} else {
332+
attachmentContainer.isHidden = true
333+
}
334+
335+
func setAttachment(title: String, file: File, state: ExtensionViewModel.State.AttachmentState) {
336+
attachmentTitleLabel.text = title
337+
let type: Attachment.Kind = .file(filename: "", contentType: file.mimeType, location: .local, linkType: .importedFile, compressed: false)
338+
let iconState = FileAttachmentView.State.stateFrom(type: type, progress: nil, error: state.error)
339+
attachmentIcon.set(state: iconState, style: .shareExtension)
311340

312-
if let item = item, let (attachmentTitle, file) = attachment {
313-
// Item with attachment was found, show their metadata
314-
let itemTitle = self.itemTitle(for: item, schemaController: self.schemaController, defaultValue: title ?? "")
315-
self.setItem(title: itemTitle, type: item.rawType)
316-
self.attachmentContainerLeft.constant = ShareViewController.childAttachmentLeftOffset
317-
self.setAttachment(title: attachmentTitle, file: file, state: attachmentState)
318-
} else if let item = item {
319-
// Only item was found, show metadata
320-
let title = self.itemTitle(for: item, schemaController: self.schemaController, defaultValue: title ?? "")
321-
self.setItem(title: title, type: item.rawType)
322-
} else if let (title, file) = attachment {
323-
// Only attachment (local/remote file) was found, show metadata
324-
self.attachmentContainerLeft.constant = 0
325-
self.setAttachment(title: title, file: file, state: attachmentState)
341+
switch state {
342+
case .downloading(let progress):
343+
attachmentProgressView.isHidden = progress == 0
344+
attachmentActivityIndicator.isHidden = progress > 0
345+
attachmentProgressView.progress = CGFloat(progress)
346+
if progress == 0 && !attachmentActivityIndicator.isAnimating {
347+
attachmentActivityIndicator.startAnimating()
348+
}
349+
attachmentIcon.alpha = 0.5
350+
attachmentTitleLabel.alpha = 0.5
351+
352+
default:
353+
attachmentProgressView.isHidden = true
354+
if attachmentActivityIndicator.isAnimating {
355+
attachmentActivityIndicator.stopAnimating()
356+
}
357+
attachmentActivityIndicator.isHidden = true
358+
attachmentIcon.alpha = 1
359+
attachmentTitleLabel.alpha = 1
360+
}
326361
}
327362
}
328363

@@ -335,37 +370,6 @@ final class ShareViewController: UIViewController {
335370
self.itemIcon.image = UIImage(named: ItemTypes.iconName(for: type))
336371
}
337372

338-
private func setAttachment(title: String, file: File, state: ExtensionViewModel.State.AttachmentState) {
339-
self.attachmentTitleLabel.text = title
340-
let type: Attachment.Kind = .file(filename: "", contentType: file.mimeType, location: .local, linkType: .importedFile, compressed: false)
341-
let iconState = FileAttachmentView.State.stateFrom(type: type, progress: nil, error: state.error)
342-
self.attachmentIcon.set(state: iconState, style: .shareExtension)
343-
344-
switch state {
345-
case .downloading(let progress):
346-
self.attachmentProgressView.isHidden = progress == 0
347-
self.attachmentActivityIndicator.isHidden = progress > 0
348-
self.attachmentProgressView.progress = CGFloat(progress)
349-
if progress == 0 && !self.attachmentActivityIndicator.isAnimating {
350-
self.attachmentActivityIndicator.startAnimating()
351-
}
352-
353-
self.attachmentIcon.alpha = 0.5
354-
self.attachmentTitleLabel.alpha = 0.5
355-
356-
default:
357-
if !self.attachmentContainer.isHidden {
358-
self.attachmentProgressView.isHidden = true
359-
if self.attachmentActivityIndicator.isAnimating {
360-
self.attachmentActivityIndicator.stopAnimating()
361-
}
362-
self.attachmentActivityIndicator.isHidden = true
363-
self.attachmentIcon.alpha = 1
364-
self.attachmentTitleLabel.alpha = 1
365-
}
366-
}
367-
}
368-
369373
private func updateNavigationItems(for state: ExtensionViewModel.State.AttachmentState, isSubmitting: Bool) {
370374
if let error = state.error {
371375
switch error {

ZShare/ViewModels/ExtensionViewModel.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ final class ExtensionViewModel {
3737
enum CollectionPickerState {
3838
case loading, failed
3939
case picked(Library, Collection?)
40+
41+
var libraryIsFilesEditable: Bool {
42+
switch self {
43+
case .loading, .failed:
44+
return false
45+
46+
case let .picked(library, _):
47+
return library.filesEditable
48+
}
49+
}
4050
}
4151

4252
struct ItemPickerState {
@@ -1049,7 +1059,12 @@ final class ExtensionViewModel {
10491059

10501060
case .itemWithAttachment(let item, let attachmentData, let attachmentFile):
10511061
let newTags = (Defaults.shared.shareExtensionIncludeTags ? item.tags + tags : tags)
1052-
attachment = .itemWithAttachment(item: item.copy(libraryId: libraryId, collectionKeys: collectionKeys, tags: newTags), attachment: attachmentData, attachmentFile: attachmentFile)
1062+
if state.collectionPickerState.libraryIsFilesEditable {
1063+
attachment = .itemWithAttachment(item: item.copy(libraryId: libraryId, collectionKeys: collectionKeys, tags: newTags), attachment: attachmentData, attachmentFile: attachmentFile)
1064+
} else {
1065+
// Picked library doesn't allow file editing, convert attachment to item only.
1066+
attachment = .item(item.copy(libraryId: libraryId, collectionKeys: collectionKeys, tags: newTags))
1067+
}
10531068

10541069
case .file: break
10551070
}

Zotero/Controllers/AttachmentCreator.swift

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@ struct AttachmentCreator {
2222

2323
private static let mainAttachmentContentTypes: Set<String> = ["text/html", "application/pdf", "image/png", "image/jpeg", "image/gif", "text/plain", "application/epub+zip"]
2424

25-
static func mainAttachment(for item: RItem, fileStorage: FileStorage) -> Attachment? {
25+
static func mainAttachment(for item: RItem, fileStorage: FileStorage, urlDetector: UrlDetector?) -> Attachment? {
2626
if item.rawType == ItemTypes.attachment {
27-
// If item is attachment, create `Attachment` and ignore linked attachments.
28-
if let attachment = attachment(for: item, fileStorage: fileStorage, urlDetector: nil) {
27+
if let attachment = attachment(for: item, fileStorage: fileStorage, urlDetector: urlDetector) {
2928
switch attachment.type {
3029
case .url:
3130
return attachment
@@ -40,20 +39,54 @@ struct AttachmentCreator {
4039
return nil
4140
}
4241

43-
var attachmentData = attachmentData(for: item)
44-
45-
guard !attachmentData.isEmpty else { return nil }
46-
47-
attachmentData.sort { lData, rData in
42+
if let fileAttachment = fileAttachmentData(for: item).sorted(by: { lData, rData in
4843
mainAttachmentsAreInIncreasingOrder(lData: (lData.1, lData.3, lData.4), rData: (rData.1, rData.3, rData.4))
44+
}).first.flatMap({ fileAttachmentDataToAttachment($0) }) {
45+
return fileAttachment
46+
}
47+
return firstLinkedURLAttachmentIfAny(for: item, urlDetector: urlDetector)
48+
49+
func fileAttachmentData(for item: RItem) -> [(Int, String, LinkMode, Bool, Date)] {
50+
let itemUrl = item.fields.first(where: { $0.key == FieldKeys.Item.url })?.value
51+
var data: [(Int, String, LinkMode, Bool, Date)] = []
52+
for (idx, child) in item.children.enumerated() {
53+
guard (child.rawType == ItemTypes.attachment) && (child.syncState != .dirty) && !child.trash,
54+
let linkMode = child.fields.first(where: { $0.key == FieldKeys.Item.Attachment.linkMode }).flatMap({ LinkMode(rawValue: $0.value) }),
55+
(linkMode == .importedUrl) || (linkMode == .importedFile),
56+
let contentType = contentType(for: child),
57+
AttachmentCreator.mainAttachmentContentTypes.contains(contentType) else { continue }
58+
59+
var hasMatchingUrlWithParent = false
60+
if let url = itemUrl, let childUrl = child.fields.first(where: { $0.key == FieldKeys.Item.Attachment.url })?.value {
61+
hasMatchingUrlWithParent = url == childUrl
62+
}
63+
data.append((idx, contentType, linkMode, hasMatchingUrlWithParent, child.dateAdded))
64+
}
65+
return data
4966
}
5067

51-
guard let (idx, contentType, linkMode, _, _) = attachmentData.first else { return nil }
52-
let rAttachment = item.children[idx]
53-
let linkType: Attachment.FileLinkType = linkMode == .importedFile ? .importedFile : .importedUrl
54-
guard let libraryId = rAttachment.libraryId else { return nil }
55-
let type = importedType(for: rAttachment, contentType: contentType, libraryId: libraryId, fileStorage: fileStorage, linkType: linkType, compressed: rAttachment.fileCompressed)
56-
return Attachment(item: rAttachment, type: type)
68+
func fileAttachmentDataToAttachment(_ data: (Int, String, LinkMode, Bool, Date)) -> Attachment? {
69+
let (idx, contentType, linkMode, _, _) = data
70+
let rAttachment = item.children[idx]
71+
let linkType: Attachment.FileLinkType = linkMode == .importedFile ? .importedFile : .importedUrl
72+
guard let libraryId = rAttachment.libraryId else { return nil }
73+
let type = importedType(for: rAttachment, contentType: contentType, libraryId: libraryId, fileStorage: fileStorage, linkType: linkType, compressed: rAttachment.fileCompressed)
74+
return Attachment(item: rAttachment, type: type)
75+
}
76+
77+
func firstLinkedURLAttachmentIfAny(for item: RItem, urlDetector: UrlDetector?) -> Attachment? {
78+
guard let urlDetector else { return nil }
79+
for child in item.children {
80+
guard (child.rawType == ItemTypes.attachment) && (child.syncState != .dirty) && !child.trash,
81+
let linkMode = child.fields.first(where: { $0.key == FieldKeys.Item.Attachment.linkMode }).flatMap({ LinkMode(rawValue: $0.value) }),
82+
linkMode == .linkedUrl,
83+
let libraryId = child.libraryId,
84+
let attachmentType = linkedUrlType(for: child, libraryId: libraryId, urlDetector: urlDetector)
85+
else { continue }
86+
return Attachment(item: child, type: attachmentType)
87+
}
88+
return nil
89+
}
5790
}
5891

5992
static func mainPdfAttachment(from attachments: [Attachment], parentUrl: String?) -> Attachment? {

Zotero/Controllers/IdentifierLookupController.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -455,16 +455,19 @@ final class IdentifierLookupController {
455455
}
456456

457457
func storeDataAndDownloadAttachmentIfNecessary(identifier: String, response: ItemResponse, attachments: [(Attachment, URL)]) throws {
458-
let request = CreateTranslatedItemsDbRequest(responses: [response], schemaController: schemaController, dateParser: dateParser)
459-
_ = try dbStorage.perform(request: request, on: backgroundQueue)
458+
var library: Library?
459+
try dbStorage.perform(on: backgroundQueue) { coordinator in
460+
_ = try coordinator.perform(request: CreateTranslatedItemsDbRequest(responses: [response], schemaController: schemaController, dateParser: dateParser))
461+
library = try coordinator.perform(request: ReadLibraryDbRequest(libraryId: libraryId))
462+
}
460463
changeLookup(
461464
for: identifier,
462465
to: .translated(.init(response: response, attachments: attachments, libraryId: libraryId, collectionKeys: collectionKeys))
463466
) { [weak self] didChange in
464467
guard let self, didChange else { return }
465468
observable.on(.next(Update(kind: .itemStored(identifier: identifier, response: response, attachments: attachments), lookupData: Array(lookupData.values))))
466469

467-
if Defaults.shared.shareExtensionIncludeAttachment, !attachments.isEmpty {
470+
if Defaults.shared.shareExtensionIncludeAttachment, library?.filesEditable == true, !attachments.isEmpty {
468471
let downloadData = attachments.map({ ($0, $1, response.key) })
469472
remoteFileDownloader.download(data: downloadData)
470473
observable.on(.next(Update(kind: .pendingAttachments(identifier: identifier, response: response, attachments: attachments), lookupData: Array(lookupData.values))))

Zotero/Scenes/Detail/DetailCoordinator.swift

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -441,15 +441,13 @@ extension DetailCoordinator: DetailItemsCoordinatorDelegate {
441441
let controller = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
442442
controller.popoverPresentationController?.sourceItem = button
443443

444-
if viewModel.state.library.metadataAndFilesEditable {
445-
controller.addAction(UIAlertAction(title: L10n.Items.lookup, style: .default, handler: { [weak self] _ in
446-
self?.showLookup(startWith: .manual(restoreLookupState: false))
447-
}))
444+
controller.addAction(UIAlertAction(title: L10n.Items.lookup, style: .default, handler: { [weak self] _ in
445+
self?.showLookup(startWith: .manual(restoreLookupState: false))
446+
}))
448447

449-
controller.addAction(UIAlertAction(title: L10n.Items.barcode, style: .default, handler: { [weak self] _ in
450-
self?.showLookup(startWith: .scanner)
451-
}))
452-
}
448+
controller.addAction(UIAlertAction(title: L10n.Items.barcode, style: .default, handler: { [weak self] _ in
449+
self?.showLookup(startWith: .scanner)
450+
}))
453451

454452
controller.addAction(UIAlertAction(title: L10n.Items.new, style: .default, handler: { [weak self, weak viewModel] _ in
455453
guard let self, let viewModel else { return }
@@ -471,7 +469,7 @@ extension DetailCoordinator: DetailItemsCoordinatorDelegate {
471469
showNote(library: viewModel.state.library, kind: .standaloneCreation(collection: viewModel.state.collection), saveCallback: nil)
472470
}))
473471

474-
if viewModel.state.library.metadataAndFilesEditable {
472+
if viewModel.state.library.filesEditable {
475473
controller.addAction(UIAlertAction(title: L10n.Items.newFile, style: .default, handler: { [weak self, weak viewModel] _ in
476474
self?.showAttachmentPicker(save: { urls in
477475
viewModel?.process(action: .addAttachments(urls))

Zotero/Scenes/Detail/ItemDetail/Views/ItemDetailCollectionViewHandler.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -995,9 +995,11 @@ extension ItemDetailCollectionViewHandler: UICollectionViewDelegate {
995995
}
996996

997997
if !viewModel.state.data.isAttachment {
998-
actions.append(UIAction(title: L10n.ItemDetail.moveToStandaloneAttachment, image: UIImage(systemName: "arrow.up.to.line"), attributes: []) { [weak self] _ in
999-
self?.viewModel.process(action: .moveAttachmentToStandalone(attachment))
1000-
})
998+
if case .file = attachment.type {
999+
actions.append(UIAction(title: L10n.ItemDetail.moveToStandaloneAttachment, image: UIImage(systemName: "arrow.up.to.line"), attributes: []) { [weak self] _ in
1000+
self?.viewModel.process(action: .moveAttachmentToStandalone(attachment))
1001+
})
1002+
}
10011003

10021004
actions.append(UIAction(title: L10n.moveToTrash, image: UIImage(systemName: "trash"), attributes: .destructive) { [weak self] _ in
10031005
self?.viewModel.process(action: .deleteAttachment(attachment))

Zotero/Scenes/General/Models/ItemAccessory.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,14 @@ extension ItemAccessory {
3939
}
4040

4141
static func create(from item: RItem, fileStorage: FileStorage, urlDetector: UrlDetector) -> ItemAccessory? {
42-
if let attachment = AttachmentCreator.mainAttachment(for: item, fileStorage: fileStorage) {
43-
return .attachment(attachment: attachment, parentKey: (item.key != attachment.key) ? item.key : nil)
42+
if let attachment = AttachmentCreator.mainAttachment(for: item, fileStorage: fileStorage, urlDetector: urlDetector) {
43+
switch attachment.type {
44+
case .file:
45+
return .attachment(attachment: attachment, parentKey: (item.key != attachment.key) ? item.key : nil)
46+
47+
case .url(let url):
48+
return .url(url)
49+
}
4450
}
4551

4652
if let urlString = item.urlString, urlDetector.isUrl(string: urlString), let url = URL(string: urlString) {

0 commit comments

Comments
 (0)