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

@@ -671,10 +671,14 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
func addServer(name: String, url: String, token: String, provider: ServerProvider) async throws -> UInt32
func deleteServer(index: UInt32) async throws
func home(page: UInt32, filter: HomeActivityFilter) async throws -> HomePage
func selectServer(index: UInt32) throws
func serverEditor(index: UInt32) throws -> ServerEditor
func servers() -> [ServerRow]
func setAppearance(index: UInt32) throws
@@ -687,6 +691,8 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
func startupError() -> String?
func updateServer(index: UInt32, name: String, url: String, token: String, provider: ServerProvider) async throws
}
open class GotchaCore: GotchaCoreProtocol, @unchecked Sendable {
fileprivate let handle: UInt64
@@ -1240,6 +1246,22 @@ open func addServer(name: String, url: String, token: String, provider: ServerPr
)
}
open func deleteServer(index: UInt32)async throws {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_delete_server(
self.uniffiCloneHandle(),FfiConverterUInt32.lower(index)
)
},
pollFunc: ffi_gotcha_core_rust_future_poll_void,
completeFunc: ffi_gotcha_core_rust_future_complete_void,
freeFunc: ffi_gotcha_core_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeGotchaError_lift
)
}
open func home(page: UInt32, filter: HomeActivityFilter)async throws -> HomePage {
return
try await uniffiRustCallAsync(
@@ -1265,6 +1287,16 @@ open func selectServer(index: UInt32)throws {try rustCallWithError(FfiConverte
}
}
open func serverEditor(index: UInt32)throws -> ServerEditor {
return try FfiConverterTypeServerEditor_lift(try rustCallWithError(FfiConverterTypeGotchaError_lift) {
uniffiCallStatus in
uniffi_gotcha_core_fn_method_gotchacore_server_editor(
self.uniffiCloneHandle(),
FfiConverterUInt32.lower(index),uniffiCallStatus
)
})
}
open func servers() -> [ServerRow] {
return try! FfiConverterSequenceTypeServerRow.lift(try! rustCall() {
uniffiCallStatus in
@@ -1319,6 +1351,22 @@ open func startupError() -> String? {
})
}
open func updateServer(index: UInt32, name: String, url: String, token: String, provider: ServerProvider)async throws {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_update_server(
self.uniffiCloneHandle(),FfiConverterUInt32.lower(index),FfiConverterString.lower(name),FfiConverterString.lower(url),FfiConverterString.lower(token),FfiConverterTypeServerProvider_lower(provider)
)
},
pollFunc: ffi_gotcha_core_rust_future_poll_void,
completeFunc: ffi_gotcha_core_rust_future_complete_void,
freeFunc: ffi_gotcha_core_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeGotchaError_lift
)
}
}
@@ -3415,6 +3463,58 @@ public func FfiConverterTypeRepositoryRow_lower(_ value: RepositoryRow) -> RustB
}
public struct ServerEditor: Equatable, Hashable {
public var name: String
public var url: String
public var provider: ServerProvider
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(name: String, url: String, provider: ServerProvider) {
self.name = name
self.url = url
self.provider = provider
}
}
#if compiler(>=6)
extension ServerEditor: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeServerEditor: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ServerEditor {
return
try ServerEditor(
name: FfiConverterString.read(from: &buf),
url: FfiConverterString.read(from: &buf),
provider: FfiConverterTypeServerProvider.read(from: &buf)
)
}
public static func write(_ value: ServerEditor, into buf: inout [UInt8]) {
FfiConverterString.write(value.name, into: &buf)
FfiConverterString.write(value.url, into: &buf)
FfiConverterTypeServerProvider.write(value.provider, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeServerEditor_lift(_ buf: RustBuffer) throws -> ServerEditor {
return try FfiConverterTypeServerEditor.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeServerEditor_lower(_ value: ServerEditor) -> RustBuffer {
return FfiConverterTypeServerEditor.lower(value)
}
public struct ServerRow: Equatable, Hashable {
public var name: String
public var url: String
@@ -5012,12 +5112,18 @@ private let initializationResult: InitializationResult = {
if (uniffi_gotcha_core_checksum_method_gotchacore_add_server() != 51168) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_delete_server() != 5208) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_home() != 37602) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_select_server() != 10204) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_server_editor() != 36193) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_servers() != 16732) {
return InitializationResult.apiChecksumMismatch
}
@@ -5036,6 +5142,9 @@ private let initializationResult: InitializationResult = {
if (uniffi_gotcha_core_checksum_method_gotchacore_startup_error() != 58843) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_method_gotchacore_update_server() != 31001) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_gotcha_core_checksum_constructor_gotchacore_new() != 35044) {
return InitializationResult.apiChecksumMismatch
}
@@ -5056,4 +5165,4 @@ public func uniffiEnsureGotchaCoreInitialized() {
}
}
// swiftlint:enable all
// swiftlint:enable all

