gemenon

[ACTIVE] The Safari of the Gemini ecosystem
git clone git://git.figbert.com/gemenon.git
Log | Files | Refs

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 }