import SwiftUI struct RoomView: View { @EnvironmentObject private var model: AppModel var body: some View { if let status = model.status { VStack(spacing: 16) { overviewCard(status) codeCard(status) filesCard(status) actionButton(status) } .animation(.easeInOut(duration: 0.22), value: status.files) .animation(.easeInOut(duration: 0.22), value: status.running) } } // MARK: - Overview private func overviewCard(_ status: RoomStatus) -> some View { GlassCard { HStack(alignment: .top, spacing: 12) { ZStack { RoundedRectangle(cornerRadius: 14, style: .continuous) .fill( LinearGradient( colors: model.mode == .send ? [Theme.mint.opacity(0.2), Theme.mint.opacity(0.06)] : [Theme.blue.opacity(0.2), Theme.blue.opacity(0.06)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 46, height: 46) Image(systemName: model.mode == .send ? "arrow.up" : "arrow.down") .font(.system(size: 18, weight: .bold)) .foregroundStyle(model.mode == .send ? Theme.mint : Theme.blue) } VStack(alignment: .leading, spacing: 4) { Text(roomTitle(status)) .font(.system(.headline, design: .rounded).weight(.semibold)) .foregroundStyle(.white) Text(roomSubtitle(status)) .font(.footnote) .foregroundStyle(Theme.secondaryText) .fixedSize(horizontal: false, vertical: true) } Spacer(minLength: 8) StatusPill(running: status.running, peers: status.peers) } if status.running { HStack(spacing: 9) { if model.isWaitingForPeer { PulsingDot() Text("Waiting for the other device to join") } else { Image(systemName: "checkmark.circle.fill") .foregroundStyle(Theme.mint) Text("Connected · \(status.peers) peer\(status.peers == 1 ? "" : "s")") } } .font(.system(.subheadline, design: .rounded).weight(.medium)) .foregroundStyle(Theme.secondaryText) .padding(.top, 14) } else if let error = status.error, !error.isEmpty { HStack(alignment: .top, spacing: 8) { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(Theme.danger) Text(error) .font(.footnote) .foregroundStyle(Theme.danger.opacity(0.95)) .fixedSize(horizontal: false, vertical: true) } .padding(.top, 13) } else if model.mode == .receive && status.allComplete { Label( "All files received", systemImage: "checkmark.seal.fill" ) .font(.system(.subheadline, design: .rounded).weight(.medium)) .foregroundStyle(Theme.mint) .padding(.top, 13) } } } private func roomTitle(_ status: RoomStatus) -> String { if !status.running { if let error = status.error, !error.isEmpty { return "Room stopped" } if model.mode == .receive && status.allComplete { return "Room complete" } return "Room stopped" } return model.mode == .send ? "Sharing files" : "Receiving files" } private func roomSubtitle(_ status: RoomStatus) -> String { if !status.running { if let error = status.error, !error.isEmpty { return "The connection to the relay was lost." } if model.mode == .receive && status.allComplete { return "The files are safe on your devices." } return "The room is no longer connected." } if model.mode == .send { return "Keep this screen open while the other device downloads." } return "Files will appear here as they arrive." } // MARK: - Code private func codeCard(_ status: RoomStatus) -> some View { GlassCard { FieldLabel(title: "Room code", hint: "The other device needs this exact code.") HStack(spacing: 8) { Text(status.code) .font(.system(size: 21, weight: .bold, design: .monospaced)) .foregroundStyle(.white) .minimumScaleFactor(0.55) .lineLimit(1) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 13) .padding(.vertical, 12) .background(Color.white.opacity(0.045), in: RoundedRectangle(cornerRadius: 13, style: .continuous)) .overlay { RoundedRectangle(cornerRadius: 13, style: .continuous) .strokeBorder(Theme.stroke, lineWidth: 1) } .onTapGesture { model.copyCode() } .accessibilityAddTraits(.isButton) .accessibilityHint("Copies the room code") Button { model.copyCode() } label: { Image(systemName: "doc.on.doc") .font(.system(size: 14, weight: .semibold)) .foregroundStyle(Theme.mint) .frame(width: 42, height: 42) .background(Theme.mint.opacity(0.1), in: RoundedRectangle(cornerRadius: 13, style: .continuous)) } .buttonStyle(.plain) .accessibilityLabel("Copy room code") ShareLink(item: status.code) { Image(systemName: "square.and.arrow.up") .font(.system(size: 14, weight: .semibold)) .foregroundStyle(Theme.blue) .frame(width: 42, height: 42) .background(Theme.blue.opacity(0.1), in: RoundedRectangle(cornerRadius: 13, style: .continuous)) } .accessibilityLabel("Share room code") } .padding(.top, 12) HStack(spacing: 6) { Image(systemName: "network") .font(.system(size: 10, weight: .semibold)) Text(status.relay.isEmpty ? "tzgyn relay" : status.relay) .lineLimit(1) .truncationMode(.middle) } .font(.system(size: 10.5, design: .monospaced)) .foregroundStyle(Theme.tertiaryText) .padding(.top, 10) } } // MARK: - Files private func filesCard(_ status: RoomStatus) -> some View { GlassCard { HStack(alignment: .firstTextBaseline) { Text("Files") .font(.system(.headline, design: .rounded).weight(.semibold)) .foregroundStyle(.white) Spacer() if !status.files.isEmpty { Text("\(status.files.count)") .font(.system(size: 11, weight: .bold, design: .rounded)) .foregroundStyle(Theme.secondaryText) .padding(.horizontal, 8) .padding(.vertical, 3) .background(Color.white.opacity(0.06), in: Capsule()) } } if status.files.isEmpty { VStack(spacing: 12) { if status.running { PulsingDot() Text("Waiting for files…") .font(.system(.subheadline, design: .rounded).weight(.medium)) .foregroundStyle(Theme.secondaryText) } else { Image(systemName: "tray") .font(.system(size: 22, weight: .medium)) .foregroundStyle(Theme.tertiaryText) Text("No files in this room") .font(.system(.subheadline, design: .rounded).weight(.medium)) .foregroundStyle(Theme.secondaryText) } } .frame(maxWidth: .infinity) .padding(.vertical, 26) } else { VStack(spacing: 8) { ForEach(sortedFiles(status.files)) { file in FileRow( file: file, shareURL: model.mode == .receive ? model.receivedURL(for: file) : nil ) } } .padding(.top, 13) if model.mode == .receive && status.allComplete { Text("Tap the share button to save a file to Photos, Files, or another app.") .font(.caption) .foregroundStyle(Theme.tertiaryText) .fixedSize(horizontal: false, vertical: true) .padding(.top, 3) } } } } private func sortedFiles(_ files: [FileProgress]) -> [FileProgress] { files.sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending } } // MARK: - Actions private func actionButton(_ status: RoomStatus) -> some View { Group { if status.running { Button { model.stopRoom() } label: { Label("Stop room", systemImage: "stop.fill") .font(.system(.headline, design: .rounded).weight(.semibold)) .foregroundStyle(Theme.danger) .frame(maxWidth: .infinity) .padding(.vertical, 14) .background( Theme.danger.opacity(0.09), in: RoundedRectangle(cornerRadius: 16, style: .continuous) ) .overlay { RoundedRectangle(cornerRadius: 16, style: .continuous) .strokeBorder(Theme.danger.opacity(0.18), lineWidth: 1) } } .buttonStyle(.plain) } else { Button { model.reset() } label: { Label("Start another room", systemImage: "arrow.counterclockwise") } .buttonStyle(PrimaryButtonStyle(tint: Theme.mint)) } } .padding(.top, 2) } }