krz/hutch

an ios client for sourcehut

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

v3.5.0: Hutch/Views/Repositories/RepositoryDetailView.swift · raw

  1import SwiftUI
  2
  3struct RepositoryDetailView: View {
  4    @Environment(\.openURL) private var openURL
  5
  6    let onRepositoryUpdated: ((RepositorySummary) -> Void)?
  7    var onDeleted: (() -> Void)?
  8
  9    @Environment(AppState.self) private var appState
 10    @Environment(\.dismiss) private var dismiss
 11    @State private var viewModel: RepositoryDetailViewModel?
 12    @State private var selectedTab: RepositoryDetailViewModel.Tab = .summary
 13    @State private var showSettings = false
 14    @State private var showACLs = false
 15    @State private var currentRepository: RepositorySummary
 16    @State private var pinChangeCount = 0
 17
 18    private var canManageRepository: Bool {
 19        guard let currentUser = appState.currentUser else { return false }
 20        return normalizedUsername(currentUser.username) == normalizedUsername(currentRepository.owner.canonicalName)
 21    }
 22
 23    private var currentUserKey: String? {
 24        appState.currentUser?.canonicalName
 25    }
 26
 27    private var isPinnedToHome: Bool {
 28        _ = pinChangeCount
 29        guard let currentUserKey else { return false }
 30        return HomePinStore.isPinned(.repository(currentRepository), for: currentUserKey, defaults: appState.accountDefaults)
 31    }
 32
 33    init(
 34        repository: RepositorySummary,
 35        onRepositoryUpdated: ((RepositorySummary) -> Void)? = nil,
 36        onDeleted: (() -> Void)? = nil
 37    ) {
 38        self.onRepositoryUpdated = onRepositoryUpdated
 39        self.onDeleted = onDeleted
 40        self._currentRepository = State(initialValue: repository)
 41    }
 42
 43    var body: some View {
 44        if currentRepository.service == .hg {
 45            HgRepositoryDetailView(repository: currentRepository, onDeleted: onDeleted)
 46        } else {
 47            Group {
 48                if let viewModel {
 49                    detailContent(viewModel)
 50                } else {
 51                    SRHTLoadingStateView(message: "Loading repository…")
 52                }
 53            }
 54            .navigationTitle(currentRepository.name)
 55            .navigationBarTitleDisplayMode(.inline)
 56            .toolbar {
 57                ToolbarItem(placement: .topBarTrailing) {
 58                    repositoryActionsMenu
 59                }
 60            }
 61            .sheet(isPresented: $showSettings) {
 62                RepositorySettingsView(
 63                    repository: currentRepository,
 64                    branches: viewModel?.branches ?? [],
 65                    client: appState.client,
 66                    onUpdated: { updatedRepository in
 67                        currentRepository = updatedRepository
 68                        onRepositoryUpdated?(updatedRepository)
 69                    },
 70                    onDeleted: {
 71                        dismiss()
 72                        onDeleted?()
 73                    }
 74                )
 75            }
 76            .sheet(isPresented: $showACLs) {
 77                NavigationStack {
 78                    RepositoryACLView(
 79                        repository: currentRepository,
 80                        client: appState.client,
 81                        showsDoneButton: true
 82                    )
 83                }
 84            }
 85            .task {
 86                if viewModel == nil {
 87                    viewModel = RepositoryDetailViewModel(
 88                        repository: currentRepository,
 89                        client: appState.client
 90                    )
 91                }
 92                RecentActivityStore.recordRepository(currentRepository, defaults: appState.accountDefaults)
 93            }
 94        }
 95    }
 96
 97    @ViewBuilder
 98    private func detailContent(_ viewModel: RepositoryDetailViewModel) -> some View {
 99        VStack(spacing: 0) {
100            Picker("Tab", selection: $selectedTab) {
101                ForEach(RepositoryDetailViewModel.Tab.allCases, id: \.self) { tab in
102                    Text(tab.rawValue).tag(tab)
103                }
104            }
105            .pickerStyle(.segmented)
106            .padding(.horizontal)
107            .padding(.vertical, 8)
108
109            Divider()
110
111            switch selectedTab {
112            case .summary:
113                ReadmeView(viewModel: viewModel)
114            case .tree:
115                FileTreeView(
116                    repository: currentRepository,
117                    client: appState.client
118                )
119            case .log:
120                CommitLogView(viewModel: viewModel)
121            case .refs:
122                ReferencesListView(viewModel: viewModel)
123            case .artifacts:
124                ArtifactsView(viewModel: viewModel)
125            }
126        }
127        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
128        .srhtErrorBanner(error: Binding(
129            get: { viewModel.error },
130            set: { viewModel.error = $0 }
131        ))
132    }
133
134    private func normalizedUsername(_ value: String) -> String {
135        let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
136        return trimmed.hasPrefix("~") ? String(trimmed.dropFirst()) : trimmed
137    }
138
139    private var repositoryActionsMenu: some View {
140        Menu {
141            if currentUserKey != nil {
142                Button {
143                    togglePinnedState()
144                } label: {
145                    Label(
146                        isPinnedToHome ? "Unpin from Home" : "Pin to Home",
147                        systemImage: isPinnedToHome ? "pin.slash" : "pin"
148                    )
149                }
150            }
151
152            if let shareURL = SRHTWebURL.repository(currentRepository) {
153                ShareLink(item: shareURL) {
154                    Label("Share", systemImage: "square.and.arrow.up")
155                }
156            }
157
158            Divider()
159
160            if let repositoryURL = SRHTWebURL.repository(currentRepository) {
161                Button {
162                    openURL(repositoryURL)
163                } label: {
164                    Label("Open in Browser", systemImage: "safari")
165                }
166
167                Button {
168                    appState.copyToPasteboard(repositoryURL.absoluteString, label: "repository URL")
169                } label: {
170                    Label("Copy URL", systemImage: "doc.on.doc")
171                }
172            }
173
174            if let httpsURL = SRHTWebURL.httpsCloneURL(currentRepository) {
175                Button {
176                    appState.copyToPasteboard(httpsURL, label: "HTTPS clone URL")
177                } label: {
178                    Label("Copy HTTPS URL", systemImage: "doc.on.doc")
179                }
180            }
181
182            Button {
183                appState.copyToPasteboard(SRHTWebURL.sshCloneURL(currentRepository), label: "SSH clone URL")
184            } label: {
185                Label("Copy SSH URL", systemImage: "terminal")
186            }
187
188            Button {
189                appState.copyToPasteboard(currentRepository.rid, label: "repository RID")
190            } label: {
191                Label("Copy RID", systemImage: "number")
192            }
193
194            if canManageRepository {
195                Divider()
196
197                Button {
198                    showACLs = true
199                } label: {
200                    Label("Manage ACLs", systemImage: "person.2")
201                }
202
203                Button {
204                    showSettings = true
205                } label: {
206                    Label("Repository Settings", systemImage: "gear")
207                }
208            }
209        } label: {
210            Image(systemName: "ellipsis.circle")
211        }
212        .accessibilityLabel("Repository actions")
213    }
214
215    private func togglePinnedState() {
216        guard let currentUserKey else { return }
217        HomePinStore.togglePin(.repository(currentRepository), for: currentUserKey, defaults: appState.accountDefaults)
218        pinChangeCount += 1
219    }
220}