190 lines
7.8 KiB
Swift
190 lines
7.8 KiB
Swift
// CxLLM Studio — iOS GitView.swift
|
|
// Gitea repository dashboard adapted for iOS
|
|
|
|
import SwiftUI
|
|
import CxGit
|
|
|
|
struct GitView: View {
|
|
@Environment(GitService.self) private var git
|
|
|
|
enum Section: String, CaseIterable {
|
|
case repos = "Repos"
|
|
case pulls = "PRs"
|
|
case issues = "Issues"
|
|
}
|
|
|
|
@State private var section: Section = .repos
|
|
@State private var selectedRepo: GitRepository?
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Connection bar
|
|
HStack(spacing: 8) {
|
|
Circle().fill(git.isConnected ? .green : .red).frame(width: 8, height: 8)
|
|
Text(git.isConnected ? "Connected" : "Disconnected").font(.caption)
|
|
Spacer()
|
|
if let user = git.currentUser {
|
|
Label(user.login, systemImage: "person.circle").font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
Button { Task { await git.refreshAll() } } label: {
|
|
Image(systemName: "arrow.clockwise").font(.caption)
|
|
}
|
|
}
|
|
.padding(.horizontal).padding(.vertical, 8)
|
|
.background(.ultraThinMaterial)
|
|
|
|
// Section picker
|
|
Picker("Section", selection: $section) {
|
|
ForEach(Section.allCases, id: \.self) { s in
|
|
Text(s.rawValue).tag(s)
|
|
}
|
|
}
|
|
.pickerStyle(.segmented)
|
|
.padding(.horizontal).padding(.vertical, 8)
|
|
|
|
// Content
|
|
switch section {
|
|
case .repos: reposList
|
|
case .pulls: pullsList
|
|
case .issues: issuesList
|
|
}
|
|
}
|
|
.task {
|
|
await git.refreshAll()
|
|
}
|
|
}
|
|
|
|
// MARK: - Repos
|
|
|
|
private var reposList: some View {
|
|
Group {
|
|
if git.repositories.isEmpty {
|
|
ContentUnavailableView("No Repositories", systemImage: "folder",
|
|
description: Text("Check your Gitea connection"))
|
|
} else {
|
|
List(git.repositories) { repo in
|
|
NavigationLink {
|
|
repoDetail(repo)
|
|
} label: {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: repo.private ? "lock.fill" : "globe")
|
|
.foregroundStyle(repo.private ? .orange : .green)
|
|
.font(.caption)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(repo.name).font(.subheadline.weight(.medium))
|
|
if let desc = repo.description, !desc.isEmpty {
|
|
Text(desc).font(.caption).foregroundStyle(.secondary).lineLimit(1)
|
|
}
|
|
}
|
|
Spacer()
|
|
HStack(spacing: 6) {
|
|
Label("\(repo.starsCount)", systemImage: "star").font(.caption2)
|
|
Label("\(repo.openIssuesCount)", systemImage: "exclamationmark.circle").font(.caption2)
|
|
}.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func repoDetail(_ repo: GitRepository) -> some View {
|
|
List {
|
|
SwiftUI.Section("Info") {
|
|
LabeledContent("Name", value: repo.name)
|
|
LabeledContent("Default Branch", value: repo.defaultBranch)
|
|
LabeledContent("Stars", value: "\(repo.starsCount)")
|
|
LabeledContent("Forks", value: "\(repo.forksCount)")
|
|
LabeledContent("Open Issues", value: "\(repo.openIssuesCount)")
|
|
if let desc = repo.description { LabeledContent("Description", value: desc) }
|
|
}
|
|
|
|
SwiftUI.Section {
|
|
Button("Load PRs") {
|
|
let parts = repo.fullName.components(separatedBy: "/")
|
|
if parts.count == 2 {
|
|
Task { await git.loadPullRequests(owner: parts[0], repo: parts[1]); section = .pulls }
|
|
}
|
|
}
|
|
Button("Load Issues") {
|
|
let parts = repo.fullName.components(separatedBy: "/")
|
|
if parts.count == 2 {
|
|
Task { await git.loadIssues(owner: parts[0], repo: parts[1]); section = .issues }
|
|
}
|
|
}
|
|
Button("Load Branches") {
|
|
let parts = repo.fullName.components(separatedBy: "/")
|
|
if parts.count == 2 {
|
|
Task { await git.loadBranches(owner: parts[0], repo: parts[1]) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle(repo.name)
|
|
}
|
|
|
|
// MARK: - Pull Requests
|
|
|
|
private var pullsList: some View {
|
|
Group {
|
|
if git.pullRequests.isEmpty {
|
|
ContentUnavailableView("No Pull Requests", systemImage: "arrow.triangle.pull",
|
|
description: Text("Select a repo to load PRs"))
|
|
} else {
|
|
List(git.pullRequests) { pr in
|
|
HStack(spacing: 8) {
|
|
Image(systemName: pr.state == "open" ? "arrow.triangle.pull" : "checkmark.circle.fill")
|
|
.foregroundStyle(pr.state == "open" ? .green : .purple)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("#\(pr.number) \(pr.title)").font(.subheadline.weight(.medium))
|
|
HStack(spacing: 6) {
|
|
if let user = pr.user { Text(user.login).font(.caption2) }
|
|
if let head = pr.head, let base = pr.base {
|
|
Text("\(head.ref) → \(base.ref)").font(.caption2).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
Spacer()
|
|
Text(pr.state).font(.caption2)
|
|
.padding(.horizontal, 6).padding(.vertical, 2)
|
|
.background(pr.state == "open" ? Color.green.opacity(0.1) : Color.purple.opacity(0.1))
|
|
.clipShape(Capsule())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Issues
|
|
|
|
private var issuesList: some View {
|
|
Group {
|
|
if git.issues.isEmpty {
|
|
ContentUnavailableView("No Issues", systemImage: "exclamationmark.circle",
|
|
description: Text("Select a repo to load issues"))
|
|
} else {
|
|
List(git.issues) { issue in
|
|
HStack(spacing: 8) {
|
|
Image(systemName: issue.state == "open" ? "circle" : "checkmark.circle.fill")
|
|
.foregroundStyle(issue.state == "open" ? .green : .secondary)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("#\(issue.number) \(issue.title)").font(.subheadline.weight(.medium))
|
|
HStack(spacing: 4) {
|
|
if let user = issue.user { Text(user.login).font(.caption2).foregroundStyle(.secondary) }
|
|
if let labels = issue.labels {
|
|
ForEach(labels.prefix(3)) { label in
|
|
Text(label.name).font(.system(size: 9))
|
|
.padding(.horizontal, 4).padding(.vertical, 1)
|
|
.background(Color.secondary.opacity(0.1)).clipShape(Capsule())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Spacer()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|