diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 32447820af66..116ecd2a5a23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -294,8 +294,9 @@ jobs: if-no-files-found: warn # The two halves put back together, and only now checked: a set is both devices - # in every locale, and neither runner above can see the other's. Republished - # under the name the listing job reads. + # in every locale, plus the feature graphic the phone's half draws, and neither + # runner above can see the other's. Republished under the name the listing job + # reads. screenshot-set: needs: screenshots runs-on: ubuntu-24.04 diff --git a/CLAUDE.md b/CLAUDE.md index 71bc6cc9a609..c6d5709a34de 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,11 +50,12 @@ lists rather than forking it. Reach for it before `adb shell input tap`. ## Store screenshots -The store *copy* is written down here; the screenshots are not. A picture of the app is +The store *copy* is written down here; the pictures are not. A picture of the app is worth what the build it came off is worth, so the release run takes its own - six screens -on a phone and a tablet in all fifteen locales - frames them and hands them to supply. -Nothing is committed, and `.gitignore` says so. `OpenDocument.ios` does the same thing -against App Store Connect, and the python is deliberately close enough to lift out later. +on a phone and a tablet in all fifteen locales - frames them, draws the feature graphic +off the first of them, and hands the lot to supply. Nothing is committed, and +`.gitignore` says so. `OpenDocument.ios` does the same thing against App Store Connect, +and the python is deliberately close enough to lift out later. **`ScreenshotTests` is the whole of it.** An instrumented test runs in the app's own process, so laying the samples out, filling the recent list and switching the app's @@ -76,6 +77,15 @@ the test can already reach. published dimensions - onto a canvas of its own, because play refuses a picture more than twice as long as it is wide and a Pixel 9 Pro XL is 2.23:1 before anything is drawn around it. `store_screenshots.py` says what a full set is and stages it. +- **The feature graphic is one of the pictures**, not a file in the tree: the same parts + laid out across a 1024x500 canvas, drawn from the first screenshot's capture and + carrying its headline, so there is no second copy to write or translate. The one that + *was* committed showed the pre-4.14 app in every storefront for three releases, which + is what a listing asset nothing regenerates comes to. Only the launcher icon is left. +- **The tablet's pictures go into both tablet slots.** Play falls back to the phone set + only where a slot is *empty*, and the 7" one was not - so it, too, showed the old app. + One capture serves both: 1600x2560 is inside the 7" slot's limits as well as the 10" + one's. - Which locale reads which language's documents is one table, in `store_screenshots.py`. The generator checks its own languages against it and writes it into the assets, and `ScreenshotTests` reads it from there - do not write a second copy diff --git a/README.md b/README.md index 5c148fb221f1..bacdf58a5fb6 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,10 @@ F-Droid and Play are the two stores. Sideloaders take `app-foss-release.apk` fro which is what Obtainium tracks - point it at this repository and it needs no filter, one apk is all a release carries. -That apk is `at.tomtasche.reader.foss`, and up to 4.13.0 it was `at.tomtasche.reader.pro`. -A different application id is a different app, so the old one neither updates nor complains: -install the new one, uninstall the old one. Nothing carries over and nothing is worth -carrying - a recent documents list whose uri permissions die with the old package anyway. +That apk is `at.tomtasche.reader.foss`. A different application id is a different app, so +a sideload carrying an older one neither updates nor complains: install the new one and +uninstall the old one. Nothing carries over - a recent documents list whose uri permissions +die with the old package anyway. ## Translations @@ -87,8 +87,7 @@ Without them `bundleProRelease` and friends still build, just unsigned. ## Releasing The `release` workflow builds both signed bundles and uploads them to the Play Store -internal track - the same thing the fastlane lanes did from a laptop. It is dispatched -by hand, with the version it should build: +internal track. It is dispatched by hand, with the version it should build: ```sh gh workflow run release.yml -f version=v4.14.0 @@ -117,10 +116,10 @@ review that a production release waits on actually happens, so the workflow fini not the same as the release being out. The listing goes up in its own job, behind the bundle: the title, both descriptions, the -release notes of that version and the screenshots, in all fifteen locales, for each app. -**This overwrites what the Play Console says**, which is the point - the copy is written -here now, not there. The release notes are no longer typed into the promotion box. The -icon and the feature graphic are still not uploaded; see `fastlane/metadata/README.md`. +release notes of that version, the screenshots and the feature graphic, in all fifteen +locales, for each app. **This overwrites what the Play Console says**, which is the point +- the copy is written here, not there. The launcher icon is the one listing asset left where +it is; see `fastlane/metadata/README.md`. Separate from the bundle upload on purpose: a bundle cannot go up twice, while the listing stays editable for as long as the release sits on the internal track - and the listing is @@ -141,13 +140,19 @@ way out is a new patch version for both flavors. ### Screenshots -The store copy is written down here; the screenshots are not. A picture of the app is worth +The store copy is written down here; the pictures of the app are not. A picture is worth what the build it came off is worth, so they are taken during the release run, from the build going out, framed there, and handed to supply from there. Nothing is committed. Six screens - the recently opened list, a text document with a search running, a spreadsheet, an edit under way, a PDF and a Word file - on a phone and on a tablet, in the -fifteen locales the listing is written in. That is 180 pictures a release. +fifteen locales the listing is written in. That is 180 pictures a release. The tablet's go +into both of Play's tablet slots: it falls back to the phone set only where a slot is +*empty*. + +The feature graphic, the one picture above the listing, is drawn beside them - the same +frame laid out across a 1024x500 canvas, off the first screenshot's capture and carrying +its headline in each of the fifteen languages. One per locale. Taking them by hand needs one emulator on adb running **Android 15 or newer**, and Pillow: @@ -159,8 +164,8 @@ ODR_SCREENSHOT_LANGUAGES=en-US,de-DE bundle exec fastlane android screenshots ``` With more than one device attached, `ANDROID_SERIAL` picks which. The raw captures land in -`fastlane/screenshots/`, the framed set in `fastlane/framed/`, and only the second is what -the store is given. Re-running `scripts/frame-screenshots.py` alone re-frames what is +`fastlane/screenshots/`, the framed set and the feature graphics in `fastlane/framed/`, and +only the second is what the store is given. Re-running `scripts/frame-screenshots.py` alone re-frames what is already captured, so changing a headline in `fastlane/frames/frames.json` costs a second of Pillow rather than a quarter hour of emulators. @@ -208,11 +213,8 @@ the lane's - and takes an optional `track:` (`... track:beta`). The version can ### Tags -No tag triggers a build, and none is pushed before one. A tag written up front is a -promise the run can fail to keep: `v4.9.0`'s tag push run failed and the upload came -from a dispatched run - the same commit that time, which was luck. - -Tags are written afterwards instead, in two kinds: +No tag triggers a build, and none is pushed before one: a tag written up front is a +promise the run can fail to keep. Tags are written afterwards instead, in two kinds: | tag | who writes it | what it means | |---|---|---| @@ -224,11 +226,10 @@ half uploaded release gets no tag at all, which is the honest answer - nothing y be published from it. A lane run from a laptop leaves none either. **The `v*` tag is written neither by hand nor by the workflow.** `record` drafts a GitHub -release named `v` at the built commit, carrying the Foss APK - the sideloadable -copy every release up to v4.6 has had, now the flavor that links nothing proprietary - -a `version.json` naming the version and its code, and the version's `CHANGELOG.md` -section above GitHub's generated list of pull requests. A draft creates no tag; publishing it does, at -exactly that commit: +release named `v` at the built commit, carrying the Foss APK - the flavor that +links nothing proprietary - a `version.json` naming the version and its code, and the +version's `CHANGELOG.md` section above GitHub's generated list of pull requests. A draft +creates no tag; publishing it does, at exactly that commit: ```sh gh release edit v4.14.0 --draft=false @@ -251,26 +252,22 @@ build refuses rather than folding `4.100.0` onto the same code as `5.0.0`. Nobod it anywhere: a commit on `main` is not a release, and no number on `main` can describe one that already went out. -All three parts have to be spelled out. A two-part `v4.7` used to be padded to `4.7.0`, -which meant one build could be tagged under two names, and the tags older than `v4.8.0` -are in both formats because of it. They are left as they are - a release asset is served -from a URL carrying its tag name, and F-Droid rebuilds old versions from those names - -so the rule only holds for what is tagged from here on. +All three parts have to be spelled out: a two-part `v4.7` is refused rather than padded, +so one build cannot be tagged under two names. Builds handed no version - local ones, PR builds, `assembleProDebug` - are `0.0.0`. Nothing reads it: no code in the app looks at its own version, and only what the release workflow builds ever leaves the machine. Any build can be given a real one anyway, with `./gradlew assembleProRelease -Podr.version=v4.8.0`. -Version codes up to 204 were counted by hand in `AndroidManifest.xml`, which is why the -first derived one is a five digit jump. That is one way: the Play Store only ever accepts -a code above the last one it saw. +Version codes only ever go up: the Play Store accepts a code only above the last one it +saw, so a version number cannot be reused or walked backwards. ## License -Mozilla Public License 2.0, in `LICENSE`, replacing the GPL-3.0-or-later this carried -before. MPL is copyleft per *file*: a changed file goes back under MPL, and a larger work -that merely links this can stay under whatever license it likes. +Mozilla Public License 2.0, in `LICENSE`. MPL is copyleft per *file*: a changed file goes +back under MPL, and a larger work that merely links this can stay under whatever license +it likes. The notice sits in `LICENSE` rather than atop every source file, which Exhibit A of the license itself allows. Two things keep their own headers because they came from elsewhere diff --git a/app/src/androidTest/res/drawable-hdpi/ic_launcher.png b/app/src/androidTest/res/drawable-hdpi/ic_launcher.png deleted file mode 100644 index 96a442e5b8e9..000000000000 Binary files a/app/src/androidTest/res/drawable-hdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/androidTest/res/drawable-ldpi/ic_launcher.png b/app/src/androidTest/res/drawable-ldpi/ic_launcher.png deleted file mode 100644 index 99238729d875..000000000000 Binary files a/app/src/androidTest/res/drawable-ldpi/ic_launcher.png and /dev/null differ diff --git a/app/src/androidTest/res/drawable-mdpi/ic_launcher.png b/app/src/androidTest/res/drawable-mdpi/ic_launcher.png deleted file mode 100644 index 359047dfa4ed..000000000000 Binary files a/app/src/androidTest/res/drawable-mdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/androidTest/res/drawable-xhdpi/ic_launcher.png b/app/src/androidTest/res/drawable-xhdpi/ic_launcher.png deleted file mode 100644 index 71c6d760f051..000000000000 Binary files a/app/src/androidTest/res/drawable-xhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/icon-web.png b/app/src/main/icon-web.png deleted file mode 100644 index bf7b64c78ffd..000000000000 Binary files a/app/src/main/icon-web.png and /dev/null differ diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 71c07b9f459c..bbda403a07b0 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -21,9 +21,10 @@ ROOT = File.expand_path("..", __dir__).freeze # same thing, and the few places they differ are read over the shared text. STAGED_LISTING = File.join(ROOT, "fastlane", ".listing").freeze -# The screenshots, which unlike the copy are not written down here: they are taken +# The pictures, which unlike the copy are not written down here: they are taken # during the release run, from the build going out, framed, and handed to supply -# from there. A picture of the app is worth what the build it came off is worth. +# from there. A picture of the app is worth what the build it came off is worth - +# as true of the feature graphic drawn beside them as of a screenshot. SCREENSHOT_DIR = File.join(ROOT, "fastlane", "screenshots").freeze FRAMED_DIR = File.join(ROOT, "fastlane", "framed").freeze @@ -110,9 +111,9 @@ def stage_listing(flavor, version) staged end -# Puts the framed screenshots into the same tree, under the `images/` directory -# supply reads a locale's pictures from - so one directory is handed over and one -# edit goes to play. +# Puts the framed pictures - the screenshots and the feature graphic - into the +# same tree, under the `images/` directory supply reads a locale's pictures from, +# so one directory is handed over and one edit goes to play. # # A run with nothing captured writes the text alone, so fixing a word in a # description does not cost a quarter hour of emulators. A run with something @@ -224,7 +225,8 @@ platform :android do end # What the store says about the app: the listing text, this version's release - # notes, and the screenshots a capture run left under fastlane/framed. + # notes, and the pictures a capture run left under fastlane/framed - the + # screenshots and the feature graphic drawn beside them. private_lane :uploadListing do |options| flavor = options[:flavor] version = require_version(options[:version]) @@ -241,9 +243,11 @@ platform :android do # without a bundle, supply has to be told which release the notes belong to skip_upload_aab: true, version_code: version_code(version), - # the icon and the feature graphic are still the pre-4.14 ones and are their - # own job - see fastlane/metadata/README.md. only the screenshots go up. - skip_upload_images: true, + # Both come off the same capture run, and without one both are left alone. + # `images` is the feature graphic here: the icon is not staged, and what + # supply does not find in the tree it does not touch - see + # fastlane/metadata/README.md. + skip_upload_images: !captured, skip_upload_screenshots: !captured ) end diff --git a/fastlane/metadata/README.md b/fastlane/metadata/README.md index 96c27350a099..b2de0d5e3cc9 100644 --- a/fastlane/metadata/README.md +++ b/fastlane/metadata/README.md @@ -73,24 +73,36 @@ there is no translation of it that fits at all. `CHANGELOG.md` at the root is the other record of the same release, written for this repository rather than for the store. -## Screenshots +## Screenshots and the feature graphic Not here, and not committed anywhere: they are taken during the release run, from the build going out, and staged into this tree beside the text - one directory to -supply, one edit to Play. `scripts/store_screenshots.py` puts them under -`/images/phoneScreenshots/` and `.../tenInchScreenshots/`, which is where -supply reads a locale's pictures from. - -The copy is written; a screenshot is taken. A picture of the app is worth what the +supply, one edit to Play. `scripts/store_screenshots.py` puts the screenshots under +`/images/phoneScreenshots/`, `.../tenInchScreenshots/` and +`.../sevenInchScreenshots/`, and the feature graphic at +`/images/featureGraphic.png`, which is where supply reads a locale's +pictures from. + +The tablet's pictures go into both tablet slots. Play falls back to the phone set +only where a slot is *empty*, and the 7" one was not - it held five pictures of the +pre-4.14 app, and went on showing them through every release that rewrote the rest. + +The feature graphic is the one Play shows above the listing. It is drawn from the +first screenshot's capture, by `scripts/frame-screenshots.py`, and says what that +screenshot says in the same fifteen languages - so it cannot be a picture of an app +that no longer looks like that, which is exactly what the committed one had become. + +The copy is written; a picture is taken. A picture of the app is worth what the build it came off is worth, so it is not a file that sits in git going quietly out of date. See the README's "Screenshots" section for how to take them by hand. ## What is not uploaded -`images/` holds an icon and a feature graphic that predate the 4.14 redesign. -`skip_upload_images` stays on so they are left where they are; only the screenshots -staged above go up. Those two graphics are their own job. +`images/` holds an icon that predates the 4.14 redesign. Nothing stages it, and +what supply does not find in the staged tree it leaves alone, so it stays where it +is - the app's own launcher icon is a job of its own and a release is a poor moment +for it. -The four phone screenshots that used to sit beside them are gone: the release now -uploads its own, taken from the build it is shipping, so a stale copy in the tree -could only ever disagree with the store. +The feature graphic and the four phone screenshots that used to sit beside it are +gone: the release now draws and uploads its own, from the build it is shipping, so +a stale copy in the tree could only ever disagree with the store. diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png deleted file mode 100644 index 5a9f0e599d96..000000000000 Binary files a/fastlane/metadata/android/en-US/images/featureGraphic.png and /dev/null differ diff --git a/scripts/frame-screenshots.py b/scripts/frame-screenshots.py index 11452e98e76e..ccc9d2951b58 100755 --- a/scripts/frame-screenshots.py +++ b/scripts/frame-screenshots.py @@ -1,7 +1,10 @@ #!/usr/bin/env python3 # -# Puts the captured screenshots into the picture the store shows: the app on a +# Puts the captured screenshots into the pictures the store shows: the app on a # phone, on a coloured ground, under a line of copy in that locale's language. +# The feature graphic, which play shows above the listing rather than in the +# gallery, is the same parts laid out across instead of down - drawn here too, off +# the first screenshot's capture, so it cannot be a picture of an older app. # # scripts/frame-screenshots.py frame the whole capture # scripts/frame-screenshots.py --locale en-US one locale, for a look @@ -177,6 +180,52 @@ }, } +# What the phone is, as opposed to where it stands: the parts of a layout that +# describe the device itself and read the same on any canvas, because each is a +# fraction of the screen or of the body rather than of the picture. +DEVICE_PARTS = ("rim", "corner", "corner_easing", "hole", "hole_top", + "buttons", "buttons_right", "tray") + +# The feature graphic, which is not a screenshot: one picture per locale, above +# the listing rather than in the gallery, and landscape where every other picture +# here stands up. So it is laid out rather than fitted - the phone to the right of +# the copy instead of below it - out of the same parts, and drawn from the first +# screenshot's capture so that it says what the app looks like today. +FEATURE = { + **{key: LAYOUT["phone"][key] for key in DEVICE_PARTS}, + "headline_top": 0.105, + "headline_size": 0.046, + "headline_width": 0.42, + "headline_leading": 1.18, + "headline_middle": 0.315, # the copy has its own column, left of the phone + "screen_left": 0.700, + "screen_top": 0.100, + "screen_width": 0.175, + "foot": 0.070, + "chip_top": 0.470, + "chip_size": (0.140, 0.070), + "chip_step": 0.080, + "chip_text": 0.041, + "dash_stroke": 0.0040, + "dash_on": 0.0166, + "dash_off": 0.0069, + # The one line, drawn once rather than handed on: there is no next picture to + # hand it to. In from the left between the copy and the tabs, down the gap + # the copy leaves before the phone, and out along the foot - behind the + # device, which is drawn over it, and out the other side. + "decoration": [(-0.2, 0.395), (0.600, 0.395), (0.600, 0.900), (1.2, 0.900)], + "radius": 0.040, +} + +# The bezel is the one number of the phone's that cannot simply be carried over: +# it is a fraction of the canvas' width, and the phone is less than a third as wide +# across this canvas as it is across a screenshot's, so the phone's own would draw +# a border three times too fat. Scaled with the device instead, and derived rather +# than written down, so that a phone made bigger here keeps its own proportions. +FEATURE["bezel"] = ( + LAYOUT["phone"]["bezel"] * FEATURE["screen_width"] / LAYOUT["phone"]["screen_width"] +) + # The device, which is drawn rather than photographed. The rim is read across the # body's width: bright where the edge turns towards the light, dark on the flat. BODY = "#08080a" @@ -262,6 +311,11 @@ def design(): # The locales written in one of them. Everything else is Nunito. WRITTEN_IN = {"hi-IN": "devanagari", "ja-JP": "japanese", "zh-CN": "chinese"} +# What the feature graphic is drawn from: the phone's first screen, which is the +# app's landing screen and already carries the line the listing opens with. So +# there is no copy of its own to write, and none to translate fifteen times. +FEATURE_FROM = f"phone-{store.SCREENS[0]}" + def face_in(path, size, marker): """One face of a font file, picked out of a collection by family name. @@ -667,9 +721,10 @@ def headline(canvas, lines, layout, locale): faces = [font(size, weight, locale) for weight in weights] leading = size * layout["headline_leading"] + middle = layout.get("headline_middle", 0.5) * width y = layout["headline_top"] * height for line, face in zip(lines, faces): - draw.text((width / 2, y), line, font=face, fill="white", anchor="ma") + draw.text((middle, y), line, font=face, fill="white", anchor="ma") y += leading @@ -746,6 +801,33 @@ def frame(shot, device, screen, locale, spec, order=0): return canvas.convert("RGB") +def feature(shot, screen, locale, spec): + """The picture above the listing: the app on a phone, beside the copy. + + The same parts as a screenshot and the same phone, laid out across instead of + down - which is the whole of what a 1024x500 canvas changes. + """ + layout = FEATURE + size = store.FEATURE_CANVAS + width, height = size + canvas = gradient(size, *spec["backgrounds"][screen["background"]]) + + dashed( + canvas, + rounded_path([(x * width, y * height) for x, y in layout["decoration"]], + layout["radius"] * width), + layout["dash_stroke"] * width, + layout["dash_on"] * width, + layout["dash_off"] * width, + ) + + device_body(canvas, shot, layout) + chips(canvas, screen["chips"], spec["chips"], layout) + headline(canvas, copy(screen, locale), layout, locale) + + return canvas.convert("RGB") + + def copy(screen, locale): """This screen's two lines in that language, or the English if it has none.""" lines = screen["headline"].get(locale) or screen["headline"][store.FALLBACK] @@ -768,7 +850,7 @@ def main(argv=None): captured, framed = Path(args.captured), Path(args.framed) wanted = args.locale or store.languages() - written = 0 + written = graphics = 0 for locale in wanted: folder = captured / locale @@ -800,7 +882,21 @@ def main(argv=None): picture.save(out / path.name) written += 1 - print(f"framed {written} screenshots into {framed}") + # The feature graphic, off the same capture the first screenshot is drawn + # from. Only the run that photographed the phone has one to draw it from, + # and only that half of a release writes it - which is why nothing here + # fails without it. + source = folder / f"{FEATURE_FROM}.png" + if source.is_file() and store.SCREENS[0] in screens: + with Image.open(source) as shot: + picture = feature( + shot.convert("RGB"), screens[store.SCREENS[0]], locale, spec + ) + + picture.save(out / f"{store.FEATURE}.png") + graphics += 1 + + print(f"framed {written} screenshots and {graphics} feature graphics into {framed}") return 0 if written else 1 diff --git a/scripts/store-listing.py b/scripts/store-listing.py index f6ea7d93b9e2..6a0b5e32e3eb 100755 --- a/scripts/store-listing.py +++ b/scripts/store-listing.py @@ -87,10 +87,10 @@ # replaces. `video.txt` is not required: a listing without a trailer is a listing. REQUIRED = ("title.txt", "short_description.txt", "full_description.txt") -# Left behind deliberately, though supply would take them: `images/` holds the icon, -# the feature graphic and four phone screenshots that predate the redesign, so -# uploading them would put pre-4.14 screenshots back over the current ones. Graphics -# are their own job, and a release is a poor moment to do it. +# Left behind deliberately, though supply would take it: `images/` holds an icon that +# predates the redesign, and the launcher icon is a job of its own. The pictures of +# the app are not in this tree at all - `scripts/store_screenshots.py` stages those, +# from a capture of the build going out. # What play refuses, rather than truncates. Checked against what is staged, since # that is what goes up - a title is short enough on its own and too long once an diff --git a/scripts/store_screenshots.py b/scripts/store_screenshots.py index 3637d9852555..2cdd4baf57dd 100755 --- a/scripts/store_screenshots.py +++ b/scripts/store_screenshots.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -# The play store screenshots: which ones there are, and the supply tree built out -# of what a capture run wrote. +# The play store pictures - the screenshots and the feature graphic: which ones +# there are, and the supply tree built out of what a capture run wrote. # # Unlike the store copy, these are not committed. A picture of the app is only # worth as much as the app it was taken from, so they are taken during the @@ -68,15 +68,18 @@ "06-office", ) -# The devices photographed, and the directory supply uploads each one to. Play +# The devices photographed, and the directories supply uploads each one to. Play # keeps a set per form factor and shows the phone one everywhere it has nothing # better, so the tablet set is what makes the listing a tablet listing. # -# `sevenInchScreenshots` is deliberately not among them: nothing is made for a -# 7" tablet in particular, and play falls back to the phone pictures there. +# The tablet's pictures go into both tablet slots. Play falls back to the phone +# set only where a slot is *empty*, and the 7" one has not been empty since long +# before the 4.14 redesign - so it went on showing the old app while every other +# slot was rewritten. One capture serves both: a 1600x2560 picture is inside the +# 7" slot's 320 to 3840 as well as the 10" slot's 1080 to 7680. DIRECTORIES = { - "phone": "phoneScreenshots", - "tablet": "tenInchScreenshots", + "phone": ("phoneScreenshots",), + "tablet": ("tenInchScreenshots", "sevenInchScreenshots"), } # What the framed picture is, per device, in pixels. Not the size of the capture: @@ -96,6 +99,17 @@ # eight and it ignores the rest. LEAST, MOST = 2, 8 +# The feature graphic: the one picture play shows above the listing rather than in +# the gallery, and the only listing asset that is not a screenshot. It is drawn +# from a capture like they are - see `frame-screenshots.py` - because the one that +# was committed here instead was a mockup of the app as it looked before the 4.14 +# redesign, and nothing in a release would ever have told us so. +# +# 1024x500 exactly, which is what play takes and not a size of our own: unlike a +# screenshot it is not scaled to fit, it is refused. +FEATURE = "featureGraphic" +FEATURE_CANVAS = (1024, 500) + def languages(): """The locales worth capturing: the ones the app can be photographed in.""" @@ -134,9 +148,16 @@ def named(stem): def collect(directory): - """What one capture run wrote. Returns (files by locale and device, problems).""" + """What one capture run wrote. + + Returns (screenshots by locale and device, feature graphics by locale, + problems). The feature graphic is kept apart from the screenshots because + that is how play keeps it: one picture per locale, in no gallery and of no + form factor. + """ directory = Path(directory) found = {} + features = {} problems = [] for locale in languages(): @@ -147,11 +168,26 @@ def collect(directory): pictures = {} for path in sorted(folder.glob("*.png")): + if path.stem == FEATURE: + try: + width, height = size(path) + except (OSError, ValueError) as reason: + problems.append(f"{locale}: {reason}") + continue + + if (width, height) != FEATURE_CANVAS: + wanted = "x".join(str(side) for side in FEATURE_CANVAS) + problems.append(f"{locale}: {path.name} is {width}x{height}, not {wanted}") + continue + + features[locale] = path + continue + device, screen = named(path.stem) if device is None: problems.append( f"{locale}: {path.name} is not one of " - + ", ".join(f"{d}-{s}" for d in DIRECTORIES for s in SCREENS) + + ", ".join([f"{d}-{s}" for d in DIRECTORIES for s in SCREENS] + [FEATURE]) ) continue @@ -173,17 +209,21 @@ def collect(directory): if missing: problems.append(f"{locale}: no {device} {', '.join(missing)}") + if locale not in features: + problems.append(f"{locale}: no {FEATURE}.png") + found[locale] = pictures - return found, problems + return found, features, problems -def stage(found, directory): - """Write the screenshots into the metadata tree supply uploads. +def stage(found, features, directory): + """Write the pictures into the metadata tree supply uploads. Into the same directory `scripts/store-listing.py` stages the text in, under the `images/` subdirectory supply reads a locale's pictures from - so one - tree is handed over and one edit goes to play. + tree is handed over and one edit goes to play. The feature graphic sits in + `images/` itself, beside the gallery's directories rather than in one. The borrowed locales are copied from the English rather than left out: what supply does not upload for a locale, play keeps - which would be whatever was @@ -191,12 +231,18 @@ def stage(found, directory): """ directory = Path(directory) + for locale, path in features.items(): + folder = directory / locale / "images" + folder.mkdir(parents=True, exist_ok=True) + shutil.copyfile(path, folder / f"{FEATURE}.png") + for locale, pictures in found.items(): for device, screens in pictures.items(): - folder = directory / locale / "images" / DIRECTORIES[device] - folder.mkdir(parents=True, exist_ok=True) - for screen, path in screens.items(): - shutil.copyfile(path, folder / f"{screen}.png") + for name in DIRECTORIES[device]: + folder = directory / locale / "images" / name + folder.mkdir(parents=True, exist_ok=True) + for screen, path in screens.items(): + shutil.copyfile(path, folder / f"{screen}.png") for locale in borrowed(): source = directory / FALLBACK / "images" @@ -245,7 +291,7 @@ def main(argv=None): if not LEAST <= len(SCREENS) <= MOST: return fail(f"play takes {LEAST} to {MOST} screenshots per device, not {len(SCREENS)}") - found, problems = collect(args.screenshots) + found, features, problems = collect(args.screenshots) if problems: return fail( @@ -256,16 +302,19 @@ def main(argv=None): if args.stage: try: - stage(found, args.stage) + stage(found, features, args.stage) except OSError as reason: return fail(str(reason)) print( - f"staged {len(SCREENS)} screenshots per device for " + f"staged {len(SCREENS)} screenshots per device and a feature graphic for " f"{len(found) + len(borrowed())} locales in {args.stage}" ) else: pictures = sum(len(screens) for locale in found.values() for screens in locale.values()) - print(f"{pictures} screenshots in all {len(found)} captured locales: {', '.join(found)}") + print( + f"{pictures} screenshots and {len(features)} feature graphics in all " + f"{len(found)} captured locales: {', '.join(found)}" + ) return 0