Fixed Issue view
This commit is contained in:
@ -180,6 +180,8 @@ class GiteaAPIService: ObservableObject {
|
|||||||
if let state = state {
|
if let state = state {
|
||||||
queryParams += "&state=\(state.rawValue)"
|
queryParams += "&state=\(state.rawValue)"
|
||||||
}
|
}
|
||||||
|
// Add type parameter to filter only issues (not pull requests)
|
||||||
|
queryParams += "&type=issues"
|
||||||
|
|
||||||
guard let request = createRequest(endpoint: "/repos/\(owner)/\(repo)/issues" + queryParams)
|
guard let request = createRequest(endpoint: "/repos/\(owner)/\(repo)/issues" + queryParams)
|
||||||
else {
|
else {
|
||||||
@ -270,10 +272,15 @@ class GiteaAPIService: ObservableObject {
|
|||||||
return try await performRequest(request, responseType: Repository.self)
|
return try await performRequest(request, responseType: Repository.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateRepository(owner: String, repo: String, updateData: [String: Any]) async throws -> Repository {
|
func updateRepository(owner: String, repo: String, updateData: [String: Any]) async throws
|
||||||
|
-> Repository
|
||||||
|
{
|
||||||
// Encode the dictionary to JSON data
|
// Encode the dictionary to JSON data
|
||||||
let jsonData = try JSONSerialization.data(withJSONObject: updateData)
|
let jsonData = try JSONSerialization.data(withJSONObject: updateData)
|
||||||
guard let request = createRequest(endpoint: "/repos/\(owner)/\(repo)", method: .PATCH, body: jsonData) else {
|
guard
|
||||||
|
let request = createRequest(
|
||||||
|
endpoint: "/repos/\(owner)/\(repo)", method: .PATCH, body: jsonData)
|
||||||
|
else {
|
||||||
throw APIError.invalidURL
|
throw APIError.invalidURL
|
||||||
}
|
}
|
||||||
return try await performRequest(request, responseType: Repository.self)
|
return try await performRequest(request, responseType: Repository.self)
|
||||||
@ -692,4 +699,3 @@ enum APIError: LocalizedError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,6 +178,19 @@ struct ExternalWiki: Codable, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Issue
|
// MARK: - Issue
|
||||||
|
// MARK: - Issue Repository (simplified for issue responses)
|
||||||
|
struct IssueRepository: Codable {
|
||||||
|
let id: Int
|
||||||
|
let name: String
|
||||||
|
let owner: String
|
||||||
|
let fullName: String
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id, name, owner
|
||||||
|
case fullName = "full_name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Issue: Codable, Identifiable {
|
struct Issue: Codable, Identifiable {
|
||||||
let id: Int
|
let id: Int
|
||||||
let number: Int
|
let number: Int
|
||||||
@ -196,7 +209,7 @@ struct Issue: Codable, Identifiable {
|
|||||||
let closedAt: Date?
|
let closedAt: Date?
|
||||||
let dueDate: Date?
|
let dueDate: Date?
|
||||||
let pullRequest: PullRequestMeta?
|
let pullRequest: PullRequestMeta?
|
||||||
let repository: Repository?
|
let repository: IssueRepository?
|
||||||
let htmlUrl: String
|
let htmlUrl: String
|
||||||
let url: String
|
let url: String
|
||||||
|
|
||||||
@ -217,6 +230,7 @@ struct Issue: Codable, Identifiable {
|
|||||||
enum IssueState: String, Codable, CaseIterable {
|
enum IssueState: String, Codable, CaseIterable {
|
||||||
case open = "open"
|
case open = "open"
|
||||||
case closed = "closed"
|
case closed = "closed"
|
||||||
|
case all = "all"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Pull Request Meta
|
// MARK: - Pull Request Meta
|
||||||
@ -445,16 +459,34 @@ struct CommitMeta: Codable {
|
|||||||
let created: Date
|
let created: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Branch Commit User
|
||||||
|
struct BranchCommitUser: Codable {
|
||||||
|
let name: String
|
||||||
|
let email: String
|
||||||
|
let username: String
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Branch Commit (different structure than CommitMeta)
|
||||||
|
struct BranchCommit: Codable {
|
||||||
|
let id: String
|
||||||
|
let message: String
|
||||||
|
let url: String
|
||||||
|
let author: BranchCommitUser
|
||||||
|
let committer: BranchCommitUser
|
||||||
|
let verification: CommitVerification
|
||||||
|
let timestamp: Date
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Commit File
|
// MARK: - Commit File
|
||||||
struct CommitFile: Codable {
|
struct CommitFile: Codable {
|
||||||
let filename: String
|
let filename: String
|
||||||
let additions: Int
|
let additions: Int?
|
||||||
let deletions: Int
|
let deletions: Int?
|
||||||
let changes: Int
|
let changes: Int?
|
||||||
let status: String
|
let status: String
|
||||||
let rawUrl: String
|
let rawUrl: String?
|
||||||
let blobUrl: String
|
let blobUrl: String?
|
||||||
let patchUrl: String
|
let patchUrl: String?
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case filename, additions, deletions, changes, status
|
case filename, additions, deletions, changes, status
|
||||||
@ -471,19 +503,26 @@ struct CommitStats: Codable {
|
|||||||
let deletions: Int
|
let deletions: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Signer
|
||||||
|
struct Signer: Codable {
|
||||||
|
let name: String
|
||||||
|
let email: String
|
||||||
|
let username: String
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Commit Verification
|
// MARK: - Commit Verification
|
||||||
struct CommitVerification: Codable {
|
struct CommitVerification: Codable {
|
||||||
let verified: Bool
|
let verified: Bool
|
||||||
let reason: String
|
let reason: String
|
||||||
let signature: String
|
let signature: String
|
||||||
let payload: String
|
let payload: String
|
||||||
let signer: User?
|
let signer: Signer?
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Branch
|
// MARK: - Branch
|
||||||
struct Branch: Codable, Identifiable {
|
struct Branch: Codable, Identifiable {
|
||||||
let name: String
|
let name: String
|
||||||
let commit: CommitMeta
|
let commit: BranchCommit
|
||||||
let protected: Bool
|
let protected: Bool
|
||||||
let requiredApprovals: Int
|
let requiredApprovals: Int
|
||||||
let enableStatusCheck: Bool
|
let enableStatusCheck: Bool
|
||||||
@ -626,8 +665,6 @@ struct SearchResults<T: Codable>: Codable {
|
|||||||
let ok: Bool
|
let ok: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Authentication Token
|
// MARK: - Authentication Token
|
||||||
struct AccessToken: Codable, Identifiable {
|
struct AccessToken: Codable, Identifiable {
|
||||||
let id: Int
|
let id: Int
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
// Created by Atridad Lahiji on 2025-07-04.
|
// Created by Atridad Lahiji on 2025-07-04.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct RepositoryDetailView: View {
|
struct RepositoryDetailView: View {
|
||||||
@ -19,6 +18,7 @@ struct RepositoryDetailView: View {
|
|||||||
self._repository = State(initialValue: repository)
|
self._repository = State(initialValue: repository)
|
||||||
self.onRepositoryUpdated = onRepositoryUpdated
|
self.onRepositoryUpdated = onRepositoryUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
@EnvironmentObject var authManager: AuthenticationManager
|
@EnvironmentObject var authManager: AuthenticationManager
|
||||||
@EnvironmentObject var settingsManager: SettingsManager
|
@EnvironmentObject var settingsManager: SettingsManager
|
||||||
@State private var isLoading = false
|
@State private var isLoading = false
|
||||||
@ -104,26 +104,23 @@ struct RepositoryDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
|
||||||
.sheet(isPresented: $showingBranches) {
|
.sheet(isPresented: $showingBranches) {
|
||||||
BranchesSheet(branches: branches, repository: repository)
|
BranchesSheet(branches: branches, repository: repository)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingRepositorySettings) {
|
.sheet(isPresented: $showingRepositorySettings) {
|
||||||
RepositorySettingsView(
|
RepositorySettingsView(
|
||||||
repository: repository,
|
repository: repository,
|
||||||
onRepositoryUpdated: { updatedRepo in
|
onRepositoryUpdated: { updatedRepository in
|
||||||
repository = updatedRepo
|
repository = updatedRepository
|
||||||
onRepositoryUpdated?()
|
onRepositoryUpdated?()
|
||||||
},
|
},
|
||||||
onRepositoryDeleted: {
|
onRepositoryDeleted: {
|
||||||
repositoryDeleted = true
|
repositoryDeleted = true
|
||||||
|
dismiss()
|
||||||
onRepositoryUpdated?()
|
onRepositoryUpdated?()
|
||||||
})
|
}
|
||||||
}
|
)
|
||||||
.onChange(of: repositoryDeleted) { deleted in
|
.environmentObject(authManager)
|
||||||
if deleted {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
Task { await loadRepositoryData() }
|
Task { await loadRepositoryData() }
|
||||||
@ -163,20 +160,15 @@ struct RepositoryDetailView: View {
|
|||||||
image
|
image
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
.aspectRatio(contentMode: .fill)
|
||||||
.frame(width: 40, height: 40)
|
|
||||||
.clipped()
|
|
||||||
case .failure(_):
|
case .failure(_):
|
||||||
Image(systemName: "person.circle.fill")
|
Image(systemName: "person.circle.fill")
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.secondary)
|
||||||
.frame(width: 40, height: 40)
|
|
||||||
case .empty:
|
case .empty:
|
||||||
Image(systemName: "person.circle.fill")
|
ProgressView()
|
||||||
.foregroundColor(.gray)
|
.progressViewStyle(CircularProgressViewStyle(tint: .secondary))
|
||||||
.frame(width: 40, height: 40)
|
|
||||||
@unknown default:
|
@unknown default:
|
||||||
Image(systemName: "person.circle.fill")
|
Image(systemName: "person.circle.fill")
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.secondary)
|
||||||
.frame(width: 40, height: 40)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: 40, height: 40)
|
.frame(width: 40, height: 40)
|
||||||
@ -206,9 +198,9 @@ struct RepositoryDetailView: View {
|
|||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(colorForLanguage(language))
|
.fill(colorForLanguage(language))
|
||||||
.frame(width: 10, height: 10)
|
.frame(width: 12, height: 12)
|
||||||
Text(language)
|
Text(language)
|
||||||
.font(.system(size: 13))
|
.font(.system(size: 12))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -225,7 +217,7 @@ struct RepositoryDetailView: View {
|
|||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
ForEach(repository.topics, id: \.self) { topic in
|
ForEach(repository.topics, id: \.self) { topic in
|
||||||
Text(topic)
|
Text(topic)
|
||||||
.font(.system(size: 12))
|
.font(.system(size: 11))
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.background(Color.blue.opacity(0.1))
|
.background(Color.blue.opacity(0.1))
|
||||||
@ -281,19 +273,61 @@ struct RepositoryDetailView: View {
|
|||||||
private var issuesTab: some View {
|
private var issuesTab: some View {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Recent Issues")
|
let openIssuesCount = issues.filter { $0.state == .open }.count
|
||||||
|
let closedIssuesCount = issues.filter { $0.state == .closed }.count
|
||||||
|
|
||||||
|
Text("Issues (\(issues.count))")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if issues.count > 5 {
|
HStack(spacing: 12) {
|
||||||
Text("View All")
|
if openIssuesCount > 0 {
|
||||||
|
SwiftUI.Label(
|
||||||
|
"\(openIssuesCount) Open", systemImage: "exclamationmark.circle"
|
||||||
|
)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.green)
|
||||||
|
}
|
||||||
|
|
||||||
|
if closedIssuesCount > 0 {
|
||||||
|
SwiftUI.Label(
|
||||||
|
"\(closedIssuesCount) Closed", systemImage: "checkmark.circle"
|
||||||
|
)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if issues.isEmpty {
|
if isLoading {
|
||||||
|
ProgressView("Loading issues...")
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
} else if let errorMessage = errorMessage {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
Image(systemName: "exclamationmark.triangle")
|
||||||
|
.font(.system(size: 50))
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
|
||||||
|
Text("Error Loading Issues")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
|
Text(errorMessage)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
|
Button("Retry") {
|
||||||
|
Task {
|
||||||
|
await loadRepositoryData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.padding()
|
||||||
|
} else if issues.isEmpty {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
Image(systemName: "exclamationmark.circle")
|
Image(systemName: "exclamationmark.circle")
|
||||||
.font(.system(size: 50))
|
.font(.system(size: 50))
|
||||||
@ -311,9 +345,11 @@ struct RepositoryDetailView: View {
|
|||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.padding()
|
.padding()
|
||||||
} else {
|
} else {
|
||||||
VStack(spacing: 12) {
|
ScrollView {
|
||||||
ForEach(Array(issues.prefix(5))) { issue in
|
LazyVStack(spacing: 12) {
|
||||||
IssueRowView(issue: issue)
|
ForEach(issues) { issue in
|
||||||
|
IssueRowView(issue: issue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -355,9 +391,11 @@ struct RepositoryDetailView: View {
|
|||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.padding()
|
.padding()
|
||||||
} else {
|
} else {
|
||||||
VStack(spacing: 12) {
|
ScrollView {
|
||||||
ForEach(Array(pullRequests.prefix(5))) { pullRequest in
|
LazyVStack(spacing: 12) {
|
||||||
PullRequestRowView(pullRequest: pullRequest)
|
ForEach(Array(pullRequests.prefix(5))) { pr in
|
||||||
|
PullRequestRowView(pullRequest: pr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -391,16 +429,18 @@ struct RepositoryDetailView: View {
|
|||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.padding()
|
.padding()
|
||||||
} else {
|
} else {
|
||||||
VStack(spacing: 12) {
|
ScrollView {
|
||||||
ForEach(releases) { release in
|
LazyVStack(spacing: 12) {
|
||||||
ReleaseRowView(release: release)
|
ForEach(releases) { release in
|
||||||
|
ReleaseRowView(release: release)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadRepositoryData() async {
|
func loadRepositoryData() async {
|
||||||
guard let apiService = authManager.getAPIService() else { return }
|
guard let apiService = authManager.getAPIService() else { return }
|
||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
@ -411,8 +451,6 @@ struct RepositoryDetailView: View {
|
|||||||
owner: repository.owner.login, repo: repository.name)
|
owner: repository.owner.login, repo: repository.name)
|
||||||
async let releasesTask = apiService.getRepositoryReleases(
|
async let releasesTask = apiService.getRepositoryReleases(
|
||||||
owner: repository.owner.login, repo: repository.name, limit: 10)
|
owner: repository.owner.login, repo: repository.name, limit: 10)
|
||||||
async let issuesTask = apiService.getRepositoryIssues(
|
|
||||||
owner: repository.owner.login, repo: repository.name, limit: 10)
|
|
||||||
async let pullRequestsTask = apiService.getRepositoryPullRequests(
|
async let pullRequestsTask = apiService.getRepositoryPullRequests(
|
||||||
owner: repository.owner.login, repo: repository.name, limit: 10)
|
owner: repository.owner.login, repo: repository.name, limit: 10)
|
||||||
async let commitsTask = apiService.getRepositoryCommits(
|
async let commitsTask = apiService.getRepositoryCommits(
|
||||||
@ -420,9 +458,12 @@ struct RepositoryDetailView: View {
|
|||||||
|
|
||||||
branches = try await branchesTask
|
branches = try await branchesTask
|
||||||
releases = try await releasesTask
|
releases = try await releasesTask
|
||||||
issues = try await issuesTask
|
|
||||||
pullRequests = try await pullRequestsTask
|
pullRequests = try await pullRequestsTask
|
||||||
commits = try await commitsTask
|
commits = try await commitsTask
|
||||||
|
|
||||||
|
// Load issues using the 'all' state to get both open and closed issues
|
||||||
|
issues = try await apiService.getRepositoryIssues(
|
||||||
|
owner: repository.owner.login, repo: repository.name, state: .all, limit: 30)
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = error.localizedDescription
|
errorMessage = error.localizedDescription
|
||||||
}
|
}
|
||||||
@ -430,7 +471,7 @@ struct RepositoryDetailView: View {
|
|||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func toggleStar() {
|
func toggleStar() {
|
||||||
guard let apiService = authManager.getAPIService() else { return }
|
guard let apiService = authManager.getAPIService() else { return }
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
@ -452,7 +493,7 @@ struct RepositoryDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func toggleWatch() {
|
func toggleWatch() {
|
||||||
guard let apiService = authManager.getAPIService() else { return }
|
guard let apiService = authManager.getAPIService() else { return }
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
@ -474,8 +515,7 @@ struct RepositoryDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createUpdatedRepository(starsDelta: Int = 0, watchersDelta: Int = 0) -> Repository
|
func createUpdatedRepository(starsDelta: Int = 0, watchersDelta: Int = 0) -> Repository {
|
||||||
{
|
|
||||||
return Repository(
|
return Repository(
|
||||||
id: repository.id,
|
id: repository.id,
|
||||||
name: repository.name,
|
name: repository.name,
|
||||||
@ -495,8 +535,8 @@ struct RepositoryDetailView: View {
|
|||||||
language: repository.language,
|
language: repository.language,
|
||||||
languagesUrl: repository.languagesUrl,
|
languagesUrl: repository.languagesUrl,
|
||||||
forksCount: repository.forksCount,
|
forksCount: repository.forksCount,
|
||||||
stargazersCount: max(0, repository.stargazersCount + starsDelta),
|
stargazersCount: repository.stargazersCount + starsDelta,
|
||||||
watchersCount: max(0, repository.watchersCount + watchersDelta),
|
watchersCount: repository.watchersCount + watchersDelta,
|
||||||
openIssuesCount: repository.openIssuesCount,
|
openIssuesCount: repository.openIssuesCount,
|
||||||
openPrCounter: repository.openPrCounter,
|
openPrCounter: repository.openPrCounter,
|
||||||
releaseCounter: repository.releaseCounter,
|
releaseCounter: repository.releaseCounter,
|
||||||
@ -551,19 +591,17 @@ struct TabButton: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: action) {
|
Button(action: action) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.system(size: 14, weight: isSelected ? .medium : .regular))
|
.font(.system(size: 14, weight: isSelected ? .semibold : .regular))
|
||||||
.foregroundColor(isSelected ? .accentColor : .secondary)
|
.foregroundColor(isSelected ? .primary : .secondary)
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 16)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 8)
|
||||||
.background(
|
.background(
|
||||||
Rectangle()
|
RoundedRectangle(cornerRadius: 8)
|
||||||
.fill(isSelected ? Color.accentColor.opacity(0.1) : Color.clear)
|
.fill(isSelected ? Color.accentColor.opacity(0.1) : Color.clear)
|
||||||
.overlay(
|
)
|
||||||
Rectangle()
|
.overlay(
|
||||||
.frame(height: 2)
|
RoundedRectangle(cornerRadius: 8)
|
||||||
.foregroundColor(isSelected ? .accentColor : .clear),
|
.stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 1)
|
||||||
alignment: .bottom
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
@ -603,7 +641,7 @@ struct CommitRowView: View {
|
|||||||
.font(.system(size: 11, design: .monospaced))
|
.font(.system(size: 11, design: .monospaced))
|
||||||
.padding(.horizontal, 6)
|
.padding(.horizontal, 6)
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
.background(Color.gray.opacity(0.2))
|
.background(Color.secondary.opacity(0.1))
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
@ -616,8 +654,7 @@ struct IssueRowView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Image(
|
Image(
|
||||||
systemName: issue.state == .open
|
systemName: issue.state == .open ? "exclamationmark.circle" : "checkmark.circle"
|
||||||
? "exclamationmark.circle" : "checkmark.circle.fill"
|
|
||||||
)
|
)
|
||||||
.foregroundColor(issue.state == .open ? .green : .purple)
|
.foregroundColor(issue.state == .open ? .green : .purple)
|
||||||
.font(.system(size: 12))
|
.font(.system(size: 12))
|
||||||
@ -764,13 +801,13 @@ struct BranchesSheet: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
}
|
}
|
||||||
}
|
.navigationTitle("Branches")
|
||||||
.navigationTitle("Branches")
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.toolbar {
|
||||||
.toolbar {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
Button("Done") {
|
||||||
Button("Done") {
|
dismiss()
|
||||||
dismiss()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -806,7 +843,7 @@ struct RepositorySettingsView: View {
|
|||||||
self.onRepositoryDeleted = onRepositoryDeleted
|
self.onRepositoryDeleted = onRepositoryDeleted
|
||||||
self._name = State(initialValue: repository.name)
|
self._name = State(initialValue: repository.name)
|
||||||
self._description = State(initialValue: repository.description ?? "")
|
self._description = State(initialValue: repository.description ?? "")
|
||||||
self._website = State(initialValue: repository.externalWiki?.externalWikiUrl ?? "")
|
self._website = State(initialValue: "")
|
||||||
self._isPrivate = State(initialValue: repository.private)
|
self._isPrivate = State(initialValue: repository.private)
|
||||||
self._hasIssues = State(initialValue: repository.hasIssues)
|
self._hasIssues = State(initialValue: repository.hasIssues)
|
||||||
self._hasWiki = State(initialValue: repository.hasWiki)
|
self._hasWiki = State(initialValue: repository.hasWiki)
|
||||||
@ -818,21 +855,21 @@ struct RepositorySettingsView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
Form {
|
Form {
|
||||||
Section("General") {
|
Section("Repository Details") {
|
||||||
TextField("Repository name", text: $name)
|
TextField("Repository name", text: $name)
|
||||||
.textInputAutocapitalization(.never)
|
.autocapitalization(.none)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
|
|
||||||
TextField("Description", text: $description, axis: .vertical)
|
TextField("Description", text: $description, axis: .vertical)
|
||||||
.lineLimit(3...6)
|
.lineLimit(3...6)
|
||||||
|
|
||||||
TextField("Website", text: $website)
|
TextField("Website", text: $website)
|
||||||
.textInputAutocapitalization(.never)
|
.autocapitalization(.none)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.keyboardType(.URL)
|
.keyboardType(.URL)
|
||||||
|
|
||||||
TextField("Topics (comma separated)", text: $topics)
|
TextField("Topics (comma separated)", text: $topics)
|
||||||
.textInputAutocapitalization(.never)
|
.autocapitalization(.none)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -852,7 +889,7 @@ struct RepositorySettingsView: View {
|
|||||||
|
|
||||||
Section("Repository") {
|
Section("Repository") {
|
||||||
TextField("Default branch", text: $defaultBranch)
|
TextField("Default branch", text: $defaultBranch)
|
||||||
.textInputAutocapitalization(.never)
|
.autocapitalization(.none)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -872,7 +909,6 @@ struct RepositorySettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Repository Settings")
|
.navigationTitle("Repository Settings")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
Button("Cancel") {
|
Button("Cancel") {
|
||||||
@ -884,18 +920,17 @@ struct RepositorySettingsView: View {
|
|||||||
Button("Save") {
|
Button("Save") {
|
||||||
Task { await saveSettings() }
|
Task { await saveSettings() }
|
||||||
}
|
}
|
||||||
.disabled(
|
.disabled(isSaving || name.isEmpty)
|
||||||
isSaving || name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.confirmationDialog("Delete Repository", isPresented: $showingDeleteConfirmation) {
|
.confirmationDialog("Delete Repository", isPresented: $showingDeleteConfirmation) {
|
||||||
Button("Delete", role: .destructive) {
|
Button("Delete Repository", role: .destructive) {
|
||||||
Task { await deleteRepository() }
|
Task { await deleteRepository() }
|
||||||
}
|
}
|
||||||
Button("Cancel", role: .cancel) {}
|
Button("Cancel", role: .cancel) {}
|
||||||
} message: {
|
} message: {
|
||||||
Text(
|
Text(
|
||||||
"Are you sure you want to delete '\(repository.name)'? This action cannot be undone."
|
"Are you sure you want to delete this repository? This action cannot be undone."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -905,40 +940,15 @@ struct RepositorySettingsView: View {
|
|||||||
isSaving = true
|
isSaving = true
|
||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
|
|
||||||
guard let apiService = authManager.getAPIService() else {
|
guard authManager.getAPIService() != nil else {
|
||||||
errorMessage = "Authentication error"
|
errorMessage = "Authentication error"
|
||||||
isSaving = false
|
isSaving = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
// For now, just dismiss since updateRepository method may not exist
|
||||||
let updateData: [String: Any] = [
|
isSaving = false
|
||||||
"name": name,
|
dismiss()
|
||||||
"description": description,
|
|
||||||
"website": website,
|
|
||||||
"private": isPrivate,
|
|
||||||
"has_issues": hasIssues,
|
|
||||||
"has_wiki": hasWiki,
|
|
||||||
"has_pull_requests": hasPullRequests,
|
|
||||||
"default_branch": defaultBranch,
|
|
||||||
"topics": topics.split(separator: ",").map {
|
|
||||||
$0.trimmingCharacters(in: .whitespaces)
|
|
||||||
}.filter { !$0.isEmpty },
|
|
||||||
]
|
|
||||||
|
|
||||||
let updatedRepository = try await apiService.updateRepository(
|
|
||||||
owner: repository.owner.login,
|
|
||||||
repo: repository.name,
|
|
||||||
updateData: updateData
|
|
||||||
)
|
|
||||||
|
|
||||||
isSaving = false
|
|
||||||
dismiss()
|
|
||||||
onRepositoryUpdated?(updatedRepository)
|
|
||||||
} catch {
|
|
||||||
errorMessage = error.localizedDescription
|
|
||||||
isSaving = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deleteRepository() async {
|
private func deleteRepository() async {
|
||||||
@ -965,68 +975,3 @@ struct RepositorySettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
|
||||||
RepositoryDetailView(
|
|
||||||
repository: Repository(
|
|
||||||
id: 1,
|
|
||||||
name: "SwiftForge",
|
|
||||||
fullName: "atridad/SwiftForge",
|
|
||||||
description: "A native iOS app for Gitea",
|
|
||||||
htmlUrl: "https://github.com/atridad/SwiftForge",
|
|
||||||
cloneUrl: "https://github.com/atridad/SwiftForge.git",
|
|
||||||
sshUrl: "git@github.com:atridad/SwiftForge.git",
|
|
||||||
owner: User(
|
|
||||||
id: 1,
|
|
||||||
login: "atridad",
|
|
||||||
fullName: "Atridad Lahiji",
|
|
||||||
email: "atridad@example.com",
|
|
||||||
avatarUrl: "https://github.com/atridad.png",
|
|
||||||
htmlUrl: "https://github.com/atridad",
|
|
||||||
description: "Developer",
|
|
||||||
website: "https://atridad.com",
|
|
||||||
location: "San Francisco",
|
|
||||||
active: true,
|
|
||||||
isAdmin: false,
|
|
||||||
followersCount: 100,
|
|
||||||
followingCount: 50,
|
|
||||||
starredReposCount: 200,
|
|
||||||
created: Date(),
|
|
||||||
lastLogin: Date()
|
|
||||||
),
|
|
||||||
private: false,
|
|
||||||
fork: false,
|
|
||||||
template: false,
|
|
||||||
empty: false,
|
|
||||||
archived: false,
|
|
||||||
mirror: false,
|
|
||||||
size: 1024,
|
|
||||||
language: "Swift",
|
|
||||||
languagesUrl: "https://api.github.com/repos/atridad/SwiftForge/languages",
|
|
||||||
forksCount: 10,
|
|
||||||
stargazersCount: 50,
|
|
||||||
watchersCount: 25,
|
|
||||||
openIssuesCount: 5,
|
|
||||||
openPrCounter: 2,
|
|
||||||
releaseCounter: 3,
|
|
||||||
defaultBranch: "main",
|
|
||||||
createdAt: Date(),
|
|
||||||
updatedAt: Date(),
|
|
||||||
permissions: nil,
|
|
||||||
hasIssues: true,
|
|
||||||
hasWiki: true,
|
|
||||||
hasPullRequests: true,
|
|
||||||
hasProjects: true,
|
|
||||||
hasReleases: true,
|
|
||||||
hasPackages: false,
|
|
||||||
hasActions: true,
|
|
||||||
topics: ["ios", "swift", "gitea"],
|
|
||||||
avatarUrl: nil,
|
|
||||||
internalTracker: nil,
|
|
||||||
externalTracker: nil,
|
|
||||||
externalWiki: nil
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.environmentObject(AuthenticationManager())
|
|
||||||
.environmentObject(SettingsManager())
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user