mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Refactor Diffable Datasources out of the Sidebar
This commit is contained in:
@@ -27,9 +27,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
}
|
||||
|
||||
private let operationQueue = MainThreadOperationQueue()
|
||||
lazy var dataSource = makeDataSource()
|
||||
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
weak var coordinator: SceneCoordinator!
|
||||
|
||||
@@ -63,7 +60,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
tableView.tableHeaderView = UIView(frame: frame)
|
||||
|
||||
tableView.register(MasterFeedTableViewSectionHeader.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
tableView.dataSource = dataSource
|
||||
tableView.dragDelegate = self
|
||||
tableView.dropDelegate = self
|
||||
tableView.dragInteractionEnabled = true
|
||||
@@ -83,7 +79,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
configureToolbar()
|
||||
becomeFirstResponder()
|
||||
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
@@ -94,7 +89,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
IconImageCache.shared.emptyCache()
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
reloadAllVisibleCells()
|
||||
// reloadAllVisibleCells()
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
@@ -123,11 +118,8 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
node = coordinator.rootNode.descendantNodeRepresentingObject(representedObject as AnyObject)
|
||||
}
|
||||
|
||||
guard let unreadCountNode = node else { return }
|
||||
let identifier = makeIdentifier(unreadCountNode)
|
||||
if dataSource.indexPath(for: identifier) != nil {
|
||||
self.reload(identifier)
|
||||
}
|
||||
guard let unreadCountNode = node, let indexPath = coordinator.indexPathFor(unreadCountNode) else { return }
|
||||
tableView.reloadRows(at: [indexPath], with: .middle)
|
||||
}
|
||||
|
||||
@objc func faviconDidBecomeAvailable(_ note: Notification) {
|
||||
@@ -152,7 +144,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
@objc func contentSizeCategoryDidChange(_ note: Notification) {
|
||||
resetEstimatedRowHeight()
|
||||
applyChanges(animated: false)
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
@objc func willEnterForeground(_ note: Notification) {
|
||||
@@ -161,6 +153,20 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
// MARK: Table View
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
coordinator.numberOfSections()
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
coordinator.numberOfRows(in: section)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell
|
||||
configure(cell, indexPath)
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
|
||||
guard let nameProvider = coordinator.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else {
|
||||
@@ -251,13 +257,13 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
renameAction.backgroundColor = UIColor.systemOrange
|
||||
actions.append(renameAction)
|
||||
|
||||
if let identifier = dataSource.itemIdentifier(for: indexPath), identifier.isWebFeed {
|
||||
if let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed {
|
||||
let moreTitle = NSLocalizedString("More", comment: "More")
|
||||
let moreAction = UIContextualAction(style: .normal, title: moreTitle) { [weak self] (action, view, completion) in
|
||||
|
||||
if let self = self {
|
||||
|
||||
let alert = UIAlertController(title: identifier.nameForDisplay, message: nil, preferredStyle: .actionSheet)
|
||||
let alert = UIAlertController(title: webFeed.nameForDisplay, message: nil, preferredStyle: .actionSheet)
|
||||
if let popoverController = alert.popoverPresentationController {
|
||||
popoverController.sourceView = view
|
||||
popoverController.sourceRect = CGRect(x: view.frame.size.width/2, y: view.frame.size.height/2, width: 1, height: 1)
|
||||
@@ -303,26 +309,25 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
guard let identifier = dataSource.itemIdentifier(for: indexPath) else {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else {
|
||||
return nil
|
||||
}
|
||||
if identifier.isWebFeed {
|
||||
return makeWebFeedContextMenu(identifier: identifier, indexPath: indexPath, includeDeleteRename: true)
|
||||
} else if identifier.isFolder {
|
||||
return makeFolderContextMenu(identifier: identifier, indexPath: indexPath)
|
||||
} else if identifier.isPsuedoFeed {
|
||||
return makePseudoFeedContextMenu(identifier: identifier, indexPath: indexPath)
|
||||
if feed is WebFeed {
|
||||
return makeWebFeedContextMenu(indexPath: indexPath, includeDeleteRename: true)
|
||||
} else if feed is Folder {
|
||||
return makeFolderContextMenu(indexPath: indexPath)
|
||||
} else if feed is PseudoFeed {
|
||||
return makePseudoFeedContextMenu(indexPath: indexPath)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
guard let identifier = configuration.identifier as? MasterFeedTableViewIdentifier,
|
||||
let indexPath = dataSource.indexPath(for: identifier),
|
||||
let cell = tableView.cellForRow(at: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
guard let identifier = configuration.identifier as? MasterFeedRowIdentifier,
|
||||
let cell = tableView.cellForRow(at: identifier.indexPath) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return UITargetedPreview(view: cell, parameters: CroppingPreviewParameters(view: cell))
|
||||
}
|
||||
@@ -342,21 +347,16 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
return coordinator.cappedIndexPath(proposedDestinationIndexPath)
|
||||
}()
|
||||
|
||||
guard let draggedIdentifier = dataSource.itemIdentifier(for: sourceIndexPath),
|
||||
let draggedFeedID = draggedIdentifier.feedID,
|
||||
let draggedNode = coordinator.nodeFor(feedID: draggedFeedID) else {
|
||||
guard let draggedNode = coordinator.nodeFor(sourceIndexPath) else {
|
||||
assertionFailure("This should never happen")
|
||||
return sourceIndexPath
|
||||
}
|
||||
|
||||
// If there is no destination node, we are dragging onto an empty Account
|
||||
guard let destIdentifier = dataSource.itemIdentifier(for: destIndexPath),
|
||||
let destFeedID = destIdentifier.feedID,
|
||||
let destNode = coordinator.nodeFor(feedID: destFeedID),
|
||||
let destParentContainerID = destIdentifier.parentContainerID,
|
||||
let destParentNode = coordinator.nodeFor(containerID: destParentContainerID) else {
|
||||
return proposedDestinationIndexPath
|
||||
}
|
||||
guard let destNode = coordinator.nodeFor(destIndexPath),
|
||||
let destParentNode = destNode.parent else {
|
||||
return proposedDestinationIndexPath
|
||||
}
|
||||
|
||||
// If this is a folder, let the users drop on it
|
||||
if destNode.representedObject is Folder {
|
||||
@@ -381,8 +381,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
if destParentNode.representedObject is Account {
|
||||
return IndexPath(row: 0, section: destIndexPath.section)
|
||||
} else {
|
||||
let identifier = makeIdentifier(sortedNodes[index])
|
||||
if let candidateIndexPath = dataSource.indexPath(for: identifier) {
|
||||
if let candidateIndexPath = coordinator.indexPathFor(sortedNodes[index]) {
|
||||
let movementAdjustment = sourceIndexPath < destIndexPath ? 1 : 0
|
||||
return IndexPath(row: candidateIndexPath.row - movementAdjustment, section: candidateIndexPath.section)
|
||||
} else {
|
||||
@@ -393,8 +392,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
} else {
|
||||
|
||||
if index >= sortedNodes.count {
|
||||
let identifier = makeIdentifier(sortedNodes[sortedNodes.count - 1])
|
||||
if let lastSortedIndexPath = dataSource.indexPath(for: identifier) {
|
||||
if let lastSortedIndexPath = coordinator.indexPathFor(sortedNodes[sortedNodes.count - 1]) {
|
||||
let movementAdjustment = sourceIndexPath > destIndexPath ? 1 : 0
|
||||
return IndexPath(row: lastSortedIndexPath.row + movementAdjustment, section: lastSortedIndexPath.section)
|
||||
} else {
|
||||
@@ -402,8 +400,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
} else {
|
||||
let movementAdjustment = sourceIndexPath < destIndexPath ? 1 : 0
|
||||
let identifer = makeIdentifier(sortedNodes[index - movementAdjustment])
|
||||
return dataSource.indexPath(for: identifer) ?? sourceIndexPath
|
||||
return coordinator.indexPathFor(sortedNodes[index - movementAdjustment]) ?? sourceIndexPath
|
||||
}
|
||||
|
||||
}
|
||||
@@ -515,14 +512,14 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
|
||||
@objc func expandSelectedRows(_ sender: Any?) {
|
||||
if let indexPath = coordinator.currentFeedIndexPath, let containerID = dataSource.itemIdentifier(for: indexPath)?.containerID {
|
||||
coordinator.expand(containerID)
|
||||
if let indexPath = coordinator.currentFeedIndexPath, let node = coordinator.nodeFor(indexPath) {
|
||||
coordinator.expand(node)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func collapseSelectedRows(_ sender: Any?) {
|
||||
if let indexPath = coordinator.currentFeedIndexPath, let containerID = dataSource.itemIdentifier(for: indexPath)?.containerID {
|
||||
coordinator.collapse(containerID)
|
||||
if let indexPath = coordinator.currentFeedIndexPath, let node = coordinator.nodeFor(indexPath) {
|
||||
coordinator.collapse(node)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,23 +559,54 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
|
||||
func updateFeedSelection(animations: Animations) {
|
||||
operationQueue.add(UpdateSelectionOperation(coordinator: coordinator, dataSource: dataSource, tableView: tableView, animations: animations))
|
||||
}
|
||||
|
||||
func reloadFeeds(initialLoad: Bool, completion: (() -> Void)? = nil) {
|
||||
updateUI()
|
||||
|
||||
// We have to reload all the visible cells because if we got here by doing a table cell move,
|
||||
// then the table itself is in a weird state. This is because we do unusual things like allowing
|
||||
// drops on a "folder" that should cause the dropped cell to disappear.
|
||||
applyChanges(animated: !initialLoad) { [weak self] in
|
||||
if !initialLoad {
|
||||
self?.reloadAllVisibleCells(completion: completion)
|
||||
} else {
|
||||
completion?()
|
||||
if let indexPath = coordinator.currentFeedIndexPath {
|
||||
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations)
|
||||
} else {
|
||||
if let indexPath = tableView.indexPathForSelectedRow {
|
||||
if animations.contains(.select) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
} else {
|
||||
tableView.deselectRow(at: indexPath, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reloadFeeds(initialLoad: Bool, changes: [ShadowTableChanges], completion: (() -> Void)? = nil) {
|
||||
updateUI()
|
||||
|
||||
guard !initialLoad else {
|
||||
tableView.reloadData()
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
|
||||
for change in changes {
|
||||
guard !change.isEmpty else { continue }
|
||||
|
||||
tableView.performBatchUpdates {
|
||||
if let deletes = change.deleteIndexPaths, !deletes.isEmpty {
|
||||
tableView.deleteRows(at: deletes, with: .middle)
|
||||
}
|
||||
|
||||
if let inserts = change.insertIndexPaths, !inserts.isEmpty {
|
||||
tableView.insertRows(at: inserts, with: .middle)
|
||||
}
|
||||
|
||||
if let moves = change.moveIndexPaths, !moves.isEmpty {
|
||||
for move in moves {
|
||||
tableView.moveRow(at: move.0, to: move.1)
|
||||
}
|
||||
}
|
||||
|
||||
if let reloads = change.reloadIndexPaths, !reloads.isEmpty {
|
||||
tableView.reloadRows(at: reloads, with: .middle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completion?()
|
||||
}
|
||||
|
||||
func updateUI() {
|
||||
if coordinator.isReadFeedsFiltered {
|
||||
@@ -741,65 +769,6 @@ private extension MasterFeedViewController {
|
||||
filterButton?.accLabelText = NSLocalizedString("Filter Read Feeds", comment: "Filter Read Feeds")
|
||||
}
|
||||
|
||||
func makeIdentifier(_ node: Node) -> MasterFeedTableViewIdentifier {
|
||||
let unreadCount = coordinator.unreadCountFor(node)
|
||||
return MasterFeedTableViewIdentifier(node: node, unreadCount: unreadCount)
|
||||
}
|
||||
|
||||
func reload(_ identifier: MasterFeedTableViewIdentifier) {
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reloadItems([identifier])
|
||||
queueApply(snapshot: snapshot, animatingDifferences: false) { [weak self] in
|
||||
self?.restoreSelectionIfNecessary(adjustScroll: false)
|
||||
}
|
||||
}
|
||||
|
||||
func applyChanges(animated: Bool, adjustScroll: Bool = false, completion: (() -> Void)? = nil) {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Int, MasterFeedTableViewIdentifier>()
|
||||
let sectionIdentifiers = Array(0...coordinator.rootNode.childNodes.count - 1)
|
||||
snapshot.appendSections(sectionIdentifiers)
|
||||
|
||||
for sectionIdentifer in sectionIdentifiers {
|
||||
let identifiers = coordinator.shadowNodesFor(section: sectionIdentifer).map { makeIdentifier($0) }
|
||||
snapshot.appendItems(identifiers, toSection: sectionIdentifer)
|
||||
}
|
||||
|
||||
queueApply(snapshot: snapshot, animatingDifferences: animated) { [weak self] in
|
||||
self?.restoreSelectionIfNecessary(adjustScroll: adjustScroll)
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
func queueApply(snapshot: NSDiffableDataSourceSnapshot<Int, MasterFeedTableViewIdentifier>, animatingDifferences: Bool = true, completion: (() -> Void)? = nil) {
|
||||
let operation = MasterFeedDataSourceOperation(dataSource: dataSource, snapshot: snapshot, animating: animatingDifferences)
|
||||
operation.completionBlock = { [weak self] _ in
|
||||
self?.enableTableViewSelection()
|
||||
completion?()
|
||||
}
|
||||
disableTableViewSelectionIfNecessary()
|
||||
operationQueue.add(operation)
|
||||
}
|
||||
|
||||
private func disableTableViewSelectionIfNecessary() {
|
||||
// We only need to disable tableView selection if the feeds are filtered by unread
|
||||
guard coordinator.isReadFeedsFiltered else { return }
|
||||
tableView.allowsSelection = false
|
||||
}
|
||||
|
||||
private func enableTableViewSelection() {
|
||||
tableView.allowsSelection = true
|
||||
}
|
||||
|
||||
func makeDataSource() -> MasterFeedDataSource {
|
||||
let dataSource = MasterFeedDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, cellContents in
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell
|
||||
self?.configure(cell, cellContents)
|
||||
return cell
|
||||
})
|
||||
dataSource.defaultRowAnimation = .middle
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func resetEstimatedRowHeight() {
|
||||
let titleLabel = NonIntrinsicLabel()
|
||||
titleLabel.text = "But I must explain"
|
||||
@@ -811,27 +780,30 @@ private extension MasterFeedViewController {
|
||||
tableView.estimatedRowHeight = layout.height
|
||||
}
|
||||
|
||||
func configure(_ cell: MasterFeedTableViewCell, _ identifier: MasterFeedTableViewIdentifier) {
|
||||
|
||||
func configure(_ cell: MasterFeedTableViewCell, _ indexPath: IndexPath) {
|
||||
guard let node = coordinator.nodeFor(indexPath) else { return }
|
||||
|
||||
cell.delegate = self
|
||||
if identifier.isFolder {
|
||||
if node.representedObject is Folder {
|
||||
cell.indentationLevel = 0
|
||||
} else {
|
||||
cell.indentationLevel = 1
|
||||
}
|
||||
|
||||
if let containerID = identifier.containerID {
|
||||
if let containerID = (node.representedObject as? Container)?.containerID {
|
||||
cell.setDisclosure(isExpanded: coordinator.isExpanded(containerID), animated: false)
|
||||
cell.isDisclosureAvailable = true
|
||||
} else {
|
||||
cell.isDisclosureAvailable = false
|
||||
}
|
||||
|
||||
cell.name = identifier.nameForDisplay
|
||||
cell.unreadCount = identifier.unreadCount
|
||||
configureIcon(cell, identifier)
|
||||
|
||||
guard let indexPath = dataSource.indexPath(for: identifier) else { return }
|
||||
if let feed = node.representedObject as? Feed {
|
||||
cell.name = feed.nameForDisplay
|
||||
cell.unreadCount = feed.unreadCount
|
||||
}
|
||||
|
||||
configureIcon(cell, indexPath)
|
||||
|
||||
let rowsInSection = tableView.numberOfRows(inSection: indexPath.section)
|
||||
if indexPath.row == rowsInSection - 1 {
|
||||
cell.isSeparatorShown = false
|
||||
@@ -841,8 +813,8 @@ private extension MasterFeedViewController {
|
||||
|
||||
}
|
||||
|
||||
func configureIcon(_ cell: MasterFeedTableViewCell, _ identifier: MasterFeedTableViewIdentifier) {
|
||||
guard let feedID = identifier.feedID else {
|
||||
func configureIcon(_ cell: MasterFeedTableViewCell, _ indexPath: IndexPath) {
|
||||
guard let node = coordinator.nodeFor(indexPath), let feed = node.representedObject as? Feed, let feedID = feed.feedID else {
|
||||
return
|
||||
}
|
||||
cell.iconImage = IconImageCache.shared.imageFor(feedID)
|
||||
@@ -859,35 +831,29 @@ private extension MasterFeedViewController {
|
||||
applyToCellsForRepresentedObject(representedObject, configure)
|
||||
}
|
||||
|
||||
func applyToCellsForRepresentedObject(_ representedObject: AnyObject, _ completion: (MasterFeedTableViewCell, MasterFeedTableViewIdentifier) -> Void) {
|
||||
applyToAvailableCells { (cell, identifier) in
|
||||
if let representedFeed = representedObject as? Feed, representedFeed.feedID == identifier.feedID {
|
||||
completion(cell, identifier)
|
||||
func applyToCellsForRepresentedObject(_ representedObject: AnyObject, _ completion: (MasterFeedTableViewCell, IndexPath) -> Void) {
|
||||
applyToAvailableCells { (cell, indexPath) in
|
||||
if let node = coordinator.nodeFor(indexPath),
|
||||
let representedFeed = representedObject as? Feed,
|
||||
let candidate = node.representedObject as? Feed,
|
||||
representedFeed.feedID == candidate.feedID {
|
||||
completion(cell, indexPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyToAvailableCells(_ completion: (MasterFeedTableViewCell, MasterFeedTableViewIdentifier) -> Void) {
|
||||
func applyToAvailableCells(_ completion: (MasterFeedTableViewCell, IndexPath) -> Void) {
|
||||
tableView.visibleCells.forEach { cell in
|
||||
guard let indexPath = tableView.indexPath(for: cell), let identifier = dataSource.itemIdentifier(for: indexPath) else {
|
||||
guard let indexPath = tableView.indexPath(for: cell) else {
|
||||
return
|
||||
}
|
||||
completion(cell as! MasterFeedTableViewCell, identifier)
|
||||
completion(cell as! MasterFeedTableViewCell, indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadAllVisibleCells(completion: (() -> Void)? = nil) {
|
||||
let visibleNodes = tableView.indexPathsForVisibleRows!.compactMap { return dataSource.itemIdentifier(for: $0) }
|
||||
reloadCells(visibleNodes, completion: completion)
|
||||
}
|
||||
|
||||
private func reloadCells(_ identifiers: [MasterFeedTableViewIdentifier], completion: (() -> Void)? = nil) {
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reloadItems(identifiers)
|
||||
queueApply(snapshot: snapshot, animatingDifferences: false) { [weak self] in
|
||||
self?.restoreSelectionIfNecessary(adjustScroll: false)
|
||||
completion?()
|
||||
}
|
||||
guard let indexPaths = tableView.indexPathsForVisibleRows else { return }
|
||||
tableView.reloadRows(at: indexPaths, with: .middle)
|
||||
}
|
||||
|
||||
private func accountForNode(_ node: Node) -> Account? {
|
||||
@@ -918,21 +884,21 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func expand(_ cell: MasterFeedTableViewCell) {
|
||||
guard let indexPath = tableView.indexPath(for: cell), let containerID = dataSource.itemIdentifier(for: indexPath)?.containerID else {
|
||||
guard let indexPath = tableView.indexPath(for: cell), let node = coordinator.nodeFor(indexPath) else {
|
||||
return
|
||||
}
|
||||
coordinator.expand(containerID)
|
||||
coordinator.expand(node)
|
||||
}
|
||||
|
||||
func collapse(_ cell: MasterFeedTableViewCell) {
|
||||
guard let indexPath = tableView.indexPath(for: cell), let containerID = dataSource.itemIdentifier(for: indexPath)?.containerID else {
|
||||
guard let indexPath = tableView.indexPath(for: cell), let node = coordinator.nodeFor(indexPath) else {
|
||||
return
|
||||
}
|
||||
coordinator.collapse(containerID)
|
||||
coordinator.collapse(node)
|
||||
}
|
||||
|
||||
func makeWebFeedContextMenu(identifier: MasterFeedTableViewIdentifier, indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration {
|
||||
return UIContextMenuConfiguration(identifier: identifier as NSCopying, previewProvider: nil, actionProvider: { [ weak self] suggestedActions in
|
||||
func makeWebFeedContextMenu(indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration {
|
||||
return UIContextMenuConfiguration(identifier: MasterFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [ weak self] suggestedActions in
|
||||
|
||||
guard let self = self else { return nil }
|
||||
|
||||
@@ -976,8 +942,8 @@ private extension MasterFeedViewController {
|
||||
|
||||
}
|
||||
|
||||
func makeFolderContextMenu(identifier: MasterFeedTableViewIdentifier, indexPath: IndexPath) -> UIContextMenuConfiguration {
|
||||
return UIContextMenuConfiguration(identifier: identifier as NSCopying, previewProvider: nil, actionProvider: { [weak self] suggestedActions in
|
||||
func makeFolderContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration {
|
||||
return UIContextMenuConfiguration(identifier: MasterFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [weak self] suggestedActions in
|
||||
|
||||
guard let self = self else { return nil }
|
||||
|
||||
@@ -999,12 +965,12 @@ private extension MasterFeedViewController {
|
||||
})
|
||||
}
|
||||
|
||||
func makePseudoFeedContextMenu(identifier: MasterFeedTableViewIdentifier, indexPath: IndexPath) -> UIContextMenuConfiguration? {
|
||||
func makePseudoFeedContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration? {
|
||||
guard let markAllAction = self.markAllAsReadAction(indexPath: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return UIContextMenuConfiguration(identifier: identifier as NSCopying, previewProvider: nil, actionProvider: { suggestedActions in
|
||||
return UIContextMenuConfiguration(identifier: MasterFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { suggestedActions in
|
||||
return UIMenu(title: "", children: [markAllAction])
|
||||
})
|
||||
}
|
||||
@@ -1035,11 +1001,10 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func copyFeedPageAction(indexPath: IndexPath) -> UIAction? {
|
||||
guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID,
|
||||
let webFeed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed,
|
||||
let url = URL(string: webFeed.url) else {
|
||||
return nil
|
||||
}
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed,
|
||||
let url = URL(string: webFeed.url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Copy Feed URL", comment: "Copy Feed URL")
|
||||
let action = UIAction(title: title, image: AppAssets.copyImage) { action in
|
||||
@@ -1049,12 +1014,11 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func copyFeedPageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID,
|
||||
let webFeed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed,
|
||||
let url = URL(string: webFeed.url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed,
|
||||
let url = URL(string: webFeed.url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Copy Feed URL", comment: "Copy Feed URL")
|
||||
let action = UIAlertAction(title: title, style: .default) { action in
|
||||
UIPasteboard.general.url = url
|
||||
@@ -1064,12 +1028,11 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func copyHomePageAction(indexPath: IndexPath) -> UIAction? {
|
||||
guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID,
|
||||
let webFeed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed,
|
||||
let homePageURL = webFeed.homePageURL,
|
||||
let url = URL(string: homePageURL) else {
|
||||
return nil
|
||||
}
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed,
|
||||
let homePageURL = webFeed.homePageURL,
|
||||
let url = URL(string: homePageURL) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Copy Home Page URL", comment: "Copy Home Page URL")
|
||||
let action = UIAction(title: title, image: AppAssets.copyImage) { action in
|
||||
@@ -1079,13 +1042,12 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func copyHomePageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID,
|
||||
let webFeed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed,
|
||||
let homePageURL = webFeed.homePageURL,
|
||||
let url = URL(string: homePageURL) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed,
|
||||
let homePageURL = webFeed.homePageURL,
|
||||
let url = URL(string: homePageURL) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Copy Home Page URL", comment: "Copy Home Page URL")
|
||||
let action = UIAlertAction(title: title, style: .default) { action in
|
||||
UIPasteboard.general.url = url
|
||||
@@ -1095,16 +1057,14 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func markAllAsReadAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let identifier = dataSource.itemIdentifier(for: indexPath),
|
||||
identifier.unreadCount > 0,
|
||||
let feedID = identifier.feedID,
|
||||
let feed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed,
|
||||
let articles = try? feed.fetchArticles(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed,
|
||||
webFeed.unreadCount > 0,
|
||||
let articles = try? webFeed.fetchArticles(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, webFeed.nameForDisplay) as String
|
||||
let cancel = {
|
||||
completion(true)
|
||||
}
|
||||
@@ -1137,13 +1097,13 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func getInfoAction(indexPath: IndexPath) -> UIAction? {
|
||||
guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID, let feed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed else {
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Get Info", comment: "Get Info")
|
||||
let action = UIAction(title: title, image: AppAssets.infoImage) { [weak self] action in
|
||||
self?.coordinator.showFeedInspector(for: feed)
|
||||
self?.coordinator.showFeedInspector(for: webFeed)
|
||||
}
|
||||
return action
|
||||
}
|
||||
@@ -1165,41 +1125,25 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func getInfoAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID, let feed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed else {
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Get Info", comment: "Get Info")
|
||||
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
|
||||
self?.coordinator.showFeedInspector(for: feed)
|
||||
self?.coordinator.showFeedInspector(for: webFeed)
|
||||
completion(true)
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
func markAllAsReadAction(indexPath: IndexPath) -> UIAction? {
|
||||
guard let identifier = dataSource.itemIdentifier(for: indexPath), identifier.unreadCount > 0 else {
|
||||
return nil
|
||||
}
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
let contentView = self.tableView.cellForRow(at: indexPath)?.contentView,
|
||||
feed.unreadCount > 0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var smartFeed: Feed?
|
||||
if identifier.isPsuedoFeed {
|
||||
if SmartFeedsController.shared.todayFeed.feedID == identifier.feedID {
|
||||
smartFeed = SmartFeedsController.shared.todayFeed
|
||||
} else if SmartFeedsController.shared.unreadFeed.feedID == identifier.feedID {
|
||||
smartFeed = SmartFeedsController.shared.unreadFeed
|
||||
} else if SmartFeedsController.shared.starredFeed.feedID == identifier.feedID {
|
||||
smartFeed = SmartFeedsController.shared.starredFeed
|
||||
}
|
||||
}
|
||||
|
||||
guard let feedID = identifier.feedID,
|
||||
let feed = smartFeed ?? AccountManager.shared.existingFeed(with: feedID),
|
||||
feed.unreadCount > 0,
|
||||
let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
|
||||
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
|
||||
@@ -1236,11 +1180,10 @@ private extension MasterFeedViewController {
|
||||
|
||||
|
||||
func rename(indexPath: IndexPath) {
|
||||
guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID, let feed = AccountManager.shared.existingFeed(with: feedID) else { return }
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else { return }
|
||||
|
||||
let name = dataSource.itemIdentifier(for: indexPath)?.nameForDisplay ?? ""
|
||||
let formatString = NSLocalizedString("Rename “%@”", comment: "Rename feed")
|
||||
let title = NSString.localizedStringWithFormat(formatString as NSString, name) as String
|
||||
let title = NSString.localizedStringWithFormat(formatString as NSString, feed.nameForDisplay) as String
|
||||
|
||||
let alertController = UIAlertController(title: title, message: nil, preferredStyle: .alert)
|
||||
|
||||
@@ -1280,7 +1223,7 @@ private extension MasterFeedViewController {
|
||||
alertController.preferredAction = renameAction
|
||||
|
||||
alertController.addTextField() { textField in
|
||||
textField.text = name
|
||||
textField.text = feed.nameForDisplay
|
||||
textField.placeholder = NSLocalizedString("Name", comment: "Name")
|
||||
}
|
||||
|
||||
@@ -1291,7 +1234,7 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func delete(indexPath: IndexPath) {
|
||||
guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID, let feed = AccountManager.shared.existingFeed(with: feedID) else { return }
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else { return }
|
||||
|
||||
let title: String
|
||||
let message: String
|
||||
@@ -1312,7 +1255,7 @@ private extension MasterFeedViewController {
|
||||
|
||||
let deleteTitle = NSLocalizedString("Delete", comment: "Delete")
|
||||
let deleteAction = UIAlertAction(title: deleteTitle, style: .destructive) { [weak self] action in
|
||||
self?.delete(indexPath: indexPath, feedID: feedID)
|
||||
self?.performDelete(indexPath: indexPath)
|
||||
}
|
||||
alertController.addAction(deleteAction)
|
||||
alertController.preferredAction = deleteAction
|
||||
@@ -1320,9 +1263,9 @@ private extension MasterFeedViewController {
|
||||
self.present(alertController, animated: true)
|
||||
}
|
||||
|
||||
func delete(indexPath: IndexPath, feedID: FeedIdentifier) {
|
||||
func performDelete(indexPath: IndexPath) {
|
||||
guard let undoManager = undoManager,
|
||||
let deleteNode = coordinator.nodeFor(feedID: feedID),
|
||||
let deleteNode = coordinator.nodeFor(indexPath),
|
||||
let deleteCommand = DeleteCommand(nodesToDelete: [deleteNode], undoManager: undoManager, errorHandler: ErrorHandler.present(self)) else {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user