| ... | @@ -21,16 +21,12 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -21,16 +21,12 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 21 | case section(notebook: String, path: String) | 21 | case section(notebook: String, path: String) |
| 22 | /// Opening, or why the notebook cannot be. | 22 | /// Opening, or why the notebook cannot be. |
| 23 | case status(notebook: String) | 23 | case status(notebook: String) |
| 24 | case newNotebook, openFolder, connect | | |
| 25 | /// Copies the Snowbound Guide to On My iPhone and opens it, until it is there. | | |
| 26 | case guide | | |
| 27 | /// New Notebook in iCloud Drive, and the way to it while iCloud Drive is off. | | |
| 28 | case newInCloud, turnOnICloud | | |
| 29 | } | 24 | } |
| 30 | | 25 | |
| 31 | private var dataSource: UICollectionViewDiffableDataSource<Location, Item>! | 26 | private var dataSource: UICollectionViewDiffableDataSource<Location, Item>! |
| 32 | private var collapsed: Set<Item> = [] | 27 | private var collapsed: Set<Item> = [] |
| 33 | private lazy var sync = SyncIndicator(in: self) | 28 | /// What each notebook's row shows of its sync status, by notebook id. |
| | 29 | private var attention: [String: Sync.Attention] = [:] |
| 34 | private var selected: Item? | 30 | private var selected: Item? |
| 35 | var onOpen: ((Tab, Notebook) -> Void)? | 31 | var onOpen: ((Tab, Notebook) -> Void)? |
| 36 | | 32 | |
| ... | @@ -77,42 +73,82 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -77,42 +73,82 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 77 | let add = UIMenu(children: [ | 73 | let add = UIMenu(children: [ |
| 78 | UIDeferredMenuElement.uncached { [weak self] provide in | 74 | UIDeferredMenuElement.uncached { [weak self] provide in |
| 79 | guard let self else { return provide([]) } | 75 | guard let self else { return provide([]) } |
| 80 | var actions: [UIMenuElement] = [] | 76 | var places: [UIMenuElement] = [] |
| 81 | if ICloud.documents != nil || Notebooks.showsOnDevice { | 77 | if ICloud.documents != nil { |
| 82 | actions.append( | 78 | places.append( |
| 83 | UIAction(title: "New Notebook…", image: UIImage(systemName: "plus")) { [weak self] _ in | 79 | UIAction(title: "iCloud Drive", image: UIImage(systemName: "icloud")) { [weak self] _ in |
| 84 | self?.newNotebook(inCloud: ICloud.documents != nil) | 80 | self?.newNotebook(inCloud: true) |
| | 81 | }) |
| | 82 | } else if !ICloud.signedIn { |
| | 83 | places.append( |
| | 84 | UIAction(title: "Turn On iCloud Drive", image: UIImage(systemName: "icloud")) { _ in |
| | 85 | if let settings = URL(string: UIApplication.openSettingsURLString) { |
| | 86 | UIApplication.shared.open(settings) |
| | 87 | } |
| 85 | }) | 88 | }) |
| 86 | } | 89 | } |
| 87 | actions += [ | 90 | places.append( |
| | 91 | UIAction(title: Self.device, image: UIImage(systemName: "iphone")) { [weak self] _ in |
| | 92 | self?.newNotebook(inCloud: false) |
| | 93 | }) |
| | 94 | var actions: [UIMenuElement] = [ |
| | 95 | UIMenu(title: "New Notebook", options: .displayInline, children: places), |
| | 96 | UIAction(title: "Open from Server…", image: UIImage(systemName: "server.rack")) { [weak self] _ in |
| | 97 | self?.connect() |
| | 98 | }, |
| 88 | UIAction(title: "Open Folder…", image: UIImage(systemName: "folder")) { [weak self] _ in | 99 | UIAction(title: "Open Folder…", image: UIImage(systemName: "folder")) { [weak self] _ in |
| 89 | self?.openFolder() | 100 | self?.openFolder() |
| 90 | }, | 101 | }, |
| 91 | UIAction(title: "Connect to Server…", image: UIImage(systemName: "server.rack")) { [weak self] _ in | | |
| 92 | self?.connect() | | |
| 93 | }, | | |
| 94 | ] | 102 | ] |
| 95 | var more: [UIMenuElement] = [] | 103 | let copied = Notebooks.onDevice.contains { $0.source == .documents(path: Notebooks.guide) } |
| | 104 | if Notebooks.guideOffered && !copied { |
| | 105 | actions.append( |
| | 106 | UIAction(title: "Open the Snowbound Guide", image: UIImage(systemName: "book")) { [weak self] _ in |
| | 107 | self?.openGuide() |
| | 108 | }) |
| | 109 | } |
| | 110 | provide(actions) |
| | 111 | } |
| | 112 | ]) |
| | 113 | let more = UIMenu(children: [ |
| | 114 | UIDeferredMenuElement.uncached { [weak self] provide in |
| | 115 | var actions: [UIMenuElement] = [ |
| | 116 | UIAction(title: "Sync Status…", image: UIImage(systemName: "arrow.triangle.2.circlepath")) { |
| | 117 | [weak self] _ in self.map(SyncViewController.present) |
| | 118 | } |
| | 119 | ] |
| 96 | if !Notebooks.showsOnDevice { | 120 | if !Notebooks.showsOnDevice { |
| 97 | more.append( | 121 | actions.append( |
| 98 | UIAction(title: "Show \(Self.device)", image: UIImage(systemName: "eye")) { [weak self] _ in | 122 | UIAction(title: "Show \(Self.device)", image: UIImage(systemName: "eye")) { [weak self] _ in |
| 99 | self?.showOnDevice(true) | 123 | self?.showOnDevice(true) |
| 100 | }) | 124 | }) |
| 101 | } | 125 | } |
| 102 | more.append( | 126 | actions.append( |
| 103 | UIAction(title: "Personalize…", image: UIImage(systemName: "person.crop.circle")) { [weak self] _ in | 127 | UIAction(title: "Personalize…", image: UIImage(systemName: "person.crop.circle")) { [weak self] _ in |
| 104 | guard let self else { return } | 128 | guard let self else { return } |
| 105 | Author.ask(from: self) {} | 129 | Author.ask(from: self) {} |
| 106 | }) | 130 | }) |
| 107 | more.append(Appearance.menu()) | 131 | actions.append(Appearance.menu()) |
| 108 | more.append(Editing.fontMenu()) | 132 | actions.append(Editing.fontMenu()) |
| 109 | provide(actions + [UIMenu(options: .displayInline, children: more)]) | 133 | provide(actions) |
| 110 | } | 134 | } |
| 111 | ]) | 135 | ]) |
| 112 | navigationItem.rightBarButtonItem = UIBarButtonItem( | 136 | navigationItem.rightBarButtonItems = [ |
| 113 | title: "Add Notebook", image: UIImage(systemName: "plus"), menu: add) | 137 | UIBarButtonItem(title: "Add Notebook", image: UIImage(systemName: "plus"), menu: add), |
| | 138 | UIBarButtonItem(title: "More", image: UIImage(systemName: "ellipsis"), menu: more), |
| | 139 | ] |
| 114 | navigationItem.searchController = SearchViewController.controller() | 140 | navigationItem.searchController = SearchViewController.controller() |
| 115 | toolbarItems = [.flexibleSpace(), sync, .flexibleSpace()] | 141 | let corner = Prototype.quickNote ? [QuickNote.item()] : [] |
| | 142 | if #available(iOS 26, *) { |
| | 143 | // Search sits in the bottom toolbar, as Notes and Files put it. |
| | 144 | navigationItem.preferredSearchBarPlacement = .integrated |
| | 145 | toolbarItems = [navigationItem.searchBarPlacementBarButtonItem] + (corner.isEmpty ? [] : [.fixedSpace()] + corner) |
| | 146 | } else { |
| | 147 | toolbarItems = corner.isEmpty ? [] : [.flexibleSpace()] + corner |
| | 148 | } |
| | 149 | for name in [Sync.changed, Section.changed] { |
| | 150 | NotificationCenter.default.addObserver(self, selector: #selector(refreshSync), name: name, object: nil) |
| | 151 | } |
| 116 | NotificationCenter.default.addObserver(forName: Notebook.listed, object: nil, queue: .main) { [weak self] _ in | 152 | NotificationCenter.default.addObserver(forName: Notebook.listed, object: nil, queue: .main) { [weak self] _ in |
| 117 | guard let self else { return } | 153 | guard let self else { return } |
| 118 | reload() | 154 | reload() |
| ... | @@ -126,7 +162,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -126,7 +162,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 126 | override func viewWillAppear(_ animated: Bool) { | 162 | override func viewWillAppear(_ animated: Bool) { |
| 127 | super.viewWillAppear(animated) | 163 | super.viewWillAppear(animated) |
| 128 | navigationController?.navigationBar.prefersLargeTitles = true | 164 | navigationController?.navigationBar.prefersLargeTitles = true |
| 129 | navigationController?.setToolbarHidden(false, animated: false) | 165 | navigationController?.setToolbarHidden(toolbarItems?.isEmpty != false, animated: false) |
| 130 | } | 166 | } |
| 131 | | 167 | |
| 132 | private func notebook(_ id: String) -> Notebook? { Notebooks.all.first { $0.id == id } } | 168 | private func notebook(_ id: String) -> Notebook? { Notebooks.all.first { $0.id == id } } |
| ... | @@ -140,23 +176,32 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -140,23 +176,32 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 140 | contentUnavailableConfiguration = welcome | 176 | contentUnavailableConfiguration = welcome |
| 141 | if welcome != nil { return dataSource.apply(.init(), animatingDifferences: false) } | 177 | if welcome != nil { return dataSource.apply(.init(), animatingDifferences: false) } |
| 142 | } | 178 | } |
| 143 | let copied = Notebooks.onDevice.contains { $0.source == .documents(path: Notebooks.guide) } | 179 | // A place with no notebooks is left out; the Add Notebook menu reaches every place. |
| 144 | let guide: [Item] = Notebooks.guideOffered && !copied ? [.guide] : [] | 180 | let locations: [(Location, [Notebook])] = [ |
| 145 | let cloud: [(Location, [Notebook], [Item])] = | 181 | (.icloud, Notebooks.inCloud), (.onDevice, Notebooks.onDevice), (.elsewhere, Notebooks.elsewhere), |
| 146 | ICloud.documents != nil | 182 | ].filter { !$0.1.isEmpty } |
| 147 | ? [(.icloud, Notebooks.inCloud, [.newInCloud])] | 183 | if locations.isEmpty { |
| 148 | : ICloud.signedIn ? [] : [(.icloud, [], [.turnOnICloud])] | 184 | var empty = UIContentUnavailableConfiguration.empty() |
| 149 | let locations: [(Location, [Notebook], [Item])] = | 185 | empty.text = "No Notebooks" |
| 150 | cloud + (Notebooks.showsOnDevice ? [(.onDevice, Notebooks.onDevice, [.newNotebook])] : []) | 186 | empty.secondaryText = "Create a notebook, or open one from a server or folder." |
| 151 | + [(.elsewhere, Notebooks.elsewhere, [.openFolder, .connect] + guide)] | 187 | var button = UIButton.Configuration.borderedProminent() |
| | 188 | button.title = "New Notebook" |
| | 189 | empty.button = button |
| | 190 | empty.buttonProperties.primaryAction = UIAction { [weak self] _ in |
| | 191 | self?.newNotebook(inCloud: ICloud.documents != nil) |
| | 192 | } |
| | 193 | contentUnavailableConfiguration = empty |
| | 194 | } else { |
| | 195 | contentUnavailableConfiguration = nil |
| | 196 | } |
| 152 | var sections = NSDiffableDataSourceSnapshot<Location, Item>() | 197 | var sections = NSDiffableDataSourceSnapshot<Location, Item>() |
| 153 | sections.appendSections(locations.map(\.0)) | 198 | sections.appendSections(locations.map(\.0)) |
| 154 | dataSource.apply(sections, animatingDifferences: false) | 199 | dataSource.apply(sections, animatingDifferences: false) |
| 155 | for (location, notebooks, actions) in locations { | 200 | for (location, notebooks) in locations { |
| 156 | var list = NSDiffableDataSourceSectionSnapshot<Item>() | 201 | var list = NSDiffableDataSourceSectionSnapshot<Item>() |
| 157 | let header = Item.location(location) | 202 | let header = Item.location(location) |
| 158 | list.append([header]) | 203 | list.append([header]) |
| 159 | list.append(notebooks.map { .notebook($0.id) } + actions, to: header) | 204 | list.append(notebooks.map { .notebook($0.id) }, to: header) |
| 160 | for notebook in notebooks { | 205 | for notebook in notebooks { |
| 161 | let parent = Item.notebook(notebook.id) | 206 | let parent = Item.notebook(notebook.id) |
| 162 | guard notebook.handle != nil else { | 207 | guard notebook.handle != nil else { |
| ... | @@ -179,6 +224,36 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -179,6 +224,36 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 179 | dataSource.apply(list, to: location, animatingDifferences: false) | 224 | dataSource.apply(list, to: location, animatingDifferences: false) |
| 180 | } | 225 | } |
| 181 | showSelection() | 226 | showSelection() |
| | 227 | refreshSync() |
| | 228 | } |
| | 229 | |
| | 230 | /// Reads every notebook's sync status, marks the rows it changed, and tells of a notebook |
| | 231 | /// that has just stopped syncing with edits waiting. |
| | 232 | @objc private func refreshSync() { |
| | 233 | Sync.status { [weak self] notebooks in |
| | 234 | guard let self else { return } |
| | 235 | var changed: [Item] = [] |
| | 236 | for (notebook, sections) in notebooks { |
| | 237 | let before = attention[notebook.id] |
| | 238 | let after = Sync.attention(sections) |
| | 239 | guard before != after else { continue } |
| | 240 | attention[notebook.id] = after |
| | 241 | changed.append(.notebook(notebook.id)) |
| | 242 | // The list is off screen on a phone while a page shows. |
| | 243 | if let toast = Sync.toast(notebook.name, from: before, to: after), |
| | 244 | let window = splitViewController?.view.window ?? view.window |
| | 245 | { |
| | 246 | SyncToast.show(toast.title, toast.detail, in: window) { [weak self] in |
| | 247 | self.map(SyncViewController.present) |
| | 248 | } |
| | 249 | } |
| | 250 | } |
| | 251 | var shown = dataSource.snapshot() |
| | 252 | let items = changed.filter { shown.indexOfItem($0) != nil } |
| | 253 | guard !items.isEmpty else { return } |
| | 254 | shown.reconfigureItems(items) |
| | 255 | dataSource.apply(shown, animatingDifferences: false) |
| | 256 | } |
| 182 | } | 257 | } |
| 183 | | 258 | |
| 184 | /// Lists Documents and iCloud Drive again and opens the notebooks new there; `opened` runs | 259 | /// Lists Documents and iCloud Drive again and opens the notebooks new there; `opened` runs |
| ... | @@ -234,13 +309,23 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -234,13 +309,23 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 234 | content.text = notebook.name | 309 | content.text = notebook.name |
| 235 | content.image = UIImage(systemName: "book.closed.fill") | 310 | content.image = UIImage(systemName: "book.closed.fill") |
| 236 | content.imageProperties.tintColor = notebook.color | 311 | content.imageProperties.tintColor = notebook.color |
| 237 | if case .server(let server) = notebook.source { content.secondaryText = server.host } | 312 | var details: [String] = [] |
| | 313 | if case .server(let server) = notebook.source { details.append(server.host) } |
| | 314 | if let state = attention[id]?.state { |
| | 315 | details.append(Sync.label(state)) |
| | 316 | let mark = UIImageView(image: UIImage(systemName: Sync.symbol(state))) |
| | 317 | mark.tintColor = state == .notConnected || Sync.offline ? .secondaryLabel : .systemOrange |
| | 318 | mark.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .body) |
| | 319 | accessories.append(.customView(configuration: .init(customView: mark, placement: .trailing(displayed: .always)))) |
| | 320 | } |
| | 321 | content.secondaryText = details.isEmpty ? nil : details.joined(separator: " · ") |
| | 322 | content.secondaryTextProperties.color = .secondaryLabel |
| 238 | let more = UIButton(type: .system) | 323 | let more = UIButton(type: .system) |
| 239 | more.setImage(UIImage(systemName: "ellipsis.circle"), for: .normal) | 324 | more.setImage(UIImage(systemName: "ellipsis.circle"), for: .normal) |
| 240 | more.accessibilityLabel = "More" | 325 | more.accessibilityLabel = "More" |
| 241 | more.showsMenuAsPrimaryAction = true | 326 | more.showsMenuAsPrimaryAction = true |
| 242 | more.menu = menu(for: notebook) | 327 | more.menu = menu(for: notebook) |
| 243 | accessories = [ | 328 | accessories += [ |
| 244 | .customView(configuration: .init(customView: more, placement: .trailing(displayed: .always))), | 329 | .customView(configuration: .init(customView: more, placement: .trailing(displayed: .always))), |
| 245 | .outlineDisclosure(options: .init(style: .cell)), | 330 | .outlineDisclosure(options: .init(style: .cell)), |
| 246 | ] | 331 | ] |
| ... | @@ -276,32 +361,25 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -276,32 +361,25 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 276 | spinner.startAnimating() | 361 | spinner.startAnimating() |
| 277 | accessories = [.customView(configuration: .init(customView: spinner, placement: .trailing()))] | 362 | accessories = [.customView(configuration: .init(customView: spinner, placement: .trailing()))] |
| 278 | } | 363 | } |
| 279 | case .turnOnICloud: | | |
| 280 | content.text = "Turn On iCloud Drive" | | |
| 281 | content.secondaryText = "Keep notebooks in iCloud to use them on all your devices." | | |
| 282 | content.image = UIImage(systemName: "icloud") | | |
| 283 | content.textProperties.color = .tintColor | | |
| 284 | case .newInCloud: | | |
| 285 | content.text = "New Notebook…" | | |
| 286 | content.image = UIImage(systemName: "plus") | | |
| 287 | content.textProperties.color = .tintColor | | |
| 288 | case .guide: | | |
| 289 | content.text = "Open the Snowbound Guide" | | |
| 290 | content.image = UIImage(systemName: "book") | | |
| 291 | content.textProperties.color = .tintColor | | |
| 292 | case .newNotebook, .openFolder, .connect: | | |
| 293 | content.text = | | |
| 294 | item == .newNotebook ? "New Notebook…" : item == .openFolder ? "Open Folder…" : "Connect to Server…" | | |
| 295 | content.image = UIImage( | | |
| 296 | systemName: item == .newNotebook ? "plus" : item == .openFolder ? "folder" : "server.rack") | | |
| 297 | content.textProperties.color = .tintColor | | |
| 298 | } | 364 | } |
| 299 | cell.contentConfiguration = content | 365 | cell.contentConfiguration = content |
| 300 | cell.accessories = accessories | 366 | cell.accessories = accessories |
| 301 | } | 367 | } |
| 302 | | 368 | |
| 303 | private func menu(for notebook: Notebook) -> UIMenu { | 369 | private func menu(for notebook: Notebook) -> UIMenu { |
| 304 | var actions = [ | 370 | // Why the notebook isn't syncing and when it last did, while it isn't. |
| | 371 | let status: [UIMenuElement] = |
| | 372 | attention[notebook.id].map { attention in |
| | 373 | let details = UIAction( |
| | 374 | title: Sync.label(attention.state), subtitle: attention.detail.isEmpty ? nil : attention.detail, |
| | 375 | image: UIImage(systemName: Sync.symbol(attention.state)) |
| | 376 | ) { [weak self] _ in self.map(SyncViewController.present) } |
| | 377 | let now = UIAction(title: "Sync Now", image: UIImage(systemName: "arrow.clockwise")) { _ in |
| | 378 | if let handle = notebook.handle { sb_library_sync_now(handle) } |
| | 379 | } |
| | 380 | return [UIMenu(options: .displayInline, children: [details, now])] |
| | 381 | } ?? [] |
| | 382 | let actions = [ |
| 305 | UIAction(title: "New Section…", image: UIImage(systemName: "plus.rectangle.portrait")) { | 383 | UIAction(title: "New Section…", image: UIImage(systemName: "plus.rectangle.portrait")) { |
| 306 | [weak self] _ in self?.newSection(in: notebook) | 384 | [weak self] _ in self?.newSection(in: notebook) |
| 307 | }, | 385 | }, |
| ... | @@ -317,7 +395,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -317,7 +395,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 317 | Notebooks.remove(notebook) | 395 | Notebooks.remove(notebook) |
| 318 | self?.reload() | 396 | self?.reload() |
| 319 | } | 397 | } |
| 320 | return UIMenu(children: actions + [close]) | 398 | return UIMenu(children: status + actions + [close]) |
| 321 | } | 399 | } |
| 322 | let file = [ | 400 | let file = [ |
| 323 | UIAction(title: "Rename…", image: UIImage(systemName: "pencil")) { [weak self] _ in | 401 | UIAction(title: "Rename…", image: UIImage(systemName: "pencil")) { [weak self] _ in |
| ... | @@ -337,7 +415,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -337,7 +415,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 337 | let delete = UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: .destructive) { | 415 | let delete = UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: .destructive) { |
| 338 | [weak self] _ in self?.delete(folder, of: notebook) | 416 | [weak self] _ in self?.delete(folder, of: notebook) |
| 339 | } | 417 | } |
| 340 | return UIMenu(children: [ | 418 | return UIMenu(children: status + [ |
| 341 | UIMenu(options: .displayInline, children: actions), UIMenu(options: .displayInline, children: file), delete, | 419 | UIMenu(options: .displayInline, children: actions), UIMenu(options: .displayInline, children: file), delete, |
| 342 | ]) | 420 | ]) |
| 343 | } | 421 | } |
| ... | @@ -538,13 +616,6 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke | ... | @@ -538,13 +616,6 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 538 | collapsed.remove(item) | 616 | collapsed.remove(item) |
| 539 | } | 617 | } |
| 540 | dataSource.apply(list, to: location) | 618 | dataSource.apply(list, to: location) |
| 541 | case .newNotebook: newNotebook(inCloud: false) | | |
| 542 | case .newInCloud: newNotebook(inCloud: true) | | |
| 543 | case .turnOnICloud: | | |
| 544 | if let settings = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(settings) } | | |
| 545 | case .openFolder: openFolder() | | |
| 546 | case .connect: connect() | | |
| 547 | case .guide: openGuide() | | |
| 548 | case .location: break | 619 | case .location: break |
| 549 | } | 620 | } |
| 550 | return false | 621 | return false |
| ... | @@ -745,7 +816,6 @@ final class PagesViewController: UITableViewController { | ... | @@ -745,7 +816,6 @@ final class PagesViewController: UITableViewController { |
| 745 | | 816 | |
| 746 | private(set) var section: Section? | 817 | private(set) var section: Section? |
| 747 | private var items: [Item] = [] | 818 | private var items: [Item] = [] |
| 748 | private lazy var sync = SyncIndicator(in: self) | | |
| 749 | private var selected: String? | 819 | private var selected: String? |
| 750 | private let search = SearchViewController.controller() | 820 | private let search = SearchViewController.controller() |
| 751 | /// Opens a page of the section. | 821 | /// Opens a page of the section. |
| ... | @@ -767,12 +837,15 @@ final class PagesViewController: UITableViewController { | ... | @@ -767,12 +837,15 @@ final class PagesViewController: UITableViewController { |
| 767 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "page") | 837 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "page") |
| 768 | navigationItem.searchController = search | 838 | navigationItem.searchController = search |
| 769 | navigationItem.hidesSearchBarWhenScrolling = false | 839 | navigationItem.hidesSearchBarWhenScrolling = false |
| 770 | let compose = UIBarButtonItem( | 840 | let add = UIBarButtonItem( |
| 771 | title: "New Page", image: UIImage(systemName: "square.and.pencil"), | 841 | title: "New Page", image: UIImage(systemName: "plus"), |
| 772 | primaryAction: UIAction { [weak self] _ in self?.newPage(under: nil) }) | 842 | primaryAction: UIAction { [weak self] _ in self?.newPage(under: nil) }) |
| 773 | toolbarItems = [ | 843 | if #available(iOS 26, *) { |
| 774 | .flexibleSpace(), sync, .flexibleSpace(), compose, | 844 | navigationItem.preferredSearchBarPlacement = .integrated |
| 775 | ] | 845 | toolbarItems = [navigationItem.searchBarPlacementBarButtonItem, .fixedSpace(), add] |
| | 846 | } else { |
| | 847 | toolbarItems = [.flexibleSpace(), add] |
| | 848 | } |
| 776 | NotificationCenter.default.addObserver( | 849 | NotificationCenter.default.addObserver( |
| 777 | self, selector: #selector(changed), name: Section.changed, object: nil) | 850 | self, selector: #selector(changed), name: Section.changed, object: nil) |
| 778 | if Prototype.sectionStrip { | 851 | if Prototype.sectionStrip { |