diff --git a/docs/STORE_LISTING.md b/docs/STORE_LISTING.md index 406d913..9192407 100644 --- a/docs/STORE_LISTING.md +++ b/docs/STORE_LISTING.md @@ -24,6 +24,31 @@ FORM_FACTOR=tablet-7in ./tools/make-store-screenshots.sh FORM_FACTOR=tablet-10in ./tools/make-store-screenshots.sh ``` +## Screenshots + +Play's slots, and what each needs: + +| Slot | How many | Target here | Notes | +|---|---|---|---| +| Phone | **2–8** | `screenshots/phone/` — 1080×2160 | A phone at its native 1080×2400 is 2.22:1 and is **rejected**; the normaliser crops chrome and clamps to 2:1 | +| 7-inch tablet | 4+ recommended | `screenshots/tablet-7in/` — 1200×1920 | A tablet frame is already inside the 2:1 cap, so 0 px of crop | +| 10-inch tablet | 4+ recommended | `screenshots/tablet-10in/` — 2560×1600 | Landscape profile (matches the `tablet10` AVD); Play accepts either orientation | + +### The library the shots are taken over + +**Never shoot over a real device's storage.** Published screenshots are public, and a file +list exposes real folder names and document titles. The library is generated instead: + +```bash +python3 tools/make-demo-library.py # -> demo-files/ (git-ignored, ~161 MB) +adb push demo-files /sdcard/Download/PlainFiles-Demo +``` + +`tools/make-demo-library.py` builds eight distinct sample images (so the grid view and +thumbnails have something to show), a handful of documents, sized stand-ins for video and +audio so the storage-usage screen has something worth ranking, and a real ZIP so the extract +action can be exercised. Every name and every pixel is fake by construction. + | Asset | Play's spec | Result | Source | |---|---|---|---| | App icon | 512×512, 32-bit PNG with alpha, ≤ 1 MB | `store-assets/icon-512.png` — 512×512, **RGBA**, ~5 KB | `tools/store-assets/icon.svg`, mirroring `ic_launcher_background.xml` + `ic_launcher_foreground.xml`. Background, paths and opacities are identical, so **the store icon is the launcher icon** rather than a lookalike that drifts. | diff --git a/tools/make-demo-library.py b/tools/make-demo-library.py new file mode 100755 index 0000000..3e65299 --- /dev/null +++ b/tools/make-demo-library.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +""" +Builds the synthetic file library the store screenshots are taken over. + + python3 tools/make-demo-library.py [output-dir] # default: ./demo-files + +Why this exists: published screenshots are public. Shooting them over a real device's +storage would put someone's real file names, folder names and document titles on the Play +listing. Everything here is generated, so every pixel of every screenshot is fake by +construction. + +Output lands in ./demo-files/ *inside* the repo and is git-ignored: it is regenerated on +demand and never committed. Push it to a device with: + + adb push demo-files /sdcard/Download/PlainFiles-Demo +""" +import os +import shutil +import sys +import zipfile + +from PIL import Image, ImageDraw, ImageFont + +FONT_BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" + +# Distinct hues so the grid view reads as eight different pictures rather than a wall of one +# colour. (name, background hue, accent hue, motif) +SCENES = [ + ("dawn-ridge", (28, 78, 150), (255, 214, 140), "peaks"), + ("pine-lake", (18, 96, 92), (198, 240, 200), "waves"), + ("desert-road", (196, 120, 48), (255, 236, 196), "peaks"), + ("neon-city", (78, 40, 130), (240, 120, 220), "bars"), + ("quiet-harbour", (36, 62, 110), (170, 220, 255), "waves"), + ("autumn-field", (150, 84, 36), (255, 208, 120), "bars"), + ("snow-pass", (110, 124, 148), (245, 250, 255), "peaks"), + ("orchard", (52, 108, 60), (214, 246, 170), "bars"), +] + + +def font(path, size): + try: + return ImageFont.truetype(path, size) + except OSError: + return ImageFont.load_default() + + +def vertical_gradient(width, height, top, bottom): + image = Image.new("RGB", (width, height), top) + draw = ImageDraw.Draw(image) + for y in range(height): + blend = y / max(height - 1, 1) + draw.line( + [(0, y), (width, y)], + fill=tuple(int(top[i] + (bottom[i] - top[i]) * blend) for i in range(3)), + ) + return image + + +def draw_motif(draw, width, height, kind, colour): + if kind == "peaks": + base = height * 0.78 + for offset, scale in ((0.05, 0.42), (0.42, 0.58), (0.74, 0.34)): + apex = base - height * scale + draw.polygon( + [ + (width * offset, base), + (width * (offset + 0.20), apex), + (width * (offset + 0.40), base), + ], + fill=colour, + ) + elif kind == "waves": + for index in range(5): + y = height * (0.55 + index * 0.08) + amplitude = height * 0.03 + points = [ + (x, y + amplitude * ((x // 40) % 2 * 2 - 1)) for x in range(0, width + 40, 40) + ] + draw.line(points, fill=colour, width=6) + else: # bars + for index in range(7): + x = width * (0.10 + index * 0.12) + bar = height * (0.20 + (index % 3) * 0.14) + draw.rounded_rectangle( + [x, height * 0.80 - bar, x + width * 0.06, height * 0.80], + radius=8, + fill=colour, + ) + + +def make_photos(root): + folder = os.path.join(root, "Photos") + os.makedirs(folder, exist_ok=True) + label_font = font(FONT_BOLD, 54) + for index, (name, top, accent, motif) in enumerate(SCENES, start=1): + width, height = 1600, 1200 + image = vertical_gradient(width, height, top, tuple(int(c * 0.45) for c in top)) + draw = ImageDraw.Draw(image) + draw.ellipse([width * 0.72, height * 0.12, width * 0.92, height * 0.32], fill=accent) + draw_motif(draw, width, height, motif, accent) + draw.text((60, height - 120), f"Sample {index:02d} - {name}", font=label_font, fill=accent) + image.save(os.path.join(folder, f"sample-{index:02d}-{name}.png"), "PNG") + + +def write(path, text): + with open(path, "w", encoding="utf-8") as handle: + handle.write(text) + + +def make_text_files(root): + folder = os.path.join(root, "Documents") + os.makedirs(folder, exist_ok=True) + + write( + os.path.join(folder, "offsite-notes.txt"), + "Team offsite - notes\n" + "====================\n\n" + "Nothing in this file is real: the whole library the screenshots are taken over\n" + "is generated by tools/make-demo-library.py, so publishing them leaks nobody's\n" + "data.\n\n" + "1. Agenda\n2. Travel\n3. Follow-ups\n", + ) + + write( + os.path.join(folder, "budget.csv"), + "item,quantity,unit_cost\n" + "laptop stand,2,39.00\n" + "usb-c hub,3,54.50\n" + "headset,1,229.00\n" + "markers,12,2.15\n", + ) + + write( + os.path.join(folder, "itinerary.md"), + "# Itinerary\n\n| Day | Item |\n| --- | --- |\n" + "| 1 | Arrival |\n| 2 | Workshops |\n| 3 | Departure |\n", + ) + + # Stand-ins for binary formats: what matters for a screenshot is the icon and the name + # in the list, and a real PDF or DOCX would add a build dependency for no visible gain. + write(os.path.join(folder, "quarterly-report.pdf"), "%PDF-1.4\n% demo placeholder\n") + write(os.path.join(folder, "service-agreement.docx"), "PK demo placeholder\n") + + +def make_sized_file(path, megabytes): + """A file of a given size, so the usage screen has something worth ranking.""" + chunk = b"\x00" * (1024 * 1024) + with open(path, "wb") as handle: + for _ in range(megabytes): + handle.write(chunk) + + +def make_media(root): + video = os.path.join(root, "Video") + os.makedirs(video, exist_ok=True) + for index in range(1, 4): + make_sized_file(os.path.join(video, f"clip-{index:02d}.mp4"), 24 + index * 6) + + music = os.path.join(root, "Music") + os.makedirs(music, exist_ok=True) + for index in range(1, 6): + make_sized_file(os.path.join(music, f"track-{index:02d}.mp3"), 4 + index) + + +def make_archive(root): + folder = os.path.join(root, "Backups") + os.makedirs(folder, exist_ok=True) + with zipfile.ZipFile(os.path.join(folder, "config-backup.zip"), "w", zipfile.ZIP_DEFLATED) as bundle: + bundle.writestr("settings.json", '{\n "theme": "system",\n "sort": "name"\n}\n') + bundle.writestr("bookmarks.txt", "Photos\nDocuments\nBackups\n") + bundle.writestr("notes/readme.txt", "Synthetic contents.\n") + + +def make_downloads(root): + folder = os.path.join(root, "Download") + os.makedirs(folder, exist_ok=True) + write(os.path.join(folder, "welcome.txt"), "Generated by tools/make-demo-library.py\n") + make_sized_file(os.path.join(folder, "installer.bin"), 18) + + +def main(): + repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + root = sys.argv[1] if len(sys.argv) > 1 else os.path.join(repo, "demo-files") + + if os.path.isdir(root): + shutil.rmtree(root) + os.makedirs(root) + + make_photos(root) + make_text_files(root) + make_media(root) + make_archive(root) + make_downloads(root) + + total = 0 + for base, _, names in os.walk(root): + for name in names: + total += os.path.getsize(os.path.join(base, name)) + print(f"Wrote {root} ({total / (1024 * 1024):.1f} MB)") + + +if __name__ == "__main__": + main() diff --git a/tools/make-store-screenshots.sh b/tools/make-store-screenshots.sh index 9a34dba..317712d 100755 --- a/tools/make-store-screenshots.sh +++ b/tools/make-store-screenshots.sh @@ -4,7 +4,7 @@ # # ./tools/make-store-screenshots.sh # phone -> 1080x2160 # FORM_FACTOR=tablet-7in ./tools/make-store-screenshots.sh # 7-inch -> 1200x1920 -# FORM_FACTOR=tablet-10in ./tools/make-store-screenshots.sh # 10-inch -> 1600x2560 +# FORM_FACTOR=tablet-10in ./tools/make-store-screenshots.sh # 10-inch -> 2560x1600 # # store-assets/raw-screenshots/