|
|
@@ -19,6 +19,31 @@ struct PrintableContact: Equatable {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+private enum ContactSortOrder: String, CaseIterable {
|
|
|
+ case nameAscending
|
|
|
+ case nameDescending
|
|
|
+
|
|
|
+ var title: String {
|
|
|
+ switch self {
|
|
|
+ case .nameAscending: "Names A to Z"
|
|
|
+ case .nameDescending: "Names Z to A"
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func sort(_ contacts: [PrintableContact]) -> [PrintableContact] {
|
|
|
+ switch self {
|
|
|
+ case .nameAscending:
|
|
|
+ contacts.sorted {
|
|
|
+ $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending
|
|
|
+ }
|
|
|
+ case .nameDescending:
|
|
|
+ contacts.sorted {
|
|
|
+ $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedDescending
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// MARK: - Contacts Service
|
|
|
|
|
|
enum ContactsError: Error {
|
|
|
@@ -141,6 +166,7 @@ final class PrintContactsOverlayView: NSView, AppearanceRefreshable {
|
|
|
var onDismiss: (() -> Void)?
|
|
|
|
|
|
private let backButton = PrintContactsToolbarButton(symbolName: "chevron.left", accessibilityLabel: "Back")
|
|
|
+ private let sortButton = PrintContactsSortButton()
|
|
|
private let searchContainer = NSView()
|
|
|
private let searchIconView = NSImageView()
|
|
|
private let searchField = NSTextField()
|
|
|
@@ -158,6 +184,7 @@ final class PrintContactsOverlayView: NSView, AppearanceRefreshable {
|
|
|
private let contactRowHeight: CGFloat = 52
|
|
|
|
|
|
private var allContacts: [PrintableContact] = []
|
|
|
+ private var sortOrder: ContactSortOrder = .nameAscending
|
|
|
private var selectedIDs = Set<String>()
|
|
|
private var rowViews: [ContactRowView] = []
|
|
|
|
|
|
@@ -195,6 +222,7 @@ final class PrintContactsOverlayView: NSView, AppearanceRefreshable {
|
|
|
searchIconView.contentTintColor = AppTheme.textSecondary
|
|
|
emptyLabel.textColor = AppTheme.textSecondary
|
|
|
backButton.refreshAppearance()
|
|
|
+ sortButton.refreshAppearance()
|
|
|
selectAllRow.refreshAppearance()
|
|
|
printButton.refreshAppearance()
|
|
|
rowViews.forEach { $0.refreshAppearance() }
|
|
|
@@ -284,14 +312,21 @@ final class PrintContactsOverlayView: NSView, AppearanceRefreshable {
|
|
|
scrollView.documentView = listDocumentView
|
|
|
|
|
|
backButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ sortButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
printButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
backButton.onClick = { [weak self] in self?.dismiss() }
|
|
|
+ sortButton.sortOrder = sortOrder
|
|
|
+ sortButton.onSortChange = { [weak self] order in
|
|
|
+ self?.sortOrder = order
|
|
|
+ self?.reloadList()
|
|
|
+ }
|
|
|
selectAllRow.applyLayout(contentInset: rowContentInset, checkboxNameSpacing: checkboxNameSpacing)
|
|
|
selectAllRow.onClick = { [weak self] in self?.toggleSelectAll() }
|
|
|
printButton.onClick = { [weak self] in self?.printContacts() }
|
|
|
|
|
|
addSubview(backButton)
|
|
|
+ addSubview(sortButton)
|
|
|
addSubview(searchContainer)
|
|
|
searchContainer.addSubview(searchIconView)
|
|
|
searchContainer.addSubview(searchField)
|
|
|
@@ -321,10 +356,15 @@ final class PrintContactsOverlayView: NSView, AppearanceRefreshable {
|
|
|
searchField.centerYAnchor.constraint(equalTo: searchContainer.centerYAnchor),
|
|
|
|
|
|
selectAllRow.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
|
|
|
- selectAllRow.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalInset),
|
|
|
selectAllRow.topAnchor.constraint(equalTo: searchContainer.bottomAnchor, constant: 12),
|
|
|
+ selectAllRow.trailingAnchor.constraint(lessThanOrEqualTo: sortButton.leadingAnchor, constant: -8),
|
|
|
selectAllRow.heightAnchor.constraint(equalToConstant: 36),
|
|
|
|
|
|
+ sortButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalInset),
|
|
|
+ sortButton.centerYAnchor.constraint(equalTo: selectAllRow.centerYAnchor),
|
|
|
+ sortButton.widthAnchor.constraint(equalToConstant: 40),
|
|
|
+ sortButton.heightAnchor.constraint(equalToConstant: 40),
|
|
|
+
|
|
|
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
|
|
|
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalInset),
|
|
|
scrollView.topAnchor.constraint(equalTo: selectAllRow.bottomAnchor, constant: 8),
|
|
|
@@ -365,8 +405,13 @@ final class PrintContactsOverlayView: NSView, AppearanceRefreshable {
|
|
|
|
|
|
private func filteredContacts() -> [PrintableContact] {
|
|
|
let query = searchField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
|
- guard !query.isEmpty else { return allContacts }
|
|
|
- return allContacts.filter { $0.searchableText.contains(query) }
|
|
|
+ let contacts: [PrintableContact]
|
|
|
+ if query.isEmpty {
|
|
|
+ contacts = allContacts
|
|
|
+ } else {
|
|
|
+ contacts = allContacts.filter { $0.searchableText.contains(query) }
|
|
|
+ }
|
|
|
+ return sortOrder.sort(contacts)
|
|
|
}
|
|
|
|
|
|
private func reloadList() {
|
|
|
@@ -686,6 +731,79 @@ private final class PrintContactsSelectAllRow: NSControl, AppearanceRefreshable
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// MARK: - Sort Button
|
|
|
+
|
|
|
+private final class PrintContactsSortButton: NSControl, AppearanceRefreshable {
|
|
|
+ var sortOrder: ContactSortOrder = .nameAscending {
|
|
|
+ didSet { rebuildMenu() }
|
|
|
+ }
|
|
|
+ var onSortChange: ((ContactSortOrder) -> Void)?
|
|
|
+
|
|
|
+ private let iconView = NSImageView()
|
|
|
+ private let sortMenu = NSMenu()
|
|
|
+
|
|
|
+ init() {
|
|
|
+ super.init(frame: .zero)
|
|
|
+ toolTip = "Sort"
|
|
|
+
|
|
|
+ wantsLayer = true
|
|
|
+ layer?.cornerRadius = 12
|
|
|
+
|
|
|
+ if let image = NSImage(systemSymbolName: "arrow.up.arrow.down", accessibilityDescription: "Sort") {
|
|
|
+ let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .semibold)
|
|
|
+ iconView.image = image.withSymbolConfiguration(config)
|
|
|
+ }
|
|
|
+ iconView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ addSubview(iconView)
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ iconView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
|
+ iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
|
+ ])
|
|
|
+ rebuildMenu()
|
|
|
+ refreshAppearance()
|
|
|
+ }
|
|
|
+
|
|
|
+ @available(*, unavailable)
|
|
|
+ required init?(coder: NSCoder) { nil }
|
|
|
+
|
|
|
+ func refreshAppearance() {
|
|
|
+ layer?.backgroundColor = AppTheme.elevatedBackground.cgColor
|
|
|
+ layer?.borderColor = AppTheme.paywallBorder.cgColor
|
|
|
+ layer?.borderWidth = 1.5
|
|
|
+ iconView.contentTintColor = AppTheme.textPrimary
|
|
|
+ }
|
|
|
+
|
|
|
+ private func rebuildMenu() {
|
|
|
+ sortMenu.removeAllItems()
|
|
|
+ for order in ContactSortOrder.allCases {
|
|
|
+ let item = NSMenuItem(title: order.title, action: #selector(menuItemSelected(_:)), keyEquivalent: "")
|
|
|
+ item.target = self
|
|
|
+ item.representedObject = order.rawValue
|
|
|
+ item.state = order == sortOrder ? .on : .off
|
|
|
+ sortMenu.addItem(item)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func menuItemSelected(_ sender: NSMenuItem) {
|
|
|
+ guard let raw = sender.representedObject as? String,
|
|
|
+ let order = ContactSortOrder(rawValue: raw) else { return }
|
|
|
+ sortOrder = order
|
|
|
+ rebuildMenu()
|
|
|
+ onSortChange?(order)
|
|
|
+ }
|
|
|
+
|
|
|
+ override func mouseDown(with event: NSEvent) {
|
|
|
+ guard bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
|
|
|
+ rebuildMenu()
|
|
|
+ sortMenu.popUp(positioning: nil, at: NSPoint(x: 0, y: bounds.height + 4), in: self)
|
|
|
+ }
|
|
|
+
|
|
|
+ override func resetCursorRects() {
|
|
|
+ addCursorRect(bounds, cursor: .pointingHand)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// MARK: - Toolbar Button
|
|
|
|
|
|
private final class PrintContactsToolbarButton: NSControl, AppearanceRefreshable {
|