GiteaPR: native iOS app to manage Gitea pull requests
Build iOS App / Build .ipa and publish source (push) Failing after 35s
Build iOS App / Build .ipa and publish source (push) Failing after 35s
Multi-repo PR inbox, review (approve/request changes/comment), merge with gating toggle, read-only diff viewer, and Gitea Actions check status with one-tap re-run. Ships unsigned via the iMac runner to the SideStore source.
This commit is contained in:
commit
1025634978
27 files changed
+2322
No files matched your search
@@ -0,0 +1,54 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
enum MergeGateMode: String, CaseIterable, Identifiable {
|
||||
case warn = "Warn on red (allow merge with confirm)"
|
||||
case block = "Block merge when CI is red"
|
||||
|
||||
var id: String { rawValue }
|
||||
}
|
||||
|
||||
/// Central settings: instance + token (Keychain) and app preferences.
|
||||
@MainActor
|
||||
final class SettingsStore: ObservableObject {
|
||||
static let shared = SettingsStore()
|
||||
|
||||
@Published var instanceURL: String {
|
||||
didSet { KeychainHelper.set(instanceURL, key: "instanceURL") }
|
||||
}
|
||||
@Published var token: String {
|
||||
didSet { KeychainHelper.set(token, key: "token") }
|
||||
}
|
||||
@Published var mergeGateMode: MergeGateMode {
|
||||
didSet { UserDefaults.standard.set(mergeGateMode.rawValue,
|
||||
forKey: "mergeGateMode") }
|
||||
}
|
||||
|
||||
private init() {
|
||||
instanceURL = KeychainHelper.get("instanceURL") ?? "https://gitea.bueso.eu"
|
||||
token = KeychainHelper.get("token") ?? ""
|
||||
let raw = UserDefaults.standard.string(forKey: "mergeGateMode")
|
||||
mergeGateMode = MergeGateMode(rawValue: raw ?? "") ?? .warn
|
||||
}
|
||||
|
||||
var isConfigured: Bool {
|
||||
!token.isEmpty && !instanceURL.isEmpty
|
||||
}
|
||||
|
||||
var baseURL: URL? {
|
||||
URL(string: instanceURL.hasSuffix("/") ?
|
||||
String(instanceURL.dropLast()) : instanceURL)
|
||||
}
|
||||
|
||||
func client() -> GiteaClient? {
|
||||
guard let base = baseURL else { return nil }
|
||||
return GiteaClient(instanceURL: base, token: token)
|
||||
}
|
||||
|
||||
func logout() {
|
||||
KeychainHelper.delete("instanceURL")
|
||||
KeychainHelper.delete("token")
|
||||
instanceURL = "https://gitea.bueso.eu"
|
||||
token = ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var instanceURL: String
|
||||
@State private var token: String
|
||||
@State private var mergeGateMode: MergeGateMode
|
||||
@State private var testResult: TestResult?
|
||||
@State private var testing = false
|
||||
@State private var showLogoutConfirm = false
|
||||
|
||||
enum TestResult: Equatable {
|
||||
case success(String)
|
||||
case failure(String)
|
||||
}
|
||||
|
||||
init() {
|
||||
_instanceURL = State(initialValue: SettingsStore.shared.instanceURL)
|
||||
_token = State(initialValue: SettingsStore.shared.token)
|
||||
_mergeGateMode = State(initialValue: SettingsStore.shared.mergeGateMode)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section("Server") {
|
||||
TextField("Instance URL", text: $instanceURL)
|
||||
.keyboardType(.URL)
|
||||
.autocapitalization(.none)
|
||||
.autocorrectionDisabled()
|
||||
.textInputAutocapitalization(.never)
|
||||
SecureField("Personal access token", text: $token)
|
||||
Button(testing ? "Testing…" : "Test connection") {
|
||||
test()
|
||||
}
|
||||
.disabled(testing || token.isEmpty)
|
||||
if let testResult {
|
||||
testResultView(testResult)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Merge gating") {
|
||||
Picker("Merge behavior", selection: $mergeGateMode) {
|
||||
ForEach(MergeGateMode.allCases) { m in
|
||||
Text(m.rawValue).tag(m)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.inline)
|
||||
Text("Warn mode shows CI status and allows merging a red PR after confirmation. Block mode disables merge while any check is failing.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("Log out", role: .destructive) {
|
||||
showLogoutConfirm = true
|
||||
}
|
||||
.disabled(token.isEmpty)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") { dismiss() }
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Save") { save() }
|
||||
.disabled(instanceURL.trimmingCharacters(in: .whitespaces)
|
||||
.isEmpty)
|
||||
}
|
||||
}
|
||||
.confirmationDialog("Log out?",
|
||||
isPresented: $showLogoutConfirm,
|
||||
titleVisibility: .visible) {
|
||||
Button("Log out", role: .destructive) {
|
||||
SettingsStore.shared.logout()
|
||||
testResult = nil
|
||||
dismiss()
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func testResultView(_ result: TestResult) -> some View {
|
||||
switch result {
|
||||
case .success(let login):
|
||||
Label("Connected as \(login)", systemImage: "checkmark.circle.fill")
|
||||
.font(.callout)
|
||||
.foregroundColor(.green)
|
||||
case .failure(let message):
|
||||
Label(message, systemImage: "xmark.circle.fill")
|
||||
.font(.callout)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
private func test() {
|
||||
testing = true
|
||||
testResult = nil
|
||||
let trimmed = instanceURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard let url = URL(string: trimmed) else {
|
||||
testResult = .failure("Invalid URL")
|
||||
testing = false
|
||||
return
|
||||
}
|
||||
let client = GiteaClient(instanceURL: url, token: token)
|
||||
Task {
|
||||
do {
|
||||
let user = try await client.currentUser()
|
||||
AppSession.currentLogin = user.login
|
||||
testResult = .success(user.login)
|
||||
} catch {
|
||||
testResult = .failure(error.localizedDescription)
|
||||
}
|
||||
testing = false
|
||||
}
|
||||
}
|
||||
|
||||
private func save() {
|
||||
SettingsStore.shared.instanceURL =
|
||||
instanceURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
SettingsStore.shared.token = token
|
||||
SettingsStore.shared.mergeGateMode = mergeGateMode
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user