import UIKit @MainActor final class ServersViewController: UITableViewController { private let context: AppContext private var servers: [ServerRow] = [] init(context: AppContext) { self.context = context super.init(style: .plain) title = "Servers" tableView.backgroundColor = .systemGroupedBackground tableView.separatorInset = .zero } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) servers = context.core.servers() tableView.reloadData() tableView.backgroundView = servers.isEmpty ? EmptyBackgroundView(title: "No servers", detail: "Add a code hosting server to get started.") : nil navigationItem.rightBarButtonItem = UIBarButtonItem( systemItem: .add, primaryAction: UIAction { [weak self] _ in self?.showAddServer() } ) } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { servers.count } override func tableView( _ tableView: UITableView, cellForRowAt indexPath: IndexPath ) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "server") ?? UITableViewCell(style: .subtitle, reuseIdentifier: "server") let server = servers[indexPath.row] configureTextCell(cell, title: server.name, detail: server.url) cell.accessoryType = .disclosureIndicator return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) do { try context.selectServer(index: UInt32(indexPath.row)) } catch { show(error: error) } } private func showAddServer() { let controller = AddServerViewController(context: context) { [weak self] in guard let self else { return } self.servers = self.context.core.servers() self.tableView.reloadData() } present(UINavigationController(rootViewController: controller), animated: true) } } @MainActor final class AddServerViewController: UITableViewController, UITextFieldDelegate { private let context: AppContext private let completion: () -> Void private let nameField = UITextField() private let urlField = UITextField() private let tokenField = UITextField() private let providerButton = UIButton(type: .system) private var provider = ServerProvider.gitea private var saveButton: UIBarButtonItem! init(context: AppContext, completion: @escaping () -> Void) { self.context = context self.completion = completion super.init(style: .insetGrouped) title = "Add Server" } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } override func viewDidLoad() { super.viewDidLoad() navigationItem.leftBarButtonItem = UIBarButtonItem( systemItem: .cancel, primaryAction: UIAction { [weak self] _ in self?.dismiss(animated: true) } ) saveButton = UIBarButtonItem( title: "Add", style: .done, target: self, action: #selector(save) ) navigationItem.rightBarButtonItem = saveButton configure(nameField, placeholder: "Work", contentType: .name) configure(urlField, placeholder: "https://gitea.example.com", contentType: .URL) urlField.keyboardType = .URL urlField.autocapitalizationType = .none configure(tokenField, placeholder: "Access token", contentType: nil) tokenField.isSecureTextEntry = true tokenField.autocapitalizationType = .none tokenField.returnKeyType = .done configureProviderButton() } override func numberOfSections(in tableView: UITableView) -> Int { 4 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { ["API provider", "Name", "Server URL", "Access token"][section] } override func tableView( _ tableView: UITableView, cellForRowAt indexPath: IndexPath ) -> UITableViewCell { let cell = UITableViewCell(style: .default, reuseIdentifier: nil) let control: UIView = indexPath.section == 0 ? providerButton : [nameField, urlField, tokenField][indexPath.section - 1] control.translatesAutoresizingMaskIntoConstraints = false cell.contentView.addSubview(control) NSLayoutConstraint.activate([ control.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 16), control.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -16), control.topAnchor.constraint(equalTo: cell.contentView.topAnchor), control.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor), cell.contentView.heightAnchor.constraint(greaterThanOrEqualToConstant: 48), ]) return cell } func textFieldShouldReturn(_ textField: UITextField) -> Bool { if textField === nameField { urlField.becomeFirstResponder() } else if textField === urlField { tokenField.becomeFirstResponder() } else { save() } return true } @objc private func save() { view.endEditing(true) saveButton.isEnabled = false let spinner = UIActivityIndicatorView(style: .medium) spinner.startAnimating() navigationItem.rightBarButtonItem = UIBarButtonItem(customView: spinner) Task { do { let index = try await context.core.addServer( name: nameField.text ?? "", url: urlField.text ?? "", token: tokenField.text ?? "", provider: provider ) try context.didAddServer(index: index) completion() dismiss(animated: true) } catch { navigationItem.rightBarButtonItem = saveButton saveButton.isEnabled = true show(error: error) } } } private func configure( _ field: UITextField, placeholder: String, contentType: UITextContentType? ) { field.placeholder = placeholder field.textContentType = contentType field.clearButtonMode = .whileEditing field.delegate = self field.returnKeyType = .next field.adjustsFontForContentSizeCategory = true field.font = .preferredFont(forTextStyle: .body) } private func configureProviderButton() { providerButton.contentHorizontalAlignment = .leading providerButton.showsMenuAsPrimaryAction = true providerButton.changesSelectionAsPrimaryAction = true providerButton.accessibilityLabel = "API provider" providerButton.accessibilityValue = "Gitea" providerButton.menu = UIMenu(options: .singleSelection, children: [ UIAction(title: "Gitea", state: .on) { [weak self] _ in self?.provider = .gitea self?.urlField.placeholder = "https://gitea.example.com" self?.providerButton.accessibilityValue = "Gitea" }, UIAction(title: "Forgejo") { [weak self] _ in self?.provider = .forgejo self?.urlField.placeholder = "https://forgejo.example.com" self?.providerButton.accessibilityValue = "Forgejo" }, ]) } }