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 } }