Prepare Gotcha 1.0 for release

This commit is contained in:
Georg Bauer
2026-08-03 17:54:07 +02:00
parent 1c4a6efd13
commit 46cca8fdd2
60 changed files with 7916 additions and 7173 deletions

View File

@@ -0,0 +1,177 @@
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 Gitea 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 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
}
override func numberOfSections(in tableView: UITableView) -> Int { 3 }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 }
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
["Name", "Server URL", "Access token"][section]
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
let field = [nameField, urlField, tokenField][indexPath.section]
field.translatesAutoresizingMaskIntoConstraints = false
cell.contentView.addSubview(field)
NSLayoutConstraint.activate([
field.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 16),
field.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -16),
field.topAnchor.constraint(equalTo: cell.contentView.topAnchor),
field.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 ?? ""
)
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)
}
}