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

Add collection remove downloads action #983

Merged
3 changes: 3 additions & 0 deletions Zotero/Assets/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"collections.expand_all" = "Expand All";
"collections.create_bibliography" = "Create Bibliography from Collection";
"collections.download_attachments" = "Download Attachments";
"collections.delete_attachment_files" = "Remove Downloads";

"sync_toolbar.starting" = "Sync starting";
"sync_toolbar.groups" = "Syncing groups";
Expand Down Expand Up @@ -532,6 +533,8 @@
"accessibility.items.restore" = "Restore selected items";
"accessibility.items.duplicate" = "Duplicate selected item";
"accessibility.items.share" = "Share selected items";
"accessibility.items.download_attachments" = "Download attachments for selected items";
"accessibility.items.remove_downloads" = "Remove downloads for selected items";
"accessibility.item_detail.download_and_open" = "Double tap to download and open";
"accessibility.item_detail.open" = "Double tap to open";
"accessibility.pdf.sidebar_open" = "Open sidebar";
Expand Down
417 changes: 204 additions & 213 deletions Zotero/Controllers/AttachmentFileCleanupController.swift

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions Zotero/Extensions/Localizable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,16 @@ internal enum L10n {
internal static let delete = L10n.tr("Localizable", "accessibility.items.delete", fallback: "Delete selected items")
/// Deselect All Items
internal static let deselectAllItems = L10n.tr("Localizable", "accessibility.items.deselect_all_items", fallback: "Deselect All Items")
/// Download attachments for selected items
internal static let downloadAttachments = L10n.tr("Localizable", "accessibility.items.download_attachments", fallback: "Download attachments for selected items")
/// Duplicate selected item
internal static let duplicate = L10n.tr("Localizable", "accessibility.items.duplicate", fallback: "Duplicate selected item")
/// Filter items
internal static let filterItems = L10n.tr("Localizable", "accessibility.items.filter_items", fallback: "Filter items")
/// Open item info
internal static let openItem = L10n.tr("Localizable", "accessibility.items.open_item", fallback: "Open item info")
/// Remove downloads for selected items
internal static let removeDownloads = L10n.tr("Localizable", "accessibility.items.remove_downloads", fallback: "Remove downloads for selected items")
/// Remove selected items from collection
internal static let removeFromCollection = L10n.tr("Localizable", "accessibility.items.remove_from_collection", fallback: "Remove selected items from collection")
/// Restore selected items
Expand Down Expand Up @@ -343,6 +347,8 @@ internal enum L10n {
internal static let createTitle = L10n.tr("Localizable", "collections.create_title", fallback: "Create Collection")
/// Delete Collection
internal static let delete = L10n.tr("Localizable", "collections.delete", fallback: "Delete Collection")
/// Remove Downloads
internal static let deleteAttachmentFiles = L10n.tr("Localizable", "collections.delete_attachment_files", fallback: "Remove Downloads")
/// Delete Collection and Items
internal static let deleteWithItems = L10n.tr("Localizable", "collections.delete_with_items", fallback: "Delete Collection and Items")
/// Download Attachments
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ struct ItemDetailActionHandler: ViewModelActionHandler, BackgroundDbProcessingAc
}

private func deleteFile(of attachment: Attachment, in viewModel: ViewModel<ItemDetailActionHandler>) {
self.fileCleanupController.delete(.individual(attachment: attachment, parentKey: viewModel.state.key), completed: nil)
self.fileCleanupController.delete(.individual(attachment: attachment, parentKey: viewModel.state.key))
}

private func updateDeletedAttachmentFiles(_ notification: AttachmentFileDeletedNotification, in viewModel: ViewModel<ItemDetailActionHandler>) {
Expand Down
2 changes: 1 addition & 1 deletion Zotero/Scenes/Detail/Items/Models/ItemAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ struct ItemAction {

case .removeDownload:
self.title = L10n.Items.Action.removeDownload
self._image = .system("trash")
self._image = .system("arrow.down.circle.dotted")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ struct ItemsActionHandler: ViewModelActionHandler, BackgroundDbProcessingActionH
self.downloadAttachments(for: keys, in: viewModel)

case .removeDownloads(let ids):
self.fileCleanupController.delete(.allForItems(ids, viewModel.state.library.identifier), completed: nil)
self.fileCleanupController.delete(.allForItems(ids, viewModel.state.library.identifier))

case .emptyTrash:
self.emptyTrash(in: viewModel)
Expand Down
271 changes: 140 additions & 131 deletions Zotero/Scenes/Detail/Items/ViewModels/ItemsToolbarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,63 +37,166 @@ final class ItemsToolbarController {
init(viewController: UIViewController, initialState: ItemsState, delegate: ItemsToolbarControllerDelegate) {
self.viewController = viewController
self.delegate = delegate
self.editingActions = Self.editingActions(for: initialState)
self.disposeBag = DisposeBag()
editingActions = createEditingActions(for: initialState)
disposeBag = DisposeBag()

createToolbarItems(for: initialState)

func createEditingActions(for state: ItemsState) -> [ItemAction] {
var types: [ItemAction.Kind] = []
if state.collection.identifier.isTrash && state.library.metadataEditable {
types.append(contentsOf: [.restore, .delete, .download, .removeDownload])
} else {
if state.library.metadataEditable {
types.append(contentsOf: [.addToCollection, .trash])
}
switch state.collection.identifier {
case .collection:
if state.library.metadataEditable {
types.insert(.removeFromCollection, at: 1)
}

self.createToolbarItems(for: initialState)
case .custom, .search:
break
}
types.append(contentsOf: [.download, .removeDownload, .share])
}
return types.map { .init(type: $0) }
}
}

func willAppear() {
self.viewController.navigationController?.setToolbarHidden(false, animated: false)
}

private static func editingActions(for state: ItemsState) -> [ItemAction] {
if state.collection.identifier.isTrash && state.library.metadataEditable {
return [ItemAction(type: .restore), ItemAction(type: .delete)]
}

var actions: [ItemAction] = []
if state.library.metadataEditable {
actions.append(contentsOf: [ItemAction(type: .addToCollection), ItemAction(type: .trash)])
}
switch state.collection.identifier {
case .collection:
if state.library.metadataEditable {
actions.insert(ItemAction(type: .removeFromCollection), at: 1)
}

case .custom, .search:
break
}
actions.append(ItemAction(type: .share))
return actions
viewController.navigationController?.setToolbarHidden(false, animated: false)
}

// MARK: - Actions

func createToolbarItems(for state: ItemsState) {
if state.isEditing {
self.viewController.toolbarItems = self.createEditingToolbarItems(from: self.editingActions)
self.updateEditingToolbarItems(for: state.selectedItems, results: state.results)
viewController.toolbarItems = createEditingToolbarItems(from: editingActions)
updateEditingToolbarItems(for: state.selectedItems, results: state.results)
} else {
let filters = self.sizeClassSpecificFilters(from: state.filters)
self.viewController.toolbarItems = self.createNormalToolbarItems(for: filters)
self.updateNormalToolbarItems(
let filters = sizeClassSpecificFilters(from: state.filters)
viewController.toolbarItems = createNormalToolbarItems(for: filters)
updateNormalToolbarItems(
for: filters,
downloadBatchData: state.downloadBatchData,
remoteDownloadBatchData: state.remoteDownloadBatchData,
identifierLookupBatchData: state.identifierLookupBatchData,
results: state.results
)
}

func createEditingToolbarItems(from actions: [ItemAction]) -> [UIBarButtonItem] {
let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let items = actions.map({ action -> UIBarButtonItem in
let item = UIBarButtonItem(image: action.image, style: .plain, target: nil, action: nil)
switch action.type {
case .addToCollection, .trash, .delete, .removeFromCollection, .restore, .share, .download, .removeDownload:
item.tag = ToolbarItem.empty.tag

case .sort, .filter, .createParent, .copyCitation, .copyBibliography, .duplicate:
break
}
switch action.type {
case .addToCollection:
item.accessibilityLabel = L10n.Accessibility.Items.addToCollection

case .trash:
item.accessibilityLabel = L10n.Accessibility.Items.trash

case .delete:
item.accessibilityLabel = L10n.Accessibility.Items.delete

case .removeFromCollection:
item.accessibilityLabel = L10n.Accessibility.Items.removeFromCollection

case .restore:
item.accessibilityLabel = L10n.Accessibility.Items.restore

case .share:
item.accessibilityLabel = L10n.Accessibility.Items.share

case .download:
item.accessibilityLabel = L10n.Accessibility.Items.downloadAttachments

case .removeDownload:
item.accessibilityLabel = L10n.Accessibility.Items.removeDownloads

case .sort, .filter, .createParent, .copyCitation, .copyBibliography, .duplicate:
break
}
item.rx.tap.subscribe(onNext: { [weak self] _ in
self?.delegate?.process(action: action.type, button: item)
})
.disposed(by: disposeBag)
return item
})
return [spacer] + (0..<(2 * items.count)).map({ idx -> UIBarButtonItem in idx % 2 == 0 ? items[idx / 2] : spacer })
}

func createNormalToolbarItems(for filters: [ItemsFilter]) -> [UIBarButtonItem] {
let fixedSpacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
fixedSpacer.width = 16
let flexibleSpacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)

let filterImageName = filters.isEmpty ? "line.horizontal.3.decrease.circle" : "line.horizontal.3.decrease.circle.fill"
let filterButton = UIBarButtonItem(image: UIImage(systemName: filterImageName), style: .plain, target: nil, action: nil)
filterButton.tag = ToolbarItem.filter.tag
filterButton.accessibilityLabel = L10n.Accessibility.Items.filterItems
filterButton.rx.tap.subscribe(onNext: { [weak self] _ in
self?.delegate?.process(action: .filter, button: filterButton)
})
.disposed(by: disposeBag)

let action = ItemAction(type: .sort)
let sortButton = UIBarButtonItem(image: action.image, style: .plain, target: nil, action: nil)
sortButton.accessibilityLabel = L10n.Accessibility.Items.sortItems
sortButton.rx.tap.subscribe(onNext: { [weak self] _ in
self?.delegate?.process(action: action.type, button: sortButton)
})
.disposed(by: disposeBag)

let titleButton = UIBarButtonItem(customView: createTitleView())
titleButton.tag = ToolbarItem.title.tag

return [fixedSpacer, filterButton, flexibleSpacer, titleButton, flexibleSpacer, sortButton, fixedSpacer]

func createTitleView() -> UIStackView {
// Filter title label
let filterLabel = UILabel()
filterLabel.adjustsFontForContentSizeCategory = true
filterLabel.textColor = .label
filterLabel.font = .preferredFont(forTextStyle: .footnote)
filterLabel.textAlignment = .center
filterLabel.isHidden = true

// Batch download view
let progressView = ItemsToolbarDownloadProgressView()
let tap = UITapGestureRecognizer()
tap.rx
.event
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
self?.delegate?.showLookup()
})
.disposed(by: self.disposeBag)
progressView.addGestureRecognizer(tap)
progressView.isHidden = true

let stackView = UIStackView(arrangedSubviews: [filterLabel, progressView])
stackView.axis = .horizontal
return stackView
}
}
}

func reloadToolbarItems(for state: ItemsState) {
if state.isEditing {
self.updateEditingToolbarItems(for: state.selectedItems, results: state.results)
updateEditingToolbarItems(for: state.selectedItems, results: state.results)
} else {
self.updateNormalToolbarItems(
for: self.sizeClassSpecificFilters(from: state.filters),
updateNormalToolbarItems(
for: sizeClassSpecificFilters(from: state.filters),
downloadBatchData: state.downloadBatchData,
remoteDownloadBatchData: state.remoteDownloadBatchData,
identifierLookupBatchData: state.identifierLookupBatchData,
Expand Down Expand Up @@ -123,7 +226,7 @@ final class ItemsToolbarController {
// MARK: - Helpers

private func updateEditingToolbarItems(for selectedItems: Set<String>, results: Results<RItem>?) {
self.viewController.toolbarItems?.forEach({ item in
viewController.toolbarItems?.forEach({ item in
switch ToolbarItem(rawValue: item.tag) {
case .empty:
item.isEnabled = !selectedItems.isEmpty
Expand All @@ -144,12 +247,12 @@ final class ItemsToolbarController {
identifierLookupBatchData: ItemsState.IdentifierLookupBatchData,
results: Results<RItem>?
) {
if let item = self.viewController.toolbarItems?.first(where: { $0.tag == ToolbarItem.filter.tag }) {
if let item = viewController.toolbarItems?.first(where: { $0.tag == ToolbarItem.filter.tag }) {
let filterImageName = filters.isEmpty ? "line.horizontal.3.decrease.circle" : "line.horizontal.3.decrease.circle.fill"
item.image = UIImage(systemName: filterImageName)
}

if let item = self.viewController.toolbarItems?.first(where: { $0.tag == ToolbarItem.title.tag }),
if let item = viewController.toolbarItems?.first(where: { $0.tag == ToolbarItem.title.tag }),
let stackView = item.customView as? UIStackView {
if let filterLabel = stackView.subviews.first as? UILabel {
let itemCount = results?.count ?? 0
Expand Down Expand Up @@ -196,98 +299,4 @@ final class ItemsToolbarController {
stackView.sizeToFit()
}
}

private func createNormalToolbarItems(for filters: [ItemsFilter]) -> [UIBarButtonItem] {
let fixedSpacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
fixedSpacer.width = 16
let flexibleSpacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)

let filterImageName = filters.isEmpty ? "line.horizontal.3.decrease.circle" : "line.horizontal.3.decrease.circle.fill"
let filterButton = UIBarButtonItem(image: UIImage(systemName: filterImageName), style: .plain, target: nil, action: nil)
filterButton.tag = ToolbarItem.filter.tag
filterButton.accessibilityLabel = L10n.Accessibility.Items.filterItems
filterButton.rx.tap.subscribe(onNext: { [weak self] _ in
self?.delegate?.process(action: .filter, button: filterButton)
})
.disposed(by: self.disposeBag)

let action = ItemAction(type: .sort)
let sortButton = UIBarButtonItem(image: action.image, style: .plain, target: nil, action: nil)
sortButton.accessibilityLabel = L10n.Accessibility.Items.sortItems
sortButton.rx.tap.subscribe(onNext: { [weak self] _ in
self?.delegate?.process(action: action.type, button: sortButton)
})
.disposed(by: self.disposeBag)

let titleButton = UIBarButtonItem(customView: self.createTitleView())
titleButton.tag = ToolbarItem.title.tag

return [fixedSpacer, filterButton, flexibleSpacer, titleButton, flexibleSpacer, sortButton, fixedSpacer]
}

private func createEditingToolbarItems(from actions: [ItemAction]) -> [UIBarButtonItem] {
let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let items = actions.map({ action -> UIBarButtonItem in
let item = UIBarButtonItem(image: action.image, style: .plain, target: nil, action: nil)
switch action.type {
case .addToCollection, .trash, .delete, .removeFromCollection, .restore:
item.tag = ToolbarItem.empty.tag
case .sort, .filter, .createParent, .copyCitation, .copyBibliography, .share, .removeDownload, .download, .duplicate: break
}
switch action.type {
case .addToCollection:
item.accessibilityLabel = L10n.Accessibility.Items.addToCollection

case .trash:
item.accessibilityLabel = L10n.Accessibility.Items.trash

case .delete:
item.accessibilityLabel = L10n.Accessibility.Items.delete

case .removeFromCollection:
item.accessibilityLabel = L10n.Accessibility.Items.removeFromCollection

case .restore:
item.accessibilityLabel = L10n.Accessibility.Items.restore

case .share:
item.accessibilityLabel = L10n.Accessibility.Items.share
case .sort, .filter, .createParent, .copyCitation, .copyBibliography, .removeDownload, .download, .duplicate: break
}
item.rx.tap.subscribe(onNext: { [weak self] _ in
guard let self = self else { return }
self.delegate?.process(action: action.type, button: item)
})
.disposed(by: self.disposeBag)
return item
})
return [spacer] + (0..<(2 * items.count)).map({ idx -> UIBarButtonItem in idx % 2 == 0 ? items[idx / 2] : spacer })
}

private func createTitleView() -> UIStackView {
// Filter title label
let filterLabel = UILabel()
filterLabel.adjustsFontForContentSizeCategory = true
filterLabel.textColor = .label
filterLabel.font = .preferredFont(forTextStyle: .footnote)
filterLabel.textAlignment = .center
filterLabel.isHidden = true

// Batch download view
let progressView = ItemsToolbarDownloadProgressView()
let tap = UITapGestureRecognizer()
tap.rx
.event
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
self?.delegate?.showLookup()
})
.disposed(by: self.disposeBag)
progressView.addGestureRecognizer(tap)
progressView.isHidden = true

let stackView = UIStackView(arrangedSubviews: [filterLabel, progressView])
stackView.axis = .horizontal
return stackView
}
}
Loading