gitbay/Views/Repos/FileView.swift
116 lines · 4228 bytes
1import SwiftUI
2
3struct FileView: View {
4
5 @Environment(\.colorScheme) private var colorScheme
6 @State private var model: FileViewModel
7 @State private var editing = false
8 @State private var draft = ""
9
10 init(client: GitbayClient, repo: String, path: String, ref: String?) {
11 _model = State(initialValue: FileViewModel(
12 client: client, repoPath: repo, filePath: path, ref: ref
13 ))
14 }
15
16 var body: some View {
17 Group {
18 if let file = model.state.value {
19 content(file)
20 } else {
21 Color.clear
22 }
23 }
24 .overlay { LoadStateOverlay(state: model.state) }
25 .navigationTitle(model.fileName)
26 .navigationBarTitleDisplayMode(.inline)
27 .toolbar {
28 ToolbarItem(placement: .topBarTrailing) {
29 if model.state.value?.binary == false {
30 Menu {
31 NavigationLink(value: RepoRoute.log(
32 repo: model.repoPath, ref: model.ref, path: model.filePath
33 )) {
34 Label("History", systemImage: "clock")
35 }
36 NavigationLink(value: RepoRoute.blame(
37 repo: model.repoPath, path: model.filePath, ref: model.ref
38 )) {
39 Label("Blame", systemImage: "person.crop.rectangle.stack")
40 }
41 if let branch = model.editableBranch {
42 Button {
43 draft = model.state.value?.content ?? ""
44 editing = true
45 } label: {
46 Label("Edit on \(branch)", systemImage: "pencil")
47 }
48 }
49 } label: {
50 Image(systemName: "ellipsis.circle")
51 }
52 .accessibilityIdentifier("file-actions-menu")
53 }
54 }
55 }
56 .sheet(isPresented: $editing) {
57 if let branch = model.editableBranch {
58 FileEditSheet(model: model, branch: branch, content: $draft) {
59 editing = false
60 }
61 }
62 }
63 .task { await model.load() }
64 }
65
66 @ViewBuilder
67 private func content(_ file: FileContent) -> some View {
68 if file.binary {
69 ContentUnavailableView {
70 Label("Binary file", systemImage: "doc.zipper")
71 } description: {
72 Text("\(file.file) · \(Int64(file.size).formatted(.byteCount(style: .file)))")
73 }
74 } else if let text = file.content {
75 VStack(spacing: 0) {
76 if file.isTruncated {
77 GBNotice(
78 "Truncated — showing the first \(Int64(file.size).formatted(.byteCount(style: .file))).",
79 .gbWarn
80 )
81 .padding(.horizontal, 12)
82 .padding(.vertical, 6)
83 }
84 CodeScrollView(text: text, fileName: model.fileName, colorScheme: colorScheme)
85 }
86 }
87 }
88}
89
90/// Both-axis scrolling for code, highlighted when the language is known.
91private struct CodeScrollView: View {
92
93 let text: String
94 let fileName: String
95 let colorScheme: ColorScheme
96
97 var body: some View {
98 ScrollView([.horizontal, .vertical]) {
99 code
100 .padding(12)
101 .frame(maxWidth: .infinity, alignment: .leading)
102 }
103 .font(.gbMono(.caption))
104 }
105
106 private var code: some View {
107 let size = UIFont.preferredFont(forTextStyle: .caption1).pointSize
108 let font = UIFont(name: "Atkinson Hyperlegible Mono", size: size)
109 ?? .monospacedSystemFont(ofSize: size, weight: .regular)
110 let highlighter = SyntaxHighlighter(colorScheme: colorScheme)
111 if let highlighted = highlighter.highlight(text, fileName: fileName, font: font) {
112 return Text(AttributedString(highlighted))
113 }
114 return Text(text)
115 }
116}