Commit 8a706b79 authored by Kourser's avatar Kourser
Browse files

feat(app-store): macOS metadata + screenshot capture tooling



- fastlane/metadata_macos: App Store metadata for the 22 locales (macOS 0.5.0)
- DemoCapture (DEBUG, macOS): -uiCapture snapshots the window via cacheDisplay,
  no Screen Recording permission needed; hooked after applyDemoScreen in RootView
- fastlane/generate_shots_macos.sh: drives the 22-language capture (seed once,
  then capture per language via `defaults write AppleLanguages`); must run in an
  interactive GUI session

Co-Authored-By: default avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 4d5867c6
Loading
Loading
Loading
Loading
+42 −0
Original line number Diff line number Diff line
#if os(macOS) && DEBUG
import AppKit

/// Screenshot harness (macOS, DEBUG only): when launched with
/// `-uiCapture <path>`, the app snapshots its own window into a PNG and exits —
/// using `cacheDisplay`, which does NOT require Screen Recording permission
/// (unlike `screencapture`/`CGWindowListCreateImage`). Paired with `-uiScreen`
/// for App Store screenshot generation. Seed the demo data with `-uiSeed` on a
/// FIRST launch (it hangs the capture flow if combined), then capture on
/// subsequent launches without `-uiSeed`. Never in Release.
enum DemoCapture {
    /// Value of the `-uiCapture <path>` launch argument, or nil.
    static var path: String? {
        let args = ProcessInfo.processInfo.arguments
        guard let i = args.firstIndex(of: "-uiCapture"), i + 1 < args.count else { return nil }
        return args[i + 1]
    }

