krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.8.2: Hutch/Views/Repositories/ReferencesListView.swift · raw
1import SwiftUI
2
3struct ReferencesListView: View {
4 let viewModel: RepositoryDetailViewModel
5
6 var body: some View {
7 List {
8 if let defaultBranch = viewModel.defaultBranchReference {
9 Section {
10 ReferenceRow(reference: defaultBranch, prefix: "refs/heads/")
11 .themedRow()
12
13 NavigationLink("See All Branches") {
14 ReferencesDetailListView(
15 title: "Branches",
16 references: viewModel.branches,
17 prefix: "refs/heads/"
18 )
19 }
20 .themedRow()
21 } header: {
22 sectionHeader(title: "Default Branch")
23 }
24 }
25
26 if let latestTag = viewModel.latestTagReference {
27 Section {
28 ReferenceRow(reference: latestTag, prefix: "refs/tags/")
29 .themedRow()
30
31 NavigationLink("See All Tags") {
32 ReferencesDetailListView(
33 title: "Tags",
34 references: viewModel.tags,
35 prefix: "refs/tags/"
36 )
37 }
38 .themedRow()
39 } header: {
40 sectionHeader(title: "Latest Tag")
41 }
42 }
43 }
44 .themedList()
45 .listStyle(.insetGrouped)
46 .overlay {
47 if viewModel.isLoadingRefs, viewModel.branches.isEmpty, viewModel.tags.isEmpty {
48 SRHTLoadingStateView(message: "Loading references…")
49 } else if let error = viewModel.error, viewModel.branches.isEmpty, viewModel.tags.isEmpty {
50 SRHTErrorStateView(
51 title: "Couldn't Load References",
52 message: error,
53 retryAction: { await viewModel.loadReferences() }
54 )
55 } else if viewModel.branches.isEmpty, viewModel.tags.isEmpty {
56 ContentUnavailableView(
57 "No References",
58 systemImage: "arrow.triangle.branch",
59 description: Text("This repository has no branches or tags.")
60 )
61 }
62 }
63 .task {
64 if viewModel.branches.isEmpty, viewModel.tags.isEmpty {
65 await viewModel.loadReferences()
66 }
67 }
68 .refreshable {
69 await viewModel.loadReferences()
70 }
71 }
72
73 @ViewBuilder
74 private func sectionHeader(title: String) -> some View {
75 HStack {
76 Text(title)
77 Spacer()
78 }
79 }
80}
81
82private struct ReferencesDetailListView: View {
83 let title: String
84 let references: [ReferenceDetail]
85 let prefix: String
86
87 var body: some View {
88 List {
89 ForEach(references, id: \.name) { reference in
90 ReferenceRow(reference: reference, prefix: prefix)
91 .themedRow()
92 }
93 }
94 .themedList()
95 .listStyle(.insetGrouped)
96 .navigationTitle(title)
97 .overlay {
98 if references.isEmpty {
99 ContentUnavailableView(
100 "No \(title)",
101 systemImage: prefix.contains("tags") ? "tag" : "arrow.triangle.branch",
102 description: Text("This repository does not have any \(title.lowercased()).")
103 )
104 }
105 }
106 }
107}
108
109private struct ReferenceRow: View {
110 let reference: ReferenceDetail
111 let prefix: String
112
113 var body: some View {
114 HStack {
115 Label {
116 VStack(alignment: .leading, spacing: 2) {
117 Text(shortName)
118 .font(.body.monospaced())
119 if let date = reference.date {
120 Text(date.relativeDescription)
121 .font(.caption)
122 .foregroundStyle(.secondary)
123 }
124 }
125 } icon: {
126 Image(systemName: icon)
127 .foregroundStyle(iconColor)
128 }
129
130 Spacer()
131
132 Text(String((reference.target ?? "").prefix(8)))
133 .font(.caption.monospaced())
134 .foregroundStyle(.secondary)
135 }
136 .frame(maxWidth: .infinity, alignment: .leading)
137 .contentShape(Rectangle())
138 }
139
140 private var shortName: String {
141 if reference.name.hasPrefix(prefix) {
142 String(reference.name.dropFirst(prefix.count))
143 } else {
144 reference.name
145 }
146 }
147
148 private var icon: String {
149 prefix.contains("tags") ? "tag" : "arrow.triangle.branch"
150 }
151
152 private var iconColor: Color {
153 prefix.contains("tags") ? .orange : .blue
154 }
155}