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,122 @@
|
||||
import SwiftUI
|
||||
|
||||
struct InboxView: View {
|
||||
@StateObject private var vm = InboxViewModel()
|
||||
@State private var showSettings = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Group {
|
||||
if !vm.isConfigured {
|
||||
ContentUnavailableView(
|
||||
"Not configured",
|
||||
systemImage: "key",
|
||||
description: Text("Add your Gitea instance and token in Settings."))
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) { settingsButton }
|
||||
}
|
||||
} else if vm.displayedRows.isEmpty && !vm.isLoading {
|
||||
ContentUnavailableView(
|
||||
"No open PRs",
|
||||
systemImage: "tray",
|
||||
description: Text(vm.filter == .all
|
||||
? "Nothing to review."
|
||||
: "No PRs match this filter."))
|
||||
} else {
|
||||
List {
|
||||
ForEach(vm.displayedRows) { row in
|
||||
NavigationLink(value: row) {
|
||||
PRRowView(row: row)
|
||||
}
|
||||
}
|
||||
}
|
||||
.refreshable { await vm.load() }
|
||||
.listStyle(.plain)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Pull Requests")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) { settingsButton }
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
Picker("Filter", selection: $vm.filter) {
|
||||
ForEach(InboxFilter.allCases) { f in
|
||||
Text(f.rawValue).tag(f)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.disabled(vm.displayedRows.isEmpty && vm.isLoading)
|
||||
}
|
||||
}
|
||||
.navigationDestination(for: PRRow.self) { row in
|
||||
PRDetailView(repo: row.repo, prNumber: row.pr.number)
|
||||
}
|
||||
.task { await vm.load() }
|
||||
.overlay {
|
||||
if vm.isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.alert("Error", isPresented: .init(
|
||||
get: { vm.errorMessage != nil },
|
||||
set: { if !$0 { vm.errorMessage = nil } })) {
|
||||
Button("OK", role: .cancel) {}
|
||||
} message: {
|
||||
Text(vm.errorMessage ?? "")
|
||||
}
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var settingsButton: some View {
|
||||
Button { showSettings = true } label: {
|
||||
Image(systemName: "gearshape")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PRRowView: View {
|
||||
let row: PRRow
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(spacing: 6) {
|
||||
Text("#\(row.pr.number)")
|
||||
.font(.subheadline.weight(.bold))
|
||||
.foregroundColor(.secondary)
|
||||
CICircle(status: row.ciStatus)
|
||||
if row.reviewState != .unknown {
|
||||
ReviewBadge(state: row.reviewState)
|
||||
}
|
||||
if row.pr.draft == true {
|
||||
Text("Draft")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
Text(row.pr.title)
|
||||
.font(.body.weight(.medium))
|
||||
.lineLimit(2)
|
||||
Text(row.fullName)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
if let updated = row.pr.updatedAt {
|
||||
Text(relative(updated))
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
||||
private func relative(_ date: Date) -> String {
|
||||
let f = RelativeDateTimeFormatter()
|
||||
f.unitsStyle = .abbreviated
|
||||
return f.localizedString(for: date, relativeTo: Date())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user