diff --git a/.gitignore b/.gitignore
index a16628d..fcec8f5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,9 @@ Textream/Textream.xcodeproj/project.xcworkspace/xcuserdata/
*.dSYM.zip
*.dSYM
+# Local App Store metadata and assets
+/app-store/
+
# macOS
.DS_Store
.AppleDouble
diff --git a/app-store/ExportOptions-iOS.plist b/app-store/ExportOptions-iOS.plist
deleted file mode 100644
index 42c8974..0000000
--- a/app-store/ExportOptions-iOS.plist
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
- destination
- upload
- manageAppVersionAndBuildNumber
-
- method
- app-store-connect
- signingStyle
- automatic
- stripSwiftSymbols
-
- teamID
- RJA7656U34
- uploadSymbols
-
-
-
diff --git a/app-store/README.md b/app-store/README.md
deleted file mode 100644
index 8b81599..0000000
--- a/app-store/README.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# Textream App Store submission drafts
-
-This directory contains working copy for an initial Mac App Store submission. Nothing here proves that an app record, build, page, screenshot, price, region, or submission is live.
-
-## Metadata
-
-The English (U.S.) draft is in `metadata/en-US/`:
-
-- `name.txt` — 8 characters; App Store limit 30.
-- `subtitle.txt` — 29 characters; App Store limit 30.
-- `description.txt` — long description; App Store limit 4,000 characters.
-- `keywords.txt` — 88 characters; App Store limit 100 bytes.
-- `promotional_text.txt` — 160 characters; App Store limit 170.
-- `support_url.txt`, `privacy_url.txt`, and `marketing_url.txt` — public web destinations.
-- `copyright.txt` — proposed rights-holder line.
-- `review_notes.txt` — draft testing instructions for App Review.
-
-The privacy and support URLs will be valid only after `docs/privacy.html` and `docs/support.html` are deployed to the canonical Textream site. Verify all three URLs in a signed-out browser before entering them in App Store Connect.
-
-## Build-dependent statements
-
-Before using this copy, verify the submitted archive itself:
-
-- App Store builds do not run or expose the GitHub update checker.
-- App Store builds do not expose an external donation or purchase link.
-- No analytics, advertising, tracking, crash-reporting, account, or developer backend was added.
-- Microphone, speech-recognition, and local-network purpose strings match actual behavior.
-- Remote and Director modes remain optional and local-network-only.
-- Review Notes steps work from a clean install with the permissions reset.
-
-If any point changes, update the description, review notes, privacy policy, and App Privacy answers together.
-
-## Account-holder decisions and attestations
-
-The account holder must review and personally confirm the following in App Store Connect:
-
-- final app name, subtitle, description, keywords, and SKU;
-- the exact copyright owner name and the right to publish all app content;
-- the App Privacy questionnaire in `app-privacy.md` against the uploaded binary;
-- export-compliance answers, including whether the build uses exempt encryption only;
-- age-rating questionnaire answers;
-- price, availability regions, and release method;
-- EU Digital Services Act trader status and any required public contact details;
-- App Review contact name, email address, and phone number;
-- all current developer agreements and tax or banking requirements;
-- the final screenshots and any accessibility or product claims;
-- the final **Submit for Review** action.
-
-Do not replace these attestations with assumptions from source code or an earlier build.
diff --git a/app-store/app-privacy.md b/app-store/app-privacy.md
deleted file mode 100644
index 215d7fb..0000000
--- a/app-store/app-privacy.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# App Privacy questionnaire draft
-
-This is a working draft for the App Store Connect questionnaire, not a completed legal attestation. The account holder must compare every answer with the exact binary being submitted and personally confirm it in App Store Connect.
-
-## Proposed top-level answers
-
-- **Does this app collect data from this app?** No.
-- **Is any data used to track users?** No.
-- **Privacy Policy URL:** https://textream.net/privacy.html
-
-These proposed answers are based on the following build assumptions:
-
-- the App Store build has no analytics, advertising, crash-reporting, attribution, or tracking SDK;
-- the App Store build does not use the direct-download GitHub update checker;
-- the developer does not operate an account, synchronization, speech, storage, or telemetry service for Textream;
-- scripts, imported presentation notes, and preferences are processed locally unless the user explicitly enables a local-network feature;
-- Remote Connection and Director Mode communicate directly with devices the user connects on the same local network; the developer does not receive that traffic;
-- Apple’s Speech framework may process microphone audio on-device or through Apple, but Textream’s developer does not receive the audio or recognition result;
-- no later code, embedded framework, or SDK changes the statements above.
-
-## Why microphone and speech do not automatically mean “data collected”
-
-App Store privacy answers describe data collected by the developer or third parties integrated into the app. Permission to use microphone audio is still privacy-sensitive and must be disclosed clearly, but Apple-framework processing that the developer cannot access is not represented here as developer collection. Re-check Apple’s current App Privacy guidance when submitting.
-
-## Permissions and user-facing disclosures to verify
-
-- Microphone purpose text explains that voice-guided modes need audio input.
-- Speech Recognition purpose text explains that microphone audio may be sent to Apple for recognition.
-- Local Network purpose text explains Remote Connection and Director Mode.
-- The in-app About screen links to the published privacy policy.
-- The public privacy policy matches the behavior of the uploaded build.
-
-## Local-network privacy note
-
-Remote Connection can expose script text and reading state to a connected device. Director Mode can also accept script edits and control commands. These features are user-enabled and local, but their HTTP and WebSocket traffic is not end-to-end encrypted. The public privacy policy tells users to use a trusted network and disable the features afterward.
-
-## Re-check immediately before attesting
-
-1. Inspect the archived app for embedded SDKs and frameworks.
-2. Exercise every feature while monitoring outbound connections.
-3. Confirm the App Store build has no direct-update request, donation flow, account flow, analytics, or developer backend.
-4. Confirm the final privacy-policy page is publicly reachable without login.
-5. Re-read Apple’s current definitions of “collect” and “tracking.”
-6. Have the account holder submit the answers personally.
diff --git a/app-store/assets/Textream-iOS-AppIcon.svg b/app-store/assets/Textream-iOS-AppIcon.svg
deleted file mode 100644
index bba07d8..0000000
--- a/app-store/assets/Textream-iOS-AppIcon.svg
+++ /dev/null
@@ -1,36 +0,0 @@
-
diff --git a/app-store/metadata-ios/README.md b/app-store/metadata-ios/README.md
deleted file mode 100644
index dc5905f..0000000
--- a/app-store/metadata-ios/README.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# Textream iOS App Store metadata
-
-This directory contains the English (U.S.) metadata for the iPhone and iPad
-version added to the existing Textream App Store record as a universal purchase.
-It is intentionally separate from `app-store/metadata/`, which describes the
-shipping Mac version.
-
-Proposed categories: Productivity (primary) and Photo & Video (secondary).
-The app has no account, advertising, analytics, purchases, or subscriptions.
diff --git a/app-store/metadata-ios/en-US/copyright.txt b/app-store/metadata-ios/en-US/copyright.txt
deleted file mode 100644
index bb8597c..0000000
--- a/app-store/metadata-ios/en-US/copyright.txt
+++ /dev/null
@@ -1 +0,0 @@
-2026 Fatih Kadir Akin
diff --git a/app-store/metadata-ios/en-US/description.txt b/app-store/metadata-ios/en-US/description.txt
deleted file mode 100644
index 19d99c0..0000000
--- a/app-store/metadata-ios/en-US/description.txt
+++ /dev/null
@@ -1,31 +0,0 @@
-Keep your eyes close to the camera and your place in the script.
-
-Textream is a teleprompter for iPhone and iPad that can follow the words you say, scroll at a steady pace, or move only while you speak. Paste or write a script, choose Read or Record, and start presenting.
-
-THREE WAYS TO STAY ON SCRIPT
-
-• Word Tracking follows and highlights the words you say using Apple Speech Recognition.
-• Classic scrolls at a steady speed without speech recognition.
-• Voice-Activated scrolls while you speak and pauses when you stop.
-
-BETTER EYE CONTACT
-
-Keep the active line just below the front camera, or place it in the center of the screen. Textream keeps the last spoken line visible in Word Tracking mode so you can continue naturally. In Classic and Voice-Activated modes, swipe left or right to change speed without interfering with vertical scrolling. If your script appears to use a different language, Textream can suggest a matching Word Tracking language.
-
-READ OR RECORD
-
-Use a clean fullscreen teleprompter in portrait or landscape. Show the camera behind your script when you want a live preview, or use Record mode to capture front- or back-camera video with microphone audio. The teleprompter stays visible to you but is not added to the saved video.
-
-YOUR RECORDINGS
-
-Completed videos are saved to Photos. Open Textream’s recordings gallery to play videos made with Textream, trim a recording with the system video editor, select multiple recordings, or delete them from Photos. If a save fails, Textream keeps a local pending copy so you can retry or discard it.
-
-MAKE IT COMFORTABLE TO READ
-
-Choose the typeface, size, text color, stage-direction color, and camera-overlay opacity. OpenDyslexic is included. Put stage directions in brackets; Textream displays them without treating them as spoken words. A fullscreen editor gives longer scripts more room.
-
-PRIVATE BY DESIGN
-
-Textream has no account, ads, analytics, or developer-operated cloud service. Scripts and settings stay on your device. Word Tracking uses Apple’s speech-recognition framework; depending on your device, selected language, and service availability, Apple may process speech on-device or send microphone audio to Apple.
-
-Textream’s source code is available on GitHub. A separate Textream app is also available for Mac.
diff --git a/app-store/metadata-ios/en-US/keywords.txt b/app-store/metadata-ios/en-US/keywords.txt
deleted file mode 100644
index aefb698..0000000
--- a/app-store/metadata-ios/en-US/keywords.txt
+++ /dev/null
@@ -1 +0,0 @@
-script,autoscroll,camera,video,presenter,speech,podcast,creator,reading,recording,cue
diff --git a/app-store/metadata-ios/en-US/marketing_url.txt b/app-store/metadata-ios/en-US/marketing_url.txt
deleted file mode 100644
index b0a566e..0000000
--- a/app-store/metadata-ios/en-US/marketing_url.txt
+++ /dev/null
@@ -1 +0,0 @@
-https://textream.net/
diff --git a/app-store/metadata-ios/en-US/name.txt b/app-store/metadata-ios/en-US/name.txt
deleted file mode 100644
index 6ba9b90..0000000
--- a/app-store/metadata-ios/en-US/name.txt
+++ /dev/null
@@ -1 +0,0 @@
-Textream
diff --git a/app-store/metadata-ios/en-US/privacy_url.txt b/app-store/metadata-ios/en-US/privacy_url.txt
deleted file mode 100644
index add79c6..0000000
--- a/app-store/metadata-ios/en-US/privacy_url.txt
+++ /dev/null
@@ -1 +0,0 @@
-https://textream.net/privacy.html
diff --git a/app-store/metadata-ios/en-US/promotional_text.txt b/app-store/metadata-ios/en-US/promotional_text.txt
deleted file mode 100644
index de2964c..0000000
--- a/app-store/metadata-ios/en-US/promotional_text.txt
+++ /dev/null
@@ -1 +0,0 @@
-Keep eye contact while Textream follows your words, scrolls at your pace, or records camera video with a presenter-only teleprompter.
diff --git a/app-store/metadata-ios/en-US/review_notes.txt b/app-store/metadata-ios/en-US/review_notes.txt
deleted file mode 100644
index 94404ba..0000000
--- a/app-store/metadata-ios/en-US/review_notes.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-Textream requires no account, purchase, subscription, demo credentials, or developer-operated service. All features can be reviewed with the included sample script.
-
-Test without permissions:
-1. Launch Textream.
-2. Leave Session set to Read, choose Auto under Follow Mode, and keep the camera toggle off.
-3. Tap Start Prompter. Classic auto-scroll starts without Camera, Microphone, Speech Recognition, or Photos permission.
-4. Swipe left or right to change speed, swipe vertically to move manually, then tap Close.
-
-Test Word Tracking:
-1. On the main screen, choose Follow under Follow Mode and tap Start Prompter.
-2. Grant Microphone and Speech Recognition access.
-3. Read the sample script aloud. Textream follows and highlights the spoken words. Apple Speech Recognition may require network availability for the selected language.
-
-Test recording on a physical iPhone or iPad:
-1. Choose Record and tap Open Camera.
-2. Grant Camera, Microphone, and Photos add-only access.
-3. Use the camera-switch control if desired, then tap the red record button. Tap the square stop control to finish.
-4. Wait for “Saved to Photos.” The on-screen teleprompter is not composited into the saved video.
-5. Tap the thumbnail to open Recordings. The first gallery visit separately requests Photos read/write access because the gallery can play, trim, and delete recordings. It filters Photos using identifiers saved when Textream creates a video; unrelated library items are not shown.
-6. Open a recording and tap Edit to use the public system video trimmer. Saving creates an edited copy and leaves the original unchanged. Select enables multi-selection and deletion from Photos.
-
-If saving to Photos fails, Textream keeps the pending movie in app-local storage and presents Retry Save and Discard Recording.
-
-The iOS Simulator can review the editor, teleprompter, orientation, and no-permission Classic flow, but Apple does not provide camera capture hardware there. A physical device is required to review camera preview, camera switching, microphone recording, and Photos saving.
diff --git a/app-store/metadata-ios/en-US/subtitle.txt b/app-store/metadata-ios/en-US/subtitle.txt
deleted file mode 100644
index 0a0149f..0000000
--- a/app-store/metadata-ios/en-US/subtitle.txt
+++ /dev/null
@@ -1 +0,0 @@
-Teleprompter That Follows You
diff --git a/app-store/metadata-ios/en-US/support_url.txt b/app-store/metadata-ios/en-US/support_url.txt
deleted file mode 100644
index 83f8a95..0000000
--- a/app-store/metadata-ios/en-US/support_url.txt
+++ /dev/null
@@ -1 +0,0 @@
-https://textream.net/support.html
diff --git a/app-store/metadata-ios/en-US/whats_new.txt b/app-store/metadata-ios/en-US/whats_new.txt
deleted file mode 100644
index 3af10ef..0000000
--- a/app-store/metadata-ios/en-US/whats_new.txt
+++ /dev/null
@@ -1 +0,0 @@
-Textream for iPhone and iPad is here. Follow your words, auto-scroll at your pace, or record camera video while keeping your script close to the lens.
diff --git a/app-store/metadata/en-US/copyright.txt b/app-store/metadata/en-US/copyright.txt
deleted file mode 100644
index bb8597c..0000000
--- a/app-store/metadata/en-US/copyright.txt
+++ /dev/null
@@ -1 +0,0 @@
-2026 Fatih Kadir Akin
diff --git a/app-store/metadata/en-US/description.txt b/app-store/metadata/en-US/description.txt
deleted file mode 100644
index 8c24f59..0000000
--- a/app-store/metadata/en-US/description.txt
+++ /dev/null
@@ -1,30 +0,0 @@
-Keep your eyes on the camera and your place in the script.
-
-Textream is a flexible teleprompter for presentations, streams, interviews, podcasts, and recorded videos. Paste a script, choose how you want to be guided, and place the teleprompter where it works best for you.
-
-THREE WAYS TO STAY ON SCRIPT
-
-• Word Tracking follows along as you speak using macOS speech recognition.
-• Classic mode scrolls at a steady, adjustable pace without using the microphone.
-• Voice-Activated mode scrolls while you speak and pauses when you stop.
-
-PUT THE SCRIPT WHERE YOU NEED IT
-
-• Pin a compact teleprompter below your MacBook notch.
-• Use a draggable, always-on-top floating window.
-• Read fullscreen on your Mac, an external display, or a Sidecar iPad.
-• Mirror the external-display output for a teleprompter rig.
-
-MAKE IT YOURS
-
-Choose from multiple typefaces, text sizes, highlight colors, overlay sizes, and glass effects. OpenDyslexic is included as a reading option. Save multi-page scripts as .textream files, or import presenter notes from a PowerPoint file.
-
-OPTIONAL LOCAL-NETWORK TOOLS
-
-Remote Connection displays your teleprompter on another device through a local-network browser. Director Mode lets a trusted person on that network edit and control the script from their browser. No companion app is required.
-
-PRIVATE BY DESIGN
-
-Textream has no account, ads, analytics, or developer-operated cloud service. Scripts and settings remain under your control. Word Tracking uses Apple’s speech-recognition framework; depending on your Mac, language, and system availability, macOS may process speech on-device or send microphone audio to Apple.
-
-No account required.
diff --git a/app-store/metadata/en-US/keywords.txt b/app-store/metadata/en-US/keywords.txt
deleted file mode 100644
index 165739e..0000000
--- a/app-store/metadata/en-US/keywords.txt
+++ /dev/null
@@ -1 +0,0 @@
-teleprompter,presenter,script,speech,streaming,podcast,notch,autoscroll,sidecar,prompter
diff --git a/app-store/metadata/en-US/marketing_url.txt b/app-store/metadata/en-US/marketing_url.txt
deleted file mode 100644
index b0a566e..0000000
--- a/app-store/metadata/en-US/marketing_url.txt
+++ /dev/null
@@ -1 +0,0 @@
-https://textream.net/
diff --git a/app-store/metadata/en-US/name.txt b/app-store/metadata/en-US/name.txt
deleted file mode 100644
index 6ba9b90..0000000
--- a/app-store/metadata/en-US/name.txt
+++ /dev/null
@@ -1 +0,0 @@
-Textream
diff --git a/app-store/metadata/en-US/privacy_url.txt b/app-store/metadata/en-US/privacy_url.txt
deleted file mode 100644
index add79c6..0000000
--- a/app-store/metadata/en-US/privacy_url.txt
+++ /dev/null
@@ -1 +0,0 @@
-https://textream.net/privacy.html
diff --git a/app-store/metadata/en-US/promotional_text.txt b/app-store/metadata/en-US/promotional_text.txt
deleted file mode 100644
index d524c90..0000000
--- a/app-store/metadata/en-US/promotional_text.txt
+++ /dev/null
@@ -1 +0,0 @@
-Keep your place without looking away. Textream follows your voice, scrolls at your pace, and puts your script in a pinned, floating, or fullscreen teleprompter.
diff --git a/app-store/metadata/en-US/review_notes.txt b/app-store/metadata/en-US/review_notes.txt
deleted file mode 100644
index 4c148fc..0000000
--- a/app-store/metadata/en-US/review_notes.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-Textream requires no account, purchase, demo credentials, or external hardware.
-
-To test the core experience:
-1. Launch Textream and keep the sample script, or paste any text into the editor.
-2. Click the Play button in the lower-left corner.
-3. Grant Microphone and Speech Recognition access when macOS asks.
-4. Read the script aloud. Word Tracking highlights the current words in the teleprompter overlay.
-5. Stop the session from the overlay or the main Textream window.
-
-The app can also be tested without microphone access: open Textream Settings → Guidance, choose Classic, return to the editor, and click Play. Classic mode scrolls at a fixed speed.
-
-Remote Connection and Director Mode are optional local-network features. To test either feature, enable it in the corresponding Settings tab, then open the address displayed by Textream on a second device on the same network. No remote Textream service is involved.
-
-PowerPoint import expects a .pptx document containing presenter notes. No sample document or external service is required for the rest of the app.
diff --git a/app-store/metadata/en-US/subtitle.txt b/app-store/metadata/en-US/subtitle.txt
deleted file mode 100644
index 0a0149f..0000000
--- a/app-store/metadata/en-US/subtitle.txt
+++ /dev/null
@@ -1 +0,0 @@
-Teleprompter That Follows You
diff --git a/app-store/metadata/en-US/support_url.txt b/app-store/metadata/en-US/support_url.txt
deleted file mode 100644
index 83f8a95..0000000
--- a/app-store/metadata/en-US/support_url.txt
+++ /dev/null
@@ -1 +0,0 @@
-https://textream.net/support.html
diff --git a/app-store/screenshots/ios/README.md b/app-store/screenshots/ios/README.md
deleted file mode 100644
index 6b44618..0000000
--- a/app-store/screenshots/ios/README.md
+++ /dev/null
@@ -1,22 +0,0 @@
-# iOS App Store screenshots
-
-The upload screenshots are composed from real Textream simulator captures. The
-headline treatment deliberately follows the existing macOS set: a dark indigo
-background, restrained blue/orange orbital light, an uppercase eyebrow, a bold
-two-line promise, and the real product UI beneath it.
-
-## Generate the upload set
-
-```sh
-swift app-store/screenshots/ios/compose.swift
-```
-
-The script writes opaque sRGB PNG files at the App Store Connect dimensions:
-
-- `upload/iphone-6.5/`: three screenshots at 1284 × 2778
-- `upload/ipad-13/`: three screenshots at 2064 × 2752
-- `contact-sheet.png`: review-only overview of both upload sets
-
-The source captures live under `raw/iphone-6.9/` and `raw/ipad-13/`. They use
-the app's DEBUG-only launch routes for stable, private-data-free views; those
-routes are not present in the App Store build.
diff --git a/app-store/screenshots/ios/compose.swift b/app-store/screenshots/ios/compose.swift
deleted file mode 100644
index e6cb940..0000000
--- a/app-store/screenshots/ios/compose.swift
+++ /dev/null
@@ -1,436 +0,0 @@
-#!/usr/bin/env swift
-
-import AppKit
-import CoreGraphics
-import CoreText
-import Foundation
-import ImageIO
-import UniformTypeIdentifiers
-
-private struct ScreenshotSpec {
- let input: String
- let output: String
- let eyebrow: String
- let title: String
-}
-
-private struct DeviceSpec {
- let rawDirectory: String
- let outputDirectory: String
- let eyebrowDeviceName: String
- let width: Int
- let height: Int
- let screenshotWidth: CGFloat
- let screenshotTop: CGFloat
- let eyebrowTop: CGFloat
- let eyebrowSize: CGFloat
- let titleTop: CGFloat
- let titleSize: CGFloat
- let titleLineHeight: CGFloat
-}
-
-private let screenshots = [
- ScreenshotSpec(
- input: "01-prompt.png",
- output: "01-word-tracking.png",
- eyebrow: "WORD TRACKING",
- title: "Stay on script.\nKeep eye contact."
- ),
- ScreenshotSpec(
- input: "03-guidance.png",
- output: "02-guidance-modes.png",
- eyebrow: "YOUR WORDS, YOUR PACE",
- title: "Choose how Textream\nfollows you."
- ),
- ScreenshotSpec(
- input: "05-settings.png",
- output: "03-accessibility.png",
- eyebrow: "MADE FOR READING",
- title: "Make every word\neasy to read."
- )
-]
-
-private let devices = [
- DeviceSpec(
- rawDirectory: "raw/iphone-6.9",
- outputDirectory: "upload/iphone-6.5",
- eyebrowDeviceName: "TEXTREAM FOR IPHONE",
- width: 1284,
- height: 2778,
- screenshotWidth: 980,
- screenshotTop: 640,
- eyebrowTop: 128,
- eyebrowSize: 37,
- titleTop: 228,
- titleSize: 92,
- titleLineHeight: 108
- ),
- DeviceSpec(
- rawDirectory: "raw/ipad-13",
- outputDirectory: "upload/ipad-13",
- eyebrowDeviceName: "TEXTREAM FOR IPAD",
- width: 2064,
- height: 2752,
- screenshotWidth: 1600,
- screenshotTop: 610,
- eyebrowTop: 110,
- eyebrowSize: 43,
- titleTop: 208,
- titleSize: 106,
- titleLineHeight: 124
- )
-]
-
-private final class Canvas {
- let width: Int
- let height: Int
- let context: CGContext
-
- init(width: Int, height: Int) {
- self.width = width
- self.height = height
-
- let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)!
- guard let context = CGContext(
- data: nil,
- width: width,
- height: height,
- bitsPerComponent: 8,
- bytesPerRow: width * 4,
- space: colorSpace,
- bitmapInfo: CGBitmapInfo.byteOrder32Big.rawValue
- | CGImageAlphaInfo.noneSkipLast.rawValue
- ) else {
- fatalError("Unable to create a \(width)×\(height) sRGB canvas")
- }
- context.interpolationQuality = .high
- self.context = context
- }
-
- func topRect(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) -> CGRect {
- CGRect(
- x: x,
- y: y,
- width: width,
- height: height
- )
- }
-
- func drawBackground(accentIndex: Int) {
- let canvasWidth = CGFloat(width)
- let canvasHeight = CGFloat(height)
- let bounds = CGRect(x: 0, y: 0, width: canvasWidth, height: canvasHeight)
- context.setFillColor(CGColor(srgbRed: 0.018, green: 0.019, blue: 0.030, alpha: 1))
- context.fill(bounds)
-
- let baseGradient = CGGradient(
- colorsSpace: CGColorSpace(name: CGColorSpace.sRGB),
- colors: [
- CGColor(srgbRed: 0.080, green: 0.082, blue: 0.145, alpha: 1),
- CGColor(srgbRed: 0.026, green: 0.028, blue: 0.048, alpha: 1),
- CGColor(srgbRed: 0.005, green: 0.006, blue: 0.011, alpha: 1)
- ] as CFArray,
- locations: [0, 0.50, 1]
- )!
- context.drawLinearGradient(
- baseGradient,
- start: CGPoint(x: 0, y: canvasHeight),
- end: CGPoint(x: canvasWidth, y: 0),
- options: []
- )
-
- let accentColors: [(CGFloat, CGFloat, CGFloat)] = [
- (0.27, 0.34, 1.00),
- (0.35, 0.28, 1.00),
- (1.00, 0.43, 0.14),
- (0.12, 0.48, 1.00)
- ]
- let accent = accentColors[accentIndex % accentColors.count]
- let glow = CGGradient(
- colorsSpace: CGColorSpace(name: CGColorSpace.sRGB),
- colors: [
- CGColor(srgbRed: accent.0, green: accent.1, blue: accent.2, alpha: 0.24),
- CGColor(srgbRed: accent.0, green: accent.1, blue: accent.2, alpha: 0.00)
- ] as CFArray,
- locations: [0, 1]
- )!
- context.drawRadialGradient(
- glow,
- startCenter: CGPoint(x: canvasWidth * 0.18, y: canvasHeight * 0.28),
- startRadius: 0,
- endCenter: CGPoint(x: canvasWidth * 0.18, y: canvasHeight * 0.28),
- endRadius: canvasWidth * 0.86,
- options: [.drawsAfterEndLocation]
- )
-
- drawOrbit(
- rect: CGRect(
- x: canvasWidth * 0.50,
- y: -canvasHeight * 0.18,
- width: canvasWidth * 0.95,
- height: canvasHeight * 0.58
- ),
- color: CGColor(srgbRed: 1.00, green: 0.56, blue: 0.20, alpha: 0.56),
- lineWidth: max(3, canvasWidth * 0.0035),
- blur: canvasWidth * 0.013
- )
- drawOrbit(
- rect: CGRect(
- x: -canvasWidth * 0.72,
- y: canvasHeight * 0.70,
- width: canvasWidth * 1.28,
- height: canvasHeight * 0.56
- ),
- color: CGColor(srgbRed: 0.17, green: 0.34, blue: 1.00, alpha: 0.42),
- lineWidth: max(3, canvasWidth * 0.0032),
- blur: canvasWidth * 0.014
- )
- }
-
- private func drawOrbit(rect: CGRect, color: CGColor, lineWidth: CGFloat, blur: CGFloat) {
- context.saveGState()
- context.setStrokeColor(color)
- context.setLineWidth(lineWidth)
- context.setShadow(offset: .zero, blur: blur, color: color)
- context.strokeEllipse(in: rect)
- context.restoreGState()
- }
-
- func drawText(
- _ string: String,
- top: CGFloat,
- height: CGFloat,
- size: CGFloat,
- weight: NSFont.Weight,
- color: NSColor,
- lineHeight: CGFloat,
- tracking: CGFloat = 0
- ) {
- let paragraph = NSMutableParagraphStyle()
- paragraph.alignment = .center
- paragraph.minimumLineHeight = lineHeight
- paragraph.maximumLineHeight = lineHeight
- paragraph.lineBreakMode = .byWordWrapping
-
- let attributed = NSAttributedString(
- string: string,
- attributes: [
- .font: NSFont.systemFont(ofSize: size, weight: weight),
- .foregroundColor: color,
- .paragraphStyle: paragraph,
- .kern: tracking
- ]
- )
- let framesetter = CTFramesetterCreateWithAttributedString(attributed)
- let canvasWidth = CGFloat(width)
- let rect = CGRect(
- x: canvasWidth * 0.055,
- y: CGFloat(self.height) - top - height,
- width: canvasWidth * 0.89,
- height: height
- )
- let path = CGPath(rect: rect, transform: nil)
- let frame = CTFramesetterCreateFrame(framesetter, CFRange(), path, nil)
-
- context.saveGState()
- context.translateBy(x: 0, y: CGFloat(self.height))
- context.scaleBy(x: 1, y: -1)
- context.textMatrix = .identity
- CTFrameDraw(frame, context)
- context.restoreGState()
- }
-
- func drawScreenshot(_ image: CGImage, width targetWidth: CGFloat, top: CGFloat) {
- let targetHeight = targetWidth * CGFloat(image.height) / CGFloat(image.width)
- let x = (CGFloat(width) - targetWidth) / 2
- let rect = topRect(x: x, y: top, width: targetWidth, height: targetHeight)
- let radius = targetWidth * 0.072
-
- context.saveGState()
- context.setShadow(
- offset: CGSize(width: 0, height: -18),
- blur: targetWidth * 0.055,
- color: CGColor(gray: 0, alpha: 0.82)
- )
- context.setFillColor(CGColor(gray: 0, alpha: 1))
- context.addPath(CGPath(roundedRect: rect.insetBy(dx: -8, dy: -8), cornerWidth: radius + 8, cornerHeight: radius + 8, transform: nil))
- context.fillPath()
- context.restoreGState()
-
- context.saveGState()
- context.addPath(CGPath(roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil))
- context.clip()
- context.translateBy(x: rect.minX, y: rect.minY + rect.height)
- context.scaleBy(x: 1, y: -1)
- context.draw(image, in: CGRect(x: 0, y: 0, width: rect.width, height: rect.height))
- context.restoreGState()
-
- context.saveGState()
- context.setStrokeColor(CGColor(gray: 1, alpha: 0.18))
- context.setLineWidth(max(2, targetWidth * 0.0024))
- context.addPath(CGPath(roundedRect: rect.insetBy(dx: 1, dy: 1), cornerWidth: radius - 1, cornerHeight: radius - 1, transform: nil))
- context.strokePath()
- context.restoreGState()
- }
-
- func writePNG(to url: URL) throws {
- let temporaryURL = url.deletingLastPathComponent().appending(
- path: ".\(url.lastPathComponent).unflipped.png"
- )
- guard let image = context.makeImage(),
- let destination = CGImageDestinationCreateWithURL(
- temporaryURL as CFURL,
- UTType.png.identifier as CFString,
- 1,
- nil
- ) else {
- throw NSError(domain: "TextreamScreenshotComposer", code: 1)
- }
- CGImageDestinationAddImage(
- destination,
- image,
- [kCGImagePropertyPNGDictionary: [:]] as CFDictionary
- )
- guard CGImageDestinationFinalize(destination) else {
- throw NSError(domain: "TextreamScreenshotComposer", code: 2)
- }
-
- // Core Graphics bitmap rows are bottom-up. Materialize the final PNG
- // with top-down rows so App Store Connect and image viewers display it
- // without orientation metadata or an alpha channel.
- let process = Process()
- process.executableURL = URL(fileURLWithPath: "/usr/bin/sips")
- process.arguments = ["--flip", "vertical", temporaryURL.path, "--out", url.path]
- process.standardOutput = FileHandle.nullDevice
- try process.run()
- process.waitUntilExit()
- try? FileManager.default.removeItem(at: temporaryURL)
- guard process.terminationStatus == 0 else {
- throw NSError(domain: "TextreamScreenshotComposer", code: 4)
- }
- }
-}
-
-private func loadImage(_ url: URL) throws -> CGImage {
- guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
- let image = CGImageSourceCreateImageAtIndex(source, 0, nil) else {
- throw NSError(
- domain: "TextreamScreenshotComposer",
- code: 3,
- userInfo: [NSLocalizedDescriptionKey: "Unable to read \(url.path)"]
- )
- }
- return image
-}
-
-private func compose(device: DeviceSpec, root: URL) throws -> [URL] {
- let outputDirectory = root.appending(path: device.outputDirectory, directoryHint: .isDirectory)
- try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
-
- // Keep each upload directory in lockstep with this manifest while leaving
- // the original simulator captures untouched.
- let generatedNames = [
- "01-word-tracking.png",
- "02-guidance-modes.png",
- "03-recordings.png",
- "03-accessibility.png",
- "04-accessibility.png"
- ]
- for name in generatedNames {
- let generatedURL = outputDirectory.appending(path: name)
- if FileManager.default.fileExists(atPath: generatedURL.path) {
- try FileManager.default.removeItem(at: generatedURL)
- }
- }
-
- return try screenshots.enumerated().map { index, spec in
- let inputURL = root.appending(path: device.rawDirectory).appending(path: spec.input)
- let outputURL = outputDirectory.appending(path: spec.output)
- let rawImage = try loadImage(inputURL)
- let canvas = Canvas(width: device.width, height: device.height)
- canvas.drawBackground(accentIndex: index)
- canvas.drawText(
- "\(device.eyebrowDeviceName) • \(spec.eyebrow)",
- top: device.eyebrowTop,
- height: device.eyebrowSize * 1.6,
- size: device.eyebrowSize,
- weight: .bold,
- color: NSColor(srgbRed: 0.70, green: 0.71, blue: 0.76, alpha: 1),
- lineHeight: device.eyebrowSize * 1.25,
- tracking: device.eyebrowSize * 0.045
- )
- canvas.drawText(
- spec.title,
- top: device.titleTop,
- height: device.titleLineHeight * 2.35,
- size: device.titleSize,
- weight: .heavy,
- color: .white,
- lineHeight: device.titleLineHeight
- )
- canvas.drawScreenshot(rawImage, width: device.screenshotWidth, top: device.screenshotTop)
- try canvas.writePNG(to: outputURL)
- print("Wrote \(outputURL.path)")
- return outputURL
- }
-}
-
-private func makeContactSheet(groups: [[URL]], root: URL) throws -> URL {
- let width = 1800
- let gutter: CGFloat = 40
- let columnCount = CGFloat(screenshots.count)
- let tileWidth = (CGFloat(width) - gutter * (columnCount + 1)) / columnCount
- let tileHeight: CGFloat = 760
- let labelHeight: CGFloat = 70
- let height = Int(gutter * 3 + (tileHeight + labelHeight) * 2)
- let canvas = Canvas(width: width, height: height)
- canvas.context.setFillColor(CGColor(srgbRed: 0.025, green: 0.026, blue: 0.038, alpha: 1))
- canvas.context.fill(CGRect(x: 0, y: 0, width: width, height: height))
-
- for (row, urls) in groups.enumerated() {
- for (column, url) in urls.enumerated() {
- let image = try loadImage(url)
- let x = gutter + CGFloat(column) * (tileWidth + gutter)
- let y = gutter + CGFloat(row) * (tileHeight + labelHeight + gutter)
- let fittedHeight = min(tileHeight, tileWidth * CGFloat(image.height) / CGFloat(image.width))
- let drawWidth = fittedHeight * CGFloat(image.width) / CGFloat(image.height)
- let rect = canvas.topRect(
- x: x + (tileWidth - drawWidth) / 2,
- y: y,
- width: drawWidth,
- height: fittedHeight
- )
- canvas.context.saveGState()
- canvas.context.translateBy(x: rect.minX, y: rect.minY + rect.height)
- canvas.context.scaleBy(x: 1, y: -1)
- canvas.context.draw(image, in: CGRect(x: 0, y: 0, width: rect.width, height: rect.height))
- canvas.context.restoreGState()
- }
- canvas.drawText(
- row == 0 ? "IPHONE 6.5-INCH UPLOAD SET" : "IPAD 13-INCH UPLOAD SET",
- top: gutter + CGFloat(row) * (tileHeight + labelHeight + gutter) + tileHeight,
- height: labelHeight,
- size: 27,
- weight: .bold,
- color: NSColor(srgbRed: 0.70, green: 0.71, blue: 0.76, alpha: 1),
- lineHeight: 34,
- tracking: 1.2
- )
- }
-
- let output = root.appending(path: "contact-sheet.png")
- try canvas.writePNG(to: output)
- print("Wrote \(output.path)")
- return output
-}
-
-let scriptURL = URL(fileURLWithPath: #filePath)
-let root = scriptURL.deletingLastPathComponent()
-
-do {
- let groups = try devices.map { try compose(device: $0, root: root) }
- _ = try makeContactSheet(groups: groups, root: root)
-} catch {
- FileHandle.standardError.write(Data("error: \(error.localizedDescription)\n".utf8))
- exit(1)
-}
diff --git a/app-store/screenshots/ios/contact-sheet.png b/app-store/screenshots/ios/contact-sheet.png
deleted file mode 100644
index 0e9caaa..0000000
Binary files a/app-store/screenshots/ios/contact-sheet.png and /dev/null differ
diff --git a/app-store/screenshots/ios/raw/ipad-13/01-prompt.png b/app-store/screenshots/ios/raw/ipad-13/01-prompt.png
deleted file mode 100644
index 555e0a3..0000000
Binary files a/app-store/screenshots/ios/raw/ipad-13/01-prompt.png and /dev/null differ
diff --git a/app-store/screenshots/ios/raw/ipad-13/03-guidance.png b/app-store/screenshots/ios/raw/ipad-13/03-guidance.png
deleted file mode 100644
index 71c1a67..0000000
Binary files a/app-store/screenshots/ios/raw/ipad-13/03-guidance.png and /dev/null differ
diff --git a/app-store/screenshots/ios/raw/ipad-13/04-recordings.png b/app-store/screenshots/ios/raw/ipad-13/04-recordings.png
deleted file mode 100644
index 72f68be..0000000
Binary files a/app-store/screenshots/ios/raw/ipad-13/04-recordings.png and /dev/null differ
diff --git a/app-store/screenshots/ios/raw/ipad-13/05-settings.png b/app-store/screenshots/ios/raw/ipad-13/05-settings.png
deleted file mode 100644
index 361db4d..0000000
Binary files a/app-store/screenshots/ios/raw/ipad-13/05-settings.png and /dev/null differ
diff --git a/app-store/screenshots/ios/raw/iphone-6.9/01-prompt.png b/app-store/screenshots/ios/raw/iphone-6.9/01-prompt.png
deleted file mode 100644
index 584fb9d..0000000
Binary files a/app-store/screenshots/ios/raw/iphone-6.9/01-prompt.png and /dev/null differ
diff --git a/app-store/screenshots/ios/raw/iphone-6.9/03-guidance.png b/app-store/screenshots/ios/raw/iphone-6.9/03-guidance.png
deleted file mode 100644
index 6a254ed..0000000
Binary files a/app-store/screenshots/ios/raw/iphone-6.9/03-guidance.png and /dev/null differ
diff --git a/app-store/screenshots/ios/raw/iphone-6.9/04-recordings.png b/app-store/screenshots/ios/raw/iphone-6.9/04-recordings.png
deleted file mode 100644
index 72f781c..0000000
Binary files a/app-store/screenshots/ios/raw/iphone-6.9/04-recordings.png and /dev/null differ
diff --git a/app-store/screenshots/ios/raw/iphone-6.9/05-settings.png b/app-store/screenshots/ios/raw/iphone-6.9/05-settings.png
deleted file mode 100644
index dfb1bd0..0000000
Binary files a/app-store/screenshots/ios/raw/iphone-6.9/05-settings.png and /dev/null differ
diff --git a/app-store/screenshots/ios/upload/ipad-13/01-word-tracking.png b/app-store/screenshots/ios/upload/ipad-13/01-word-tracking.png
deleted file mode 100644
index ee57f00..0000000
Binary files a/app-store/screenshots/ios/upload/ipad-13/01-word-tracking.png and /dev/null differ
diff --git a/app-store/screenshots/ios/upload/ipad-13/02-guidance-modes.png b/app-store/screenshots/ios/upload/ipad-13/02-guidance-modes.png
deleted file mode 100644
index 94181b0..0000000
Binary files a/app-store/screenshots/ios/upload/ipad-13/02-guidance-modes.png and /dev/null differ
diff --git a/app-store/screenshots/ios/upload/ipad-13/03-accessibility.png b/app-store/screenshots/ios/upload/ipad-13/03-accessibility.png
deleted file mode 100644
index 9c66174..0000000
Binary files a/app-store/screenshots/ios/upload/ipad-13/03-accessibility.png and /dev/null differ
diff --git a/app-store/screenshots/ios/upload/iphone-6.5/01-word-tracking.png b/app-store/screenshots/ios/upload/iphone-6.5/01-word-tracking.png
deleted file mode 100644
index b0d5c63..0000000
Binary files a/app-store/screenshots/ios/upload/iphone-6.5/01-word-tracking.png and /dev/null differ
diff --git a/app-store/screenshots/ios/upload/iphone-6.5/02-guidance-modes.png b/app-store/screenshots/ios/upload/iphone-6.5/02-guidance-modes.png
deleted file mode 100644
index e4b6934..0000000
Binary files a/app-store/screenshots/ios/upload/iphone-6.5/02-guidance-modes.png and /dev/null differ
diff --git a/app-store/screenshots/ios/upload/iphone-6.5/03-accessibility.png b/app-store/screenshots/ios/upload/iphone-6.5/03-accessibility.png
deleted file mode 100644
index 1b54c52..0000000
Binary files a/app-store/screenshots/ios/upload/iphone-6.5/03-accessibility.png and /dev/null differ