From 9878290856560a2bf19a57a1f752920510c18f35 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sat, 3 Oct 2026 15:05:54 +0200 Subject: [PATCH] Give Pro and Lite their own edit screenshot Lite dims the Pro tools in an edit, so the shared edit picture showed Lite tools that it does not have. The run now takes the edit a second time as Lite shows it, from the same Pro build. Each listing gets its own picture in the fourth place, under the same headline. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01P9mp6LYWFtb4rYnGaGuiAm --- .../DocumentViewController.swift | 4 +- OpenDocumentReader/Features.swift | 5 ++- OpenDocumentReader/ScreenshotMode.swift | 7 ++- .../ScreenshotTests.swift | 19 ++++++-- fastlane/Fastfile | 9 ++-- fastlane/screenshots/README.md | 12 +++-- scripts/frame-screenshots.py | 9 +++- scripts/store_screenshots.py | 45 ++++++++++++++----- 8 files changed, 82 insertions(+), 28 deletions(-) diff --git a/OpenDocumentReader/DocumentViewController.swift b/OpenDocumentReader/DocumentViewController.swift index 9e5b5327..eb1f141a 100644 --- a/OpenDocumentReader/DocumentViewController.swift +++ b/OpenDocumentReader/DocumentViewController.swift @@ -379,7 +379,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel return - case .edit: + case .edit, .editLite: // ready once the tools are up, which `beginEditSession` says editDocument() @@ -810,7 +810,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel private func editSessionReady() { isEditSessionReady = true - if ScreenshotMode.screen == .edit { + if ScreenshotMode.screen?.isEdit == true { ScreenshotMode.markReady(view) } } diff --git a/OpenDocumentReader/Features.swift b/OpenDocumentReader/Features.swift index 6a52a651..27b20187 100644 --- a/OpenDocumentReader/Features.swift +++ b/OpenDocumentReader/Features.swift @@ -6,6 +6,7 @@ enum Features { /// The ad banner and the consent form in front of it: Lite only. static var withAds: Bool { LINKS_ADS } - /// Formatting, paragraph changes and pdf marks: Pro only. - static var advancedEditing: Bool { ADVANCED_EDITING } + /// Formatting, paragraph changes and pdf marks: Pro only. The Lite edit + /// screenshot turns them off, because it is taken from the Pro build. + static var advancedEditing: Bool { ADVANCED_EDITING && ScreenshotMode.screen != .editLite } } diff --git a/OpenDocumentReader/ScreenshotMode.swift b/OpenDocumentReader/ScreenshotMode.swift index 90153ce9..35f83a69 100644 --- a/OpenDocumentReader/ScreenshotMode.swift +++ b/OpenDocumentReader/ScreenshotMode.swift @@ -29,12 +29,17 @@ enum ScreenshotMode { case pdf /// a document being edited, keyboard up case edit + /// the same edit as Lite shows it: the Pro tools dimmed, behind a badge + case editLite = "edit-lite" + + /// Whether the screen is an edit, in either app. + var isEdit: Bool { self == .edit || self == .editLite } /// The sample this screen opens, `nil` where it opens none. var sample: String? { switch self { case .browser: return nil - case .text, .edit: return "text" + case .text, .edit, .editLite: return "text" case .sheet: return "sheet" case .office: return "word" case .pdf: return "paper" diff --git a/OpenDocumentReaderUITests/ScreenshotTests.swift b/OpenDocumentReaderUITests/ScreenshotTests.swift index c464d63b..d0d636cc 100644 --- a/OpenDocumentReaderUITests/ScreenshotTests.swift +++ b/OpenDocumentReaderUITests/ScreenshotTests.swift @@ -13,6 +13,19 @@ import XCTest /// the order the store shows them in. final class ScreenshotTests: XCTestCase { + /// Each picture's name, and the screen it is of. The number is the place + /// in the store. Lite's edit takes the place of Pro's in Lite's listing, + /// so both are `04`. + private static let pictures = [ + ("01-browser", "browser"), + ("02-text", "text"), + ("03-sheet", "sheet"), + ("04-edit", "edit"), + ("04-edit-lite", "edit-lite"), + ("05-pdf", "pdf"), + ("06-office", "office"), + ] + /// Long, because it covers translating a document on a simulator that is /// also running eleven other languages' worth of tests today. private let readyTimeout: TimeInterval = 180 @@ -40,7 +53,7 @@ final class ScreenshotTests: XCTestCase { let arguments = app.launchArguments - for (index, screen) in ["browser", "text", "sheet", "edit", "pdf", "office"].enumerated() { + for (name, screen) in Self.pictures { app.launchArguments = arguments + ["-ODRScreenshot", screen] app.launch() @@ -53,11 +66,11 @@ final class ScreenshotTests: XCTestCase { waitForTheFolderToFill(in: app) } - if screen == "edit" { + if screen.hasPrefix("edit") { raiseTheKeyboard(in: app) } - Snapshots.take(String(format: "%02d-%@", index + 1, screen)) + Snapshots.take(name) // rather than leaving it running: the next launch has to go through // didFinishLaunching again to be handed the next screen diff --git a/fastlane/Fastfile b/fastlane/Fastfile index a76a63f8..0b8b6d86 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -245,9 +245,10 @@ platform :ios do ) # The Pro scheme, so no ad sdk is linked and no consent form can come up in - # front of a picture. The two apps are the same app, and what differs - the - # banner Lite carries - is not in a screenshot either way, so one set of - # pictures goes to both listings. + # front of a picture. The two apps differ only in the edit, where Lite dims + # the Pro tools: the run takes that screen a second time as Lite shows it, + # and each listing gets its own edit. The banner Lite carries is in no + # picture either way. capture_ios_screenshots( project: "OpenDocumentReader.xcodeproj", scheme: "ODR Screenshots", @@ -503,7 +504,7 @@ platform :ios do # something captured has to have all of it: half a set is worse in the # store than the set already up there. captured = !Dir.glob(File.join(FRAMED_DIR, "*", "*.png")).empty? - sh(STORE_SCREENSHOTS, "--screenshots", FRAMED_DIR, "--stage", shots_dir) if captured + sh(STORE_SCREENSHOTS, "--screenshots", FRAMED_DIR, "--stage", shots_dir, "--app", options[:name]) if captured UI.important("no screenshots under #{FRAMED_DIR} - writing the text only") unless captured key_path = api_key_file diff --git a/fastlane/screenshots/README.md b/fastlane/screenshots/README.md index dbffa5f2..b9f59bf2 100644 --- a/fastlane/screenshots/README.md +++ b/fastlane/screenshots/README.md @@ -24,7 +24,7 @@ The lane does these steps: ## The set -Six screens per device, in store order: +Six screens per device, in store order. The edit is taken twice: | | | | --- | --- | @@ -32,6 +32,7 @@ Six screens per device, in store order: | `02-text` | a text document | | `03-sheet` | a spreadsheet, with its sheet tabs | | `04-edit` | a document in edit mode, keyboard up | +| `04-edit-lite` | the same, as Lite shows it: the Pro tools dimmed | | `05-pdf` | a PDF, with a search under way | | `06-office` | a Word file | @@ -48,5 +49,10 @@ English pictures, because the app has no Hindi or Swedish UI. ## Both apps The set is taken once, with the `ODR Screenshots` scheme. That scheme builds -the Pro target, which links no ad sdk, so no consent form can appear. Both -listings get the same pictures. +the Pro target, which links no ad sdk, so no consent form can appear. + +The two apps differ only in the edit. `-ODRScreenshot edit-lite` turns the Pro +tools off in the Pro build, so Lite's edit is taken from the same build. +`scripts/store_screenshots.py --stage DIR --app lite` puts that picture in +place of `04-edit`, and each listing gets its own set. Both edits share the +headline of `04-edit` in `fastlane/frames/frames.json`. diff --git a/scripts/frame-screenshots.py b/scripts/frame-screenshots.py index 07949aa1..415cf21c 100755 --- a/scripts/frame-screenshots.py +++ b/scripts/frame-screenshots.py @@ -659,9 +659,14 @@ def main(argv=None): shutil.rmtree(out, ignore_errors=True) out.mkdir(parents=True, exist_ok=True) + # Lite's edit is framed as the screen it replaces: the same place, + # and the same headline, which is true of both apps + replaces = {lite: screen for screen, lite in store.LITE.items()} + for path in sorted(folder.glob("*.png")): - name = next((n for n in screens if path.stem.endswith(n)), None) - if name is None: + taken = next((n for n in store.CAPTURED if path.stem.endswith(n)), None) + name = replaces.get(taken, taken) + if name not in screens: continue with Image.open(path) as shot: diff --git a/scripts/store_screenshots.py b/scripts/store_screenshots.py index d0c86059..f125846f 100755 --- a/scripts/store_screenshots.py +++ b/scripts/store_screenshots.py @@ -10,7 +10,7 @@ # # scripts/store_screenshots.py --languages what to capture # scripts/store_screenshots.py check what was captured -# scripts/store_screenshots.py --stage DIR check it and stage it +# scripts/store_screenshots.py --stage DIR --app A check it and stage A's set # # The store has eleven locales and the app is translated into nine of them. # The other two get the English pictures, which is what their storefront would @@ -57,6 +57,18 @@ "06-office", ) +# What Lite shows in place of a screen of Pro's. The edit is the one screen +# where the two apps differ: Lite dims the Pro tools behind a badge. +LITE = { + "04-edit": "04-edit-lite", +} + +APPS = ("pro", "lite") + +# Every picture a run takes, longest name first, so a name that ends another +# one is never matched in its place. +CAPTURED = tuple(sorted(SCREENS + tuple(LITE.values()), key=len, reverse=True)) + # What App Store Connect accepts, upright, in pixels. An app that runs on both # has to hand in both, and the store fits every smaller iPhone and iPad from # these two. More than one size per device because which simulator a runner has @@ -125,9 +137,9 @@ def collect(directory): pictures = {} for path in sorted(folder.glob("*.png")): - screen = next((name for name in SCREENS if path.stem.endswith(name)), None) + screen = next((name for name in CAPTURED if path.stem.endswith(name)), None) if screen is None: - problems.append(f"{locale}: {path.name} is not one of {', '.join(SCREENS)}") + problems.append(f"{locale}: {path.name} is not one of {', '.join(CAPTURED)}") continue try: @@ -148,7 +160,7 @@ def collect(directory): pictures.setdefault(kind, {})[screen] = path for kind in SIZES: - missing = [screen for screen in SCREENS if screen not in pictures.get(kind, {})] + missing = [screen for screen in CAPTURED if screen not in pictures.get(kind, {})] if missing: problems.append(f"{locale}: no {kind} {', '.join(missing)}") @@ -157,12 +169,14 @@ def collect(directory): return found, problems -def stage(found, directory): - """Write the screenshot tree deliver uploads. +def stage(found, directory, app): + """Write the screenshot tree deliver uploads for `app`. A folder per store locale, the borrowed ones copied from the English rather than left out: what deliver does not upload for a locale, App Store Connect - keeps - which would be whatever was there before this release. + keeps - which would be whatever was there before this release. Lite's + pictures go under the name of the screen they replace, so they keep its + place in the store. """ directory = Path(directory) @@ -170,8 +184,9 @@ def stage(found, directory): folder = directory / locale folder.mkdir(parents=True, exist_ok=True) for kind, screens in pictures.items(): - for screen, path in screens.items(): - shutil.copyfile(path, folder / f"{kind}-{screen}.png") + for screen in SCREENS: + picture = LITE.get(screen, screen) if app == "lite" else screen + shutil.copyfile(screens[picture], folder / f"{kind}-{screen}.png") for locale in borrowed(): source = directory / FALLBACK @@ -211,8 +226,16 @@ def main(argv=None): metavar="DIR", help="also write the deliver screenshot tree into DIR", ) + parser.add_argument( + "--app", + choices=APPS, + help="the app whose set --stage writes; the two differ in the edit", + ) args = parser.parse_args(argv) + if args.stage and not args.app: + parser.error("--stage needs --app, because Pro and Lite show a different edit") + if args.languages: print("\n".join(languages())) return 0 @@ -228,11 +251,11 @@ def main(argv=None): if args.stage: try: - stage(found, args.stage) + stage(found, args.stage, args.app) except OSError as reason: return fail(str(reason)) print( - f"staged {len(SCREENS)} screenshots per device for " + f"staged {args.app}'s {len(SCREENS)} screenshots per device for " f"{len(found) + len(borrowed())} locales in {args.stage}" ) else: