crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

screenshots-marketing.sh (8011B)


      1 #!/bin/bash
      2 set -euo pipefail
      3 
      4 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
      5 REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
      6 source "${SCRIPT_DIR}/select-simulator.sh"
      7 
      8 PLATFORM="iphone"
      9 ADD_DYNAMIC_ISLAND=true
     10 while getopts "p:n" opt; do
     11   case $opt in
     12     p) PLATFORM="$OPTARG" ;;
     13     n) ADD_DYNAMIC_ISLAND=false ;;
     14     *) ;;
     15   esac
     16 done
     17 shift $((OPTIND - 1))
     18 
     19 MAJOR="${1:-26}"
     20 case "$PLATFORM" in
     21   iphone|ipad) ;;
     22   *)
     23     echo "Unsupported platform '${PLATFORM}'. Use iphone or ipad." >&2
     24     exit 1
     25     ;;
     26 esac
     27 
     28 set_simulator_landscape_left() {
     29   # The Device ▸ Orientation ▸ Landscape Left menu path (the extra
     30   # `menu bar item "Device"` level is required on the iOS 26 Simulator).
     31   # "Landscape Left" is absolute, so clicking it repeatedly is idempotent.
     32   osascript \
     33     -e 'tell application "Simulator" to activate' \
     34     -e 'delay 0.3' \
     35     -e 'tell application "System Events" to tell process "Simulator" to click menu item "Landscape Left" of menu "Orientation" of menu item "Orientation" of menu "Device" of menu bar item "Device" of menu bar 1' \
     36     >/dev/null 2>&1 || true
     37 }
     38 
     39 set_ipad_landscape() {
     40   local udid="$1"
     41 
     42   # Bring the target device's window to the front — the orientation menu acts on
     43   # the frontmost device window, so this must run whether or not the Simulator
     44   # (or this device) was already booted.
     45   open -a Simulator --args -CurrentDeviceUDID "$udid" >/dev/null 2>&1 || true
     46   sleep 1
     47 
     48   # Click a few times: right after the window comes forward the orientation menu
     49   # item can be briefly disabled, no-oping the click. The command is idempotent,
     50   # so extra clicks are harmless once landscape has taken.
     51   local attempt
     52   for attempt in 1 2 3; do
     53     set_simulator_landscape_left
     54     sleep 1
     55   done
     56 }
     57 
     58 if [ "$PLATFORM" = "iphone" ]; then
     59   select_simulator "$MAJOR"
     60   CANVAS_ARGS=()
     61 else
     62   MARKETING_IPAD_NAME="Crossmate Marketing iPad"
     63   RUNTIME=$(xcrun simctl list runtimes available \
     64     | grep "iOS ${MAJOR}\." \
     65     | sed 's/.*iOS \([0-9.]*\).*/\1/' \
     66     | sort -t. -k1,1n -k2,2n \
     67     | tail -1 || true)
     68   DESTINATION_OS=$(xcrun simctl list runtimes available \
     69     | grep "iOS ${MAJOR}\." \
     70     | sed 's/.*(\([0-9.]*\) -.*/\1/' \
     71     | sort -t. -k1,1n -k2,2n -k3,3n \
     72     | tail -1 || true)
     73   if [ -z "$RUNTIME" ] || [ -z "$DESTINATION_OS" ]; then
     74     echo "No available iOS ${MAJOR}.x simulator runtime found." >&2
     75     exit 1
     76   fi
     77 
     78   UDID=$(xcrun simctl list devices \
     79     | grep -F "${MARKETING_IPAD_NAME} (" \
     80     | head -1 \
     81     | sed -E 's/.*\(([0-9A-Fa-f-]{36})\).*/\1/' || true)
     82   if [ -n "$UDID" ]; then
     83     DEVICE="$MARKETING_IPAD_NAME"
     84   else
     85     DEVICE_TYPE_LINE=$(xcrun simctl list devicetypes \
     86       | grep -F "iPad Pro 13-inch" \
     87       | grep -v "16GB" \
     88       | head -1 \
     89       || true)
     90     if [ -z "$DEVICE_TYPE_LINE" ]; then
     91       DEVICE_TYPE_LINE=$(xcrun simctl list devicetypes \
     92         | grep -F "iPad" \
     93         | head -1 \
     94         || true)
     95     fi
     96     DEVICE=$(sed -E 's/ \(com\.apple[^)]*\)$//' <<<"$DEVICE_TYPE_LINE")
     97     DEVICE_TYPE_ID=$(sed -E 's/.*\((com\.apple[^)]*)\)$/\1/' <<<"$DEVICE_TYPE_LINE")
     98     RUNTIME_ID=$(xcrun simctl list runtimes available \
     99       | grep "iOS ${RUNTIME} (" \
    100       | head -1 \
    101       | sed -E 's/.* - (com\.apple[^ ]*).*/\1/' || true)
    102     if [ -z "$DEVICE_TYPE_ID" ] || [ -z "$RUNTIME_ID" ]; then
    103       echo "Couldn't resolve a device type / runtime to create '${MARKETING_IPAD_NAME}'." >&2
    104       exit 1
    105     fi
    106     echo "Creating '${MARKETING_IPAD_NAME}' (${DEVICE}, iOS ${RUNTIME})"
    107     UDID=$(xcrun simctl create "${MARKETING_IPAD_NAME}" "${DEVICE_TYPE_ID}" "${RUNTIME_ID}")
    108     DEVICE="$MARKETING_IPAD_NAME"
    109   fi
    110   CANVAS_ARGS=(2752 2064)
    111 fi
    112 
    113 echo "Using ${DEVICE}, iOS ${RUNTIME}"
    114 
    115 if [ -z "${UDID:-}" ]; then
    116   UDID=$(xcrun simctl list devices available "iOS ${RUNTIME}" \
    117     | grep "$DEVICE" \
    118     | head -1 \
    119     | sed 's/.*(\([A-F0-9-]*\)).*/\1/')
    120 fi
    121 
    122 xcrun simctl boot "$UDID" 2>/dev/null || true
    123 xcrun simctl bootstatus "$UDID" -b
    124 
    125 # Marketing status-bar time/date: 9:41 AM (Apple's canonical time) on 2026-04-09,
    126 # the date of this repo's first commit to master. Passing a full ISO 8601 string
    127 # to --time sets BOTH the date and the time on devices whose status bar shows a
    128 # date (iPad); a bare "9:41" sets only the time. The date comes from the UTC
    129 # instant, so convert the wall-clock 9:41 in the local zone first.
    130 # NOTE: current Simulator versions apply only the time from this string and fall
    131 # back to today's date; the date half may start working if Apple fixes that.
    132 MARKETING_EPOCH=$(date -j -f "%Y-%m-%d %H:%M:%S" "2026-04-09 09:41:00" "+%s")
    133 MARKETING_TIME=$(date -u -r "$MARKETING_EPOCH" "+%Y-%m-%dT%H:%M:%S.000Z")
    134 
    135 xcrun simctl status_bar "$UDID" override \
    136   --time "$MARKETING_TIME" \
    137   --batteryState discharging \
    138   --batteryLevel 100 \
    139   --wifiBars 3 \
    140   --cellularBars 4 \
    141   --cellularMode active \
    142   --dataNetwork wifi
    143 
    144 echo "Status bar overridden (time/date: ${MARKETING_TIME})"
    145 
    146 # The import scene renders the real (system-appearance) Game List + sheet; the
    147 # other scenes force .light in code. Pin the sim to light so all scenes match.
    148 xcrun simctl ui "$UDID" appearance light 2>/dev/null || true
    149 
    150 DERIVED_DATA="/tmp/crossmate-marketing-derived"
    151 WORK_DIR="/tmp/crossmate-marketing-${PLATFORM}"
    152 MARKETING_DIR="${REPO_DIR}/Marketing"
    153 BUNDLE_ID="net.inqk.crossmate"
    154 
    155 SCENES=(together solo import replay undo-redo)
    156 CAPTIONS=(
    157   "Solve Crosswords Together"
    158   "Solve Crosswords Solo"
    159   "Import Across Lite and XD"
    160   "Watch Puzzle Replays"
    161   "Undo/Redo Moves"
    162 )
    163 
    164 rm -rf "$DERIVED_DATA" "$WORK_DIR"
    165 mkdir -p "$WORK_DIR" "$MARKETING_DIR"
    166 
    167 if [ "$PLATFORM" = "ipad" ]; then
    168   set_ipad_landscape "$UDID"
    169 fi
    170 
    171 xcodebuild build \
    172   -quiet \
    173   -scheme "Crossmate" \
    174   -project "${REPO_DIR}/Crossmate.xcodeproj" \
    175   -destination "id=${UDID}" \
    176   -derivedDataPath "$DERIVED_DATA"
    177 
    178 APP_PATH="${DERIVED_DATA}/Build/Products/Debug-iphonesimulator/Crossmate.app"
    179 if [ ! -d "$APP_PATH" ]; then
    180   echo "Built app not found at ${APP_PATH}" >&2
    181   exit 1
    182 fi
    183 
    184 xcrun simctl install "$UDID" "$APP_PATH"
    185 
    186 for index in "${!SCENES[@]}"; do
    187   scene="${SCENES[$index]}"
    188   caption="${CAPTIONS[$index]}"
    189   raw_path="${WORK_DIR}/${scene}.png"
    190   rotated_path="${WORK_DIR}/${scene}-rotated.png"
    191   island_path="${WORK_DIR}/${scene}-island.png"
    192   framed_path="${WORK_DIR}/${scene}-framed.png"
    193   output_path="${MARKETING_DIR}/${PLATFORM}_${index}.png"
    194 
    195   launch_args=(
    196     --crossmate-marketing-screenshot
    197     --crossmate-marketing-scene "$scene"
    198   )
    199   if [ "$scene" = "undo-redo" ]; then
    200     launch_args+=(--crossmate-marketing-symbol-keyboard)
    201   fi
    202   if [ "$scene" = "replay" ]; then
    203     launch_args+=(--crossmate-marketing-replay-position 11)
    204   fi
    205   if [ "$scene" = "import" ]; then
    206     # Boots the real Game List (seeded, three-player-max) as the sheet backdrop.
    207     launch_args+=(--crossmate-seed-marketing)
    208   fi
    209 
    210   echo ""
    211   echo "Capturing ${PLATFORM}_${index}.png - ${caption}"
    212   xcrun simctl launch --terminate-running-process "$UDID" "$BUNDLE_ID" "${launch_args[@]}"
    213   sleep 3
    214   xcrun simctl io "$UDID" screenshot --mask ignored "$raw_path"
    215 
    216   frame_input="$raw_path"
    217   if [ "$PLATFORM" = "ipad" ]; then
    218     sips -r 270 "$raw_path" --out "$rotated_path" >/dev/null
    219     frame_input="$rotated_path"
    220   fi
    221   if [ "$PLATFORM" = "iphone" ] && [ "$ADD_DYNAMIC_ISLAND" = true ]; then
    222     swift "${SCRIPT_DIR}/screenshots-iphone-island.swift" "$raw_path" "$island_path"
    223     frame_input="$island_path"
    224   fi
    225 
    226   shortcuts run "Frame Screenshots" -i "$frame_input" -o "$framed_path"
    227   if [ "$PLATFORM" = "ipad" ]; then
    228     swift "${SCRIPT_DIR}/screenshots-ios-compose.swift" \
    229       "$framed_path" \
    230       "$output_path" \
    231       "$caption" \
    232       "${CANVAS_ARGS[@]}"
    233   else
    234     swift "${SCRIPT_DIR}/screenshots-ios-compose.swift" \
    235       "$framed_path" \
    236       "$output_path" \
    237       "$caption"
    238   fi
    239   ls -la "$output_path"
    240 done
    241 
    242 xcrun simctl status_bar "$UDID" clear 2>/dev/null || true
    243 rm -rf "$WORK_DIR"
    244 
    245 echo ""
    246 echo "Screenshots saved to ${MARKETING_DIR}/"
    247 ls -la "${MARKETING_DIR}/${PLATFORM}_"*.png