Add cached TOTP search (#77)
This commit is contained in:
@@ -1744,17 +1744,23 @@ private final class TotpDiscoveryView: UIView {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class TotpListViewController: UITableViewController, MobileTabRoot {
|
||||
private final class TotpListViewController: UITableViewController, MobileTabRoot,
|
||||
UISearchResultsUpdating
|
||||
{
|
||||
fileprivate let shellTab = MobileTab.totp
|
||||
private let authentication: MobileAuthentication?
|
||||
private let searchController = UISearchController(searchResultsController: nil)
|
||||
private var shellPage: MobilePage
|
||||
private var page: MobileTotpPage?
|
||||
private var unfilteredPage: MobileTotpPage?
|
||||
private var loadTask: Task<Void, Never>?
|
||||
private var searchTask: Task<Void, Never>?
|
||||
private var unlockTask: Task<Void, Never>?
|
||||
private var shellTask: Task<Void, Never>?
|
||||
private var progressTask: Task<Void, Never>?
|
||||
private var operation: MobileTotpOperation?
|
||||
private var loadGeneration = 0
|
||||
private var searchGeneration = 0
|
||||
private var refreshAfterUnlock = false
|
||||
|
||||
init(shellPage: MobilePage, authentication: MobileAuthentication?) {
|
||||
@@ -1763,6 +1769,15 @@ private final class TotpListViewController: UITableViewController, MobileTabRoot
|
||||
super.init(style: .insetGrouped)
|
||||
title = shellPage.title
|
||||
navigationItem.largeTitleDisplayMode = .always
|
||||
searchController.searchResultsUpdater = self
|
||||
searchController.obscuresBackgroundDuringPresentation = false
|
||||
searchController.searchBar.placeholder = "TOTP entry names and folders"
|
||||
searchController.searchBar.autocapitalizationType = .none
|
||||
searchController.searchBar.autocorrectionType = .no
|
||||
searchController.searchBar.spellCheckingType = .no
|
||||
navigationItem.searchController = searchController
|
||||
navigationItem.hidesSearchBarWhenScrolling = false
|
||||
definesPresentationContext = true
|
||||
refreshControl = UIRefreshControl()
|
||||
refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
|
||||
}
|
||||
@@ -1775,6 +1790,7 @@ private final class TotpListViewController: UITableViewController, MobileTabRoot
|
||||
deinit {
|
||||
operation?.cancel()
|
||||
loadTask?.cancel()
|
||||
searchTask?.cancel()
|
||||
unlockTask?.cancel()
|
||||
shellTask?.cancel()
|
||||
progressTask?.cancel()
|
||||
@@ -1809,6 +1825,10 @@ private final class TotpListViewController: UITableViewController, MobileTabRoot
|
||||
reloadShell()
|
||||
}
|
||||
|
||||
func updateSearchResults(for searchController: UISearchController) {
|
||||
search(searchController.searchBar.text ?? "")
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int { 1 }
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
@@ -1912,12 +1932,15 @@ private final class TotpListViewController: UITableViewController, MobileTabRoot
|
||||
@objc private func localStoreDidChange() {
|
||||
operation?.cancel()
|
||||
loadTask?.cancel()
|
||||
searchTask?.cancel()
|
||||
progressTask?.cancel()
|
||||
loadGeneration += 1
|
||||
searchGeneration += 1
|
||||
operation = nil
|
||||
progressTask = nil
|
||||
tableView.tableHeaderView = nil
|
||||
page = nil
|
||||
unfilteredPage = nil
|
||||
loadCachedPage()
|
||||
}
|
||||
|
||||
@@ -1942,7 +1965,10 @@ private final class TotpListViewController: UITableViewController, MobileTabRoot
|
||||
|
||||
private func refreshState(force: Bool = false) {
|
||||
guard shellPage.state == .ready else {
|
||||
searchTask?.cancel()
|
||||
searchGeneration += 1
|
||||
page = nil
|
||||
unfilteredPage = nil
|
||||
showUnavailable(
|
||||
title: shellPage.stateTitle,
|
||||
detail: shellPage.stateDetail,
|
||||
@@ -2146,21 +2172,66 @@ private final class TotpListViewController: UITableViewController, MobileTabRoot
|
||||
}
|
||||
}
|
||||
|
||||
private func apply(_ page: MobileTotpPage) {
|
||||
private func apply(_ page: MobileTotpPage, remember: Bool = true) {
|
||||
if remember {
|
||||
unfilteredPage = page
|
||||
let query = searchController.searchBar.text ?? ""
|
||||
if !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
search(query)
|
||||
return
|
||||
}
|
||||
}
|
||||
self.page = page
|
||||
configureLockButton()
|
||||
tableView.reloadData()
|
||||
if page.rows.isEmpty {
|
||||
let query = searchController.searchBar.text ?? ""
|
||||
let searching = !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
showUnavailable(
|
||||
title: "No TOTP Codes",
|
||||
detail: "No valid time-based OTP entries were found in the password store.",
|
||||
image: "timer"
|
||||
title: searching ? "No Results" : "No TOTP Codes",
|
||||
detail: searching
|
||||
? "No cached TOTP entries match “\(query)”."
|
||||
: "No valid time-based OTP entries were found in the password store.",
|
||||
image: searching ? "magnifyingglass" : "timer"
|
||||
)
|
||||
} else {
|
||||
contentUnavailableConfiguration = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func search(_ query: String) {
|
||||
searchGeneration += 1
|
||||
let generation = searchGeneration
|
||||
searchTask?.cancel()
|
||||
guard !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||
if let unfilteredPage { apply(unfilteredPage, remember: false) }
|
||||
return
|
||||
}
|
||||
guard let authentication else { return }
|
||||
searchTask = Task { [weak self] in
|
||||
let result = await Task.detached(priority: .userInitiated) {
|
||||
do {
|
||||
return Result<MobileTotpPage?, AuthenticationFailure>.success(
|
||||
try authentication.searchCachedTotpPage(query: query)
|
||||
)
|
||||
} catch let error as MobileAuthenticationFfiError {
|
||||
return .failure(AuthenticationFailure(error))
|
||||
} catch {
|
||||
return .failure(.unexpected)
|
||||
}
|
||||
}.value
|
||||
guard !Task.isCancelled, let self, generation == searchGeneration else { return }
|
||||
switch result {
|
||||
case let .success(.some(page)):
|
||||
apply(page, remember: false)
|
||||
case .success(.none):
|
||||
break
|
||||
case let .failure(failure):
|
||||
handle(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func configureLockButton() {
|
||||
let unlocked = (try? authentication?.state().unlocked) == true
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
@@ -2175,6 +2246,7 @@ private final class TotpListViewController: UITableViewController, MobileTabRoot
|
||||
|
||||
private func showLocked() {
|
||||
page = nil
|
||||
unfilteredPage = nil
|
||||
tableView.reloadData()
|
||||
navigationItem.rightBarButtonItem = nil
|
||||
var configuration = UIContentUnavailableConfiguration.empty()
|
||||
|
||||
Reference in New Issue
Block a user