#if os(iOS) import SwiftUI final class PhoneDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { Notifier.shared.install() UITextView.appearance().textContainerInset = .zero return true } } @main struct PhoneApp: App { @UIApplicationDelegateAdaptor private var delegate: PhoneDelegate var body: some Scene { WindowGroup { ServerWindow() } } } private enum PhoneTab: Hashable { case lead, tasks, more } private enum PhoneRoute: Hashable { case task } private enum MoreRoute: Hashable { case notes, workflows, status, settings } private let moreTitles: [MoreRoute: String] = [ .notes: "Notes", .workflows: "Workflows", .status: "Status", .settings: "Settings", ] struct PhoneView: View { @Environment(Store.self) private var store @Environment(Overlays.self) private var overlays @ViewState private var tab = PhoneTab.lead @ViewState private var path: [PhoneRoute] = [] var body: some View { VStack(spacing: 0) { IdentityBanner() ZStack { shown(.lead) { LeadScreen() } shown(.tasks) { NavigationStack(path: $path) { TaskListScreen() .navigationDestination(for: PhoneRoute.self) { _ in PhoneTask() } } } shown(.more) { MoreScreen() } } .frame(maxWidth: .infinity, maxHeight: .infinity) HStack(spacing: 4) { tabButton("Lead", .lead) HStack(spacing: 0) { tabButton("Tasks", .tasks) if !store.inbox.isEmpty { Badge(count: store.inbox.count) } } tabButton("More", .more) Spacer(minLength: 0) ConnectionMark() Button { tab = .tasks path = [] } label: { Image(systemName: "list.bullet") .font(Fonts.font(17, weight: 600)) .foregroundStyle(Palette.accent) .padding(.horizontal, 10) .padding(.vertical, 8) .contentShape(Rectangle()) } .buttonStyle(.plain) .accessibilityLabel("Task list") } .padding(.horizontal, 12) .overlay(alignment: .top) { Rectangle().fill(Palette.border).frame(height: 1) } } .overlay { OverlayHost() } .coordinateSpace(.named("window")) .onGeometryChange(for: CGRect.self) { g in g.frame(in: .named("window")) } action: { frame in overlays.paneFrame = frame } .environment(\.openURL, OpenURLAction { url in openAppLink(url, store, overlays) }) .onChange(of: store.reveals, initial: true) { if store.reveals == 0 { return } tab = .tasks if path.isEmpty { path = [.task] } } .onChange(of: overlays.leadFocus) { tab = .lead } .onChange(of: tab == .tasks && !path.isEmpty, initial: true) { _, shown in store.paneFocus.setShown(shown) } .fullScreenCover(isPresented: Binding(get: { zoomed != nil }, set: { shown in if !shown { store.sheet = nil } })) { if let zoomed { ZoomedView(view: zoomed) .environment(store) .environment(overlays) } } .onAppear { store.paneFocus.attach() } .onDisappear { let store = store Task { await store.paneFocus.leave() } } } private func shown(_ which: PhoneTab, @ViewBuilder _ content: () -> some View) -> some View { content() .opacity(tab == which ? 1 : 0) .allowsHitTesting(tab == which) .accessibilityHidden(tab != which) } private func tabButton(_ label: String, _ which: PhoneTab) -> some View { PaneTabButton(label: label, active: tab == which) { if tab == which && which == .tasks { path = [] } tab = which } } private var zoomed: SheetView? { switch store.sheet?.view { case .image, .diagram: store.sheet?.view default: nil } } } private struct ZoomedView: View { let view: SheetView @Environment(Store.self) private var store var body: some View { VStack(spacing: 0) { HStack { Spacer() Button("Done") { store.sheet = nil } .font(Fonts.font(15, weight: 700)) .padding(.horizontal, 16) .padding(.vertical, 10) } SheetContent(view: view) .frame(maxWidth: .infinity, maxHeight: .infinity) } .foregroundStyle(Palette.text) .tint(Palette.accent) .background(Palette.bg) .font(Fonts.font(13)) } } private struct LeadScreen: View { var body: some View { ChatView(kind: .lead) .background(Palette.bg) } } private struct ConnectionMark: View { @Environment(Store.self) private var store var body: some View { HStack(spacing: 8) { if !store.connected { Text("reconnecting…") .foregroundStyle(Palette.muted) } Circle() .fill(store.connected ? Palette.ok : Palette.muted) .frame(width: 8, height: 8) } .lineLimit(1) } } private struct TaskListScreen: View { @Environment(Store.self) private var store @Environment(Overlays.self) private var overlays var body: some View { ScrollView { VStack(alignment: .leading, spacing: 0) { ForEach(store.projects, id: \.id) { project in let active = (store.tasks[project.id] ?? []).filter { $0.status == .active } Button { store.sheet = nil store.focusProject(project.id) } label: { HStack(alignment: .firstTextBaseline, spacing: 6) { Text(project.name) .font(Fonts.font(13, weight: 700)) Text(project.defaultBranch) .font(Fonts.font(11)) .foregroundStyle(Palette.muted) Spacer(minLength: 0) Image(systemName: "chevron.right") .font(Fonts.font(11)) .foregroundStyle(Palette.muted) } .padding(.horizontal, 10) .padding(.top, 16) .padding(.bottom, 6) .contentShape(Rectangle()) } .buttonStyle(.plain) if active.isEmpty { Text("No active tasks.") .foregroundStyle(Palette.muted) .padding(.horizontal, 10) .padding(.vertical, 6) } TaskRows(active: active) } } .padding(.horizontal, 8) .padding(.bottom, 16) } .safeAreaInset(edge: .top, spacing: 0) { HStack { Spacer(minLength: 0) Button("New task") { overlays.openSwitcher(store) } .buttonStyle(WebButtonStyle()) .onGeometryChange(for: CGRect.self) { g in g.frame(in: .named("window")) } action: { frame in overlays.pickerFrame = frame } } .padding(.horizontal, 16) .padding(.vertical, 6) .background(Palette.bg) .overlay(alignment: .bottom) { Rectangle().fill(Palette.border).frame(height: 1) } } .background(Palette.bg) .toolbar(.hidden, for: .navigationBar) } } private struct PhoneTask: View { @Environment(Store.self) private var store @Environment(\.dismiss) private var dismiss var body: some View { let selected = store.selectedTaskId.flatMap { store.findTask($0) } let accent = selected.map { taskPalette[$0.color] } ?? Palette.accent let tab = store.sheet.flatMap { $0.owner == .lead ? nil : $0.tab } ?? store.primaryTab VStack(spacing: 0) { TaskBar() .overlay(alignment: .leading) { Button { dismiss() } label: { Image(systemName: "chevron.left") .font(Fonts.font(15, weight: 600)) .foregroundStyle(accent) .padding(.horizontal, 14) .frame(maxHeight: .infinity) .contentShape(Rectangle()) } .buttonStyle(.plain) } if store.selectedTaskId == nil { ProjectView() } else { TabStrip() TaskTabBody(tab: tab) .frame(maxWidth: .infinity, maxHeight: .infinity) } } .background(Palette.bg) .environment(\.paneAccent, accent) .environment(\.inTask, true) .toolbar(.hidden, for: .navigationBar) } } private struct MoreScreen: View { @ViewState private var route = MoreRoute.notes var body: some View { VStack(spacing: 0) { HStack(spacing: 4) { ForEach([MoreRoute.notes, .workflows, .status, .settings], id: \.self) { r in PaneTabButton(label: moreTitles[r]!, active: route == r) { route = r } } Spacer(minLength: 0) } .padding(.horizontal, 12) .overlay(alignment: .bottom) { Rectangle().fill(Palette.border).frame(height: 1) } Group { switch route { case .notes: NotesView() case .workflows: WorkflowsView() case .status: StatusView() case .settings: SettingsView() } } .frame(maxWidth: .infinity, maxHeight: .infinity) } .background(Palette.bg) } } #endif