Files
hapi/ios/Hapi/RootView.swift
T
weishu e1093466c9 feat(ios): support macOS through Mac Catalyst
Build the app and notification extension for Mac Catalyst with the Mac
idiom, so Macs get a 1:1 native window instead of the scaled iPad app.
The Mac build keeps the run.hapi.app bundle ID and adds Mac entitlements
(sandbox, network, microphone, user-selected files, Mac APNs).

- Always use the split on Mac with a sidebar list style, denser rows,
  14pt reading text, a hidden duplicate titlebar title, a minimum window
  size and page-sized sheets; open web links in the default browser and
  hide the unavailable live QR scanner.
- Add menu bar / hardware-keyboard commands: New Session, Settings,
  Toggle Sidebar, a Session menu (settings, files, rename, export, stop,
  previous/next session) and a guide link.
- Composer: Return sends on Mac (Shift/Option-Return for a newline, IME
  untouched, first suggestion picked); dropped files and pasted images or
  copied files become attachments on all platforms.
- CI builds Mac Catalyst and runs the navigation, split and transcript
  geometry tests there; README documents the Mac build.
2026-11-03 11:15:11 +08:00

85 lines
3.0 KiB
Swift

import HapiClient
import HapiUI
import SwiftUI
/// Switches the app root on pairing state and hosts the presentation that
/// must survive that switch: the deep-link pairing confirm sheet, the
/// "already paired" notice, the HapiUI palette resolved from the persisted
/// theme choice (A-M4e: system follows the OS scheme, explicit modes
/// override it, OLED = dark palette on pure black), and the app-wide
/// markdown link handler.
struct RootView: View {
@Environment(AppModel.self) private var model
@Environment(\.colorScheme) private var colorScheme
@State private var themePrefs = ThemePrefs()
var body: some View {
@Bindable var model = model
Group {
switch model.state {
case .unpaired:
PairingFlowView()
case .paired:
if let session = model.session {
// `.id` resets navigation + list state on hub switch —
// each hub gets a fresh HomeView over its own stores.
HomeView(session: session)
.id(session.hubUrl)
} else {
// Defensive: .paired always carries a session; fall back
// to pairing rather than a dead screen.
PairingFlowView()
}
}
}
.sheet(item: $model.pendingPairing) { pending in
NavigationStack {
PairingConfirmView(pending: pending)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
model.pendingPairing = nil
}
}
}
}
.macSheetSizing()
}
.alert(
"Hub already paired",
isPresented: Binding(
get: { model.infoNotice != nil },
set: { presented in
if !presented {
model.infoNotice = nil
}
}
)
) {
Button("OK") {
model.infoNotice = nil
}
} message: {
Text(model.infoNotice ?? "")
}
.background(MacWindowConfigurator())
.hapiTypography()
.hapiTheme(themePrefs.mode.hapiTheme(systemColorScheme: colorScheme))
// Explicit modes force the presentation's chrome to match their
// palette; `.system` passes nil so the OS scheme flows through.
// (Under an explicit mode `colorScheme` reflects the override, but
// the palette resolution only consults it in `.system` mode.)
.preferredColorScheme(themePrefs.mode.preferredColorScheme)
.environment(themePrefs)
.handlesHapiLinks()
}
}
#Preview {
RootView()
.environment(AppModel(
registry: HubRegistry(defaults: UserDefaults(suiteName: "preview") ?? .standard),
credentialStore: InMemoryCredentialStore()
))
}