From b0d5991df00a9762dfbeab2382b8b66526452983 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Thu, 24 Sep 2026 11:25:50 +0200 Subject: [PATCH] Cut the docs down to what a reader needs The README and the two fastlane notes had grown into long prose with the reasons behind each decision. This keeps the facts and the rules, in tables and numbered steps, and drops the backstories. Three errors are fixed along the way: the search in the screenshots is on the PDF screen, not the text screen; the release run has a `screenshot-set` job that joins the two device halves; and the framing step, `scripts/frame-screenshots.py`, was never named. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012cUrpMSNS7LocmwZB22G2r --- CHANGELOG.md | 26 +-- README.md | 387 ++++++++++----------------------- fastlane/metadata/README.md | 124 ++++------- fastlane/screenshots/README.md | 81 +++---- 4 files changed, 198 insertions(+), 420 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebc22f7b..c4bf7ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,17 @@ # Changelog Developer-facing changes to OpenDocument Reader for iOS, in [Keep a -Changelog](https://keepachangelog.com/en/1.1.0/) format. Changes to the shared -OpenDocument core are listed under the release that shipped them. The shorter -"What's New" copy the store shows lives in -`fastlane/metadata//changelogs/`, written from these entries by -`scripts/store-copy.py`. - -Entries go under `Unreleased` in the pull request that makes the change. The -heading is cut when the release is **submitted**, in one pull request that also -writes the store copy for that version. - -A release run refuses a version with no section here, and makes that section the -body of the GitHub release it drafts. Until the release is out the section stays -open: **a second build under the same version goes under the already cut -heading, not back under `Unreleased`.** Date the heading and add its compare link -once the version tag exists. +Changelog](https://keepachangelog.com/en/1.1.0/) format. Changes to odrcore are +listed under the release that shipped them. The store's "What's New" text is +written from these entries by `scripts/store-copy.py`. + +Rules: + +- Add an entry under `Unreleased` in the pull request that makes the change. +- Cut the heading in the pull request that also writes the store copy. +- A second build of the same version goes under the cut heading, not under + `Unreleased`. +- Add the date and the compare link once the version tag exists. ## [1.47] diff --git a/README.md b/README.md index 13507f0c..d8383385 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,15 @@ # OpenDocument.ios ![](https://github.com/opendocument-app/OpenDocument.ios/actions/workflows/build_test.yml/badge.svg) ![](https://github.com/opendocument-app/OpenDocument.ios/actions/workflows/format.yml/badge.svg) -It's Android's first OpenOffice Document Reader... for iOS! -This is an iOS frontend for our C++ [OpenDocument.core](https://github.com/opendocument-app/OpenDocument.core) library. +The iOS app of [OpenDocument.core](https://github.com/opendocument-app/OpenDocument.core). +It opens and edits office documents and PDFs. ## Setup -Open `OpenDocumentReader.xcodeproj` in Xcode. Everything comes from Swift -Package Manager and is resolved by Xcode — odrcore included, as the prebuilt -`OdrCoreObjC.xcframework` the -[OdrCore](https://github.com/opendocument-app/OpenDocument.core) package -downloads from its release. There is no conan step and no C++ toolchain to set -up. +Open `OpenDocumentReader.xcodeproj` in Xcode. Swift Package Manager resolves +everything, including odrcore as the prebuilt `OdrCoreObjC.xcframework`. There +is no conan step and no C++ toolchain. -To try an unreleased odrcore, point the package reference at a local checkout +To use an unreleased odrcore, point the package reference at a local checkout and build the xcframework there: ```sh @@ -20,24 +17,19 @@ cd ../OpenDocument.core apple/build_xcframework.py slice && apple/build_xcframework.py assemble ``` -Its `Package.swift` then takes `ODR_XCFRAMEWORK=OdrCoreObjC.xcframework` from the -environment of every `xcodebuild` invocation instead of the release artifact. +Then set `ODR_XCFRAMEWORK=OdrCoreObjC.xcframework` in the environment of every +`xcodebuild` call. ## The two apps -Two targets, and what separates them is what they *link*: - -| target | scheme | bundle id | ad + consent sdks | +| target | scheme | bundle id | ad sdk | | --- | --- | --- | --- | | `OpenDocumentReader` | `ODR Full` | `at.tomtasche.reader` | no | | `OpenDocumentReader Lite` | `ODR Lite` | `at.tomtasche.reader.lite1` | yes | -Two targets rather than two configurations of one, because a Swift package -product is linked by a target and no build setting takes it back out. Pro's -release executable is 0.5 MB against Lite's 4.4 MB. - -Four folders, each a synchronized group, so adding a file is all it takes to -add it to the build: +They are two targets, not two configurations, because a linked package cannot +be removed by a build setting. Each folder is a synchronized group, so a new +file joins the build on its own: | folder | in | | --- | --- | @@ -45,316 +37,163 @@ add it to the build: | `Ads/` | Lite | | `NoAds/` | Full | | `OpenDocumentReaderTests/` | the test bundle | - -`Ads/` is the only place that names a type from an ad sdk. `AdSlot` (the banner -and the consent form) and `AdPrivacy` (the way back to that choice) have a no-op -twin of the same shape in `NoAds/`; a method added to one copy has to be added to -the other, which building both schemes catches. - -Code that has to *ask* reads `Features.withAds`, never the bundle id. `LINKS_ADS` -behind it sits in `Ads/` and `NoAds/` next to the classes it stands for, so the -flag cannot end up in a build whose code says otherwise. `AnalyticsManager` and -`CrashManager` take no switch at all - both write to `os.Logger` and nowhere -else, so there is nothing to withhold. - -Pro also has `Features.advancedEditing`, from `ADVANCED_EDITING` in the same -two files. Lite renders with the editing scope `paragraph`, so odrcore refuses -a change that reaches past one paragraph with `outOfScope`, and the app offers -Pro. - -**The gate is on the tool, not on the mode.** Both editions open every kind the -core calls editable, a PDF included. A locked `EditToolBar` dims what only -offers Pro and leaves the **highlighter** working under both its names - -`highlight` is the formatting style and the PDF's marking tool alike. Do not put -the whole-mode gate back. `OpenDocument.droid` draws the same line. - -`configs/full` and `configs/lite` hold each bundle's `Info.plist` and privacy -manifest, out of the synchronized folder, since anything left in there would be -copied into both apps. For the same reason `scripts/make-test-fixtures.py`, which -writes the small sample documents, sits outside the test folder: everything in -there is copied into the test bundle, and the tests want the documents, not the -script that made them. +| `configs/full`, `configs/lite` | `Info.plist` and privacy manifest of each app | + +Rules: + +- Only `Ads/` names a type from an ad sdk. `AdSlot` and `AdPrivacy` have a + no-op twin in `NoAds/`. Build both schemes after you change one of them. +- Code asks `Features.withAds` or `Features.advancedEditing`, never the bundle + id. The constants behind them live in `Ads/Linked.swift` and + `NoAds/Linked.swift`. +- Lite edits with the scope `paragraph`. odrcore refuses a larger change with + `outOfScope`, and the app offers Pro. +- The gate is on the tool, not on the mode. Both apps open every editable + document, and a locked `EditToolBar` dims the Pro tools. The highlighter + works in both apps. Do not gate the whole edit mode. +- `scripts/make-test-fixtures.py` writes the test documents. It stays outside + the test folder, because everything in there goes into the test bundle. ## How a document reaches the screen -`CoreWrapper` hands the file to odrcore, which returns an `HtmlService`: a -handle that knows which views the document has but has not rendered any of -them. That service is connected to odrcore's HTTP server, bound to `127.0.0.1` -on whichever port was free, and the web view is pointed at -`http://127.0.0.1:/file//.html`. odrcore renders a page when -the web view asks for it, on one of the server's threads. - -The same thing OpenDocument.droid does, and for the same reasons: rendering -happens off the thread that opened the document, only the pages that are looked -at are rendered at all, and turning a page is a navigation rather than another -translation. The `` changes on every translation, because the web view -caches by URL and a document re-translated after a password or an edit has to -land on an address it has not seen. - -There is no file-writing fallback: a socket that cannot be opened fails the -translate, and the document is reported as failed. The web view still loads -`file:` URLs, but only for the formats odrcore does not handle at all, which -`DocumentViewController` hands it directly. - -None of this needs a capability or prompts the user. A listening socket on -loopback takes no entitlement, and the local network permission introduced in -iOS 14 covers the local subnet and multicast, not `127.0.0.1`. It does need an -App Transport Security exception, since ATS blocks plain HTTP: -`NSAllowsLocalNetworking` in both `Info.plist`s, which is the narrow one for -local addresses and — unlike `NSAllowsArbitraryLoads` — needs no justification -in App Store review. +`CoreWrapper` gives the file to odrcore and gets an `HtmlService`. odrcore's +HTTP server binds to `127.0.0.1` on a free port, and the web view loads +`http://127.0.0.1:/file//.html`. odrcore renders a page +only when the web view asks for it. The `` changes on every +translation, because the web view caches by URL. + +If the socket cannot open, the translate fails. There is no file fallback. +`file:` URLs are used only for formats odrcore does not handle. + +This needs no entitlement and no permission prompt. It needs +`NSAllowsLocalNetworking` in both `Info.plist`s, because App Transport +Security blocks plain HTTP. ## Editing -Every document is rendered editable, so the pencil only calls -`odr.editing.enable()` and the page stays where it is. The same pencil edits a -document and marks up a PDF: the two never stand in the bar together, so the -label separates them. - -**The bar holds what is done to the document, the strip what is done to the -text.** Undo, redo and save are bar buttons, so they do not scroll away. Redo -stays out over a PDF, where a mark is never put back, and the magnifier stands -down while an edit is on to leave the three their width. - -`EditToolBar` is the formatting alone, so a sheet or a plain text file shows no -strip. A tap does the tool's one job and a **long press** opens the colours it -applies; the bar under the icon is what the next tap uses, not what the -selection is. The text colour and the text size open on a tap, having no state -to turn off. Do not put the chevrons back. The tools match the website and -OpenDocument.droid, and the page reports to the app through one -`WKScriptMessageHandler`. - -The glyphs are the system set's, bar one: it has no wavy underline, so the -squiggly mark is drawn. - -**A PDF's tools are the page's to arm.** `odr.annotation.press` marks a standing -selection and arms where there is none, and `markOnSelection` marks each -selection as it is made. Do not disarm on the app's side: a tool the reader -turned on is theirs to turn off. - -A save reads the page's log (`odr.editing.getOperations()`, or -`odr.annotation.getAnnotations()` for a PDF), and odrcore writes the file next -to the open one before it moves into place. The page then renders again and -stays in the edit. +- The pencil calls `odr.editing.enable()`. The page stays in place. The same + pencil marks up a PDF. +- The bar holds undo, redo and save. The strip (`EditToolBar`) holds the text + formatting. A sheet or a plain text file shows no strip. +- A tap uses a tool. A long press opens its colours. Do not add chevrons. +- Redo is hidden over a PDF. The magnifier is hidden during an edit. +- A PDF tool is armed by the page (`odr.annotation.press`), and the app never + disarms it. +- A save reads `odr.editing.getOperations()` or + `odr.annotation.getAnnotations()`. odrcore writes the file next to the open + one and then moves it into place. +- The page talks to the app through one `WKScriptMessageHandler`. ## Formatting -Swift sources are formatted with `swift-format` from the active Xcode -toolchain, configured in `.swift-format`. Run `scripts/format.sh` before -committing; CI runs `scripts/format.sh --check` and fails on any difference. +`scripts/format.sh` runs `swift-format` from the active Xcode toolchain, with +`.swift-format` as its configuration. Run it before you commit. CI runs +`scripts/format.sh --check`. ## Continuous integration | workflow | what it does | | --- | --- | -| `format` | `scripts/format.sh --check`, on every push and pull request | -| `build_test` | unit tests on the simulator plus a device build of both flavors | -| `release` | upload to App Store Connect, by hand, see below | - -`format` needs nothing but the Xcode toolchain and reports style breakage in a -minute, so it is kept apart from the build. +| `format` | `scripts/format.sh --check` on every push and pull request | +| `build_test` | unit tests on the simulator, plus a device build of both apps | +| `release` | upload to App Store Connect, started by hand | ## Releasing -The `release` workflow uploads a build to App Store Connect. It is dispatched by -hand, and never submits for review, so promoting a build stays a deliberate step -in App Store Connect: - ```sh gh workflow run release.yml -f version=1.38 ``` -It runs as five jobs: +The run uploads both apps and never submits for review. Its jobs: | job | what it does | | --- | --- | -| `build` | one run producing both signed `.ipa`s, archived on the run | -| `screenshots` | beside the build: photographs the app on two devices, in every locale | -| `upload` | one job per app, uploading its `.ipa` | -| `listing` | one job per app, writing what the store says and shows about it | -| `record` | once both landed: tag the build, draft the GitHub release | - -Both apps always go out together, and nothing chooses one: Pro and Lite are the -same sources built as two targets, one of which links no ad sdk. +| `build` | builds and signs both `.ipa`s | +| `screenshots` | one job per device, takes the store screenshots in every locale | +| `screenshot-set` | joins the two device halves, checks the set, archives it as `framed` | +| `upload` | one job per app, uploads its `.ipa` | +| `listing` | one job per app, writes the store text and screenshots | +| `record` | tags the build and drafts the GitHub release | -**If a `listing` job fails saying screenshots are not listed**, they are up: -App Store Connect was slow to list them and nothing was sent twice. Look before -submitting, and re-run the job if a locale really is short. +Before the run, do these steps in the pull request that cuts the version: -**To write a listing again from pictures already taken**, start a release with -`screenshots_from_run` set to that run's id - the `listing` jobs alone, against -its `framed` artifact, five minutes and no build number spent. Each locale is -cleared first, so it repairs a set in any state. +1. Cut the `Unreleased` heading in `CHANGELOG.md`. +2. Run `scripts/store-copy.py ` and read the diff. See + `fastlane/metadata/README.md`. -**If one app's upload fails, press "Re-run failed jobs".** Only that upload runs -again, against the `.ipa` already built and signed - build number included, since -it is baked in at archive time - and `record` runs behind it once it lands. +The run refuses a version without a changelog section or without release notes +in every locale. `scripts/resolve-version.py` and +`scripts/changelog_section.py` show what a dispatch would do. -### The release notes +Inputs: -The "What's New" text of every locale is written before the release, not typed -into App Store Connect during it: - -```sh -scripts/store-copy.py 1.41 -``` +| input | what it does | +| --- | --- | +| `version` | the marketing version, without `v` | +| `dry_run` | builds and signs, uploads nothing, needs no version | +| `screenshots_from_run` | runs the `listing` jobs alone, with the `framed` artifact of that run | -The English comes from the `CHANGELOG.md` section of that version - or from -`Unreleased`, where a version being cut still sits - and every other locale is -translated by an agent of its own, given that locale's store description and the -release before it, so the notes keep the words the listing already uses in that -language. A second agent reads each draft back against the English before it is -written. Read the diff, then commit it with the pull request that cuts the -heading. - -The copy lives in `fastlane/metadata//changelogs/1.41.txt`, one file per -version per locale, because App Store Connect keeps only the notes of the -submission in flight. `scripts/store_listing.py` checks it - the release run -refuses a version any locale is missing, before it builds anything - and stages -it into the shape `deliver` reads. - -The rest of the listing goes up with it: name, subtitle, description, keywords -and the URLs are written in `fastlane/metadata/` and pushed by the same job, so -the store says what is committed here rather than what someone last typed into -App Store Connect. Both apps say it. What they share is in `fastlane/metadata/` -and what one of them says instead is in `fastlane/metadata-pro/` or -`fastlane/metadata-lite/`, read in that order - which is the name outright, since -an app's name is unique in the store, and the one sentence about ads inside the -description. `review_information` and the categories are left out. See -`fastlane/metadata/README.md`. - -### The screenshots - -Taken during the release run rather than committed, because a screenshot is only -worth what the build it was taken from is worth: +Common cases: -```sh -bundle exec fastlane ios screenshots -``` +- One upload fails: press "Re-run failed jobs". The `.ipa` and its build number + are kept. +- A `listing` job says the screenshots are not listed: they are up. App Store + Connect was slow. Look, then re-run the job if a locale is short. -Six pictures per device - the folder, a text document, a spreadsheet, an edit -under way, a pdf and a Word file - on a 6.9" iPhone and a 13" iPad, in the nine -store locales the app is translated into. `hi` and `sv` are given the English -set, which is what those storefronts would show anyway. +### Version and build number -The documents in them are localized too. They are not committed: the lane runs -`scripts/make-screenshot-documents.py` before the build, because they are build -output and nothing but a screenshot run opens them. Building the app needs -none of it. +Nothing in the tree changes for a release: -Nothing is tapped to get there. The app takes `-ODRScreenshot ` in Debug -builds and puts itself on that screen, so the same picture comes out in every -language without driving Apple's document browser in eleven of them - see -`OpenDocumentReader/ScreenshotMode.swift` and the `ODR Screenshots` scheme. -`scripts/store_screenshots.py` checks the set against the sizes App Store -Connect takes, and the `listing` job hands it to `deliver` alongside the text. -The same set goes to both apps. +| | comes from | checked in | +| --- | --- | --- | +| `MARKETING_VERSION` | the `version` input | `0.0.0` | +| `CURRENT_PROJECT_VERSION` | one above the highest build of either app in App Store Connect | `1` | -The run archives them as the `screenshots` artifact, on a dry run too - which is -how to look at them before the store does. See `fastlane/screenshots/README.md`. +Both apps get the same build number, so one `(version, build)` pair names one +commit. -Nothing has to be committed to cut a release, and a release leaves no commit -behind either. Both halves of the version come from outside the tree: +### Tags -| | where it comes from | what is checked in | +| tag | who writes it | what it means | | --- | --- | --- | -| `MARKETING_VERSION` (`CFBundleShortVersionString`) | the `version` input | `0.0.0` | -| `CURRENT_PROJECT_VERSION` (`CFBundleVersion`) | one above the highest build either app has | `1` | - -The version in `project.pbxproj` is a placeholder that only local and CI builds -ever see; nobody bumps it, because a commit on `main` is not a release. The -version has to be above what is live in the store - App Store Connect is the only -thing that knows what that is, and it rejects the upload otherwise. +| `build/v/` | the workflow, after both uploads | this commit went up as that build | +| `v` | you, when you publish the draft release | this is what shipped | -The build number is resolved once and given to both apps, so one `(version, build)` -pair names one commit in both listings. App Store Connect only requires it to -increase, not to be contiguous, so whichever app was behind skips ahead. +No tag is pushed before a build, and no tag starts a build. Publish the draft +after App Store Connect shows the build as live: -`scripts/resolve-version.py` decides which version a run builds and -refuses runs that cannot name one; `scripts/changelog_section.py` refuses a version with -no `CHANGELOG.md` section, before anything is built, since that section becomes -the release body. Run either by hand to see what a dispatch would do. - -The `dry_run` input builds, signs and archives both `.ipa`s without uploading -either - the only way to exercise the signing path without putting a build on -TestFlight. It is also the only kind of run allowed to go without a version, and -the only one that leaves neither tag nor draft. +```sh +gh release edit v1.38 --draft=false +``` -It needs these repository secrets: +### Secrets | secret | what it is | | --- | --- | | `ASC_KEY_ID` | App Store Connect API key id | | `ASC_ISSUER_ID` | issuer id of that key | | `ASC_KEY_CONTENT` | the `.p8` private key, base64 encoded | -| `SIGNING_CERTIFICATE_P12` | Apple Distribution certificate + key as a base64 encoded `.p12` | +| `SIGNING_CERTIFICATE_P12` | Apple Distribution certificate and key, base64 encoded `.p12` | | `SIGNING_CERTIFICATE_PASSWORD` | password of that `.p12` | -The certificate is imported into a temporary keychain that is discarded with the -runner, and signing is manual: fastlane downloads the App Store provisioning -profile for the bundle id, and both the archive and the export use that -certificate and profile. Automatic signing would instead have Xcode mint -distribution assets of its own, which only an Admin key may do - anything less -fails the export with "Cloud signing permission error". - -Downloading a profile is something any key may do; creating one wants an Admin -key. So a lesser key works as long as both apps have an App Store profile -already - the run says so in its first seconds otherwise, and either an Admin key -or a profile made by hand in the developer portal gets past it. Profiles expire -after a year, which is the other moment this matters. +Signing is manual. The run downloads the App Store provisioning profile of +each bundle id, so both apps need one. A key below Admin cannot create a +profile, and the run stops in its first seconds if one is missing. Profiles +expire after a year. -The same lanes work locally once those variables are exported, and take the -version and the dry run the same way the workflow hands them over: +### Local lanes -```bash +```sh ODR_VERSION=1.36 bundle exec fastlane deployPro ODR_VERSION=1.36 bundle exec fastlane deployLite ODR_DRY_RUN=true bundle exec fastlane deployPro # build and sign only +bundle exec fastlane uploadListingPro # text, plus screenshots if present +bundle exec fastlane screenshots # see fastlane/screenshots/README.md ``` -`deployPro` is `buildPro` followed by `uploadPro`, which the workflow runs as -separate jobs. `uploadPro` takes the `.ipa` already in `build/` rather than making -one, and `resolveBuildNumber` prints the number both apps would get. - -`uploadListingPro` writes the text and, if `fastlane/screenshots` holds a set, -the pictures with it - so fixing a word in a description by hand does not cost a -quarter of an hour of simulators, while a release run, which always captures -first, sends both. - -### Tags - -Nothing is triggered by a tag, and no tag is pushed before a build: a version -often takes more than one build to get through review, so a tag pushed up front -names a commit that may never ship. That is what happened to `v1.37`. Tags are -written afterwards instead, in two kinds: - -| tag | who writes it | what it means | -| --- | --- | --- | -| `build/v/` | the workflow, once both apps are up | this commit was uploaded as that build | -| `v` | publishing the drafted release | this is what shipped | - -Both are prefixed with a `v`; the `version` input is not (`-f version=1.39` -writes `v1.39`). - -One build tag, not one per app, since both share a build number. It is never -moved: a rebuild gets the next number, so a version that takes three builds to -clear review leaves three build tags. A half uploaded release gets none, and -neither does a lane run locally. - -**The version tag is written neither by hand nor by the workflow.** `record` drafts -a GitHub release named `v` - the changelog section with the generated list -of pull requests below it - pointing at the built commit. A draft creates no tag; -publishing it does, at exactly that commit: - -```sh -gh release edit v1.38 --draft=false -``` - -That step stays human because App Store Connect is the only thing that knows a -build went live. A rebuild re-points the same draft rather than making a second -one, and if Pro clears review while Lite does not, wait: the build tags already -record what went out. +`deployPro` is `buildPro` and then `uploadPro`. `fastlane/README.md` lists all +lanes. ## License -This project is licensed under the [Mozilla Public License 2.0](LICENSE). +[Mozilla Public License 2.0](LICENSE). diff --git a/fastlane/metadata/README.md b/fastlane/metadata/README.md index 69a7d235..9a290e9a 100644 --- a/fastlane/metadata/README.md +++ b/fastlane/metadata/README.md @@ -1,109 +1,61 @@ # Store metadata -What App Store Connect shows about the apps, one directory per locale. +The text App Store Connect shows about the apps, one directory per locale. A +release run uploads what is committed here: name, subtitle, description, +keywords, the URLs and the release notes of the version. It does not upload +`review_information` or the category files. -This is where the listing is written, and a release run uploads it: name, -subtitle, description, keywords, the URLs, and the release notes of the version -going out. What the store says is what is committed here. +## Layout -Two things are left out of the upload on purpose. `review_information` is the -account's contact details and the note to the reviewer, and the category files -say where the app sits in the store - neither is release copy. -`scripts/store_listing.py` names what is staged, so adding a file to that list is -a decision rather than an accident. - -## The two apps - -Pro and Lite are the same app, and they say almost the same thing about -themselves. What is here is what they share. Where they have to differ: +Pro and Lite say almost the same thing. `scripts/store_listing.py` reads these +places in order, and the last one wins: | | | | --- | --- | -| `fastlane/metadata//` | what both say | +| `fastlane/metadata//` | what both apps say | | `fastlane/metadata-/all/` | what this app says instead, in every locale | -| `fastlane/metadata-//` | what this app says instead, here | +| `fastlane/metadata-//` | what this app says instead, in this locale | -Read in that order, last one wins. `` is `pro` or `lite`. +`` is `pro` or `lite`. -Only the name differs outright, and it has to: an app's name is unique in the -store, so one file each, `OpenDocument Reader Pro` and `OpenDocument Reader`. -There is no `name.txt` in this directory - the apps own their names. +- The name differs, because a store name is unique. Each app has one + `all/name.txt`, and this directory has none. +- The shared description holds `${ads}` and `${editing}`. Each app fills them + from its own `ads.txt` and `editing.txt` in that locale. Lite has an + `ads.txt`, Pro has none, and an empty fill-in leaves no space behind. +- `FILL_INS` in `scripts/store_listing.py` lists the allowed names, so a + misspelt fill-in is an error. -Two sentences differ inside otherwise shared text. The first is the advertising -line: Lite shows ads and Pro does not. Rather than keep two descriptions per -locale and let them drift, the shared one holds `${ads}` and each app fills it -in from its own `ads.txt` - Lite has one per locale, Pro has none, and a -fill-in nobody answers leaves nothing behind, the space in front of it -included. +## Limits -`${editing}` works the same way, but both apps fill it in. Pro alone adds new -paragraphs, formats text and marks a pdf past the highlighter. The shared -description says only what both apps do, and each `editing.txt` says the rest: -Lite's names it as Pro's, Pro's names it as its own. +| file | limit | +| --- | --- | +| `name.txt`, `subtitle.txt` | 30 characters | +| `keywords.txt` | 100 characters, commas included | +| `changelogs/.txt` | 4000 characters | -`FILL_INS` in `scripts/store_listing.py` lists the names one may have, so a -misspelt `${adds}` is an error rather than a sentence that quietly vanishes from -the store. +The name is the same in every locale. The local search words, `LibreOffice` +among them, go in the subtitle and the keywords. ## Release notes -`/changelogs/1.41.txt`, one file per marketing version per locale, -holding the "What's New" text of that submission. Written for people using the -app, not for this repository - the developer-facing record of the same release -is `CHANGELOG.md` at the root. - -Named by marketing version, not by build number. The build number is a live -query of what TestFlight already has, so it is not known until a release run -starts and cannot name a file committed ahead of it. +`/changelogs/.txt` holds the "What's New" text of one +marketing version. App Store Connect keeps only the notes of the current +submission, so these files are the history. `scripts/store_listing.py` stages +the version's file as `release_notes.txt` for `deliver`. -App Store Connect keeps only the notes of the version being submitted, so these -files are the history the store does not keep. The limit is 4000 characters per -locale. - -`deliver` does not read this layout. It reads one `release_notes.txt` per -locale, so `scripts/store_listing.py` stages the version's file under that name -into a throwaway directory at upload time, with the rest of the listing beside -it. - -## Writing them +Write them with: ```sh scripts/store-copy.py 1.41 ``` -The English text comes from the `CHANGELOG.md` section of that version, or from -`Unreleased` while the heading is still open; a file already written by hand is -left alone. Every other locale is then translated by an agent of its own, given -that locale's `description.txt` and the release before it, so the notes reach -for the words the listing already uses in that language. - -A second agent then reads that draft against the English, in the same language, -because what a first draft gets wrong is not something it can see: a word -borrowed for its sound rather than its sense reads fine to whoever wrote it. - -Both apps get the same notes, so they say what changed and not who gets it: no -"free", no "Pro". - -It writes; it does not upload, and it does not judge. Read the diff before -committing it - it goes to the store as written. - -The release run refuses a version any locale has no copy for, before it builds -anything. - -## Name, subtitle, keywords - -30 characters for the name, 30 for the subtitle, 100 for the keywords, counting -the commas. `scripts/store_listing.py` checks all three against what it stages -rather than against what is written here, since an app's own name is what -finally has to fit. - -The name is the same in every storefront and is not translated: `OpenDocument` -is the format's own name and goes untranslated in every language anyway, and one -name is one app that people can pass to each other. Nothing is lost to search by -it, because the App Store indexes name, subtitle and keywords alike - so the -local words, `LibreOffice` among them, live in the subtitle and the keywords -instead. That also means a keyword repeating a word from the name or from its -own subtitle is a wasted slot; none of these do. +1. The English comes from the `CHANGELOG.md` section of that version, or from + `Unreleased` while the heading is open. An existing file is kept. +2. One agent per locale translates it, with that locale's `description.txt` + and the previous notes as context. +3. A second agent reviews each draft against the English. +4. Read the diff, then commit it in the pull request that cuts the version. -None of it sells the app as an editor. It edits text, but that is young and does -not reach every document, so the listing says so once and calls itself a reader. +Both apps get the same notes, so the notes never say "free" or "Pro". The +release run refuses a version that has no notes in some locale. diff --git a/fastlane/screenshots/README.md b/fastlane/screenshots/README.md index 502a55bf..dbffa5f2 100644 --- a/fastlane/screenshots/README.md +++ b/fastlane/screenshots/README.md @@ -1,61 +1,52 @@ # Store screenshots -What App Store Connect shows of the app, one directory per locale - written -here by a capture run, and not committed. This directory is empty in the -repository on purpose. - -The store copy next door is text somebody wrote, so it lives in git and the -release uploads what is committed. A screenshot is not written, it is taken: -it is only worth what the build it was taken from is worth, and a picture of -1.38 sitting in git through 1.41 is a picture of an app nobody can install any -more. So they are taken during the release run, from the build going out. +The pictures App Store Connect shows of the app. Nothing here is committed. A +capture run writes the raw pictures to this directory and the framed set to +`fastlane/framed`, and the release run archives both as artifacts. Screenshots +are taken during the release run, from the build that goes out. ```sh -bundle exec fastlane ios screenshots +bundle exec fastlane ios screenshots # capture, then frame +scripts/frame-screenshots.py # frame again, without a capture ``` -That drives the app on both devices in every locale and writes them here. -`.gitignore` keeps what it wrote out of commits, and the release run archives -the same set as the `screenshots` artifact - which is how you look at what went -to the store, before it does on a dry run and after it on a real one. +The lane does these steps: + +1. `scripts/make-screenshot-documents.py` writes the localized sample + documents. They are build output and go into Debug builds only. +2. `fastlane snapshot` builds the `ODR Screenshots` scheme once and launches + the app once per screen and locale, with `-ODRScreenshot `. See + `OpenDocumentReader/ScreenshotMode.swift`. +3. `scripts/frame-screenshots.py` puts each capture on a device frame with a + headline from `fastlane/frames/frames.json`. It needs Pillow. +4. `scripts/store_screenshots.py` checks the framed set against the sizes the + store accepts. -## What is in a set +## The set -Six pictures per device, taken by relaunching the app onto one screen at a -time rather than by tapping through it. The screens, in the order the store -shows them: +Six screens per device, in store order: | | | | --- | --- | -| `01-browser` | the document browser, with one of each format sitting in it | -| `02-text` | a text document open, with a search under way | -| `03-sheet` | a spreadsheet, with the sheet tabs under the tool bar | -| `04-edit` | a document being edited, keyboard up | -| `05-pdf` | a pdf | -| `06-office` | the same reader on a Word file | - -Two devices, because an app that runs on iPhone and iPad has to hand in both: a -6.9" iPhone and a 13" iPad. `scripts/store_screenshots.py` holds the sizes App -Store Connect accepts and checks the set against them; `Fastfile` holds the -simulators to look for, newest first, because what a simulator is called -changes with every Xcode and what it is worth does not. +| `01-browser` | the document browser, with one file of each format | +| `02-text` | a text document | +| `03-sheet` | a spreadsheet, with its sheet tabs | +| `04-edit` | a document in edit mode, keyboard up | +| `05-pdf` | a PDF, with a search under way | +| `06-office` | a Word file | -## Locales +Two devices: a 6.9" iPhone and a 13" iPad. `Fastfile` lists the simulator +names to look for, newest first. `scripts/store_screenshots.py` lists the +pixel sizes the store accepts. -Eleven in the store, nine in the app. `de-DE`, `en-US`, `es-ES`, `fr-FR`, `it`, -`pl`, `pt-BR`, `ru` and `tr` are photographed in their own language. `hi` and -`sv` are given the English pictures, because the app has no Hindi or Swedish UI -either - that is what those storefronts would show whatever we upload. +## Locales -The documents in the pictures are localized too, which is most of what a reader -has to show. The lane writes them with `scripts/make-screenshot-documents.py` -before it builds, rather than keeping them in git: they are build output, and -they are bundled into Debug builds only. `-ODRScreenshot ` is how the -app is asked to open one. See `OpenDocumentReader/ScreenshotMode.swift`. +The store has eleven locales and the app has nine. `hi` and `sv` get the +English pictures, because the app has no Hindi or Swedish UI. +`scripts/store_screenshots.py --languages` prints the locales to capture. -## Both apps get the same pictures +## Both apps -Pro and Lite are one app built twice, and the one thing that differs on screen - -the banner Lite carries - is not in a screenshot either way. The set is taken -once, with the Pro scheme, which links no ad sdk and so cannot raise a consent -form in front of the camera. Both listings are then given it. +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.