Files
voice-cat/clients/apple/macOS/VoiceCatMac/Sheets/AccountsSheet.swift

210 lines
8.9 KiB
Swift
Raw Normal View History

import AppKit
import VoiceCatCore
final class AccountsSheet: NSViewController {
private let client: VoiceCatClient
private var accounts: [Account] = []
private let tableView = NSTableView()
private let statusLabel = NSTextField(labelWithString: "Loading…")
init(client: VoiceCatClient) {
self.client = client
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError() }
override func loadView() {
view = NSView(frame: NSRect(x: 0, y: 0, width: 480, height: 340))
}
override func viewDidLoad() {
super.viewDidLoad()
buildUI()
refreshAccounts()
}
private func buildUI() {
let titleLabel = NSTextField(labelWithString: "Server Accounts")
titleLabel.font = .boldSystemFont(ofSize: 13)
let userCol = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("username"))
userCol.title = "Username"; userCol.width = 160
let adminCol = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("admin"))
adminCol.title = "Admin"; adminCol.width = 60
let createdCol = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("created"))
createdCol.title = "Created"; createdCol.width = 120
let loginCol = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("login"))
loginCol.title = "Last Login"; loginCol.width = 120
tableView.addTableColumn(userCol)
tableView.addTableColumn(adminCol)
tableView.addTableColumn(createdCol)
tableView.addTableColumn(loginCol)
tableView.dataSource = self; tableView.delegate = self
tableView.allowsMultipleSelection = false
tableView.setAccessibilityLabel("Server accounts")
let sv = NSScrollView()
sv.documentView = tableView; sv.hasVerticalScroller = true
sv.borderType = .bezelBorder
let refreshButton = NSButton(title: "Refresh", target: self, action: #selector(refreshClicked))
refreshButton.bezelStyle = .rounded
refreshButton.setAccessibilityLabel("Refresh account list")
let addButton = NSButton(title: "Add…", target: self, action: #selector(addClicked))
addButton.bezelStyle = .rounded
addButton.setAccessibilityLabel("Add new account")
let resetPwButton = NSButton(title: "Reset Password…", target: self, action: #selector(resetPwClicked))
resetPwButton.bezelStyle = .rounded
resetPwButton.setAccessibilityLabel("Reset selected account password")
let deleteButton = NSButton(title: "Delete…", target: self, action: #selector(deleteClicked))
deleteButton.bezelStyle = .rounded
deleteButton.setAccessibilityLabel("Delete selected account")
let doneButton = NSButton(title: "Done", target: self, action: #selector(doneClicked))
doneButton.bezelStyle = .rounded; doneButton.keyEquivalent = "\r"
doneButton.setAccessibilityLabel("Close accounts sheet")
statusLabel.textColor = .secondaryLabelColor
statusLabel.setAccessibilityLabel("Status")
let toolbar = NSStackView(views: [refreshButton, addButton, resetPwButton, deleteButton, NSView()])
toolbar.orientation = .horizontal; toolbar.spacing = 8
let bottomRow = NSStackView(views: [statusLabel, NSView(), doneButton])
bottomRow.orientation = .horizontal; bottomRow.spacing = 8
let stack = NSStackView(views: [titleLabel, sv, toolbar, bottomRow])
stack.orientation = .vertical; stack.spacing = 10
stack.edgeInsets = NSEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: view.topAnchor),
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor),
stack.bottomAnchor.constraint(equalTo: view.bottomAnchor),
sv.heightAnchor.constraint(equalToConstant: 200),
])
client.onEvent = { [weak self] event in
if event.type == .accountList { self?.pullAccounts() }
}
}
private func refreshAccounts() {
statusLabel.stringValue = "Loading…"
client.requestAccountList()
}
private func pullAccounts() {
accounts = client.listAccounts()
tableView.reloadData()
statusLabel.stringValue = "\(accounts.count) account\(accounts.count == 1 ? "" : "s")"
}
@objc private func refreshClicked() { refreshAccounts() }
@objc private func addClicked() {
let sheet = InputSheet(title: "New Account", prompt: "Username:", defaultValue: "")
sheet.onComplete = { [weak self] username in
guard let self, let username, !username.isEmpty else { return }
let pwSheet = PasswordPromptSheet(prompt: "Password for \(username):")
pwSheet.onComplete = { [weak self] password in
guard let self, let password, !password.isEmpty else { return }
let r = self.client.createAccount(username, password: password)
if r == .ok {
self.statusLabel.stringValue = "Account '\(username)' created."
self.refreshAccounts()
} else {
self.statusLabel.stringValue = "Failed: \(r.description)"
}
}
self.presentAsSheet(pwSheet)
}
presentAsSheet(sheet)
}
@objc private func resetPwClicked() {
let row = tableView.selectedRow
guard row >= 0, row < accounts.count else { return }
let account = accounts[row]
let sheet = PasswordPromptSheet(prompt: "New password for \(account.username):")
sheet.onComplete = { [weak self] password in
guard let self, let password, !password.isEmpty else { return }
let r = self.client.resetPassword(account.username, newPassword: password)
self.statusLabel.stringValue = r == .ok
? "Password reset for '\(account.username)'."
: "Failed: \(r.description)"
}
presentAsSheet(sheet)
}
@objc private func deleteClicked() {
let row = tableView.selectedRow
guard row >= 0, row < accounts.count else { return }
let account = accounts[row]
let alert = NSAlert()
alert.messageText = "Delete account '\(account.username)'?"
alert.informativeText = "This cannot be undone."
alert.addButton(withTitle: "Delete"); alert.addButton(withTitle: "Cancel")
alert.alertStyle = .warning
guard let window = view.window else { return }
alert.beginSheetModal(for: window) { [weak self] response in
guard response == .alertFirstButtonReturn, let self else { return }
let r = self.client.deleteAccount(account.username)
self.statusLabel.stringValue = r == .ok
? "Account '\(account.username)' deleted."
: "Failed: \(r.description)"
if r == .ok { self.refreshAccounts() }
}
}
@objc private func doneClicked() { dismiss(nil) }
private func dateString(_ ms: UInt64) -> String {
guard ms > 0 else { return "" }
let date = Date(timeIntervalSince1970: Double(ms) / 1000.0)
return DateFormatter.localizedString(from: date, dateStyle: .short, timeStyle: .none)
}
}
// MARK: - NSTableViewDataSource / Delegate
extension AccountsSheet: NSTableViewDataSource, NSTableViewDelegate {
func numberOfRows(in tableView: NSTableView) -> Int { accounts.count }
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let acc = accounts[row]
let id = tableColumn?.identifier ?? NSUserInterfaceItemIdentifier("cell")
let cell = tableView.makeView(withIdentifier: id, owner: nil) as? NSTableCellView
?? makeCell(id)
switch tableColumn?.identifier.rawValue {
case "username": cell.textField?.stringValue = acc.username
case "admin": cell.textField?.stringValue = acc.isAdmin ? "Yes" : ""
case "created": cell.textField?.stringValue = dateString(acc.createdAtUnixMs)
case "login": cell.textField?.stringValue = dateString(acc.lastLoginUnixMs)
default: break
}
return cell
}
private func makeCell(_ id: NSUserInterfaceItemIdentifier) -> NSTableCellView {
let cell = NSTableCellView(); cell.identifier = id
let tf = NSTextField(labelWithString: "")
tf.translatesAutoresizingMaskIntoConstraints = false
cell.addSubview(tf); cell.textField = tf
NSLayoutConstraint.activate([
tf.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 4),
tf.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -4),
tf.centerYAnchor.constraint(equalTo: cell.centerYAnchor),
])
return cell
}
}