333 lines
13 KiB
Swift
333 lines
13 KiB
Swift
import UIKit
|
|
import RealmSwift
|
|
import PKHUD
|
|
import ExceptionCatcher
|
|
import AutoCatCore
|
|
|
|
class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDelegate, UIScrollViewDelegate, UISearchBarDelegate {
|
|
|
|
@IBOutlet weak var tableView: UITableView!
|
|
@IBOutlet weak var showMapButton: UIBarButtonItem?
|
|
|
|
private var refreshButton: UIBarButtonItem!
|
|
private var refreshIndicator: UIBarButtonItem!
|
|
private var moreActionsButton: UIBarButtonItem?
|
|
|
|
private lazy var searchController: UISearchController = .default
|
|
.placeholder(NSLocalizedString("Search plate numbers", comment: ""))
|
|
.resultsUpdater(self)
|
|
.searchBarDelegate(self)
|
|
.makeDumb()
|
|
.scopeButtons(SearchScope.allCases.map(\.title))
|
|
|
|
private var refreshControl = UIRefreshControl()
|
|
private var datasource: SectionedDataSource<VehicleDto,VehicleCell>!
|
|
private var isLoadingPage = false
|
|
private var pageLoadingIndicator = UIActivityIndicatorView(style: .medium)
|
|
|
|
var filter = Filter()
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
self.showMapButton?.isEnabled = false
|
|
|
|
navigationItem.searchController = searchController
|
|
definesPresentationContext = true
|
|
|
|
if #available(iOS 14.0, *) {
|
|
setupActionsMenu()
|
|
}
|
|
|
|
self.refreshButton = UIBarButtonItem(image: UIImage(systemName: "arrow.triangle.2.circlepath"),
|
|
style: .plain,
|
|
target: self,
|
|
action: #selector(refresh))
|
|
|
|
self.refreshIndicator = UIBarButtonItem(customView: self.pageLoadingIndicator)
|
|
#if targetEnvironment(macCatalyst)
|
|
self.navigationItem.leftBarButtonItem = self.refreshButton
|
|
#endif
|
|
|
|
//self.refreshControl.attributedTitle = NSAttributedString(string: "")
|
|
self.refreshControl.addTarget(self, action: #selector(self.refresh(_:)), for: .valueChanged)
|
|
self.tableView.addSubview(self.refreshControl)
|
|
|
|
self.datasource = SectionedDataSource(table: self.tableView)
|
|
self.tableView.delegate = self
|
|
self.tableView.keyboardDismissMode = .onDrag
|
|
|
|
updateSearchResults(with: filter)
|
|
}
|
|
|
|
func updateSearchResults(with filter: Filter) {
|
|
Task {
|
|
showProgress()
|
|
|
|
if filter.needReset {
|
|
self.datasource.reset()
|
|
}
|
|
|
|
let vehicles = (try? await ApiService.shared.getVehicles(with: filter, pageToken: self.datasource.pageToken, pageSize: 50)) ?? PagedResponse<VehicleDto>()
|
|
|
|
if let count = vehicles.count {
|
|
self.navigationItem.title = String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""), count)
|
|
self.showMapButton?.isEnabled = count > 0
|
|
}
|
|
|
|
refreshControl.endRefreshing()
|
|
isLoadingPage = false
|
|
pageLoadingIndicator.stopAnimating()
|
|
hideProgress()
|
|
datasource.update(with: vehicles)
|
|
}
|
|
}
|
|
|
|
func showProgress() {
|
|
navigationItem.leftBarButtonItem = self.refreshIndicator
|
|
pageLoadingIndicator.startAnimating()
|
|
moreActionsButton?.isEnabled = false
|
|
}
|
|
|
|
func hideProgress() {
|
|
#if targetEnvironment(macCatalyst)
|
|
navigationItem.leftBarButtonItem = self.refreshButton
|
|
#else
|
|
navigationItem.leftBarButtonItem = nil
|
|
#endif
|
|
|
|
moreActionsButton?.isEnabled = true
|
|
}
|
|
|
|
// FIXME: Code duplication
|
|
func updateDetailController(with vehicle: VehicleDto, indexPath: IndexPath) {
|
|
if let splitViewController = self.view.window?.rootViewController as? UISplitViewController
|
|
{
|
|
var detail: UINavigationController?
|
|
if splitViewController.viewControllers.count == 2 {
|
|
detail = splitViewController.viewControllers.last as? UINavigationController
|
|
} else {
|
|
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
|
detail = storyboard.instantiateViewController(identifier: "ReportNavController")
|
|
}
|
|
|
|
if let detail = detail {
|
|
detail.popToRootViewController(animated: true)
|
|
let report = detail.viewControllers.first as? ReportController
|
|
report?.vehicle = vehicle
|
|
report?.vehicleUpdated = { vehicle in
|
|
self.datasource.set(item: vehicle, at: indexPath)
|
|
self.tableView.reloadData()
|
|
}
|
|
splitViewController.showDetailViewController(detail, sender: self)
|
|
//self.performSegue(withIdentifier: "OpenDetailSegue", sender: self)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - UISearchResultsUpdating
|
|
|
|
func updateSearchResults(for searchController: UISearchController) {
|
|
let newQuery = searchController.searchBar.text?.uppercased() ?? ""
|
|
guard self.filter.searchString != newQuery else { return }
|
|
|
|
self.filter.searchString = newQuery
|
|
self.filter.needReset = true
|
|
self.filter.scope = SearchScope(rawValue: searchController.searchBar.selectedScopeButtonIndex) ?? .plateNumber
|
|
|
|
updateSearchResults(with: filter)
|
|
}
|
|
|
|
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
|
guard let scope = SearchScope(rawValue: selectedScope) else {
|
|
return
|
|
}
|
|
|
|
filter.scope = scope
|
|
updateSearchResults(with: filter)
|
|
}
|
|
|
|
// MARK: NavigationBar actions
|
|
|
|
@available(iOS 14.0, *)
|
|
func setupActionsMenu() {
|
|
|
|
let menu = UIMenu(children: [
|
|
UIAction(title: NSLocalizedString("Filter results", comment: ""),
|
|
image: UIImage(systemName: "line.horizontal.3.decrease"),
|
|
handler: { [weak self] _ in Task { try? await self?.showFilter() } }),
|
|
UIAction(title: NSLocalizedString("Show on map", comment: ""),
|
|
image: UIImage(systemName: "map"),
|
|
handler: { _ in self.showOnMap() }),
|
|
UIAction(title: NSLocalizedString("Export", comment: ""),
|
|
image: UIImage(systemName: "square.and.arrow.up"),
|
|
handler: { _ in self.exportSearchResults() })
|
|
])
|
|
|
|
let menuBarButton = UIBarButtonItem(title: nil, image: UIImage(systemName: "ellipsis"), primaryAction: nil, menu: menu)
|
|
self.navigationItem.rightBarButtonItems = [menuBarButton]
|
|
self.moreActionsButton = menuBarButton
|
|
}
|
|
|
|
@IBAction func onFilterTapped(_ sender: UIBarButtonItem) {
|
|
Task { try? await showFilter() }
|
|
}
|
|
|
|
@IBAction func onMapTapped(_ sender: UIBarButtonItem) {
|
|
showOnMap()
|
|
}
|
|
|
|
@objc func refresh(_ sender: AnyObject) {
|
|
self.showMapButton?.isEnabled = false
|
|
self.filter.needReset = true
|
|
updateSearchResults(with: filter)
|
|
}
|
|
|
|
func showFilter() async throws {
|
|
// let sb = UIStoryboard(name: "Main", bundle: nil)
|
|
// let controller = sb.instantiateViewController(identifier: "FiltersController") as FiltersController
|
|
// controller.filter = self.filter
|
|
// controller.onDone = {
|
|
// self.filter = controller.filter
|
|
// self.datasource.setSortParameter(self.filter.sortBy ?? .updatedDate)
|
|
// self.filter.needReset = true
|
|
// self.filter.scope = SearchScope(rawValue: self.searchController.searchBar.selectedScopeButtonIndex) ?? .plateNumber
|
|
// self.updateSearchResults(with: self.filter)
|
|
// }
|
|
// self.navigationController?.pushViewController(controller, animated: true)
|
|
|
|
if let navigationController = self.navigationController {
|
|
let coordinator = FiltersCoordinator(navController: navigationController, filter: filter)
|
|
if let newFilter = try await coordinator.start() {
|
|
filter = newFilter
|
|
datasource.setSortParameter(self.filter.sortBy)
|
|
filter.needReset = true
|
|
filter.scope = SearchScope(rawValue: self.searchController.searchBar.selectedScopeButtonIndex) ?? .plateNumber
|
|
updateSearchResults(with: self.filter)
|
|
}
|
|
}
|
|
}
|
|
|
|
func showOnMap() {
|
|
let sb = UIStoryboard(name: "Main", bundle: nil)
|
|
let controller = sb.instantiateViewController(identifier: "GlobalEventsNavigation") as UINavigationController
|
|
if let eventsVC = controller.viewControllers.first as? GlobalEventsController {
|
|
eventsVC.filter = self.filter
|
|
}
|
|
|
|
controller.modalPresentationStyle = .fullScreen
|
|
self.present(controller, animated: true)
|
|
}
|
|
|
|
func exportSearchResults() {
|
|
Task {
|
|
do {
|
|
showProgress()
|
|
let resp = try await ApiService.shared.getVehicles(with: filter, pageSize: 0)
|
|
|
|
let newLine = "\r\n"
|
|
var csvString = VehicleDto.csvHeader + newLine
|
|
|
|
for vehicle in resp.items {
|
|
csvString.append(vehicle.csvLine)
|
|
csvString.append(newLine)
|
|
}
|
|
|
|
let tmpUrl = FileManager.default.tmpUrl(name: "search", ext: "csv")
|
|
try csvString.write(to: tmpUrl, atomically: true, encoding: .utf8)
|
|
#if targetEnvironment(macCatalyst)
|
|
self.save(file: tmpUrl)
|
|
#else
|
|
self.share(file: tmpUrl)
|
|
#endif
|
|
|
|
hideProgress()
|
|
} catch {
|
|
hideProgress()
|
|
HUD.show(error: error)
|
|
}
|
|
}
|
|
}
|
|
|
|
func share(file url: URL) {
|
|
let activityController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
|
|
self.present(activityController, animated: true)
|
|
}
|
|
|
|
func save(file url: URL) {
|
|
if #available(iOS 14, *) {
|
|
let controller = UIDocumentPickerViewController(forExporting: [url])
|
|
self.present(controller, animated: true)
|
|
} else {
|
|
let controller = UIDocumentPickerViewController(url: url, in: .exportToService)
|
|
present(controller, animated: true)
|
|
}
|
|
}
|
|
|
|
// MARK: - UITableViewDelegate
|
|
|
|
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
let vehicle = self.datasource.item(at: indexPath)
|
|
let updateAction = UIContextualAction(style: .normal, title: NSLocalizedString("Update", comment: "")) { action, view, completion in
|
|
self.update(vehicle: vehicle, at: indexPath)
|
|
completion(true)
|
|
}
|
|
updateAction.image = UIImage(systemName: "arrow.2.circlepath")
|
|
updateAction.backgroundColor = .systemBlue
|
|
|
|
let configuration = UISwipeActionsConfiguration(actions: [updateAction])
|
|
configuration.performsFirstActionWithFullSwipe = false
|
|
return configuration
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
|
let vehicle = self.datasource.item(at: indexPath)
|
|
|
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in
|
|
let update = UIAction(title: NSLocalizedString("Update", comment: ""), image: UIImage(systemName: "arrow.2.circlepath")) { action in
|
|
self.update(vehicle: vehicle, at: indexPath)
|
|
}
|
|
|
|
return UIMenu(title: NSLocalizedString("Actions", comment: ""), children: [update])
|
|
}
|
|
}
|
|
|
|
func update(vehicle: VehicleDto, at indexPath: IndexPath) {
|
|
|
|
Task {
|
|
do {
|
|
HUD.show(.progress)
|
|
let newVehicle = try await ApiService.shared.checkVehicle(by: vehicle.getNumber(), notes: vehicle.notes, events: [], force: true)
|
|
HUD.hide()
|
|
let realm = try await Realm()
|
|
if realm.object(ofType: Vehicle.self, forPrimaryKey: vehicle.getNumber()) != nil {
|
|
try realm.write {
|
|
realm.add(Vehicle(dto: newVehicle), update: .all)
|
|
}
|
|
}
|
|
datasource.set(item: newVehicle, at: indexPath)
|
|
updateDetailController(with: newVehicle, indexPath: indexPath)
|
|
} catch {
|
|
HUD.hide()
|
|
show(error: error)
|
|
}
|
|
}
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
let vehicle = self.datasource.item(at: indexPath)
|
|
self.updateDetailController(with: vehicle, indexPath: indexPath)
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
guard tableView.contentSize.height > 0 else { return }
|
|
|
|
let toBottom = tableView.contentSize.height - (tableView.contentOffset.y + tableView.frame.size.height)
|
|
if toBottom < 100 && !self.isLoadingPage && self.datasource.needMoreData() {
|
|
self.isLoadingPage = true
|
|
self.filter.needReset = false
|
|
updateSearchResults(with: filter)
|
|
}
|
|
}
|
|
}
|