| ... | @@ -97,9 +97,17 @@ enum Sync { | ... | @@ -97,9 +97,17 @@ enum Sync { |
| 97 | } | 97 | } |
| 98 | | 98 | |
| 99 | /// The sync status in a list's toolbar, as Mail shows when it last checked; opens the sync | 99 | /// The sync status in a list's toolbar, as Mail shows when it last checked; opens the sync |
| 100 | /// sheet. | 100 | /// sheet. The item leaves `owner`'s toolbar while there is nothing to report (hidden, its |
| 101 | final class SyncIndicator: UIButton { | 101 | /// custom view would leave the bar's glass behind), so the owner keeps it. |
| 102 | init() { | 102 | final class SyncIndicator: UIBarButtonItem { |
| | 103 | private let button = UIButton(type: .system) |
| | 104 | private weak var owner: UIViewController? |
| | 105 | /// Where in the toolbar the item goes back to. |
| | 106 | private var slot = 0 |
| | 107 | |
| | 108 | init(in owner: UIViewController) { |
| | 109 | self.owner = owner |
| | 110 | super.init() |
| 103 | var configuration = UIButton.Configuration.plain() | 111 | var configuration = UIButton.Configuration.plain() |
| 104 | configuration.imagePadding = 4 | 112 | configuration.imagePadding = 4 |
| 105 | configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .caption1) | 113 | configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .caption1) |
| ... | @@ -110,9 +118,9 @@ final class SyncIndicator: UIButton { | ... | @@ -110,9 +118,9 @@ final class SyncIndicator: UIButton { |
| 110 | attributes.font = .preferredFont(forTextStyle: .caption1) | 118 | attributes.font = .preferredFont(forTextStyle: .caption1) |
| 111 | return attributes | 119 | return attributes |
| 112 | } | 120 | } |
| 113 | super.init(frame: .zero) | 121 | button.configuration = configuration |
| 114 | self.configuration = configuration | 122 | button.addAction(UIAction { [weak self] _ in self?.present() }, for: .primaryActionTriggered) |
| 115 | addAction(UIAction { [weak self] _ in self?.present() }, for: .primaryActionTriggered) | 123 | customView = button |
| 116 | for name in [Sync.changed, Section.changed] { | 124 | for name in [Sync.changed, Section.changed] { |
| 117 | NotificationCenter.default.addObserver(self, selector: #selector(refresh), name: name, object: nil) | 125 | NotificationCenter.default.addObserver(self, selector: #selector(refresh), name: name, object: nil) |
| 118 | } | 126 | } |
| ... | @@ -124,23 +132,35 @@ final class SyncIndicator: UIButton { | ... | @@ -124,23 +132,35 @@ final class SyncIndicator: UIButton { |
| 124 | @objc private func refresh() { | 132 | @objc private func refresh() { |
| 125 | Sync.status { [weak self] notebooks in | 133 | Sync.status { [weak self] notebooks in |
| 126 | let sections = notebooks.flatMap(\.1) | 134 | let sections = notebooks.flatMap(\.1) |
| 127 | guard let self, let worst = sections.map(\.state).max() else { | 135 | guard let self else { return } |
| 128 | self?.isHidden = true | 136 | guard let worst = sections.map(\.state).max() else { return show(false) } |
| 129 | return | | |
| 130 | } | | |
| 131 | isHidden = false | | |
| 132 | let queued = sections.map(\.queued).reduce(0, +) | 137 | let queued = sections.map(\.queued).reduce(0, +) |
| 133 | let label = Sync.label(worst) | 138 | let label = Sync.label(worst) |
| 134 | configuration?.title = queued > 0 && worst != .syncing ? "\(label), \(Sync.changes(queued))" : label | 139 | button.configuration?.title = queued > 0 && worst != .syncing ? "\(label), \(Sync.changes(queued))" : label |
| 135 | configuration?.image = UIImage(systemName: Sync.symbol(worst)) | 140 | button.configuration?.image = UIImage(systemName: Sync.symbol(worst)) |
| 136 | configuration?.baseForegroundColor = worst >= .notConnected && !Sync.offline ? .systemOrange : .secondaryLabel | 141 | button.configuration?.baseForegroundColor = |
| 137 | accessibilityLabel = "Sync status: \(configuration?.title ?? label)" | 142 | worst >= .notConnected && !Sync.offline ? .systemOrange : .secondaryLabel |
| 138 | sizeToFit() | 143 | button.accessibilityLabel = "Sync status: \(button.configuration?.title ?? label)" |
| | 144 | button.sizeToFit() |
| | 145 | show(true) |
| | 146 | } |
| | 147 | } |
| | 148 | |
| | 149 | /// Takes the item out of its owner's toolbar, or puts it back where it was. |
| | 150 | private func show(_ shown: Bool) { |
| | 151 | guard let owner, var items = owner.toolbarItems else { return } |
| | 152 | switch (shown, items.firstIndex { $0 === self }) { |
| | 153 | case (true, nil): items.insert(self, at: min(slot, items.count)) |
| | 154 | case (false, let at?): |
| | 155 | slot = at |
| | 156 | items.remove(at: at) |
| | 157 | default: return |
| 139 | } | 158 | } |
| | 159 | owner.setToolbarItems(items, animated: true) |
| 140 | } | 160 | } |
| 141 | | 161 | |
| 142 | private func present() { | 162 | private func present() { |
| 143 | guard let controller = window?.rootViewController else { return } | 163 | guard let controller = button.window?.rootViewController else { return } |
| 144 | let navigation = UINavigationController(rootViewController: SyncViewController()) | 164 | let navigation = UINavigationController(rootViewController: SyncViewController()) |
| 145 | navigation.sheetPresentationController?.detents = [.medium(), .large()] | 165 | navigation.sheetPresentationController?.detents = [.medium(), .large()] |
| 146 | navigation.sheetPresentationController?.prefersGrabberVisible = true | 166 | navigation.sheetPresentationController?.prefersGrabberVisible = true |