diff --git a/Scripts/run-mobile-marketing-screenshots.sh b/Scripts/run-mobile-marketing-screenshots.sh index 9ddeee55..287b64c0 100755 --- a/Scripts/run-mobile-marketing-screenshots.sh +++ b/Scripts/run-mobile-marketing-screenshots.sh @@ -65,10 +65,12 @@ run_capture() { xcrun simctl boot "$device_id" 2>/dev/null || true xcrun simctl bootstatus "$device_id" -b - if [[ "$name" == iPad* ]]; then - set_simulator_orientation "$name" "Landscape Right" - else - set_simulator_orientation "$name" "Portrait" + if pgrep -x Simulator >/dev/null; then + if [[ "$name" == iPad* ]]; then + set_simulator_orientation "$name" "Landscape Right" + else + set_simulator_orientation "$name" "Portrait" + fi fi xcrun simctl status_bar "$device_id" override \ --time '9:41' --batteryState charged --batteryLevel 100 --wifiBars 3 --cellularBars 4 @@ -94,7 +96,18 @@ run_capture() { mkdir -p "${destination:h}" xcrun simctl io "$device_id" screenshot --type=png "$destination" >/dev/null if [[ "$device" == "ipad" ]]; then - sips -r 90 "$destination" >/dev/null + local width height + width="$(sips -g pixelWidth "$destination" | awk '/pixelWidth/ { print $2 }')" + height="$(sips -g pixelHeight "$destination" | awk '/pixelHeight/ { print $2 }')" + if [[ "$width" == "2064" && "$height" == "2752" ]]; then + sips -r 90 "$destination" >/dev/null + elif [[ "$width" != "2752" || "$height" != "2064" ]]; then + print -u2 "Unexpected iPad screenshot dimensions: ${width}x${height}" + kill "$test_pid" 2>/dev/null || true + wait "$test_pid" 2>/dev/null || true + xcrun simctl status_bar "$device_id" clear + exit 70 + fi fi touch "$app_container/Documents/marketing-capture-control/ack-$token" last_token="$token" diff --git a/TFITests/MobileMarketingScreenshotTests.swift b/TFITests/MobileMarketingScreenshotTests.swift index b771ace1..374b6192 100644 --- a/TFITests/MobileMarketingScreenshotTests.swift +++ b/TFITests/MobileMarketingScreenshotTests.swift @@ -25,13 +25,7 @@ struct MobileMarketingScreenshotTests { let app = try makeApp(language: language) if includes("vault") { try await capture("vault", language: language) { - NavigationStack { - MobileVaultView( - app: app, editingHost: .constant(nil), activateHost: { _ in }, - open: { _, _ in } - ) - .toolbar { marketingSidebarButton(language: language) } - } + MarketingVaultScene(app: app) } } @@ -74,7 +68,13 @@ struct MobileMarketingScreenshotTests { if includes("metrics") { let metrics = makeMetrics(language: language) try await capture("metrics", language: language) { - NavigationStack { MarketingMetricsScene(servers: metrics, language: language) } + if UIDevice.current.userInterfaceIdiom == .pad { + NavigationStack { MarketingMetricsScene(servers: metrics, language: language) } + } else { + MarketingPhoneTabShell(selected: .vault) { + MarketingPhoneMetricsNavigation(app: app, servers: metrics, language: language) + } + } } } @@ -83,9 +83,17 @@ struct MobileMarketingScreenshotTests { let conversation = ConversationModel.demo(language: try #require(AppLanguage(rawValue: language))) assistant.library.insertAndSelect(conversation) try await capture("assistant", language: language) { - NavigationStack { - MobileAssistantView(app: app, model: assistant, libraryRoute: .constant(nil)) - .toolbar { marketingBackButton(language: language) } + if UIDevice.current.userInterfaceIdiom == .pad { + NavigationStack { + MobileAssistantView(app: app, model: assistant, libraryRoute: .constant(nil)) + .toolbar { marketingBackButton(language: language) } + } + } else { + MarketingPhoneTabShell(selected: .assistant) { + NavigationStack { + MobileAssistantView(app: app, model: assistant, libraryRoute: .constant(nil)) + } + } } } } @@ -164,8 +172,12 @@ struct MobileMarketingScreenshotTests { _ name: String, language: String, @ViewBuilder view: () -> V ) async throws { let scene = try #require(UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first) - if UIDevice.current.userInterfaceIdiom == .pad, scene.interfaceOrientation.isPortrait { - scene.requestGeometryUpdate(.iOS(interfaceOrientations: .landscapeLeft)) { error in + let isPad = UIDevice.current.userInterfaceIdiom == .pad + let targetOrientation: UIInterfaceOrientationMask = isPad ? .landscapeLeft : .portrait + let hasTargetOrientation = + isPad ? scene.interfaceOrientation == .landscapeLeft : scene.interfaceOrientation == .portrait + if !hasTargetOrientation { + scene.requestGeometryUpdate(.iOS(interfaceOrientations: targetOrientation)) { error in print("Unable to rotate the marketing scene: \(error.localizedDescription)") } try await Task.sleep(for: .milliseconds(800)) @@ -247,14 +259,6 @@ struct MobileMarketingScreenshotTests { } } - @ToolbarContentBuilder - private func marketingSidebarButton(language: String) -> some ToolbarContent { - ToolbarItem(placement: .navigationBarLeading) { - Button(text(language, "Sidebar", "侧边栏", "側邊欄"), systemImage: "sidebar.left") {} - .labelStyle(.iconOnly) - } - } - private func text(_ language: String, _ english: String, _ simplified: String, _ traditional: String) -> String { switch language { case "zh-Hans": simplified @@ -268,6 +272,94 @@ private enum MarketingCaptureError: Error { case systemCaptureTimedOut(String) } +private struct MarketingVaultScene: View { + let app: MobileAppModel + private let features = MobileFeatureRegistry.standard() + @State private var selectedDestination: MobileDestination? = .vault + + var body: some View { + if UIDevice.current.userInterfaceIdiom == .pad { + NavigationSplitView(columnVisibility: .constant(.all)) { + List(selection: $selectedDestination) { + ForEach(features.registrations(on: .padSidebar)) { feature in + NavigationLink(value: feature.id) { + Label(feature.title, systemImage: feature.symbol) + } + } + } + .navigationSplitViewColumnWidth(min: 240, ideal: 280, max: 320) + } detail: { + NavigationStack { + MobileVaultView( + app: app, editingHost: .constant(nil), activateHost: { _ in }, + open: { _, _ in }, showsMetrics: false + ) + } + } + } else { + MarketingPhoneTabShell(selected: .vault) { + NavigationStack { + MobileVaultView( + app: app, editingHost: .constant(nil), activateHost: { _ in }, + open: { _, _ in } + ) + } + } + } + } +} + +private struct MarketingPhoneTabShell: View { + let selected: MobileDestination + let content: Content + private let features = MobileFeatureRegistry.standard() + + init(selected: MobileDestination, @ViewBuilder content: () -> Content) { + self.selected = selected + self.content = content() + } + + var body: some View { + TabView(selection: .constant(selected)) { + ForEach(features.registrations(on: .phoneTab)) { feature in + Group { + if feature.id == selected { + content + } else { + NavigationStack { Color.clear } + } + } + .tabItem { Label(feature.title, systemImage: feature.symbol) } + .tag(feature.id) + } + } + .toolbar(.visible, for: .tabBar) + } +} + +private struct MarketingPhoneMetricsNavigation: View { + private enum Route: Hashable { + case metrics + } + + let app: MobileAppModel + let servers: [ServerMetric] + let language: String + @State private var path: [Route] = [.metrics] + + var body: some View { + NavigationStack(path: $path) { + MobileVaultView( + app: app, editingHost: .constant(nil), activateHost: { _ in }, + open: { _, _ in } + ) + .navigationDestination(for: Route.self) { _ in + MarketingMetricsScene(servers: servers, language: language, showsSidebarButton: false) + } + } + } +} + private struct MarketingTerminalScene: View { @ObservedObject var terminal: TerminalViewState let language: String @@ -301,6 +393,7 @@ private struct MarketingTerminalScene: View { private struct MarketingMetricsScene: View { let servers: [ServerMetric] let language: String + var showsSidebarButton = true var body: some View { MobileCardPage { @@ -319,8 +412,10 @@ private struct MarketingMetricsScene: View { } .navigationTitle("Metrics") .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Sidebar", systemImage: "sidebar.left") {}.labelStyle(.iconOnly) + if showsSidebarButton { + ToolbarItem(placement: .navigationBarLeading) { + Button("Sidebar", systemImage: "sidebar.left") {}.labelStyle(.iconOnly) + } } ToolbarItem(placement: .primaryAction) { Button("Add Server", systemImage: "plus") {}.labelStyle(.iconOnly)