krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v3.11.0: 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}