Fixed Issue view

This commit is contained in:
2025-07-05 16:08:17 -06:00
parent 3da2341f64
commit 65878ab27f
3 changed files with 174 additions and 186 deletions

View File

@ -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 {
} }
} }
} }

View File

@ -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

View File

@ -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
onRepositoryUpdated?()
})
}
.onChange(of: repositoryDeleted) { deleted in
if deleted {
dismiss() dismiss()
onRepositoryUpdated?()
} }
)
.environmentObject(authManager)
} }
.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,14 +345,16 @@ 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) {
ForEach(issues) { issue in
IssueRowView(issue: issue) IssueRowView(issue: issue)
} }
} }
} }
} }
} }
}
private var pullRequestsTab: some View { private var pullRequestsTab: some View {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
@ -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,7 +429,8 @@ struct RepositoryDetailView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.padding() .padding()
} else { } else {
VStack(spacing: 12) { ScrollView {
LazyVStack(spacing: 12) {
ForEach(releases) { release in ForEach(releases) { release in
ReleaseRowView(release: release) ReleaseRowView(release: release)
} }
@ -399,8 +438,9 @@ struct RepositoryDetailView: View {
} }
} }
} }
}
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()
.frame(height: 2)
.foregroundColor(isSelected ? .accentColor : .clear),
alignment: .bottom
) )
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 1)
) )
} }
.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,7 +801,6 @@ struct BranchesSheet: View {
} }
.padding(.vertical, 4) .padding(.vertical, 4)
} }
}
.navigationTitle("Branches") .navigationTitle("Branches")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
@ -775,6 +811,7 @@ struct BranchesSheet: View {
} }
} }
} }
}
} }
struct RepositorySettingsView: View { struct RepositorySettingsView: View {
@ -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] = [
"name": name,
"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 isSaving = false
dismiss() 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())
}