Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ Windows/test-output/
# Linux client
Linux/build/
LinuxHost/build/

# Mac client
dist/
80 changes: 33 additions & 47 deletions Client/ConnectView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,15 @@ import SwiftUI
// Visual language shared with Pinry Saver: one centered column (max 480 pt) on the system
// background, a big mark up top, labeled rounded fields, and a single pill-shaped action.

extension Color {
static let iframeTeal = Color(red: 0x2E / 255, green: 0xE6 / 255, blue: 0xD6 / 255) // #2EE6D6
/// Text on teal: the accent is light, so dark text reads better than white.
static let iframeInk = Color(red: 0x04 / 255, green: 0x16 / 255, blue: 0x1A / 255)
}

struct ConnectView: View {
@EnvironmentObject private var session: StreamSession
@StateObject private var browser = HostBrowser()
@AppStorage("pin") private var pin = ""
@AppStorage("manualHost") private var manualHost = ""
@AppStorage("uiScale") private var uiScale = 2.0
@AppStorage("lastHostName") private var lastHostName = ""
@AppStorage("savedHosts") private var savedHostsJSON = "[]"
@State private var pendingSave: SavedHost?
@State private var selectedHostID: String?
@State private var showingError = false
@State private var errorMessage = ""
Expand All @@ -27,6 +23,13 @@ struct ConnectView: View {

private var trimmedAddress: String { manualHost.trimmingCharacters(in: .whitespaces) }

private var savedHosts: [SavedHost] {
get { (try? JSONDecoder().decode([SavedHost].self, from: Data(savedHostsJSON.utf8))) ?? [] }
nonmutating set {
savedHostsJSON = (try? String(decoding: JSONEncoder().encode(newValue), as: UTF8.self)) ?? "[]"
}
}

private var targetName: String? {
if !trimmedAddress.isEmpty { return trimmedAddress }
return selectedHost?.name
Expand Down Expand Up @@ -69,6 +72,11 @@ struct ConnectView: View {
selectedHostID = (hosts.first { $0.name == lastHostName } ?? (hosts.count == 1 ? hosts.first : nil))?.id
}
.onChange(of: session.phase) { _, phase in
// Only bookmark addresses that actually worked, most recent first.
if phase == .streaming, let saved = pendingSave {
savedHosts = [saved] + savedHosts.filter { $0.address != saved.address }
}
if phase != .connecting { pendingSave = nil }
if case .failed(let message) = phase {
errorMessage = message
showingError = true
Expand Down Expand Up @@ -105,7 +113,7 @@ struct ConnectView: View {
VStack(alignment: .leading, spacing: 24) {
VStack(alignment: .leading, spacing: 8) {
FieldLabel("Mac:")
if browser.hosts.isEmpty {
if browser.hosts.isEmpty && savedHosts.isEmpty {
HStack(spacing: 10) {
ProgressView()
Text("Looking for Macs running iframe-host…")
Expand All @@ -123,6 +131,19 @@ struct ConnectView: View {
manualHost = ""
}
}
ForEach(savedHosts) { saved in
HostRow(name: saved.address, symbol: "bookmark",
selected: saved.address == trimmedAddress) {
manualHost = saved.address
pin = saved.pin
}
.contextMenu {
Button("Forget \(saved.address)", systemImage: "trash", role: .destructive) {
savedHosts = savedHosts.filter { $0.address != saved.address }
if trimmedAddress == saved.address { manualHost = "" }
}
}
}
}
}
}
Expand Down Expand Up @@ -220,15 +241,9 @@ struct ConnectView: View {
private func connect() {
guard canConnect else { return }
if !trimmedAddress.isEmpty {
var host = trimmedAddress
var port = IFrame.defaultPort
if let colon = host.lastIndex(of: ":"), !host.contains("::"),
let p = UInt16(host[host.index(after: colon)...]) {
port = p
host = String(host[..<colon])
}
guard let nwPort = NWEndpoint.Port(rawValue: port) else { return }
session.connect(to: .hostPort(host: NWEndpoint.Host(host), port: nwPort), pin: pin, label: host, uiScale: uiScale)
guard let (endpoint, host) = SavedHost.endpoint(for: trimmedAddress) else { return }
pendingSave = SavedHost(address: trimmedAddress, pin: pin)
session.connect(to: endpoint, pin: pin, label: host, uiScale: uiScale)
} else if let host = selectedHost {
lastHostName = host.name
session.connect(to: host.endpoint, pin: pin, label: host.name, uiScale: uiScale)
Expand Down Expand Up @@ -271,13 +286,14 @@ private struct FrameInputField: View {

private struct HostRow: View {
let name: String
var symbol = "macmini"
let selected: Bool
let action: () -> Void

var body: some View {
Button(action: action) {
HStack(spacing: 12) {
Image(systemName: "macmini")
Image(systemName: symbol)
.font(.system(size: 17))
.foregroundColor(selected ? .iframeTeal : Color(uiColor: .secondaryLabel))
Text(name)
Expand Down Expand Up @@ -308,33 +324,3 @@ private extension View {
)
}
}

/// The cursor mark streaking up and to the right, with a short motion trail
/// (iFrame's take on Pinry's falling-pin loader).
struct IFrameLoadingView: View {
let size: CGFloat
@State private var progress: CGFloat = -1

var body: some View {
ZStack {
ForEach(0..<3, id: \.self) { index in
Image("IFrameMark")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: size * 0.7, height: size * 0.7)
.opacity(1.0 - CGFloat(index) * 0.3)
.offset(x: (progress - CGFloat(index) * 0.18) * size,
y: -(progress - CGFloat(index) * 0.18) * size)
}
}
.frame(width: size, height: size)
.clipped()
.drawingGroup()
.onAppear {
progress = -1
withAnimation(.easeInOut(duration: 0.6).repeatForever(autoreverses: false)) {
progress = 1
}
}
}
}
34 changes: 9 additions & 25 deletions Client/IFrameApp.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Network
import SwiftUI
import UIKit

@main
struct IFrameApp: App {
Expand All @@ -24,32 +25,15 @@ struct IFrameApp: App {
}
}

final class HostBrowser: ObservableObject {
struct Found: Identifiable, Hashable {
let id: String
let name: String
let endpoint: NWEndpoint
extension StreamSession {
/// The iPad's screen in pixels, in the current orientation.
static func screenPixels() -> CGSize {
let screen = UIScreen.main
return CGSize(width: screen.bounds.width * screen.scale, height: screen.bounds.height * screen.scale)
}

@Published private(set) var hosts: [Found] = []
private var browser: NWBrowser?

func start() {
guard browser == nil else { return }
let browser = NWBrowser(for: .bonjour(type: IFrame.serviceType, domain: nil), using: .tcp)
browser.browseResultsChangedHandler = { [weak self] results, _ in
let found = results.compactMap { result -> Found? in
guard case let .service(name, _, _, _) = result.endpoint else { return nil }
return Found(id: "\(result.endpoint)", name: name, endpoint: result.endpoint)
}
self?.hosts = found.sorted { $0.name < $1.name }
}
browser.start(queue: .main)
self.browser = browser
}

func stop() {
browser?.cancel()
browser = nil
func connect(to endpoint: NWEndpoint, pin: String, label: String, uiScale: Double) {
connect(to: endpoint, pin: pin, label: label, deviceName: UIDevice.current.name,
maxFPS: UIScreen.main.maximumFramesPerSecond, pixels: Self.screenPixels(), uiScale: uiScale)
}
}
Loading
Loading