1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-12-21 11:10:08 +01:00
Files
app/phpmon/Modules/Domain List/UI/DomainListVC.swift

374 lines
12 KiB
Swift

//
// DomainListVC.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 30/03/2021.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa
import Carbon
import SwiftUI
import NVAlert
// swiftlint:disable type_body_length file_length
class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
var container: Container {
return App.shared.container
}
// MARK: - Outlets
@IBOutlet weak var tableView: PMTableView!
@IBOutlet weak var noResultsView: NSView!
@IBOutlet weak var progressIndicator: NSProgressIndicator!
@IBOutlet weak var progressIndicatorContainer: NSVisualEffectView!
@IBOutlet weak var labelProgressIndicator: NSTextField!
// MARK: - Variables
/// List of sites that will be displayed in this view. Originates from the `Valet` object.
var domains: [ValetListable] = []
/// Array that contains various apps that might open a particular site directory.
var applications: [Application] {
return App.shared.detectedApplications
}
/// The last sort descriptor used.
var sortDescriptor: NSSortDescriptor?
/// String that was last searched for. Empty by default.
var lastSearchedFor = ""
// MARK: - Helper Variables
var selectedSite: ValetSite? {
if tableView.selectedRow == -1 {
return nil
}
return domains[tableView.selectedRow] as? ValetSite
}
var selectedProxy: ValetProxy? {
if tableView.selectedRow == -1 {
return nil
}
return domains[tableView.selectedRow] as? ValetProxy
}
var selected: ValetListable? {
if tableView.selectedRow == -1 {
return nil
}
return domains[tableView.selectedRow]
}
var timer: Timer?
// MARK: - Display
public static func create(delegate: NSWindowDelegate?) {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController(
withIdentifier: "domainListWindow"
) as! DomainListWindowController
guard let window = windowController.window else { return }
window.title = "domain_list.title".localized
window.subtitle = "domain_list.subtitle".localized
window.delegate = delegate ?? windowController
window.styleMask = [.titled, .closable, .resizable, .miniaturizable]
window.minSize = NSSize(width: 550, height: 200)
window.setFrameAutosaveName("domainListWindow")
App.shared.domainListWindowController = windowController
}
public static func show(delegate: NSWindowDelegate? = nil) {
if App.shared.domainListWindowController == nil {
Self.create(delegate: delegate)
}
App.shared.domainListWindowController!.showWindow(self)
NSApp.activate(ignoringOtherApps: true)
App.shared.domainListWindowController?.window?.orderFrontRegardless()
}
// MARK: - Lifecycle
override func viewDidLoad() {
tableView.doubleAction = #selector(self.doubleClicked(sender:))
addNoResultsView()
let mapping = [
"SECURE": "domain_list.columns.secure",
"DOMAIN": "domain_list.columns.domain",
"ENVIRONMENT": "domain_list.columns.php",
"KIND": "domain_list.columns.kind",
"TYPE": "domain_list.columns.project_type"
]
for (id, key) in mapping {
let column = tableView.tableColumn(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: id))
column?.title = key.localized
}
if !Valet.shared.sites.isEmpty {
// Preloaded list
reloadDomainListables()
searchedFor(text: lastSearchedFor)
} else {
Task { await reloadDomains() }
}
}
override func viewDidAppear() {
super.viewDidAppear()
checkForCertificateRenewal()
}
private func reloadDomainListables() {
domains = Valet.getDomainListable()
}
private func addNoResultsView() {
let child = NSHostingController(
rootView: UnavailableContentView(
title: "domain_list.domains_empty.title".localized,
description: "domain_list.domains_empty.desc".localized,
icon: "globe",
button: "domain_list.domains_empty.button".localized,
action: {
App.shared.domainListWindowController?
.pressedAddLink(nil)
}
)
.frame(width: 400, height: 300)
).view
self.noResultsView.addSubview(child)
child.frame = self.noResultsView.bounds
}
// MARK: - Async Operations
/**
Disables the UI so the user cannot interact with it.
Also shows a spinner to indicate that we're busy.
*/
@MainActor public func setUIBusy() {
// If it takes more than 0.5s to set the UI to not busy, show a spinner
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { _ in
Task {
@MainActor in self.progressIndicator.startAnimation(true)
self.labelProgressIndicator.stringValue = "phpman.steps.wait".localized
self.progressIndicatorContainer.layer?.cornerRadius = 10
self.progressIndicatorContainer.isHidden = false
}
})
tableView.alphaValue = 0.3
tableView.isEnabled = false
tableView.selectRowIndexes([], byExtendingSelection: true)
noResultsView.isHidden = true
}
/**
Re-enables the UI so the user can interact with it.
*/
@MainActor public func setUINotBusy() {
timer?.invalidate()
progressIndicatorContainer.isHidden = true
progressIndicator.stopAnimation(nil)
tableView.alphaValue = 1.0
tableView.isEnabled = true
updateNoResultsView()
}
/**
Executes a specific callback and fires the completion callback,
while updating the UI as required. As long as the completion callback
does not fire, the app is presumed to be busy and the UI reflects this.
- Parameter execute: Callback of the work that needs to happen.
- Parameter completion: Callback that is fired when the work is done.
*/
internal func waitAndExecute(_ execute: @escaping () async -> Void, completion: @escaping () -> Void = {}) {
Task { // Legacy `waitAndExecute` with UI
setUIBusy()
await execute()
Task { @MainActor in
await delay(seconds: 0.2)
completion()
setUINotBusy()
}
}
}
// MARK: - Site Data Loading
func reloadDomains() async {
waitAndExecute {
await Valet.shared.reloadSites()
} completion: { [self] in
reloadDomainListables()
searchedFor(text: lastSearchedFor)
}
}
func reloadDomainsWithoutUI() async {
await Valet.shared.reloadSites()
reloadDomainListables()
searchedFor(text: lastSearchedFor)
}
func applySortDescriptor(_ descriptor: NSSortDescriptor) {
sortDescriptor = descriptor
var sorted = self.domains
switch descriptor.key {
case "Secure": sorted = self.domains.sorted { $0.getListableSecured() && !$1.getListableSecured() }
case "Domain": sorted = self.domains.sorted { $0.getListableName() < $1.getListableName() }
case "PHP": sorted = self.domains.sorted { $0.getListablePhpVersion() < $1.getListablePhpVersion() }
case "Kind": sorted = self.domains.sorted { $0.getListableKind() < $1.getListableKind() }
case "Type": sorted = self.domains.sorted { $0.getListableType() < $1.getListableType() }
default: break
}
sorted = descriptor.ascending ? sorted.reversed() : sorted
self.domains = sorted.sorted { $0.getListableFavorited() && !$1.getListableFavorited() }
}
func addedNewSite(name: String, secureAfterLinking: Bool) async {
waitAndExecute {
await Valet.shared.reloadSites()
} completion: { [self] in
find(name, secureAfterLinking)
}
}
private func find(_ name: String, _ shouldSecure: Bool = false) {
reloadDomainListables()
searchedFor(text: "")
if let site = domains.enumerated().first(where: { $0.element.getListableName() == name }) {
Task { @MainActor in
self.tableView.selectRowIndexes([site.offset], byExtendingSelection: false)
self.tableView.scrollRowToVisible(site.offset)
if shouldSecure && !site.element.getListableSecured() {
self.toggleSecure()
}
}
}
}
// MARK: - Table View Delegate
func numberOfRows(in tableView: NSTableView) -> Int {
return domains.count
}
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
guard let sortDescriptor = tableView.sortDescriptors.first else { return }
// Kinda scuffed way of applying sort descriptors here, but it works.
Log.info("Applying sort descriptor for column: \(sortDescriptor.key ?? "Unknown")")
applySortDescriptor(sortDescriptor)
searchedFor(text: lastSearchedFor)
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let mapping: [String: DomainListCellProtocol.Type] = [
"TLS": DomainListTLSCell.self,
"DOMAIN": DomainListNameCell.self,
"ENVIRONMENT": DomainListPhpCell.self,
"KIND": DomainListKindCell.self,
"TYPE": DomainListTypeCell.self
]
let columnName = tableColumn!.identifier.rawValue
guard let cellType = mapping[columnName] else { return nil }
let identifier = NSUserInterfaceItemIdentifier(rawValue: cellType.getCellIdentifier(for: domains[row]))
guard let userCell = tableView.makeView(withIdentifier: identifier, owner: self)
as? DomainListCellProtocol else { return nil }
if let site = domains[row] as? ValetSite {
userCell.populateCell(with: site)
}
if let proxy = domains[row] as? ValetProxy {
userCell.populateCell(with: proxy)
}
return userCell as? NSView
}
func tableViewSelectionDidChange(_ notification: Notification) {
reloadContextMenu()
}
@objc func doubleClicked(sender: Any) {
guard self.selected != nil else {
return
}
self.openInBrowser()
}
// MARK: - (Search) Text Field Delegate
func reloadTable() {
if let sortDescriptor = sortDescriptor {
self.applySortDescriptor(sortDescriptor)
}
Task { @MainActor in
self.tableView.reloadData()
updateNoResultsView()
}
}
func updateNoResultsView() {
self.noResultsView.isHidden = !domains.isEmpty
}
func searchedFor(text: String) {
lastSearchedFor = text
let searchString = text.lowercased()
if searchString.isEmpty {
domains = Valet.getDomainListable()
reloadTable()
return
}
let splitSearchString: [String] = searchString
.split(separator: " ")
.map { return String($0) }
domains = Valet.getDomainListable().filter({ site in
return !splitSearchString.map { searchString in
return site.getListableName().lowercased().contains(searchString)
}.contains(false)
})
reloadTable()
}
// MARK: - Deinitialization
deinit {
Log.perf("deinit: \(String(describing: self)).\(#function)")
}
}
// swiftlint:enable type_body_length file_length