// The About page — the app's identity card, on the shape every Mac user already knows from an // About panel: the icon, the name, the version, and then the ways out (documentation, community, // source, licenses). It replaced a bare header over an 885 KB wall of license text, which answered // the one question nobody opens About to ask. // // The license wall itself is still `AcknowledgementsView`; About opens it as a sheet — pushed // only on tvOS, where this page really is inside a navigation stack. The iPad settings detail // column deliberately isn't one, so a push there had no way back. // // Wording note: this page says the SOURCE is open, never that the app is "free software". The // term means liberty, but it is read as price — and this app is sold on the App Store, so to the // person most likely to read this line it would be a claim about what they just paid for. import PunktfunkKit import SwiftUI struct AboutView: View { /// Where the product actually lives. Kept here rather than scattered through the view so the /// three of them can be checked against the README in one glance. private enum Destination { static let docs = URL(string: "https://docs.punktfunk.unom.io")! static let community = URL(string: "https://discord.gg/kaPNvzMuGU")! static let source = URL(string: "https://git.unom.io/unom/punktfunk")! } private static let tagline = "Low-latency desktop and game streaming with first-class Linux and Windows hosts." #if !os(tvOS) @State private var showAcknowledgements = false #endif var body: some View { #if os(tvOS) tvBody #else Form { Section { identity .frame(maxWidth: .infinity) .padding(.vertical, 12) // The header is a statement, not a control: no separator, no row insets // fighting the centring. .listRowInsets(EdgeInsets()) .listRowBackground(Color.clear) } Section { linkRow("Documentation", systemImage: "book", url: Destination.docs) linkRow("Community", systemImage: "bubble.left.and.bubble.right", url: Destination.community) linkRow("Source Code", systemImage: "chevron.left.forwardslash.chevron.right", url: Destination.source) } Section { acknowledgementsRow } footer: { Text("Punktfunk's source is open under MIT or Apache-2.0.") .font(.geist(12, relativeTo: .caption)) .foregroundStyle(.secondary) } } .formStyle(.grouped) // A SHEET, not a push — on iPad the settings detail column is deliberately not a // NavigationStack (an inner one doubles the title bar), so a NavigationLink from here // pushed into a context with no back button and stranded the licenses on screen. .sheet(isPresented: $showAcknowledgements) { NavigationStack { AcknowledgementsView() .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Done") { showAcknowledgements = false } } } } #if os(macOS) .frame(width: 640, height: 560) #endif } #endif } // MARK: - Identity /// Icon, name, version, tagline — centred, in that order, at the sizes an About panel uses. private var identity: some View { VStack(spacing: 10) { AppIconView(side: Self.iconSide) Text("Punktfunk") .font(.geist(Self.nameFont, .bold, relativeTo: .largeTitle)) Text(Self.versionLine) .font(.geist(Self.versionFont, relativeTo: .callout)) .monospacedDigit() .foregroundStyle(.secondary) .modifier(SelectableIfPossible()) Text(Self.tagline) .font(.geist(Self.taglineFont, relativeTo: .footnote)) .foregroundStyle(.secondary) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) .frame(maxWidth: 320) .padding(.top, 2) } } /// "Version 0.21.0 (1)" — the build number only when it says something the version doesn't. /// A bug report is worth more with it, which is why it's selectable above. private static var versionLine: String { let info = Bundle.main.infoDictionary let short = info?["CFBundleShortVersionString"] as? String ?? "—" let build = info?["CFBundleVersion"] as? String guard let build, !build.isEmpty, build != short else { return "Version \(short)" } return "Version \(short) (\(build))" } // MARK: - Rows #if !os(tvOS) private func linkRow(_ title: String, systemImage: String, url: URL) -> some View { Link(destination: url) { HStack { Label(title, systemImage: systemImage) Spacer(minLength: 8) Image(systemName: "arrow.up.right") .font(.footnote.weight(.semibold)) .foregroundStyle(.tertiary) // The row already announces itself as a link. .accessibilityHidden(true) } .contentShape(Rectangle()) } #if os(macOS) // A Link in a grouped Form renders as raw blue body text on macOS; the row is the button. .buttonStyle(.plain) #endif .foregroundStyle(.primary) } private var acknowledgementsRow: some View { Button { showAcknowledgements = true } label: { HStack { Label("Acknowledgements", systemImage: "text.document") Spacer(minLength: 8) Image(systemName: "chevron.right") .font(.footnote.weight(.semibold)) .foregroundStyle(.tertiary) .accessibilityHidden(true) } .contentShape(Rectangle()) } .buttonStyle(.plain) .foregroundStyle(.primary) } #endif // MARK: - tvOS #if os(tvOS) /// tvOS has no browser and no `openURL`, so the addresses are text to read off the screen /// rather than links to nowhere. Acknowledgements still pushes — that one is in the app. private var tvBody: some View { ScrollView { VStack(spacing: 28) { identity VStack(spacing: 10) { tvAddress("Documentation", Destination.docs) tvAddress("Community", Destination.community) tvAddress("Source code", Destination.source) } NavigationLink("Acknowledgements") { AcknowledgementsView() } Text("Punktfunk's source is open under MIT or Apache-2.0.") .font(.geist(20, relativeTo: .caption)) .foregroundStyle(.secondary) } .frame(maxWidth: 1000) .frame(maxWidth: .infinity) .padding(60) } .navigationTitle("About") } private func tvAddress(_ title: String, _ url: URL) -> some View { VStack(spacing: 2) { Text(title) .font(.geist(22, .semibold, relativeTo: .headline)) Text(url.absoluteString.replacingOccurrences(of: "https://", with: "")) .font(.geist(20, relativeTo: .caption)) .foregroundStyle(.secondary) } } #endif // Panel-sized on the Mac, a touch smaller in hand, and read-from-the-couch on TV. #if os(tvOS) private static let iconSide: CGFloat = 160 private static let nameFont: CGFloat = 44 private static let versionFont: CGFloat = 24 private static let taglineFont: CGFloat = 22 #else private static let iconSide: CGFloat = 96 private static let nameFont: CGFloat = 28 private static let versionFont: CGFloat = 14 private static let taglineFont: CGFloat = 13 #endif } /// The app's own icon, at whatever size About asks for. /// /// Every platform hides it somewhere different, and one of them can't hand it over at all: macOS /// has the running app's icon; iOS keeps the primary icon's file names in `CFBundleIcons`; tvOS /// app icons are layered assets with no single image to load, and an unbundled `swift run` build /// has no icon in the first place. So there is a drawn fallback, built from the same brand /// gradient and Geist monogram as the host cards — where the real icon is unavailable this reads /// as a mark, not as a missing image. struct AppIconView: View { let side: CGFloat var body: some View { Group { if let icon = Self.bundleIcon { icon.image .resizable() .interpolation(.high) .aspectRatio(contentMode: .fit) // iOS ships the icon UNMASKED — the springboard applies the rounded shape at // draw time, so used raw it is a hard-cornered square. macOS bakes its own // shape (and margins) into the image, and clipping that would cut into it. .clipShape(RoundedRectangle( cornerRadius: icon.needsMask ? side * Self.iOSCornerRatio : 0, style: .continuous)) } else { monogram } } .frame(width: side, height: side) .accessibilityHidden(true) // the app's name is the next line } /// The iOS icon's corner radius as a fraction of its side — the squircle's ~22.37%. private static let iOSCornerRatio: CGFloat = 0.2237 private var monogram: some View { let shape = RoundedRectangle(cornerRadius: side * 0.22, style: .continuous) return shape .fill(LinearGradient( colors: [Color.brand, Color.brand.opacity(0.7)], startPoint: .top, endPoint: .bottom)) .overlay { Text("P") .font(.geistFixed(side * 0.5, .bold)) .foregroundStyle(.white) } } /// `needsMask` = the platform hands over a bare square that the OS normally rounds for us. private static var bundleIcon: (image: Image, needsMask: Bool)? { #if os(macOS) return (Image(nsImage: NSApplication.shared.applicationIconImage), false) #elseif os(iOS) // The last entry is the largest — `CFBundleIconFiles` is ordered small to large. guard let icons = Bundle.main.infoDictionary?["CFBundleIcons"] as? [String: Any], let primary = icons["CFBundlePrimaryIcon"] as? [String: Any], let files = primary["CFBundleIconFiles"] as? [String], let name = files.last, let image = UIImage(named: name) else { return nil } return (Image(uiImage: image), true) #else return nil // tvOS: layered icons have no single image to load #endif } } /// `textSelection(.enabled)` doesn't exist on tvOS, and a version string is the one thing here /// worth copying into a bug report. private struct SelectableIfPossible: ViewModifier { func body(content: Content) -> some View { #if os(tvOS) content #else content.textSelection(.enabled) #endif } }