CapsuleView.swift (9843B)
1 // 2 // CapsuleView.swift 3 // Gemenon 4 // 5 // Created by figbert on 9/6/22. 6 // 7 8 import SwiftUI 9 import AVKit 10 11 struct CapsuleView: View { 12 @EnvironmentObject var data: BrowserData 13 14 var body: some View { 15 if data.tab.home { 16 StartPage() 17 } else { 18 switch data.tab.response?.status { 19 case .Input, .SensitiveInput: 20 InputView() 21 case .Success: 22 SuccessView() 23 case .TemporaryRedirect, .PermanentRedirect: 24 RedirectView() 25 case .TemporaryFailure, .ServerUnavailable, .CGIError, 26 .ProxyError, .SlowDown, .PermanentFailure, 27 .NotFound, .Gone, .ProxyRequestRefused, .BadRequest: 28 FailureView() 29 case .ClientCertRequired, .ClientCertNotAuth, .ClientCertNotValid: 30 CertErrorView() 31 default: 32 Text(data.tab.response?.status.description ?? "boop") 33 } 34 } 35 } 36 } 37 38 struct SuccessView: View { 39 @EnvironmentObject var data: BrowserData 40 41 @State private var player: AVPlayer? 42 @State private var showMedia = false 43 44 var body: some View { 45 Group { 46 if case .MIME(let mime) = data.tab.response?.header { 47 if mime.hasPrefix("text/") { 48 ScrollView { 49 HStack { 50 Spacer() 51 if mime.hasPrefix("text/gemini") { 52 VStack(alignment: .leading, spacing: 10) { 53 renderGemtext((data.tab.response?.body?.gemtext)!, url: data.tab.url) 54 } 55 } else { 56 Text(data.tab.response?.body?.text ?? "error") 57 .monospaced(true).font(.system(size: 14)) 58 } 59 Spacer() 60 } 61 .padding(.vertical) 62 } 63 } else if mime.hasPrefix("image/") { 64 Image(nsImage: NSImage(data: data.tab.response?.body?.raw ?? Data()) ?? NSImage()) 65 } else if mime.hasPrefix("video/") || mime.hasPrefix("audio/") { 66 if showMedia { 67 VideoPlayer(player: player!) 68 .onAppear { player?.play() } 69 .onDisappear { player?.pause() } 70 } else { 71 VStack { 72 Text("Loading media...") 73 .font(.system(size: 35, weight: .bold)) 74 Text("Initializing AVKit player") 75 .font(.system(size: 15)) 76 .foregroundColor(.gray) 77 } 78 .onAppear { 79 var ext = String(mime.trimmingPrefix("video/").trimmingPrefix("audio/")) 80 if ext.contains(";") { 81 ext = String(ext.prefix(upTo: ext.firstIndex(of: ";")!)) 82 } 83 84 let name = "\(UUID().uuidString).\(ext)" 85 let file = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].appendingPathComponent(name) 86 try! data.tab.response?.body?.raw.write(to: file) 87 88 player = AVPlayer(url: file) 89 showMedia = true 90 } 91 } 92 } 93 } 94 } 95 } 96 97 func renderGemtext(_ gemtext: String, url: URL?) -> some View { 98 ForEach(data.parser.parse(gemtext, url: url), id: \.self) { line in 99 switch line { 100 case .Text(let str): 101 Text(str) 102 .font(.system(size: 17)) 103 case .Link(let url, let str): 104 if url.scheme == "gemini" { 105 Link(destination: url) { 106 Text(str != nil ? str! : url.absoluteString) 107 } 108 .font(.system(size: 16.5)) 109 } else { 110 Link(destination: url) { 111 Text(str != nil ? str! : url.absoluteString) 112 } 113 .foregroundColor(.purple) 114 .font(.system(size: 16.5)) 115 } 116 case .PreformattedText(let code, _): 117 Text(code).monospaced(true).font(.system(size: 14)) 118 case .Heading(let level, let str): 119 if level == 1 { 120 Text(str).font(.title).fontWeight(.bold) 121 } else if level == 2 { 122 Text(str).font(.title).fontWeight(.semibold).padding(.top) 123 } else if level == 3 { 124 Text(str).font(.title).padding(.top) 125 } 126 case .UnorderedList(let str): 127 HStack(alignment: .center) { 128 Text("•").font(.system(size: 20)) 129 Text(str).font(.system(size: 16.5)) 130 } 131 case .Quote(let str): 132 if !str.isEmpty { 133 HStack { 134 Rectangle() 135 .foregroundColor(.blue.opacity(0.8)) 136 .frame(width: 2) 137 Text(str) 138 .font(.system(size: 16.5)) 139 } 140 } 141 case .EmptyLine: 142 EmptyView() 143 } 144 } 145 } 146 } 147 148 struct InputView: View { 149 @EnvironmentObject var data: BrowserData 150 @State var input = "" 151 152 var body: some View { 153 VStack(spacing: 40) { 154 VStack { 155 Text(data.tab.response?.status.description ?? "Input") 156 .font(.system(size: 35, weight: .bold)) 157 if case .Prompt(let prompt) = data.tab.response?.header { 158 Text(prompt).font(.system(size: 15)) 159 } 160 } 161 162 TextField("Type here", text: $input) 163 .frame(maxWidth: 500) 164 .textFieldStyle(.roundedBorder) 165 .privacySensitive(data.tab.response?.status == .SensitiveInput) 166 Button("Submit") { 167 Task { 168 var components = URLComponents(url: data.tab.url!, resolvingAgainstBaseURL: false) 169 components?.query = input 170 if let url = components?.url { 171 await data.openURL(url) 172 } 173 } 174 } 175 } 176 } 177 } 178 179 struct RedirectView: View { 180 @EnvironmentObject var data: BrowserData 181 var body: some View { 182 VStack { 183 Text("Redirecting...") 184 .font(.system(size: 35, weight: .bold)) 185 if case .Redirect(let url) = data.tab.response?.header { 186 HStack { 187 Text(data.tab.url?.absoluteString ?? "") 188 Image(systemName: "arrow.forward") 189 Text(url.absoluteString) 190 } 191 .font(.system(size: 15)) 192 .foregroundColor(.gray) 193 } 194 } 195 .onAppear { 196 if case .Redirect(let url) = data.tab.response?.header { 197 Task { 198 await data.openURL(url) 199 } 200 } 201 } 202 } 203 } 204 205 struct FailureView: View { 206 @EnvironmentObject var data: BrowserData 207 var body: some View { 208 VStack { 209 Text(data.tab.response?.status.description ?? "Error") 210 .font(.system(size: 35, weight: .bold)) 211 if case .FailureInfo(let info) = data.tab.response?.header { 212 Text(info) 213 .font(.system(size: 15)) 214 .foregroundColor(.gray) 215 } else if case .Seconds(let seconds) = data.tab.response?.header { 216 Text("Please wait \(seconds) seconds before requesting again") 217 .font(.system(size: 15)) 218 .foregroundColor(.gray) 219 } 220 } 221 } 222 } 223 224 struct CertErrorView: View { 225 @EnvironmentObject var data: BrowserData 226 var body: some View { 227 VStack { 228 Text(data.tab.response?.status.description ?? "Certificate Error") 229 .font(.system(size: 35, weight: .bold)) 230 switch data.tab.response?.status { 231 case .ClientCertRequired: 232 Text("This page requires a client certificate to access.") 233 .font(.system(size: 15)) 234 .foregroundColor(.gray) 235 case .ClientCertNotAuth: 236 VStack { 237 Text("The client certificate in use is not authorised for this particular page.") 238 Text("The certificate itself is not the issue, and can be used for other purposes.") 239 } 240 .font(.system(size: 15)) 241 .foregroundColor(.gray) 242 case .ClientCertNotValid: 243 Text("The client certificate in use was not accepted because it is invalid.") 244 .font(.system(size: 15)) 245 .foregroundColor(.gray) 246 default: 247 Text("error") 248 .font(.system(size: 15)) 249 .foregroundColor(.gray) 250 } 251 if case .CertInfo(let info) = data.tab.response?.header { 252 Text(info) 253 .font(.system(size: 15)) 254 .foregroundColor(.gray) 255 } 256 } 257 } 258 } 259 260 struct CapsuleView_Previews: PreviewProvider { 261 static var previews: some View { 262 CapsuleView() 263 } 264 }