Add server editing and deletion

This commit is contained in:
Georg Bauer
2026-08-03 20:34:39 +02:00
parent 0f1d109af7
commit 9a0b179a0e
10 changed files with 664 additions and 40 deletions

View File

@@ -47,12 +47,16 @@ final class AppContext {
func selectServer(index: UInt32) throws {
try core.selectServer(index: index)
reloadAfterServerChange()
}
func reloadAfterServerChange() {
let replacements: [(Int, UIViewController)] = [
(0, HomeViewController(context: self)),
(1, RepositoriesViewController(context: self, mode: .issues)),
(2, RepositoriesViewController(context: self, mode: .commits)),
(1, repositoryRoot(mode: .issues)),
(2, repositoryRoot(mode: .commits)),
(3, PullsViewController(context: self)),
(4, RepositoriesViewController(context: self, mode: .milestones)),
(4, repositoryRoot(mode: .milestones)),
]
for (index, root) in replacements {
navigationControllers[index].setViewControllers([root], animated: false)

View File

@@ -4,6 +4,7 @@ import UIKit
final class ServersViewController: UITableViewController {
private let context: AppContext
private var servers: [ServerRow] = []
private var mutationTask: Task<Void, Never>?
init(context: AppContext) {
self.context = context
@@ -16,16 +17,16 @@ final class ServersViewController: UITableViewController {
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
deinit {
mutationTask?.cancel()
}
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
reloadServers()
navigationItem.rightBarButtonItem = UIBarButtonItem(
systemItem: .add,
primaryAction: UIAction { [weak self] _ in self?.showAddServer() }
primaryAction: UIAction { [weak self] _ in self?.showServerEditor() }
)
}
@@ -54,18 +55,101 @@ final class ServersViewController: UITableViewController {
}
}
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()
override func tableView(
_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
) -> UISwipeActionsConfiguration? {
let server = servers[indexPath.row]
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") {
[weak self] _, _, completion in
self?.confirmDelete(server, index: indexPath.row, completion: completion)
?? completion(false)
}
present(UINavigationController(rootViewController: controller), animated: true)
deleteAction.image = context.symbol("trash")
let editAction = UIContextualAction(style: .normal, title: "Edit") {
[weak self] _, _, completion in
self?.showServerEditor(index: indexPath.row, completion: completion)
?? completion(false)
}
editAction.image = context.symbol("pencil")
let configuration = UISwipeActionsConfiguration(actions: [deleteAction, editAction])
configuration.performsFirstActionWithFullSwipe = true
return configuration
}
private func reloadServers() {
servers = context.core.servers()
tableView.reloadData()
tableView.backgroundView = servers.isEmpty
? EmptyBackgroundView(title: "No servers", detail: "Add a code hosting server to get started.")
: nil
}
private func showServerEditor(
index: Int? = nil,
completion swipeCompletion: ((Bool) -> Void)? = nil
) {
do {
let editor = try index.map { try context.core.serverEditor(index: UInt32($0)) }
let controller = ServerEditorViewController(
context: context,
index: index.map(UInt32.init),
editor: editor
) { [weak self] in
self?.reloadServers()
}
present(UINavigationController(rootViewController: controller), animated: true) {
swipeCompletion?(true)
}
} catch {
swipeCompletion?(false)
show(error: error)
}
}
private func confirmDelete(
_ server: ServerRow,
index: Int,
completion: @escaping (Bool) -> Void
) {
let alert = UIAlertController(
title: "Delete “\(server.name)”?",
message: "This removes the server configuration and access token from this device. It doesnt change anything on the server.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in completion(false) })
alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in
guard let self else {
completion(false)
return
}
self.mutationTask?.cancel()
self.mutationTask = Task {
do {
try await self.context.core.deleteServer(index: UInt32(index))
guard !Task.isCancelled else {
completion(false)
return
}
completion(true)
self.reloadServers()
self.context.reloadAfterServerChange()
} catch {
completion(false)
if !Task.isCancelled { self.show(error: error) }
}
}
})
present(alert, animated: true)
}
}
@MainActor
final class AddServerViewController: UITableViewController, UITextFieldDelegate {
final class ServerEditorViewController: UITableViewController, UITextFieldDelegate {
private let context: AppContext
private let index: UInt32?
private let editor: ServerEditor?
private let completion: () -> Void
private let nameField = UITextField()
private let urlField = UITextField()
@@ -74,11 +158,19 @@ final class AddServerViewController: UITableViewController, UITextFieldDelegate
private var provider = ServerProvider.gitea
private var saveButton: UIBarButtonItem!
init(context: AppContext, completion: @escaping () -> Void) {
init(
context: AppContext,
index: UInt32?,
editor: ServerEditor?,
completion: @escaping () -> Void
) {
self.context = context
self.index = index
self.editor = editor
self.completion = completion
provider = editor?.provider ?? .gitea
super.init(style: .insetGrouped)
title = "Add Server"
title = index == nil ? "Add Server" : "Edit Server"
}
@available(*, unavailable)
@@ -91,7 +183,7 @@ final class AddServerViewController: UITableViewController, UITextFieldDelegate
primaryAction: UIAction { [weak self] _ in self?.dismiss(animated: true) }
)
saveButton = UIBarButtonItem(
title: "Add",
title: index == nil ? "Add" : "Save",
style: .done,
target: self,
action: #selector(save)
@@ -106,6 +198,11 @@ final class AddServerViewController: UITableViewController, UITextFieldDelegate
tokenField.autocapitalizationType = .none
tokenField.returnKeyType = .done
configureProviderButton()
nameField.text = editor?.name
urlField.text = editor?.url
if editor != nil {
tokenField.placeholder = "Leave unchanged"
}
}
override func numberOfSections(in tableView: UITableView) -> Int { 4 }
@@ -150,13 +247,24 @@ final class AddServerViewController: UITableViewController, UITextFieldDelegate
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)
if let index {
try await context.core.updateServer(
index: index,
name: nameField.text ?? "",
url: urlField.text ?? "",
token: tokenField.text ?? "",
provider: provider
)
context.reloadAfterServerChange()
} else {
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 {
@@ -186,14 +294,14 @@ final class AddServerViewController: UITableViewController, UITextFieldDelegate
providerButton.showsMenuAsPrimaryAction = true
providerButton.changesSelectionAsPrimaryAction = true
providerButton.accessibilityLabel = "API provider"
providerButton.accessibilityValue = "Gitea"
providerButton.accessibilityValue = provider == .gitea ? "Gitea" : "Forgejo"
providerButton.menu = UIMenu(options: .singleSelection, children: [
UIAction(title: "Gitea", state: .on) { [weak self] _ in
UIAction(title: "Gitea", state: provider == .gitea ? .on : .off) { [weak self] _ in
self?.provider = .gitea
self?.urlField.placeholder = "https://gitea.example.com"
self?.providerButton.accessibilityValue = "Gitea"
},
UIAction(title: "Forgejo") { [weak self] _ in
UIAction(title: "Forgejo", state: provider == .forgejo ? .on : .off) { [weak self] _ in
self?.provider = .forgejo
self?.urlField.placeholder = "https://forgejo.example.com"
self?.providerButton.accessibilityValue = "Forgejo"