Commit ca2495e3 authored by Kourser's avatar Kourser
Browse files

Full App Store screenshot set via a DEBUG demo harness



- Add a #if DEBUG screenshot harness: launch args -uiSeed (seed a demo
  subscription with episodes, a queue and a resume point) and -uiScreen
  <library|detail|player|queue> (open that screen at launch). Compiled out of
  Release builds.
- Generate the full screenshot set at exact App Store sizes for iPhone 6.9"
  (1320×2868) and iPad 13" (2064×2752): library, podcast detail, full-screen
  player (with resume slider) and queue.
- Update the submission checklist.

Release build verified (harness excluded).

Co-Authored-By: default avatarClaude <claude@anthropic.com>
parent e9a8bc59
Loading
Loading
Loading
Loading
+66 −0
Original line number Diff line number Diff line
@@ -87,10 +87,76 @@ final class AppModel {

    func start() async {
        await load()
        #if DEBUG
        if AppModel.isUITestSeed && podcasts.isEmpty {
            await seedDemoData()
            await load()
        }
        #endif
        await loadQueue()
        await downloads.loadPersisted()
    }

    /// Launch argument `-uiScreen <name>` used by the screenshot harness to open
    /// a specific screen. Always `nil` in Release.
    static var uiDemoScreen: String? {
        #if DEBUG
        let args = ProcessInfo.processInfo.arguments
        if let i = args.firstIndex(of: "-uiScreen"), i + 1 < args.count { return args[i + 1] }
        #endif
        return nil
    }

    #if DEBUG
    static var isUITestSeed: Bool { ProcessInfo.processInfo.arguments.contains("-uiSeed") }

    /// Seeds a demo subscription with episodes, a queue and a resume point, for
    /// generating App Store screenshots. Never compiled into Release.
    private func seedDemoData() async {
        let artwork = URL(string: "https://is1-ssl.mzstatic.com/image/thumb/Podcasts211/v4/47/93/f2/4793f27b-a446-bddc-8ecd-412897249e20/mza_4377126082050666835.jpg/600x600bb.jpg")
        let podcast = Podcast(
            feedURL: URL(string: "https://feeds.audiomeans.fr/feed/64ee3763-1a46-44c2-8640-3a69405a3ad8.xml")!,
            title: "Le Collimateur",
            author: "Alexandre Jubelin / Binge Audio",
            summary: "Le podcast consacré aux questions militaires et stratégiques : entretiens, récits d'opérations et analyses de l'actualité de la défense.",
            imageURL: artwork,
            language: "fr"
        )
        let titles = [
            "Algérie-Maroc, le conflit sans fin",
            "Soldate de l'image. Germaine Kanova",
            "Campagne ukrainienne et SCAF de fin",
            "Chasseurs de Shahed",
            "2 Gaulle 2 Furious",
            "Renseignement : la montée en puissance allemande",
            "Un bilan de la guerre contre l'Iran",
            "Dans le viseur : opérations spéciales",
        ]
        let base = 1_750_000_000.0
        let episodes = titles.enumerated().map { i, title in
            Episode(
                guid: "demo-\(i)",
                title: title,
                summary: "Épisode de démonstration.",
                publicationDate: Date(timeIntervalSince1970: base - Double(i) * 172_800),
                enclosureURL: URL(string: "https://feeds.audiomeans.fr/demo/\(i).mp3"),
                duration: Double(3600 + i * 420),
                imageURL: artwork
            )
        }
        guard let stored = try? await store.save(podcast: podcast, episodes: episodes) else { return }
        let saved = (try? await store.episodes(forPodcastID: stored.id)) ?? []
        for index in [1, 2, 3] where saved.count > index {
            await enqueue(saved[index])
        }
        if let first = saved.first {
            try? await store.setPlayState(
                episodeID: first.id, position: (first.duration ?? 0) * 0.32, isPlayed: false
            )
        }
    }
    #endif

    // MARK: Library

    func load() async {
+35 −0
Original line number Diff line number Diff line
import SwiftUI
import UniformTypeIdentifiers
import PodcastModel
import PlaybackKit

/// Adaptive root: a sidebar (queue + subscriptions) with a detail pane on
/// iPad/macOS, automatically collapsing to a navigation stack on iPhone.
@@ -12,6 +13,11 @@ struct RootView: View {
    @State private var showingOPMLImporter = false
    @State private var importMessage: String?

    #if DEBUG
    @Environment(PlaybackController.self) private var demoPlayback
    @State private var showDemoPlayer = false
    #endif

    private static let opmlTypes: [UTType] = [UTType(filenameExtension: "opml") ?? .xml, .xml]

    enum Destination: Hashable {
@@ -78,6 +84,35 @@ struct RootView: View {
        } message: {
            Text(importMessage ?? "")
        }
        .task { await applyDemoScreen() }
        #if DEBUG
        .sheet(isPresented: $showDemoPlayer) {
            PlayerView().environment(demoPlayback)
        }
        #endif
    }

    /// Screenshot harness: opens the screen named by `-uiScreen` once data is
    /// loaded. No-op in Release.
    private func applyDemoScreen() async {
        #if DEBUG
        guard let screen = AppModel.uiDemoScreen else { return }
        for _ in 0..<60 where model.podcasts.isEmpty {
            try? await Task.sleep(for: .milliseconds(100))
        }
        switch screen {
        case "queue": selection = .queue
        case "sync": selection = .sync
        case "detail", "player": selection = model.podcasts.first.map { Destination.podcast($0.id) }
        default: selection = nil
        }
        if screen == "player", let first = model.podcasts.first {
            let episodes = await model.episodes(for: first)
            if let episode = episodes.first { await model.play(episode) }
            try? await Task.sleep(for: .milliseconds(500))
            showDemoPlayer = true
        }
        #endif
    }

    private var importAlertBinding: Binding<Bool> {
+353 KiB
Loading image diff...
+941 KiB
Loading image diff...
+266 KiB
Loading image diff...
Loading