| author | |
| committer | |
| log | fccec9f097ab6b89714161ccab2efc7e19d59c00 |
| tree | 73f88a2548d9790f0a2c4a674c25ee29921c85fd |
| parent | 50a3d6b872406bb15af72d439740dd63e89f820d |
| signature | Signed by PGP key B5690EEEBB952194 |
4 files changed, 76 insertions(+), 11 deletions(-)
docs/operations/mobile-app-store-screenshots.md+3-2| ... | @@ -61,7 +61,7 @@ The default matrix is: | ... | @@ -61,7 +61,7 @@ The default matrix is: |
| 61 | | ----------------------------- | ------------------------- | ----------------- | ----------------------------------------- | | 61 | | ----------------------------- | ------------------------- | ----------------- | ----------------------------------------- | |
| 62 | | `apple/iphone-6.9/dark/` | iPhone 17 Pro Max | 1320×2868 | App Store Connect iPhone 6.9-inch | | 62 | | `apple/iphone-6.9/dark/` | iPhone 17 Pro Max | 1320×2868 | App Store Connect iPhone 6.9-inch | |
| 63 | | `apple/iphone-6.5/dark/` | disposable iPhone 14 Plus | 1284×2778 | App Store Connect iPhone 6.5-inch | | 63 | | `apple/iphone-6.5/dark/` | disposable iPhone 14 Plus | 1284×2778 | App Store Connect iPhone 6.5-inch | |
| 64 | | `apple/ipad-13/dark/` | iPad Pro 13-inch (M5) | 2064×2752 | App Store Connect iPad 13-inch | | 64 | | `apple/ipad-13/dark/` | iPad Pro 13-inch (M5) | 2752×2064 | App Store Connect iPad 13-inch, landscape | |
| 65 | | `google-play/phone/dark/` | Pixel AVD at 420 dpi | 1080×1920 | Google Play phone, portrait 9:16 | | 65 | | `google-play/phone/dark/` | Pixel AVD at 420 dpi | 1080×1920 | Google Play phone, portrait 9:16 | |
| 66 | | `google-play/tablet-7/dark/` | Pixel AVD at 600dp width | 1080×1920 | Google Play 7-inch tablet, portrait 9:16 | | 66 | | `google-play/tablet-7/dark/` | Pixel AVD at 600dp width | 1080×1920 | Google Play 7-inch tablet, portrait 9:16 | |
| 67 | | `google-play/tablet-10/dark/` | Pixel AVD at 800dp width | 1440×2560 | Google Play 10-inch tablet, portrait 9:16 | | 67 | | `google-play/tablet-10/dark/` | Pixel AVD at 800dp width | 1440×2560 | Google Play 10-inch tablet, portrait 9:16 | |
| ... | @@ -86,7 +86,8 @@ A light-only run writes the same tree under `light/`; `--appearance both` writes | ... | @@ -86,7 +86,8 @@ A light-only run writes the same tree under `light/`; `--appearance both` writes |
| 86 | folders. | 86 | folders. |
| 87 | 87 | ||
| 88 | Edit [mobile-showcase.config.ts](../../scripts/mobile-showcase.config.ts) to change simulator or AVD | 88 | Edit [mobile-showcase.config.ts](../../scripts/mobile-showcase.config.ts) to change simulator or AVD |
| 89 | names, light/dark appearance, scenes, output directory, capture delay, Android ABI, or viewport. | 89 | names, light/dark appearance, iOS orientation, scenes, output directory, capture delay, Android ABI, |
| 90 | or viewport. | ||
| 90 | 91 | ||
| 91 | ## Capture in GitHub Actions | 92 | ## Capture in GitHub Actions |
| 92 | 93 |
scripts/mobile-showcase.config.ts+5-2| ... | @@ -25,6 +25,8 @@ export interface ShowcaseIosDevice { | ... | @@ -25,6 +25,8 @@ export interface ShowcaseIosDevice { |
| 25 | readonly simulatorDeviceType?: string; | 25 | readonly simulatorDeviceType?: string; |
| 26 | /** Appearance used when the CLI does not pass --appearance. */ | 26 | /** Appearance used when the CLI does not pass --appearance. */ |
| 27 | readonly appearance: ShowcaseAppearance; | 27 | readonly appearance: ShowcaseAppearance; |
| 28 | /** Orientation applied by the capture harness. Defaults to portrait. */ | ||
| 29 | readonly orientation?: "portrait" | "landscape"; | ||
| 28 | readonly scenes: ReadonlyArray<ShowcaseScene>; | 30 | readonly scenes: ReadonlyArray<ShowcaseScene>; |
| 29 | readonly storeAsset: ShowcaseStoreAssetSpec; | 31 | readonly storeAsset: ShowcaseStoreAssetSpec; |
| 30 | } | 32 | } |
| ... | @@ -122,12 +124,13 @@ const config: ShowcaseConfig = { | ... | @@ -122,12 +124,13 @@ const config: ShowcaseConfig = { |
| 122 | simulator: "iPad Pro 13-inch (M5)", | 124 | simulator: "iPad Pro 13-inch (M5)", |
| 123 | simulatorDeviceType: "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-13-inch-M5-16GB", | 125 | simulatorDeviceType: "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-13-inch-M5-16GB", |
| 124 | appearance: "dark", | 126 | appearance: "dark", |
| 127 | orientation: "landscape", | ||
| 125 | scenes: ["thread", "terminal", "review", "threads", "environments"], | 128 | scenes: ["thread", "terminal", "review", "threads", "environments"], |
| 126 | storeAsset: { | 129 | storeAsset: { |
| 127 | store: "apple", | 130 | store: "apple", |
| 128 | directory: "apple/ipad-13", | 131 | directory: "apple/ipad-13", |
| 129 | width: 2064, | 132 | width: 2752, |
| 130 | height: 2752, | 133 | height: 2064, |
| 131 | minimumUploadCount: 1, | 134 | minimumUploadCount: 1, |
| 132 | maximumUploadCount: 10, | 135 | maximumUploadCount: 10, |
| 133 | }, | 136 | }, |
scripts/mobile-showcase.test.ts+7-6| ... | @@ -216,17 +216,18 @@ it("configures every default device with an exact upload-ready store target", () | ... | @@ -216,17 +216,18 @@ it("configures every default device with an exact upload-ready store target", () |
| 216 | assert.deepStrictEqual( | 216 | assert.deepStrictEqual( |
| 217 | showcaseConfig.devices.map((device) => [ | 217 | showcaseConfig.devices.map((device) => [ |
| 218 | device.id, | 218 | device.id, |
| 219 | device.platform === "ios" ? (device.orientation ?? "portrait") : null, | ||
| 219 | device.storeAsset.directory, | 220 | device.storeAsset.directory, |
| 220 | device.storeAsset.width, | 221 | device.storeAsset.width, |
| 221 | device.storeAsset.height, | 222 | device.storeAsset.height, |
| 222 | ]), | 223 | ]), |
| 223 | [ | 224 | [ |
| 224 | ["iphone-6.9", "apple/iphone-6.9", 1320, 2868], | 225 | ["iphone-6.9", "portrait", "apple/iphone-6.9", 1320, 2868], |
| 225 | ["iphone-6.5", "apple/iphone-6.5", 1284, 2778], | 226 | ["iphone-6.5", "portrait", "apple/iphone-6.5", 1284, 2778], |
| 226 | ["ipad-13", "apple/ipad-13", 2064, 2752], | 227 | ["ipad-13", "landscape", "apple/ipad-13", 2752, 2064], |
| 227 | ["pixel", "google-play/phone", 1080, 1920], | 228 | ["pixel", null, "google-play/phone", 1080, 1920], |
| 228 | ["android-tablet-7", "google-play/tablet-7", 1080, 1920], | 229 | ["android-tablet-7", null, "google-play/tablet-7", 1080, 1920], |
| 229 | ["android-tablet-10", "google-play/tablet-10", 1440, 2560], | 230 | ["android-tablet-10", null, "google-play/tablet-10", 1440, 2560], |
| 230 | ], | 231 | ], |
| 231 | ); | 232 | ); |
| 232 | }); | 233 | }); |
scripts/mobile-showcase.ts+61-1| ... | @@ -87,9 +87,11 @@ export interface ShowcaseCapture { | ... | @@ -87,9 +87,11 @@ export interface ShowcaseCapture { |
| 87 | } | 87 | } |
| 88 | 88 | ||
| 89 | interface IosCaptureCleanup { | 89 | interface IosCaptureCleanup { |
| 90 | readonly name: string; | ||
| 90 | readonly udid: string; | 91 | readonly udid: string; |
| 91 | readonly startedByRunner: boolean; | 92 | readonly startedByRunner: boolean; |
| 92 | readonly createdByRunner: boolean; | 93 | readonly createdByRunner: boolean; |
| 94 | readonly restorePortrait: boolean; | ||
| 93 | } | 95 | } |
| 94 | 96 | ||
| 95 | interface AndroidCaptureCleanup { | 97 | interface AndroidCaptureCleanup { |
| ... | @@ -792,6 +794,49 @@ async function normalizeIosSimulator(appearance: ShowcaseAppearance, udid: strin | ... | @@ -792,6 +794,49 @@ async function normalizeIosSimulator(appearance: ShowcaseAppearance, udid: strin |
| 792 | ]); | 794 | ]); |
| 793 | } | 795 | } |
| 794 | 796 | ||
| 797 | async function setIosSimulatorOrientation( | ||
| 798 | orientation: NonNullable<ShowcaseIosDevice["orientation"]>, | ||
| 799 | simulator: Pick<SimctlDevice, "name" | "udid">, | ||
| 800 | ): Promise<void> { | ||
| 801 | await runCommand("open", ["-a", "Simulator", "--args", "-CurrentDeviceUDID", simulator.udid]); | ||
| 802 | const menuItem = orientation === "landscape" ? "Landscape Right" : "Portrait"; | ||
| 803 | await runCommand("osascript", [ | ||
| 804 | "-e", | ||
| 805 | "on run argv", | ||
| 806 | "-e", | ||
| 807 | "set simulatorName to item 1 of argv", | ||
| 808 | "-e", | ||
| 809 | 'tell application "Simulator" to activate', | ||
| 810 | "-e", | ||
| 811 | 'tell application "System Events" to tell process "Simulator"', | ||
| 812 | "-e", | ||
| 813 | "set simulatorWindows to {}", | ||
| 814 | "-e", | ||
| 815 | "repeat 40 times", | ||
| 816 | "-e", | ||
| 817 | 'set simulatorWindows to menu items of menu "Window" of menu bar item "Window" of menu bar 1 whose name starts with simulatorName', | ||
| 818 | "-e", | ||
| 819 | "if (count of simulatorWindows) is greater than 0 then exit repeat", | ||
| 820 | "-e", | ||
| 821 | "delay 0.25", | ||
| 822 | "-e", | ||
| 823 | "end repeat", | ||
| 824 | "-e", | ||
| 825 | 'if (count of simulatorWindows) is not 1 then error "Expected exactly one Simulator window for " & simulatorName', | ||
| 826 | "-e", | ||
| 827 | "click item 1 of simulatorWindows", | ||
| 828 | "-e", | ||
| 829 | `click menu item "${menuItem}" of menu "Orientation" of menu item "Orientation" of menu "Device" of menu bar item "Device" of menu bar 1`, | ||
| 830 | "-e", | ||
| 831 | "end tell", | ||
| 832 | "-e", | ||
| 833 | "delay 1", | ||
| 834 | "-e", | ||
| 835 | "end run", | ||
| 836 | simulator.name, | ||
| 837 | ]); | ||
| 838 | } | ||
| 839 | |||
| 795 | async function iosAppContainer(udid: string): Promise<string> { | 840 | async function iosAppContainer(udid: string): Promise<string> { |
| 796 | return ( | 841 | return ( |
| 797 | await commandOutput("xcrun", ["simctl", "get_app_container", udid, ANDROID_PACKAGE, "data"]) | 842 | await commandOutput("xcrun", ["simctl", "get_app_container", udid, ANDROID_PACKAGE, "data"]) |
| ... | @@ -828,7 +873,13 @@ async function captureIos( | ... | @@ -828,7 +873,13 @@ async function captureIos( |
| 828 | ): Promise<void> { | 873 | ): Promise<void> { |
| 829 | const { simulator, createdByRunner } = await ensureIosSimulator(capture.device); | 874 | const { simulator, createdByRunner } = await ensureIosSimulator(capture.device); |
| 830 | const startedByRunner = simulator.state !== "Booted"; | 875 | const startedByRunner = simulator.state !== "Booted"; |
| 831 | registerCleanup({ udid: simulator.udid, startedByRunner, createdByRunner }); | 876 | registerCleanup({ |
| 877 | name: simulator.name, | ||
| 878 | udid: simulator.udid, | ||
| 879 | startedByRunner, | ||
| 880 | createdByRunner, | ||
| 881 | restorePortrait: capture.device.orientation === "landscape", | ||
| 882 | }); | ||
| 832 | if (!startedByRunner) { | 883 | if (!startedByRunner) { |
| 833 | // Clear transient SpringBoard state (permission prompts, stale URL-open | 884 | // Clear transient SpringBoard state (permission prompts, stale URL-open |
| 834 | // confirmations, keyboards) without erasing the developer's simulator. | 885 | // confirmations, keyboards) without erasing the developer's simulator. |
| ... | @@ -887,6 +938,9 @@ async function captureIos( | ... | @@ -887,6 +938,9 @@ async function captureIos( |
| 887 | "--showcaseScene", | 938 | "--showcaseScene", |
| 888 | firstScene, | 939 | firstScene, |
| 889 | ]); | 940 | ]); |
| 941 | if (capture.device.orientation === "landscape") { | ||
| 942 | await setIosSimulatorOrientation("landscape", simulator); | ||
| 943 | } | ||
| 890 | }; | 944 | }; |
| 891 | await NodeFSP.rm(readyPath, { force: true }); | 945 | await NodeFSP.rm(readyPath, { force: true }); |
| 892 | await NodeFSP.writeFile(scenePath, firstScene); | 946 | await NodeFSP.writeFile(scenePath, firstScene); |
| ... | @@ -917,6 +971,9 @@ async function captureIos( | ... | @@ -917,6 +971,9 @@ async function captureIos( |
| 917 | `${scene}.png`, | 971 | `${scene}.png`, |
| 918 | ); | 972 | ); |
| 919 | await runCommand("xcrun", ["simctl", "io", simulator.udid, "screenshot", destination]); | 973 | await runCommand("xcrun", ["simctl", "io", simulator.udid, "screenshot", destination]); |
| 974 | if (capture.device.orientation === "landscape") { | ||
| 975 | await runCommand("sips", ["--rotate", "90", destination]); | ||
| 976 | } | ||
| 920 | await finalizeCapture(destination, capture.device); | 977 | await finalizeCapture(destination, capture.device); |
| 921 | } | 978 | } |
| 922 | } | 979 | } |
| ... | @@ -1332,6 +1389,9 @@ async function main(): Promise<void> { | ... | @@ -1332,6 +1389,9 @@ async function main(): Promise<void> { |
| 1332 | } | 1389 | } |
| 1333 | } | 1390 | } |
| 1334 | for (const cleanup of iosCleanups) { | 1391 | for (const cleanup of iosCleanups) { |
| 1392 | if (cleanup.restorePortrait) { | ||
| 1393 | await setIosSimulatorOrientation("portrait", cleanup).catch(() => undefined); | ||
| 1394 | } | ||
| 1335 | if (cleanup.startedByRunner || cleanup.createdByRunner) { | 1395 | if (cleanup.startedByRunner || cleanup.createdByRunner) { |
| 1336 | await runCommand("xcrun", ["simctl", "shutdown", cleanup.udid]).catch(() => undefined); | 1396 | await runCommand("xcrun", ["simctl", "shutdown", cleanup.udid]).catch(() => undefined); |
| 1337 | } | 1397 | } |