    /// Sizes the main window, renders it to a PNG at `path`, then quits.
    @MainActor
    static func run(to path: String) async {
        // Let the demo screen and its data finish rendering.
        try? await Task.sleep(for: .milliseconds(3500))
        guard let window = NSApp.windows.first(where: { $0.isVisible && $0.contentView != nil }) else {
            exit(2)
        }
        window.setContentSize(NSSize(width: 1280, height: 800))
        window.center()
        try? await Task.sleep(for: .milliseconds(1500))

        // Capture the whole window (title bar + toolbar + content).
        let view = window.contentView?.superview ?? window.contentView
        guard let view, let rep = view.bitmapImageRepForCachingDisplay(in: view.bounds) else {
            exit(3)
        }
        view.cacheDisplay(in: view.bounds, to: rep)
        guard let data = rep.representation(using: .png, properties: [:]) else { exit(4) }
        try? data.write(to: URL(fileURLWithPath: path))
        exit(0)
    }
}
#endif
+6 −1
Original line number Diff line number Diff line
@@ -162,7 +162,12 @@ struct RootView: View {
        } message: { update in
            Text("La version \(update.version) est disponible sur l'App Store.")
        }
        .task { await applyDemoScreen() }
        .task {
            await applyDemoScreen()
            #if os(macOS) && DEBUG
            if let capturePath = DemoCapture.path { await DemoCapture.run(to: capturePath) }
            #endif
        }
        .onChange(of: scenePhase) { _, phase in
            if phase == .background {
                Task { await model.persistPlaybackPosition() }
+18 −0
Original line number Diff line number Diff line
@@ -125,6 +125,24 @@ App Transport Security allows HTTP (NSAllowsArbitraryLoads) because podcast RSS
The app collects no personal data (see the privacy manifest).
```

## macOS (0.5.0)

- **Métadonnées** : `fastlane/metadata_macos/` — 22 langues, identiques à l'iOS
  pour l'instant (adaptables : mentionner menus, raccourcis clavier, Touch Bar).
  Upload : `fastlane deliver --platform osx --metadata_path fastlane/metadata_macos --screenshots_path fastlane/screenshots_macos`.
- **Captures** : `fastlane/screenshots_macos/` — à générer avec
  `./fastlane/generate_shots_macos.sh`. Le mode DEBUG `-uiCapture` (fichier
  `Skingomz/DemoCapture.swift`) photographie la fenêtre via `cacheDisplay`, **sans
  permission Enregistrement de l'écran**. ⚠️ **À lancer dans une session graphique
  interactive** : la génération headless échoue (l'app ne rend pas sa fenêtre hors
  session Aqua interactive). Vérifie la première capture avant de laisser tourner.

> ⚠️ Rappel distribution : l'app Mac « native » est distribuée en **Developer ID**
> (téléchargement GitLab), pas sur le Mac App Store. La présence Mac sur l'App
> Store est l'app iOS sur Apple Silicon (même fiche), qui réutilise déjà les
> métadonnées iOS. Un vrai canal Mac App Store natif nécessiterait un build MAS
> sandboxé + l'ajout d'une plateforme macOS dans App Store Connect.

## Rappels

- **Ne pas oublier** : mettre à jour `docs/appcast.json` en 0.5.0 **seulement
+72 −0
Original line number Diff line number Diff line
#!/usr/bin/env bash
#
# Captures App Store macOS localisées (22 langues) via le mode DEBUG -uiCapture,
# qui photographie la fenêtre avec cacheDisplay — SANS permission « Enregistrement
# de l'écran ».
#
#   ./fastlane/generate_shots_macos.sh
#
# ⚠️ À LANCER DANS TA SESSION GRAPHIQUE INTERACTIVE (pas via SSH / agent headless) :
# l'app doit pouvoir créer sa fenêtre et rendre son contenu. Vérifie la première
# capture produite avant de laisser tourner les 22 langues.
#
# Recette (apprise à la dure) :
#   - le seed (-uiSeed) BLOQUE le flux de capture s'il est combiné → on sème UNE
#     fois d'abord, puis on capture sans -uiSeed (les données persistent) ;
#   - l'argument -AppleLanguages n'est pas honoré par l'app macOS → on force la
#     langue via `defaults write <bundle> AppleLanguages` avant chaque lancement.
set -uo pipefail
cd "$(dirname "$0")/.."
export DEVELOPER_DIR="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}"

BUNDLE=eu.cythin.skingomz
OUT="fastlane/screenshots_macos"
DD="build/dd-macos-shots"
HERE="build/appstore-screenshots"     # compose.swift
RAW="$(mktemp -d)/raw.png"

LOCALES=(
  "fr-FR:fr" "en-US:en" "de-DE:de" "es-ES:es" "it:it" "pt-PT:pt" "pt-BR:pt-BR"
  "nl-NL:nl" "pl:pl" "sv:sv" "da:da" "fi:fi" "el:el" "hu:hu" "ro:ro" "sk:sk"
  "cs:cs" "hr:hr" "zh-Hans:zh-Hans" "ja:ja" "ko:ko" "ru:ru"
)
SCREENS=( "01-library:" "02-inbox:inbox" "03-detail:detail" "04-queue:queue" )

echo "▶︎ Build Debug macOS…"
APP=$(find "$DD/Build/Products" -name 'Skingomz.app' -type d 2>/dev/null | head -1)
if [ -z "$APP" ]; then
  xcodebuild -project Skingomz.xcodeproj -scheme Skingomz -configuration Debug \
    -destination 'platform=macOS' -derivedDataPath "$DD" CODE_SIGNING_ALLOWED=NO build >/dev/null
  APP=$(find "$DD/Build/Products" -name 'Skingomz.app' -type d | head -1)
fi
[ -n "$APP" ] || { echo "✗ Skingomz.app introuvable"; exit 1; }

quit() { pkill -f "Skingomz.app/Contents/MacOS/Skingomz" 2>/dev/null || true; sleep 1; }

echo "▶︎ Seed unique (données démo persistées)…"
quit
open "$APP" --args -uiSeed
sleep 12
quit

for entry in "${LOCALES[@]}"; do
  IFS=":" read -r asc code <<< "$entry"
  defaults write "$BUNDLE" AppleLanguages -array "$code"
  mkdir -p "$OUT/$asc"
  for sc in "${SCREENS[@]}"; do
    IFS=":" read -r fname arg <<< "$sc"
    quit; rm -f "$RAW"
    if [ -z "$arg" ]; then open "$APP" --args -uiCapture "$RAW"
    else open "$APP" --args -uiScreen "$arg" -uiCapture "$RAW"; fi
    i=0; until [ -s "$RAW" ] || [ "$i" -ge 20 ]; do sleep 1; i=$((i+1)); done
    if [ ! -s "$RAW" ]; then echo "  ✗ $asc/$fname"; continue; fi
    W=$(sips -g pixelWidth "$RAW" | awk '/pixelWidth/{print $2}')
    CW=2880; CH=1800; [ "$W" -le 1440 ] && { CW=1440; CH=900; }
    swift "$HERE/compose.swift" "$RAW" "$OUT/$asc/mac-${fname}.png" "$CW" "$CH" >/dev/null \
      && echo "  ✓ $asc/mac-${fname}.png" || echo "  ✗ compose $asc/$fname"
  done
  echo "[$asc] ✓"
done
quit
defaults delete "$BUNDLE" AppleLanguages 2>/dev/null || true
echo "✓ Terminé -> $OUT"
+1 −0
Original line number Diff line number Diff line
© 2026 Cythin
 No newline at end of file
Loading