MastoSearch/MastoSearch/ViewController.swift

210 lines
6.8 KiB
Swift

//
// 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<Section, Item>?
private var cancellables = Set<AnyCancellable>()
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<Section, Item> {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
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<Section, Item> {
unowned var owner: ViewController
init(owner: ViewController, cellProvider: @escaping NSTableViewDiffableDataSource<ViewController.Section, ViewController.Item>.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")
}