feat: add daily fiat and crypto currency conversion
This commit is contained in:
@@ -13,6 +13,12 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
private var applications: [Application] = []
|
||||
private var results: [Application] = []
|
||||
private var calculation: CalculatorResult?
|
||||
private var isCurrencyCalculation = false
|
||||
private var currencyRates: CurrencyRates?
|
||||
private var currencyFetchedAt: Date?
|
||||
private var currencyTask: Task<Void, Never>?
|
||||
private var currencyError: String?
|
||||
private var currencyFailureQuery: String?
|
||||
private var locationLabels: [URL: String] = [:]
|
||||
private var discoveryTask: Task<Void, Never>?
|
||||
private var isLaunching = false
|
||||
@@ -73,6 +79,10 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
fatalError("Conjure creates its window programmatically")
|
||||
}
|
||||
|
||||
deinit {
|
||||
currencyTask?.cancel()
|
||||
}
|
||||
|
||||
func present() {
|
||||
if launcherFrameBeforeReview != nil {
|
||||
showWindow(nil)
|
||||
@@ -181,7 +191,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
let searchFont = NSFont.systemFont(ofSize: 20)
|
||||
searchField.font = searchFont
|
||||
searchField.placeholderAttributedString = NSAttributedString(
|
||||
string: "Search apps, calculate, or convert units…",
|
||||
string: "Search apps, calculate, or convert…",
|
||||
attributes: [.foregroundColor: SunsetPalette.secondaryText, .font: searchFont]
|
||||
)
|
||||
searchField.textColor = SunsetPalette.text
|
||||
@@ -195,7 +205,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
searchField.sendsSearchStringImmediately = true
|
||||
searchField.sendsWholeSearchString = false
|
||||
searchField.delegate = self
|
||||
searchField.setAccessibilityLabel("Search applications, calculate, or convert units")
|
||||
searchField.setAccessibilityLabel("Search applications, calculate, or convert units and currencies")
|
||||
|
||||
let symbol = NSImageView()
|
||||
symbol.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
|
||||
@@ -348,6 +358,28 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshCurrencyRates() {
|
||||
guard currencyTask == nil else { return }
|
||||
currencyTask = Task { [weak self] in
|
||||
let result: Result<CurrencyRates, any Error>
|
||||
do { result = .success(try await CurrencyAPI.fetch()) }
|
||||
catch { result = .failure(error) }
|
||||
guard !Task.isCancelled, let self else { return }
|
||||
self.currencyTask = nil
|
||||
switch result {
|
||||
case .success(let rates):
|
||||
self.currencyRates = rates
|
||||
self.currencyFetchedAt = Date()
|
||||
self.currencyError = nil
|
||||
self.currencyFailureQuery = nil
|
||||
case .failure(let error):
|
||||
self.currencyError = error.localizedDescription
|
||||
self.currencyFailureQuery = self.searchField.stringValue
|
||||
}
|
||||
self.updateResults(preservingSelection: true, allowingCurrencyFetch: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateLocationLabels() {
|
||||
var nameCounts: [String: Int] = [:]
|
||||
nameCounts.reserveCapacity(applications.count)
|
||||
@@ -370,10 +402,37 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
}
|
||||
}
|
||||
|
||||
private func updateResults(preservingSelection: Bool = false) {
|
||||
private func updateResults(preservingSelection: Bool = false, allowingCurrencyFetch: Bool = true) {
|
||||
let selectedURL = preservingSelection && results.indices.contains(tableView.selectedRow)
|
||||
? results[tableView.selectedRow].id : nil
|
||||
calculation = Calculator.evaluate(searchField.stringValue)
|
||||
let query = searchField.stringValue
|
||||
if currencyFailureQuery != query {
|
||||
currencyError = nil
|
||||
currencyFailureQuery = nil
|
||||
}
|
||||
isCurrencyCalculation = Calculator.isCurrencyQuery(query, currencyRates: currencyRates)
|
||||
let needsCurrencyRefresh = isCurrencyCalculation && (currencyFetchedAt.map {
|
||||
!Calendar.current.isDate($0, inSameDayAs: Date())
|
||||
} ?? true)
|
||||
if allowingCurrencyFetch, needsCurrencyRefresh, currencyError == nil {
|
||||
refreshCurrencyRates()
|
||||
}
|
||||
calculation = Calculator.evaluate(query, currencyRates: currencyRates)
|
||||
if isCurrencyCalculation, let currencyError {
|
||||
let cached = currencyRates.map { " Cached snapshot \($0.date) is not used." } ?? ""
|
||||
calculation = CalculatorResult(
|
||||
displayValue: "Currency rates unavailable", copyValue: nil,
|
||||
detail: "Daily rates: \(currencyError). Change the query to retry.\(cached)"
|
||||
)
|
||||
} else if isCurrencyCalculation, currencyRates != nil, let answer = calculation {
|
||||
let cacheStatus = needsCurrencyRefresh
|
||||
? (currencyTask == nil ? "Stale cached daily rates" : "Stale cache; refreshing daily rates…")
|
||||
: "Cached daily rates"
|
||||
calculation = CalculatorResult(
|
||||
displayValue: answer.displayValue, copyValue: answer.copyValue,
|
||||
detail: "\(answer.detail) · \(cacheStatus)"
|
||||
)
|
||||
}
|
||||
if calculation != nil {
|
||||
results = []
|
||||
} else if searchField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
@@ -381,8 +440,10 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
} else {
|
||||
results = history.results(for: searchField.stringValue, in: applications)
|
||||
}
|
||||
sectionLabel.stringValue = calculation == nil ? "Applications" : "Calculator"
|
||||
tableView.setAccessibilityLabel(calculation == nil ? "Applications" : "Calculator result")
|
||||
sectionLabel.stringValue = isCurrencyCalculation ? "Daily currency rates"
|
||||
: calculation == nil ? "Applications" : "Calculator"
|
||||
tableView.setAccessibilityLabel(isCurrencyCalculation ? "Daily currency conversion result"
|
||||
: calculation == nil ? "Applications" : "Calculator result")
|
||||
tableView.reloadData()
|
||||
if resultCount > 0 {
|
||||
let row = selectedURL.flatMap { url in results.firstIndex { $0.id == url } } ?? 0
|
||||
@@ -457,7 +518,8 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
if let calculation {
|
||||
cell.textField?.stringValue = calculation.displayValue
|
||||
cell.locationLabel.isHidden = true
|
||||
cell.kindLabel.stringValue = calculation.copyValue == nil ? "Error" : "Calculator"
|
||||
cell.kindLabel.stringValue = isCurrencyCalculation ? "Daily rates"
|
||||
: calculation.copyValue == nil ? "Error" : "Calculator"
|
||||
cell.imageView?.image = NSImage(systemSymbolName: "equal", accessibilityDescription: nil)?
|
||||
.withSymbolConfiguration(.init(pointSize: LauncherRowView.iconSize, weight: .regular))
|
||||
cell.imageView?.contentTintColor = SunsetPalette.accent
|
||||
|
||||
Reference in New Issue
Block a user