View File

@@ -429,6 +429,11 @@ RustBuffer uniffi_gotcha_core_fn_method_gotchacore_active_server_name(uint64_t p
uint64_t uniffi_gotcha_core_fn_method_gotchacore_add_server(uint64_t ptr, RustBuffer name, RustBuffer url, RustBuffer token, RustBuffer provider
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_DELETE_SERVER
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_DELETE_SERVER
uint64_t uniffi_gotcha_core_fn_method_gotchacore_delete_server(uint64_t ptr, uint32_t index
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_HOME
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_HOME
uint64_t uniffi_gotcha_core_fn_method_gotchacore_home(uint64_t ptr, uint32_t page, RustBuffer filter
@@ -439,6 +444,11 @@ uint64_t uniffi_gotcha_core_fn_method_gotchacore_home(uint64_t ptr, uint32_t pag
void uniffi_gotcha_core_fn_method_gotchacore_select_server(uint64_t ptr, uint32_t index, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SERVER_EDITOR
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SERVER_EDITOR
RustBuffer uniffi_gotcha_core_fn_method_gotchacore_server_editor(uint64_t ptr, uint32_t index, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SERVERS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SERVERS
RustBuffer uniffi_gotcha_core_fn_method_gotchacore_servers(uint64_t ptr, RustCallStatus *_Nonnull out_status
@@ -469,6 +479,11 @@ RustBuffer uniffi_gotcha_core_fn_method_gotchacore_settings(uint64_t ptr, RustCa
RustBuffer uniffi_gotcha_core_fn_method_gotchacore_startup_error(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_UPDATE_SERVER
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_UPDATE_SERVER
uint64_t uniffi_gotcha_core_fn_method_gotchacore_update_server(uint64_t ptr, uint32_t index, RustBuffer name, RustBuffer url, RustBuffer token, RustBuffer provider
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUSTBUFFER_ALLOC
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUSTBUFFER_ALLOC
RustBuffer ffi_gotcha_core_rustbuffer_alloc(uint64_t size, RustCallStatus *_Nonnull out_status
@@ -931,6 +946,12 @@ uint16_t uniffi_gotcha_core_checksum_method_gotchacore_active_server_name(void
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ADD_SERVER
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_add_server(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_DELETE_SERVER
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_DELETE_SERVER
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_delete_server(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_HOME
@@ -943,6 +964,12 @@ uint16_t uniffi_gotcha_core_checksum_method_gotchacore_home(void
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SELECT_SERVER
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_select_server(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SERVER_EDITOR
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SERVER_EDITOR
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_server_editor(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SERVERS
@@ -979,6 +1006,12 @@ uint16_t uniffi_gotcha_core_checksum_method_gotchacore_settings(void
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_STARTUP_ERROR
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_startup_error(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_UPDATE_SERVER
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_UPDATE_SERVER
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_update_server(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_CONSTRUCTOR_GOTCHACORE_NEW

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"