// // ViewController.swift // MastoSearch // // Created by Shadowfacts on 12/14/21. // import Cocoa import Combine import SwiftSoup import MastoSearchCore class ViewController: NSViewController { private static let formatter: DateFormatter = { let f = DateFormatter() f.locale = .current f.setLocalizedDateFormatFromTemplate("yyyy-MM-dd hh:mm a") return f }() private static let searchThread = DispatchQueue(label: "Search", qos: .userInitiated) @IBOutlet weak var table: NSTableView! @IBOutlet weak var progressIndicator: NSProgressIndicator! private var dataSource: DataSource! private var allStatusesSnapshot: NSDiffableDataSourceSnapshot? private var cancellables = Set() private var query: String? override func viewDidLoad() { super.viewDidLoad() dataSource = DataSource(owner: self) { tableView, tableColumn, row, item in let cell = tableView.makeView(withIdentifier: tableColumn.identifier, owner: self) as! NSTableCellView switch cell.identifier! { case .date: cell.textField!.font = .monospacedDigitSystemFont(ofSize: 13, weight: .regular) cell.textField!.stringValue = ViewController.formatter.string(from: item.status.published) case .contentWarning: cell.textField!.stringValue = item.status.summary ?? "" case .content: let doc = try! SwiftSoup.parse(item.status.content) cell.textField!.stringValue = try! doc.text() default: fatalError() } return cell } table.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)] let menu = NSMenu() menu.addItem(withTitle: "Open in Browser", action: #selector(openURL(_:)), keyEquivalent: "") menu.addItem(withTitle: "Copy URL", action: #selector(copyURL(_:)), keyEquivalent: "c") table.menu = menu DatabaseController.shared.onInitialize .sink { [unowned self] in self.loadAll() } .store(in: &cancellables) SyncController.shared.onSync .sink { [unowned self] in self.allStatusesSnapshot = nil self.loadAll() } .store(in: &cancellables) } override func viewWillAppear() { super.viewWillAppear() progressIndicator.startAnimation(nil) } private func loadStatuses(_ statuses: StatusSequence) -> NSDiffableDataSourceSnapshot { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.statuses]) snapshot.appendItems(statuses.map { Item(status: $0) }, toSection: .statuses) DispatchQueue.main.async { self.dataSource.apply(snapshot, animatingDifferences: false) { self.progressIndicator.stopAnimation(nil) } } return snapshot } private func loadAll() { self.query = nil if let snapshot = allStatusesSnapshot { dispatchPrecondition(condition: .onQueue(.main)) self.dataSource.apply(snapshot, animatingDifferences: false) } else { let sortDescriptor = table.sortDescriptors.first ViewController.searchThread.async { DatabaseController.shared.getStatuses(sortDescriptor: sortDescriptor) { statuses in let snapshot = self.loadStatuses(statuses) self.allStatusesSnapshot = snapshot } } } } func search(_ query: String) { let query = query.trimmingCharacters(in: .whitespacesAndNewlines) self.query = query guard !query.isEmpty else { loadAll() return } let sortDescriptor = table.sortDescriptors.first ViewController.searchThread.async { DatabaseController.shared.getStatuses(query: query, sortDescriptor: sortDescriptor) { statuses in _ = self.loadStatuses(statuses) } } } func sortDescriptorsChanged() { guard DatabaseController.shared.isInitialized else { return } allStatusesSnapshot = nil self.dataSource.apply(NSDiffableDataSourceSnapshot(), animatingDifferences: false) if let query = query { search(query) } else { loadAll() } } @objc func openURL(_ sender: Any) { guard table.clickedRow != -1, let item = dataSource.itemIdentifier(forRow: table.clickedRow) else { return } NSWorkspace.shared.open(URL(string: item.status.url)!) } @objc func copyURL(_ sender: Any) { guard table.clickedRow != -1, let item = dataSource.itemIdentifier(forRow: table.clickedRow) else { return } NSPasteboard.general.clearContents() NSPasteboard.general.setString(item.status.url, forType: .string) } } extension ViewController { enum Section { case statuses } struct Item: Hashable { let status: Status static func ==(lhs: Item, rhs: Item) -> Bool { return lhs.status.url == rhs.status.url } func hash(into hasher: inout Hasher) { hasher.combine(status.url) } } } extension ViewController { class DataSource: NSTableViewDiffableDataSource { unowned var owner: ViewController init(owner: ViewController, cellProvider: @escaping NSTableViewDiffableDataSource.CellProvider) { self.owner = owner super.init(tableView: owner.table, cellProvider: cellProvider) } // without @objc the table view doesn't detect it, even though it overrides an NSTableViewDataSource method and so should be exposed automatically @objc func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldSortDescriptors: [NSSortDescriptor]) { owner.sortDescriptorsChanged() } } } extension ViewController: NSMenuItemValidation { func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { if menuItem.action == #selector(copyURL) || menuItem.action == #selector(openURL) { return table.clickedRow != -1 } else { return false } } } private extension NSUserInterfaceItemIdentifier { static let date = NSUserInterfaceItemIdentifier("date") static let contentWarning = NSUserInterfaceItemIdentifier("contentWarning") static let content = NSUserInterfaceItemIdentifier("content") }