| ... | @@ -47,7 +47,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -47,7 +47,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 47 | var list = UICollectionLayoutListConfiguration(appearance: compact ? .insetGrouped : .sidebar) | 47 | var list = UICollectionLayoutListConfiguration(appearance: compact ? .insetGrouped : .sidebar) |
| 48 | list.headerMode = .firstItemInSection | 48 | list.headerMode = .firstItemInSection |
| 49 | list.trailingSwipeActionsConfigurationProvider = { indexPath in | 49 | list.trailingSwipeActionsConfigurationProvider = { indexPath in |
| 50 | guard let self, let item = self.dataSource.itemIdentifier(for: indexPath) else { return nil } | 50 | guard let self, !self.isEditing, let item = self.dataSource.itemIdentifier(for: indexPath) else { return nil } |
| 51 | if case .notebook(let id) = item, let notebook = self.notebook(id) { | 51 | if case .notebook(let id) = item, let notebook = self.notebook(id) { |
| 52 | return UISwipeActionsConfiguration(actions: self.swipeActions(for: notebook, at: indexPath)) | 52 | return UISwipeActionsConfiguration(actions: self.swipeActions(for: notebook, at: indexPath)) |
| 53 | } | 53 | } |
| ... | @@ -141,14 +141,11 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -141,14 +141,11 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 141 | ] | 141 | ] |
| 142 | navigationItem.searchController = SearchViewController.controller() | 142 | navigationItem.searchController = SearchViewController.controller() |
| 143 | setUpArranging() | 143 | setUpArranging() |
| 144 | let corner = Prototype.quickNote ? [QuickNote.item()] : [] | | |
| 145 | if #available(iOS 26, *) { | 144 | if #available(iOS 26, *) { |
| 146 | // Search sits in the bottom toolbar, as Notes and Files put it. | 145 | // Search sits in the bottom toolbar, as Notes and Files put it. |
| 147 | navigationItem.preferredSearchBarPlacement = .integrated | 146 | navigationItem.preferredSearchBarPlacement = .integrated |
| 148 | toolbarItems = [navigationItem.searchBarPlacementBarButtonItem] + (corner.isEmpty ? [] : [.fixedSpace()] + corner) | | |
| 149 | } else { | | |
| 150 | toolbarItems = corner.isEmpty ? [] : [.flexibleSpace()] + corner | | |
| 151 | } | 147 | } |
| | 148 | updateSelectionActions() |
| 152 | for name in [Sync.changed, Section.changed] { | 149 | for name in [Sync.changed, Section.changed] { |
| 153 | NotificationCenter.default.addObserver(self, selector: #selector(refreshSync), name: name, object: nil) | 150 | NotificationCenter.default.addObserver(self, selector: #selector(refreshSync), name: name, object: nil) |
| 154 | } | 151 | } |
| ... | @@ -172,6 +169,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -172,6 +169,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 172 | | 169 | |
| 173 | func reload(animated: Bool = false) { | 170 | func reload(animated: Bool = false) { |
| 174 | guard isViewLoaded else { return } | 171 | guard isViewLoaded else { return } |
| | 172 | let selection = collectionView.indexPathsForSelectedItems?.compactMap { dataSource.itemIdentifier(for: $0) } ?? [] |
| 175 | if Prototype.welcome { | 173 | if Prototype.welcome { |
| 176 | let welcome = Prototype.welcome( | 174 | let welcome = Prototype.welcome( |
| 177 | new: { [weak self] in self?.newNotebook(inCloud: ICloud.documents != nil) }, | 175 | new: { [weak self] in self?.newNotebook(inCloud: ICloud.documents != nil) }, |
| ... | @@ -230,6 +228,14 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -230,6 +228,14 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 230 | dataSource.apply(list, to: location, animatingDifferences: animated) | 228 | dataSource.apply(list, to: location, animatingDifferences: animated) |
| 231 | } | 229 | } |
| 232 | showSelection() | 230 | showSelection() |
| | 231 | if isEditing { |
| | 232 | for item in selection { |
| | 233 | if let index = dataSource.indexPath(for: item) { |
| | 234 | collectionView.selectItem(at: index, animated: false, scrollPosition: []) |
| | 235 | } |
| | 236 | } |
| | 237 | updateSelectionActions() |
| | 238 | } |
| 233 | refreshSync() | 239 | refreshSync() |
| 234 | } | 240 | } |
| 235 | | 241 | |
| ... | @@ -294,10 +300,142 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -294,10 +300,142 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 294 | | 300 | |
| 295 | /// Marks the open section, as the sidebar of a wide window keeps it. | 301 | /// Marks the open section, as the sidebar of a wide window keeps it. |
| 296 | private func showSelection() { | 302 | private func showSelection() { |
| 297 | guard let selected, let indexPath = dataSource.indexPath(for: selected) else { return } | 303 | guard !isEditing, let selected, let indexPath = dataSource.indexPath(for: selected) else { return } |
| 298 | collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) | 304 | collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) |
| 299 | } | 305 | } |
| 300 | | 306 | |
| | 307 | override func setEditing(_ editing: Bool, animated: Bool) { |
| | 308 | super.setEditing(editing, animated: animated) |
| | 309 | for index in collectionView.indexPathsForSelectedItems ?? [] { |
| | 310 | collectionView.deselectItem(at: index, animated: false) |
| | 311 | } |
| | 312 | collectionView.isEditing = editing |
| | 313 | collectionView.dragInteractionEnabled = !editing |
| | 314 | if !editing { showSelection() } |
| | 315 | updateSelectionActions() |
| | 316 | } |
| | 317 | |
| | 318 | private func updateSelectionActions() { |
| | 319 | guard isEditing else { |
| | 320 | let corner = Prototype.quickNote ? [QuickNote.item()] : [] |
| | 321 | if #available(iOS 26, *) { |
| | 322 | toolbarItems = [navigationItem.searchBarPlacementBarButtonItem] + (corner.isEmpty ? [] : [.fixedSpace()] + corner) |
| | 323 | } else { |
| | 324 | toolbarItems = corner.isEmpty ? [] : [.flexibleSpace()] + corner |
| | 325 | } |
| | 326 | navigationController?.setToolbarHidden(toolbarItems?.isEmpty != false, animated: false) |
| | 327 | return |
| | 328 | } |
| | 329 | let selection = (collectionView.indexPathsForSelectedItems ?? []).sorted().compactMap { |
| | 330 | dataSource.itemIdentifier(for: $0) |
| | 331 | } |
| | 332 | var actions: [UIMenuElement] = [] |
| | 333 | let notebooks = selection.compactMap { item -> Notebook? in |
| | 334 | guard case .notebook(let id) = item else { return nil } |
| | 335 | return notebook(id) |
| | 336 | } |
| | 337 | if !selection.isEmpty, notebooks.count == selection.count { |
| | 338 | let folders = notebooks.compactMap { folder(of: $0) } |
| | 339 | if folders.count == notebooks.count { |
| | 340 | actions.append(UIAction(title: "Share", image: UIImage(systemName: "square.and.arrow.up")) { [weak self] _ in |
| | 341 | guard let self else { return } |
| | 342 | let sheet = UIActivityViewController(activityItems: folders, applicationActivities: nil) |
| | 343 | sheet.popoverPresentationController?.barButtonItem = toolbarItems?.last |
| | 344 | present(sheet, animated: true) |
| | 345 | }) |
| | 346 | let deletable = notebooks.allSatisfy { $0.handle != nil } |
| | 347 | actions.append(UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: deletable ? .destructive : [.destructive, .disabled]) { [weak self] _ in |
| | 348 | self?.delete(notebooks) |
| | 349 | }) |
| | 350 | } else if notebooks.allSatisfy({ notebook in Notebooks.elsewhere.contains { $0 === notebook } }) { |
| | 351 | actions.append(UIAction(title: "Close", image: UIImage(systemName: "xmark.circle")) { [weak self] _ in |
| | 352 | for notebook in notebooks { |
| | 353 | for scene in UIApplication.shared.connectedScenes { |
| | 354 | (scene.delegate as? SceneDelegate)?.close(notebook) |
| | 355 | } |
| | 356 | Notebooks.remove(notebook) |
| | 357 | } |
| | 358 | self?.reload() |
| | 359 | }) |
| | 360 | } |
| | 361 | } |
| | 362 | let entries = selection.compactMap { item -> (Notebook, String)? in |
| | 363 | switch item { |
| | 364 | case .section(let id, let path), .group(let id, let path): return notebook(id).map { ($0, path) } |
| | 365 | default: return nil |
| | 366 | } |
| | 367 | } |
| | 368 | if let notebook = entries.first?.0, entries.count == selection.count, |
| | 369 | entries.allSatisfy({ $0.0 === notebook }) |
| | 370 | { |
| | 371 | let paths = entries.map(\.1).filter { path in !entries.contains { path.hasPrefix($0.1 + "/") } } |
| | 372 | let destinations = notebook.destinations(of: paths[0]).filter { destination in |
| | 373 | paths.allSatisfy { notebook.destinations(of: $0).contains(destination) } |
| | 374 | } |
| | 375 | if !destinations.isEmpty { |
| | 376 | actions.append(UIMenu(title: "Move", image: UIImage(systemName: "folder"), children: destinations.map { folder in |
| | 377 | UIAction(title: folder.isEmpty ? notebook.name : folder.split(separator: "/").joined(separator: " › ")) { [weak self] _ in |
| | 378 | self?.setEditing(false, animated: true) |
| | 379 | for path in paths { Arranging.place(path, of: notebook, in: folder) } |
| | 380 | } |
| | 381 | })) |
| | 382 | } |
| | 383 | } |
| | 384 | let action = UIBarButtonItem(title: "Actions", image: UIImage(systemName: "ellipsis.circle"), menu: UIMenu(children: actions)) |
| | 385 | action.isEnabled = !actions.isEmpty |
| | 386 | toolbarItems = [.flexibleSpace(), action] |
| | 387 | navigationController?.setToolbarHidden(false, animated: false) |
| | 388 | } |
| | 389 | |
| | 390 | private func delete(_ notebooks: [Notebook]) { |
| | 391 | let alert = UIAlertController( |
| | 392 | title: "Delete \(notebooks.count) Notebooks?", |
| | 393 | message: "Notebooks that can’t be moved to Recently Deleted will be deleted permanently.", preferredStyle: .alert) |
| | 394 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) |
| | 395 | alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in |
| | 396 | guard let self else { return } |
| | 397 | setEditing(false, animated: true) |
| | 398 | collectionView.isUserInteractionEnabled = false |
| | 399 | for notebook in notebooks { |
| | 400 | for scene in UIApplication.shared.connectedScenes { |
| | 401 | (scene.delegate as? SceneDelegate)?.close(notebook) |
| | 402 | } |
| | 403 | } |
| | 404 | background({ () -> (String?, [Notebook]) in |
| | 405 | // No folder is removed until every notebook has published its edits. |
| | 406 | for notebook in notebooks { |
| | 407 | guard let handle = notebook.handle else { |
| | 408 | return ("\(notebook.name): Open this notebook before deleting it.", notebooks) |
| | 409 | } |
| | 410 | var error: UnsafeMutablePointer<CChar>? |
| | 411 | let closed = sb_library_close(handle, 10, &error) |
| | 412 | let problem = take(error) |
| | 413 | if !closed { return ("\(notebook.name): \(problem ?? "Some changes haven’t been saved yet.")", notebooks) } |
| | 414 | } |
| | 415 | var failed: [Notebook] = [] |
| | 416 | var problems: [String] = [] |
| | 417 | for notebook in notebooks { |
| | 418 | guard let folder = self.folder(of: notebook) else { continue } |
| | 419 | do { |
| | 420 | do { try FileManager.default.trashItem(at: folder, resultingItemURL: nil) } |
| | 421 | catch { try FileManager.default.removeItem(at: folder) } |
| | 422 | } catch { |
| | 423 | failed.append(notebook) |
| | 424 | problems.append("\(notebook.name): \(error.localizedDescription)") |
| | 425 | } |
| | 426 | } |
| | 427 | return (problems.isEmpty ? nil : problems.joined(separator: "\n\n"), failed) |
| | 428 | }) { [weak self] problem, failed in |
| | 429 | self?.collectionView.isUserInteractionEnabled = true |
| | 430 | for notebook in notebooks { notebook.pause() } |
| | 431 | for notebook in failed { notebook.open { self?.reload() } } |
| | 432 | self?.rescan() |
| | 433 | if let problem { self?.alert("Can’t Delete", problem) } |
| | 434 | } |
| | 435 | }) |
| | 436 | present(alert, animated: true) |
| | 437 | } |
| | 438 | |
| 301 | private func configure(_ cell: UICollectionViewListCell, _ item: Item) { | 439 | private func configure(_ cell: UICollectionViewListCell, _ item: Item) { |
| 302 | var content = cell.defaultContentConfiguration() | 440 | var content = cell.defaultContentConfiguration() |
| 303 | var accessories: [UICellAccessory] = [] | 441 | var accessories: [UICellAccessory] = [] |
| ... | @@ -589,7 +727,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -589,7 +727,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 589 | override func collectionView( | 727 | override func collectionView( |
| 590 | _ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint | 728 | _ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint |
| 591 | ) -> UIContextMenuConfiguration? { | 729 | ) -> UIContextMenuConfiguration? { |
| 592 | guard indexPaths.count == 1, let item = dataSource.itemIdentifier(for: indexPaths[0]) else { return nil } | 730 | guard !isEditing, indexPaths.count == 1, let item = dataSource.itemIdentifier(for: indexPaths[0]) else { return nil } |
| 593 | guard case .notebook(let id) = item, let notebook = notebook(id) else { | 731 | guard case .notebook(let id) = item, let notebook = notebook(id) else { |
| 594 | let moves = arrangeMenu(item) | 732 | let moves = arrangeMenu(item) |
| 595 | return moves.isEmpty ? nil : UIContextMenuConfiguration(actionProvider: { _ in UIMenu(children: moves) }) | 733 | return moves.isEmpty ? nil : UIContextMenuConfiguration(actionProvider: { _ in UIMenu(children: moves) }) |
| ... | @@ -601,6 +739,12 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -601,6 +739,12 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 601 | | 739 | |
| 602 | override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { | 740 | override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { |
| 603 | guard let item = dataSource.itemIdentifier(for: indexPath) else { return false } | 741 | guard let item = dataSource.itemIdentifier(for: indexPath) else { return false } |
| | 742 | if isEditing { |
| | 743 | switch item { |
| | 744 | case .notebook, .group, .section: return true |
| | 745 | default: return false |
| | 746 | } |
| | 747 | } |
| 604 | switch item { | 748 | switch item { |
| 605 | case .section(let id, let path): | 749 | case .section(let id, let path): |
| 606 | let tab = notebook(id)?.tabs.first(where: { $0.path == path }) | 750 | let tab = notebook(id)?.tabs.first(where: { $0.path == path }) |
| ... | @@ -631,6 +775,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -631,6 +775,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 631 | } | 775 | } |
| 632 | | 776 | |
| 633 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { | 777 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
| | 778 | if isEditing { return updateSelectionActions() } |
| 634 | guard case .section(let id, let path) = dataSource.itemIdentifier(for: indexPath), let notebook = notebook(id), | 779 | guard case .section(let id, let path) = dataSource.itemIdentifier(for: indexPath), let notebook = notebook(id), |
| 635 | let tab = notebook.tabs.first(where: { $0.path == path }) | 780 | let tab = notebook.tabs.first(where: { $0.path == path }) |
| 636 | else { return } | 781 | else { return } |
| ... | @@ -638,6 +783,10 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -638,6 +783,10 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 638 | onOpen?(tab, notebook) | 783 | onOpen?(tab, notebook) |
| 639 | } | 784 | } |
| 640 | | 785 | |
| | 786 | override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { |
| | 787 | if isEditing { updateSelectionActions() } |
| | 788 | } |
| | 789 | |
| 641 | /// Asks for a protected section's password, as OneNote's Protected Section dialog does, | 790 | /// Asks for a protected section's password, as OneNote's Protected Section dialog does, |
| 642 | /// and opens it once unlocked. | 791 | /// and opens it once unlocked. |
| 643 | private func unlock(_ tab: Tab, of notebook: Notebook, wrong: Bool) { | 792 | private func unlock(_ tab: Tab, of notebook: Notebook, wrong: Bool) { |
| ... | @@ -846,15 +995,10 @@ final class PagesViewController: UITableViewController { | ... | @@ -846,15 +995,10 @@ final class PagesViewController: UITableViewController { |
| 846 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "page") | 995 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "page") |
| 847 | navigationItem.searchController = search | 996 | navigationItem.searchController = search |
| 848 | navigationItem.hidesSearchBarWhenScrolling = false | 997 | navigationItem.hidesSearchBarWhenScrolling = false |
| 849 | let add = UIBarButtonItem( | | |
| 850 | title: "New Page", image: UIImage(systemName: "plus"), | | |
| 851 | primaryAction: UIAction { [weak self] _ in self?.newPage(under: nil) }) | | |
| 852 | if #available(iOS 26, *) { | 998 | if #available(iOS 26, *) { |
| 853 | navigationItem.preferredSearchBarPlacement = .integrated | 999 | navigationItem.preferredSearchBarPlacement = .integrated |
| 854 | toolbarItems = [navigationItem.searchBarPlacementBarButtonItem, .fixedSpace(), add] | | |
| 855 | } else { | | |
| 856 | toolbarItems = [.flexibleSpace(), add] | | |
| 857 | } | 1000 | } |
| | 1001 | updateSelectionActions() |
| 858 | NotificationCenter.default.addObserver( | 1002 | NotificationCenter.default.addObserver( |
| 859 | self, selector: #selector(changed), name: Section.changed, object: nil) | 1003 | self, selector: #selector(changed), name: Section.changed, object: nil) |
| 860 | setUpArranging() | 1004 | setUpArranging() |
| ... | @@ -885,6 +1029,7 @@ final class PagesViewController: UITableViewController { | ... | @@ -885,6 +1029,7 @@ final class PagesViewController: UITableViewController { |
| 885 | } | 1029 | } |
| 886 | | 1030 | |
| 887 | func loading(_ tab: Tab) { | 1031 | func loading(_ tab: Tab) { |
| | 1032 | if isEditing { setEditing(false, animated: false) } |
| 888 | section = nil | 1033 | section = nil |
| 889 | items = [] | 1034 | items = [] |
| 890 | title = tab.name | 1035 | title = tab.name |
| ... | @@ -901,6 +1046,7 @@ final class PagesViewController: UITableViewController { | ... | @@ -901,6 +1046,7 @@ final class PagesViewController: UITableViewController { |
| 901 | | 1046 | |
| 902 | /// Lets go of the section shown, as when its notebook goes. | 1047 | /// Lets go of the section shown, as when its notebook goes. |
| 903 | func clear() { | 1048 | func clear() { |
| | 1049 | if isEditing { setEditing(false, animated: false) } |
| 904 | section = nil | 1050 | section = nil |
| 905 | items = [] | 1051 | items = [] |
| 906 | title = nil | 1052 | title = nil |
| ... | @@ -929,6 +1075,7 @@ final class PagesViewController: UITableViewController { | ... | @@ -929,6 +1075,7 @@ final class PagesViewController: UITableViewController { |
| 929 | | 1075 | |
| 930 | func select(_ id: String) { | 1076 | func select(_ id: String) { |
| 931 | selected = id | 1077 | selected = id |
| | 1078 | guard !isEditing else { return } |
| 932 | if let index = items.firstIndex(where: { $0.id == id }) { | 1079 | if let index = items.firstIndex(where: { $0.id == id }) { |
| 933 | tableView.selectRow(at: IndexPath(row: index, section: 0), animated: false, scrollPosition: .none) | 1080 | tableView.selectRow(at: IndexPath(row: index, section: 0), animated: false, scrollPosition: .none) |
| 934 | } | 1081 | } |
| ... | @@ -939,12 +1086,72 @@ final class PagesViewController: UITableViewController { | ... | @@ -939,12 +1086,72 @@ final class PagesViewController: UITableViewController { |
| 939 | DispatchQueue.main.async { self.search.searchBar.becomeFirstResponder() } | 1086 | DispatchQueue.main.async { self.search.searchBar.becomeFirstResponder() } |
| 940 | } | 1087 | } |
| 941 | | 1088 | |
| | 1089 | override func setEditing(_ editing: Bool, animated: Bool) { |
| | 1090 | super.setEditing(editing, animated: animated) |
| | 1091 | for index in tableView.indexPathsForSelectedRows ?? [] { tableView.deselectRow(at: index, animated: false) } |
| | 1092 | tableView.dragInteractionEnabled = !editing |
| | 1093 | if !editing, let selected { select(selected) } |
| | 1094 | updateSelectionActions() |
| | 1095 | } |
| | 1096 | |
| | 1097 | private func updateSelectionActions() { |
| | 1098 | guard isEditing else { |
| | 1099 | let add = UIBarButtonItem(title: "New Page", image: UIImage(systemName: "plus"), primaryAction: UIAction { [weak self] _ in |
| | 1100 | self?.newPage(under: nil) |
| | 1101 | }) |
| | 1102 | if #available(iOS 26, *) { |
| | 1103 | toolbarItems = [navigationItem.searchBarPlacementBarButtonItem, .fixedSpace(), add] |
| | 1104 | } else { toolbarItems = [.flexibleSpace(), add] } |
| | 1105 | return |
| | 1106 | } |
| | 1107 | let selection = (tableView.indexPathsForSelectedRows ?? []).sorted().map { items[$0.row] } |
| | 1108 | let delete = UIBarButtonItem(title: "Delete", image: UIImage(systemName: "trash"), primaryAction: UIAction { [weak self] _ in |
| | 1109 | guard let self, let section else { return } |
| | 1110 | let remove = { [weak self] in |
| | 1111 | self?.setEditing(false, animated: true) |
| | 1112 | let ids = selection.filter { item in |
| | 1113 | item.version == nil || !selection.contains { $0.version == nil && $0.id == item.row.id } |
| | 1114 | }.map(\.id) |
| | 1115 | section.delete(ids) |
| | 1116 | } |
| | 1117 | if selection.contains(where: { $0.version != nil }) { |
| | 1118 | let alert = UIAlertController(title: "Delete \(selection.count) Pages?", message: "Conflict pages will be deleted permanently.", preferredStyle: .alert) |
| | 1119 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) |
| | 1120 | alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { _ in remove() }) |
| | 1121 | present(alert, animated: true) |
| | 1122 | } else { remove() } |
| | 1123 | }) |
| | 1124 | delete.tintColor = .systemRed |
| | 1125 | delete.isEnabled = !selection.isEmpty |
| | 1126 | var destinations: [UIAction] = [] |
| | 1127 | if let section, !selection.isEmpty, selection.allSatisfy({ $0.version == nil }) { |
| | 1128 | destinations = section.notebook.sections(besides: section.tab.path).map { tab in |
| | 1129 | UIAction(title: tab.name) { [weak self] _ in |
| | 1130 | self?.setEditing(false, animated: true) |
| | 1131 | for item in selection { |
| | 1132 | Arranging.move(page: item.id, of: section.notebook, from: section.tab.path, to: tab.path) |
| | 1133 | } |
| | 1134 | } |
| | 1135 | } |
| | 1136 | } |
| | 1137 | let move = UIBarButtonItem(title: "Move to Section", image: UIImage(systemName: "folder"), menu: UIMenu(children: destinations)) |
| | 1138 | move.isEnabled = !destinations.isEmpty |
| | 1139 | toolbarItems = [move, .flexibleSpace(), delete] |
| | 1140 | navigationController?.setToolbarHidden(false, animated: false) |
| | 1141 | } |
| | 1142 | |
| 942 | private func reload() { | 1143 | private func reload() { |
| | 1144 | let selection = tableView.indexPathsForSelectedRows?.map { items[$0.row].id } ?? [] |
| 943 | items = (section?.rows ?? []).flatMap { row in | 1145 | items = (section?.rows ?? []).flatMap { row in |
| 944 | [Item(row: row, version: nil)] + row.versions.map { Item(row: row, version: $0) } | 1146 | [Item(row: row, version: nil)] + row.versions.map { Item(row: row, version: $0) } |
| 945 | } | 1147 | } |
| 946 | tableView.reloadData() | 1148 | tableView.reloadData() |
| 947 | if let selected { select(selected) } | 1149 | if isEditing { |
| | 1150 | for (index, item) in items.enumerated() where selection.contains(item.id) { |
| | 1151 | tableView.selectRow(at: IndexPath(row: index, section: 0), animated: false, scrollPosition: .none) |
| | 1152 | } |
| | 1153 | updateSelectionActions() |
| | 1154 | } else if let selected { select(selected) } |
| 948 | var empty = UIContentUnavailableConfiguration.empty() | 1155 | var empty = UIContentUnavailableConfiguration.empty() |
| 949 | empty.text = "No Pages" | 1156 | empty.text = "No Pages" |
| 950 | contentUnavailableConfiguration = section != nil && items.isEmpty ? empty : nil | 1157 | contentUnavailableConfiguration = section != nil && items.isEmpty ? empty : nil |
| ... | @@ -961,7 +1168,7 @@ final class PagesViewController: UITableViewController { | ... | @@ -961,7 +1168,7 @@ final class PagesViewController: UITableViewController { |
| 961 | } | 1168 | } |
| 962 | | 1169 | |
| 963 | private func delete(_ item: Item) { | 1170 | private func delete(_ item: Item) { |
| 964 | section?.delete(item.id) | 1171 | section?.delete([item.id]) |
| 965 | } | 1172 | } |
| 966 | | 1173 | |
| 967 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { items.count } | 1174 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { items.count } |
| ... | @@ -997,16 +1204,21 @@ final class PagesViewController: UITableViewController { | ... | @@ -997,16 +1204,21 @@ final class PagesViewController: UITableViewController { |
| 997 | } | 1204 | } |
| 998 | | 1205 | |
| 999 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { | 1206 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
| | 1207 | if isEditing { return updateSelectionActions() } |
| 1000 | guard let section else { return } | 1208 | guard let section else { return } |
| 1001 | selected = items[indexPath.row].id | 1209 | selected = items[indexPath.row].id |
| 1002 | onOpen?(section, items[indexPath.row].id) | 1210 | onOpen?(section, items[indexPath.row].id) |
| 1003 | } | 1211 | } |
| 1004 | | 1212 | |
| | 1213 | override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { |
| | 1214 | if isEditing { updateSelectionActions() } |
| | 1215 | } |
| | 1216 | |
| 1005 | override func tableView( | 1217 | override func tableView( |
| 1006 | _ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath | 1218 | _ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath |
| 1007 | ) -> UISwipeActionsConfiguration? { | 1219 | ) -> UISwipeActionsConfiguration? { |
| 1008 | // A swipe across the list changes section instead; the context menu deletes. | 1220 | // A swipe across the list changes section instead; the context menu deletes. |
| 1009 | if Prototype.sectionStrip { return nil } | 1221 | if isEditing || Prototype.sectionStrip { return nil } |
| 1010 | let item = items[indexPath.row] | 1222 | let item = items[indexPath.row] |
| 1011 | let delete = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, done in | 1223 | let delete = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, done in |
| 1012 | self?.delete(item) | 1224 | self?.delete(item) |
| ... | @@ -1019,6 +1231,7 @@ final class PagesViewController: UITableViewController { | ... | @@ -1019,6 +1231,7 @@ final class PagesViewController: UITableViewController { |
| 1019 | override func tableView( | 1231 | override func tableView( |
| 1020 | _ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint | 1232 | _ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint |
| 1021 | ) -> UIContextMenuConfiguration? { | 1233 | ) -> UIContextMenuConfiguration? { |
| | 1234 | guard !isEditing else { return nil } |
| 1022 | let item = items[indexPath.row] | 1235 | let item = items[indexPath.row] |
| 1023 | return UIContextMenuConfiguration(actionProvider: { [weak self] _ in | 1236 | return UIContextMenuConfiguration(actionProvider: { [weak self] _ in |
| 1024 | var actions: [UIMenuElement] = [] | 1237 | var actions: [UIMenuElement] = [] |