diff --git a/.gitignore b/.gitignore index 47dee57..9370b08 100644 --- a/.gitignore +++ b/.gitignore @@ -350,3 +350,15 @@ hs_err_pid* *.idea* **/.DS_store + +# Fastlane (Play-Store-Upload) +android/fastlane/report.xml +android/fastlane/Preview.html +android/fastlane/play-service-account.json +android/fastlane/README.md.bak +# lokale Screenshots außerhalb der Fastlane-Metadaten +/screenshots/ +# Fastlane-Bild-PNGs sind generierte Kopien; kanonisch/versioniert liegen die +# Screenshots unter materials/screenshots/. Nur die leeren Ordner (.gitkeep) +# bleiben eingecheckt, damit supply die Struktur vorfindet. +android/fastlane/metadata/android/**/images/**/*.png diff --git a/android/fastlane/Appfile b/android/fastlane/Appfile new file mode 100644 index 0000000..3868131 --- /dev/null +++ b/android/fastlane/Appfile @@ -0,0 +1,6 @@ +package_name("eu.mhsl.marianum.mobile.client") + +# Service-Account-JSON für die Google Play Developer API. Pfad über die +# Umgebungsvariable SUPPLY_JSON_KEY setzen (die Datei selbst wird per +# .gitignore nicht eingecheckt). +json_key_file(ENV["SUPPLY_JSON_KEY"] || "fastlane/play-service-account.json") diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile new file mode 100644 index 0000000..399214e --- /dev/null +++ b/android/fastlane/Fastfile @@ -0,0 +1,29 @@ +default_platform(:android) + +# Play-Store-Auslieferung über fastlane supply. Die Screenshots erzeugt +# ../tool/screenshots.sh (Demo-Login + integration_test) direkt in +# metadata/android//images/. Texte/Changelogs liegen daneben in +# metadata/android//. +platform :android do + desc "Nur Screenshots hochladen (verändert keine Texte/Binaries)" + lane :upload_screenshots do + upload_to_play_store( + skip_upload_apk: true, + skip_upload_aab: true, + skip_upload_metadata: true, + skip_upload_changelogs: true, + skip_upload_images: false, + skip_upload_screenshots: false, + metadata_path: "./fastlane/metadata/android", + ) + end + + desc "Store-Texte, Changelogs und Screenshots hochladen (keine Binaries)" + lane :upload_metadata do + upload_to_play_store( + skip_upload_apk: true, + skip_upload_aab: true, + metadata_path: "./fastlane/metadata/android", + ) + end +end diff --git a/android/fastlane/Pluginfile b/android/fastlane/Pluginfile new file mode 100644 index 0000000..27bb66f --- /dev/null +++ b/android/fastlane/Pluginfile @@ -0,0 +1,2 @@ +# fastlane-Plugins (aktuell keine). Datei vorhanden, damit `fastlane` das +# Plugin-Handling initialisiert. diff --git a/android/fastlane/README.md b/android/fastlane/README.md new file mode 100644 index 0000000..669086c --- /dev/null +++ b/android/fastlane/README.md @@ -0,0 +1,35 @@ +# Fastlane – Play-Store-Screenshots & Upload + +Schritt 2 der Play-Store-Automatisierung. Die App muss dafür **nicht** neu +gebaut werden – der Demo-Modus (`demo@`-Login) liefert die kompletten Inhalte +aus Fixtures. + +## 1. Screenshots aufnehmen + +Ein Gerät/Emulator (≥ Android 8) per `adb devices` sichtbar, dann aus dem +**Client**-Wurzelverzeichnis: + +```bash +tool/screenshots.sh +``` + +Fährt Phone (1080×1920), 7″ (1200×1920) und 10″ (1600×2560) nacheinander durch +(setzt die Displaygröße per `adb shell wm size/density`) und legt die PNGs in +`metadata/android/de-DE/images/{phone,sevenInch,tenInch}Screenshots/` ab. + +- Nur eine Größe: `PROFILES="phone" tool/screenshots.sh` +- Profile-Build (flüssigere Frames): `BUILD_MODE=profile tool/screenshots.sh` + +## 2. Hochladen + +Service-Account-JSON der Google Play Developer API besorgen und referenzieren: + +```bash +export SUPPLY_JSON_KEY=/pfad/zu/play-service-account.json +cd android +fastlane upload_screenshots # nur Bilder +# oder +fastlane upload_metadata # Bilder + Texte/Changelogs, keine Binaries +``` + +Der Service-Account-Key wird **nicht** eingecheckt (siehe `.gitignore`). diff --git a/android/fastlane/metadata/android/de-DE/images/phoneScreenshots/.gitkeep b/android/fastlane/metadata/android/de-DE/images/phoneScreenshots/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/android/fastlane/metadata/android/de-DE/images/sevenInchScreenshots/.gitkeep b/android/fastlane/metadata/android/de-DE/images/sevenInchScreenshots/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/android/fastlane/metadata/android/de-DE/images/tenInchScreenshots/.gitkeep b/android/fastlane/metadata/android/de-DE/images/tenInchScreenshots/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/integration_test/screenshot_test.dart b/integration_test/screenshot_test.dart new file mode 100644 index 0000000..d467b96 --- /dev/null +++ b/integration_test/screenshot_test.dart @@ -0,0 +1,215 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:marianum_mobile/app.dart'; +import 'package:marianum_mobile/main.dart' as app; +import 'package:marianum_mobile/routing/app_routes.dart'; +import 'package:marianum_mobile/state/app/modules/app_modules.dart'; +import 'package:marianum_mobile/state/app/modules/settings/bloc/settings_cubit.dart'; +import 'package:marianum_mobile/view/pages/talk/widgets/chat_tile.dart'; + +/// Marketing-Screenshot-Lauf für die Stores. Loggt sich über den Demo-Login +/// (`demo@…`, siehe DemoMode) ein und nimmt einen kuratierten Satz Screens auf: +/// Stundenplan (hell + dunkel), Talk-Liste, Talk-Chat, Dateien, „Mehr"-Bereich +/// und Einstellungen. Ausgeführt über `flutter drive` mit +/// test_driver/integration_test.dart, orchestriert von tool/screenshots.sh +/// (Phone / 7″ / 10″). +/// +/// Kein Netzwerk: der Demo-Modus beantwortet allen Backend-Verkehr aus +/// Fixtures, daher ist der Lauf deterministisch. +Future main() async { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Store-Screenshots über den Demo-Login', (tester) async { + // Der integration_test-Harness installiert eigene Error-Handler. app() + // überschreibt sie in main() durch die produktiven (ClientErrorReporter), + // was die End-of-Test-Buchführung des Harness sprengt (_pendingExceptionDetails). + // Daher vorher sichern und nach main() zurücksetzen. + final harnessFlutterOnError = FlutterError.onError; + final harnessDispatcherOnError = PlatformDispatcher.instance.onError; + + _log('main() starten'); + await app.main(); + + FlutterError.onError = harnessFlutterOnError; + PlatformDispatcher.instance.onError = harnessDispatcherOnError; + await tester.pump(const Duration(seconds: 1)); + + _log('Login'); + await _login(tester); + + _log('warte auf App-Shell'); + await _pumpUntil( + tester, + find.byType(App), + timeout: const Duration(seconds: 20), + ); + _log('warte auf Post-Login-Splash-Ende'); + await _pumpUntilGone( + tester, + find.byKey(const ValueKey('post-login-splash')), + timeout: const Duration(seconds: 12), + ); + await tester.pump(const Duration(seconds: 1)); + + // Screenshots auf Android brauchen eine einmalige Umwandlung der + // Flutter-Surface in eine lesbare Image-Texture. Erst hier – nach Login und + // Splash –, damit der sichtbare Screen erst kurz vor der Aufnahme „einfriert". + if (Platform.isAndroid) { + _log('convertFlutterSurfaceToImage'); + await binding.convertFlutterSurfaceToImage(); + } + + // 1 + 2: Stundenplan in beiden Themes. + await _goTo(tester, Modules.timetable); + _setTheme(tester, ThemeMode.light); + await _capture(tester, binding, '1_stundenplan_hell'); + _setTheme(tester, ThemeMode.dark); + await _capture(tester, binding, '2_stundenplan_dunkel'); + + // Restlicher Satz einheitlich im hellen Theme. + _setTheme(tester, ThemeMode.light); + + // 3: Talk-Liste. + await _goTo(tester, Modules.talk); + await _capture(tester, binding, '3_talk_liste'); + + // 4: Talk-Chat (ersten Chat der Liste öffnen). + await tester.tap(find.byType(ChatTile).first); + await _capture(tester, binding, '4_talk_chat'); + _popPushedPage(tester); + await tester.pump(const Duration(milliseconds: 500)); + + // 5: Dateien. + await _goTo(tester, Modules.files); + await _capture(tester, binding, '5_dateien'); + + // 6: „Mehr"-Bereich (letzter Tab, hinter den Modul-Tabs). + _goToMore(tester); + await _capture(tester, binding, '6_mehr'); + + // 7: Einstellungen (Vollbild-Push). + AppRoutes.openSettings(tester.element(find.byType(App))); + await _capture(tester, binding, '7_einstellungen'); + _popPushedPage(tester); + + _log('fertig'); + }); +} + +void _log(String message) => debugPrint('SHOTS: $message'); + +Future _login(WidgetTester tester) async { + final loginVisible = await _pumpUntil( + tester, + find.byKey(const Key('login-username-field')), + ); + if (!loginVisible) { + _log('kein Login-Screen sichtbar – bereits angemeldet, überspringe Login'); + return; + } + await tester.enterText( + find.byKey(const Key('login-username-field')), + 'demo@screenshots', + ); + await tester.enterText(find.byKey(const Key('login-password-field')), 'demo'); + await tester.pump(); + await tester.tap(find.byKey(const Key('login-submit-button'))); + await tester.pump(const Duration(milliseconds: 500)); +} + +/// Setzt das App-Theme über den [SettingsCubit] (MaterialApp folgt +/// `settings.appTheme`). `val(write: true)` plant den Emit als Microtask. +void _setTheme(WidgetTester tester, ThemeMode mode) { + final context = tester.element(find.byType(App)); + context.read().val(write: true).appTheme = mode; +} + +/// Navigiert zu [module] – über den Bottom-Tab, wenn er in der Leiste liegt, +/// sonst als Vollbild-Push. Größenunabhängig, da die Tab-Anzahl je Gerät variiert. +Future _goTo(WidgetTester tester, Modules module) async { + final appFinder = find.byType(App); + if (appFinder.evaluate().isEmpty) { + _log('WARN: App-Shell fehlt, kann $module nicht ansteuern'); + return; + } + final context = tester.element(appFinder); + if (!AppRoutes.goToTab(context, module)) { + final resolved = AppModule.modules(context)[module]; + if (resolved != null) AppRoutes.openModule(context, resolved); + } +} + +/// Springt auf den „Mehr"-Tab. Der liegt hinter den Modul-Tabs, sein Index ist +/// also die Anzahl der Bottom-Bar-Module. +void _goToMore(WidgetTester tester) { + final context = tester.element(find.byType(App)); + final moreIndex = AppModule.getBottomBarModules(context).length; + app.Main.bottomNavigator.jumpToTab(moreIndex); +} + +/// Schließt einen per [AppRoutes] gepushten Vollbild-Screen wieder. +void _popPushedPage(WidgetTester tester) { + final navigator = AppRoutes.rootNavigatorKey.currentState; + if (navigator != null && navigator.canPop()) navigator.pop(); +} + +/// Lässt die Oberfläche zur Ruhe kommen und legt den Screenshot ab. +Future _capture( + WidgetTester tester, + IntegrationTestWidgetsFlutterBinding binding, + String name, +) async { + // Feste Setzzeit statt „warte auf Spinner-Ende": die Demo-Fixtures kommen + // synchron (keine künstlichen Delays), aber Module wie Dateien/Chat laden erst + // beim Navigieren nach. Eine globale CircularProgressIndicator-Prüfung taugt + // nicht, weil die PersistentTabView alle Tabs am Leben hält und + // Avatar-Platzhalter dauerhaft einen Spinner zeigen. In 500ms-Schritten + // pumpen, damit Timer/Microtasks (u.a. der Theme-Emit) durchlaufen. + await _settle(tester, const Duration(seconds: 6)); + _log('-> $name: takeScreenshot'); + await binding.takeScreenshot(name); +} + +/// Pumpt [duration] in 500ms-Schritten ab, damit die App async Fixtures laden +/// und ihre Frames rendern kann (Ersatz für pumpAndSettle, das an den +/// Dauer-Animationen hängen bleibt). +Future _settle(WidgetTester tester, Duration duration) async { + final steps = duration.inMilliseconds ~/ 500; + for (var i = 0; i < steps; i++) { + await tester.pump(const Duration(milliseconds: 500)); + } +} + +/// Pumpt in kurzen Schritten, bis [finder] erscheint oder [timeout] abläuft. +/// Gibt zurück, ob [finder] gefunden wurde. +Future _pumpUntil( + WidgetTester tester, + Finder finder, { + Duration timeout = const Duration(seconds: 10), +}) async { + final end = DateTime.now().add(timeout); + while (DateTime.now().isBefore(end)) { + await tester.pump(const Duration(milliseconds: 200)); + if (finder.evaluate().isNotEmpty) return true; + } + return false; +} + +/// Gegenstück zu [_pumpUntil]: pumpt, bis [finder] nichts mehr trifft. +Future _pumpUntilGone( + WidgetTester tester, + Finder finder, { + Duration timeout = const Duration(seconds: 10), +}) async { + final end = DateTime.now().add(timeout); + while (DateTime.now().isBefore(end)) { + await tester.pump(const Duration(milliseconds: 200)); + if (finder.evaluate().isEmpty) return true; + } + return false; +} diff --git a/lib/view/login/widgets/login_card.dart b/lib/view/login/widgets/login_card.dart index 2ca2990..3f48dcc 100644 --- a/lib/view/login/widgets/login_card.dart +++ b/lib/view/login/widgets/login_card.dart @@ -111,6 +111,7 @@ class _LoginCardState extends State { ), const SizedBox(height: 20), TextFormField( + key: const Key('login-username-field'), controller: _usernameController, enabled: !loading, validator: _required, @@ -125,6 +126,7 @@ class _LoginCardState extends State { ), const SizedBox(height: 12), TextFormField( + key: const Key('login-password-field'), controller: _passwordController, focusNode: _passwordFocus, enabled: !loading, @@ -146,6 +148,7 @@ class _LoginCardState extends State { SizedBox( height: 50, child: FilledButton( + key: const Key('login-submit-button'), onPressed: loading ? null : _submit, style: FilledButton.styleFrom( shape: RoundedRectangleBorder( diff --git a/materials/app-banner.png b/materials/app-banner.png index 9c6f6c6..1b995d5 100644 Binary files a/materials/app-banner.png and b/materials/app-banner.png differ diff --git a/materials/app-symbol.png b/materials/app-symbol.png index 6b4a25e..9a1524e 100644 Binary files a/materials/app-symbol.png and b/materials/app-symbol.png differ diff --git a/materials/screenshots/android/1.png b/materials/screenshots/android/1.png deleted file mode 100644 index 700d62a..0000000 Binary files a/materials/screenshots/android/1.png and /dev/null differ diff --git a/materials/screenshots/android/10-inch/1_stundenplan_hell.png b/materials/screenshots/android/10-inch/1_stundenplan_hell.png new file mode 100644 index 0000000..3e41b6a Binary files /dev/null and b/materials/screenshots/android/10-inch/1_stundenplan_hell.png differ diff --git a/materials/screenshots/android/10-inch/2_stundenplan_dunkel.png b/materials/screenshots/android/10-inch/2_stundenplan_dunkel.png new file mode 100644 index 0000000..fe30205 Binary files /dev/null and b/materials/screenshots/android/10-inch/2_stundenplan_dunkel.png differ diff --git a/materials/screenshots/android/10-inch/3_talk_liste.png b/materials/screenshots/android/10-inch/3_talk_liste.png new file mode 100644 index 0000000..19e56e7 Binary files /dev/null and b/materials/screenshots/android/10-inch/3_talk_liste.png differ diff --git a/materials/screenshots/android/10-inch/4_talk_chat.png b/materials/screenshots/android/10-inch/4_talk_chat.png new file mode 100644 index 0000000..26c827f Binary files /dev/null and b/materials/screenshots/android/10-inch/4_talk_chat.png differ diff --git a/materials/screenshots/android/10-inch/5_dateien.png b/materials/screenshots/android/10-inch/5_dateien.png new file mode 100644 index 0000000..7db2eed Binary files /dev/null and b/materials/screenshots/android/10-inch/5_dateien.png differ diff --git a/materials/screenshots/android/10-inch/6_mehr.png b/materials/screenshots/android/10-inch/6_mehr.png new file mode 100644 index 0000000..875762f Binary files /dev/null and b/materials/screenshots/android/10-inch/6_mehr.png differ diff --git a/materials/screenshots/android/10-inch/7_einstellungen.png b/materials/screenshots/android/10-inch/7_einstellungen.png new file mode 100644 index 0000000..2898b7e Binary files /dev/null and b/materials/screenshots/android/10-inch/7_einstellungen.png differ diff --git a/materials/screenshots/android/2.png b/materials/screenshots/android/2.png deleted file mode 100644 index b5cde71..0000000 Binary files a/materials/screenshots/android/2.png and /dev/null differ diff --git a/materials/screenshots/android/3.png b/materials/screenshots/android/3.png deleted file mode 100644 index c92fc96..0000000 Binary files a/materials/screenshots/android/3.png and /dev/null differ diff --git a/materials/screenshots/android/4.png b/materials/screenshots/android/4.png deleted file mode 100644 index d97acf2..0000000 Binary files a/materials/screenshots/android/4.png and /dev/null differ diff --git a/materials/screenshots/android/5.png b/materials/screenshots/android/5.png deleted file mode 100644 index 8b1b440..0000000 Binary files a/materials/screenshots/android/5.png and /dev/null differ diff --git a/materials/screenshots/android/6.png b/materials/screenshots/android/6.png deleted file mode 100644 index d725a97..0000000 Binary files a/materials/screenshots/android/6.png and /dev/null differ diff --git a/materials/screenshots/android/7-inch/1_stundenplan_hell.png b/materials/screenshots/android/7-inch/1_stundenplan_hell.png new file mode 100644 index 0000000..f4ccbac Binary files /dev/null and b/materials/screenshots/android/7-inch/1_stundenplan_hell.png differ diff --git a/materials/screenshots/android/7-inch/2_stundenplan_dunkel.png b/materials/screenshots/android/7-inch/2_stundenplan_dunkel.png new file mode 100644 index 0000000..a9198d1 Binary files /dev/null and b/materials/screenshots/android/7-inch/2_stundenplan_dunkel.png differ diff --git a/materials/screenshots/android/7-inch/3_talk_liste.png b/materials/screenshots/android/7-inch/3_talk_liste.png new file mode 100644 index 0000000..f9fe203 Binary files /dev/null and b/materials/screenshots/android/7-inch/3_talk_liste.png differ diff --git a/materials/screenshots/android/7-inch/4_talk_chat.png b/materials/screenshots/android/7-inch/4_talk_chat.png new file mode 100644 index 0000000..6c632d6 Binary files /dev/null and b/materials/screenshots/android/7-inch/4_talk_chat.png differ diff --git a/materials/screenshots/android/7-inch/5_dateien.png b/materials/screenshots/android/7-inch/5_dateien.png new file mode 100644 index 0000000..e11f554 Binary files /dev/null and b/materials/screenshots/android/7-inch/5_dateien.png differ diff --git a/materials/screenshots/android/7-inch/6_mehr.png b/materials/screenshots/android/7-inch/6_mehr.png new file mode 100644 index 0000000..9930eca Binary files /dev/null and b/materials/screenshots/android/7-inch/6_mehr.png differ diff --git a/materials/screenshots/android/7-inch/7_einstellungen.png b/materials/screenshots/android/7-inch/7_einstellungen.png new file mode 100644 index 0000000..76281dc Binary files /dev/null and b/materials/screenshots/android/7-inch/7_einstellungen.png differ diff --git a/materials/screenshots/android/7.png b/materials/screenshots/android/7.png deleted file mode 100644 index 629cc6a..0000000 Binary files a/materials/screenshots/android/7.png and /dev/null differ diff --git a/materials/screenshots/android/8.png b/materials/screenshots/android/8.png deleted file mode 100644 index a5aa363..0000000 Binary files a/materials/screenshots/android/8.png and /dev/null differ diff --git a/materials/screenshots/android/phone/1_stundenplan_hell.png b/materials/screenshots/android/phone/1_stundenplan_hell.png new file mode 100644 index 0000000..af90f9f Binary files /dev/null and b/materials/screenshots/android/phone/1_stundenplan_hell.png differ diff --git a/materials/screenshots/android/phone/2_stundenplan_dunkel.png b/materials/screenshots/android/phone/2_stundenplan_dunkel.png new file mode 100644 index 0000000..6efb74b Binary files /dev/null and b/materials/screenshots/android/phone/2_stundenplan_dunkel.png differ diff --git a/materials/screenshots/android/phone/3_talk_liste.png b/materials/screenshots/android/phone/3_talk_liste.png new file mode 100644 index 0000000..7b5bf7a Binary files /dev/null and b/materials/screenshots/android/phone/3_talk_liste.png differ diff --git a/materials/screenshots/android/phone/4_talk_chat.png b/materials/screenshots/android/phone/4_talk_chat.png new file mode 100644 index 0000000..c6c65a7 Binary files /dev/null and b/materials/screenshots/android/phone/4_talk_chat.png differ diff --git a/materials/screenshots/android/phone/5_dateien.png b/materials/screenshots/android/phone/5_dateien.png new file mode 100644 index 0000000..a396804 Binary files /dev/null and b/materials/screenshots/android/phone/5_dateien.png differ diff --git a/materials/screenshots/android/phone/6_mehr.png b/materials/screenshots/android/phone/6_mehr.png new file mode 100644 index 0000000..c5160a2 Binary files /dev/null and b/materials/screenshots/android/phone/6_mehr.png differ diff --git a/materials/screenshots/android/phone/7_einstellungen.png b/materials/screenshots/android/phone/7_einstellungen.png new file mode 100644 index 0000000..eb01fc6 Binary files /dev/null and b/materials/screenshots/android/phone/7_einstellungen.png differ diff --git a/pubspec.yaml b/pubspec.yaml index 276ada9..96e2543 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -98,6 +98,11 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + # Screenshot-Automatisierung für den Play Store: treibt den Demo-Login über + # flutter drive und ruft binding.takeScreenshot pro Hauptscreen auf + # (integration_test/ + test_driver/, orchestriert von tool/screenshots.sh). + integration_test: + sdk: flutter fake_async: ^1.3.1 flutter_launcher_icons: ^0.14.3 diff --git a/test_driver/integration_test.dart b/test_driver/integration_test.dart new file mode 100644 index 0000000..a03b533 --- /dev/null +++ b/test_driver/integration_test.dart @@ -0,0 +1,21 @@ +import 'dart:io'; + +import 'package:integration_test/integration_test_driver_extended.dart'; + +/// Host-seitiger Treiber für den Screenshot-Lauf. Schreibt jeden von +/// `binding.takeScreenshot(name)` gelieferten PNG-Frame nach +/// `$SCREENSHOT_OUT_DIR/.png` (Default: ./screenshots). Das Zielverzeichnis +/// setzt tool/screenshots.sh pro Gerätegröße auf das passende Fastlane-Bildverzeichnis. +Future main() async { + final outDir = Platform.environment['SCREENSHOT_OUT_DIR'] ?? 'screenshots'; + + await integrationDriver( + onScreenshot: (name, bytes, [args]) async { + final file = File('$outDir/$name.png'); + await file.create(recursive: true); + await file.writeAsBytes(bytes); + stdout.writeln('Screenshot gespeichert: ${file.path}'); + return true; + }, + ); +} diff --git a/tool/screenshots.sh b/tool/screenshots.sh new file mode 100755 index 0000000..c453888 --- /dev/null +++ b/tool/screenshots.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# +# Store-Screenshots über den Demo-Login aufnehmen. +# +# Fährt den integration_test-Screenshot-Lauf (integration_test/screenshot_test.dart) +# über flutter drive für die drei von Google Play verlangten Formfaktoren durch +# (Phone + 7″ + 10″ Tablet sind inzwischen Pflicht). Statt drei AVDs zu verwalten, +# wird EIN laufendes Gerät/Emulator pro Durchlauf per `adb shell wm size/density` +# auf die Zielauflösung gebracht; takeScreenshot liefert dann Frames in genau +# dieser Pixelgröße. +# +# Ablage: +# - kanonisch (versioniert): materials/screenshots/android// +# - Kopie für den Upload: android/fastlane/metadata/android/de-DE/images/<...>Screenshots/ +# Danach: `cd android && fastlane upload_screenshots`. +# +# Voraussetzungen: +# - genau ein per `adb devices` sichtbares Gerät/Emulator (>= Android 8) +# - flutter, adb im PATH +# +# Aufruf (vom Client-Projektwurzelverzeichnis): +# tool/screenshots.sh # debug-Build (schnell) +# BUILD_MODE=profile tool/screenshots.sh +# PROFILES="phone" tool/screenshots.sh # nur eine Größe + +set -euo pipefail + +cd "$(dirname "$0")/.." + +BUILD_MODE="${BUILD_MODE:-debug}" +MATERIALS_BASE="materials/screenshots/android" +FASTLANE_BASE="android/fastlane/metadata/android/de-DE/images" +APP_ID="eu.mhsl.marianum.mobile.client" + +# name | breite x höhe (px) | dichte (dpi) | materials-Ordner | fastlane-Ordner +# Phone nutzt die NATIVE Displayauflösung des Geräts (voller Hochformat, nicht +# auf 16:9 gestaucht). Tablets im QUERFORMAT (Breite > Höhe); bei den +# Landscape-Breiten (>1000dp) zeigt Talk zudem die Master-Detail-Splitview. +# Dichten so gewählt, dass die adaptive Bottom-Bar die passende Tab-Anzahl zeigt. +declare -A PROFILE_SIZE=( + [phone]="native" [seven]="1920x1200" [ten]="2560x1600" +) +declare -A PROFILE_DENSITY=( + [phone]="native" [seven]="240" [ten]="280" +) +declare -A PROFILE_MATERIALS=( + [phone]="phone" [seven]="7-inch" [ten]="10-inch" +) +declare -A PROFILE_FASTLANE=( + [phone]="phoneScreenshots" + [seven]="sevenInchScreenshots" + [ten]="tenInchScreenshots" +) + +PROFILES="${PROFILES:-phone seven ten}" + +# App-APK einmal vorab bauen, damit sie vor jedem `flutter drive` installiert +# und die Notification-Permission gegrantet werden kann (drive deinstalliert die +# App nach jedem Lauf, sonst wäre der Grant weg und ein Systemdialog könnte die +# Screenshots verdecken). Der eigentliche drive-Lauf reinstalliert sie per -r. +APK="build/app/outputs/flutter-apk/app-${BUILD_MODE}.apk" +echo "Baue App-APK ($BUILD_MODE) vorab…" +flutter build apk "--$BUILD_MODE" + +reset_display() { + echo "Setze Displaygröße/-dichte zurück…" + adb shell wm size reset || true + adb shell wm density reset || true +} +trap reset_display EXIT + +for profile in $PROFILES; do + size="${PROFILE_SIZE[$profile]}" + density="${PROFILE_DENSITY[$profile]}" + materials_dir="$MATERIALS_BASE/${PROFILE_MATERIALS[$profile]}" + fastlane_dir="$FASTLANE_BASE/${PROFILE_FASTLANE[$profile]}" + + echo "" + echo "=== Profil '$profile' ($size @ ${density}dpi) -> $materials_dir ===" + mkdir -p "$materials_dir" "$fastlane_dir" + rm -f "$materials_dir"/*.png "$fastlane_dir"/*.png + + # "native" -> Geräteauflösung/-dichte verwenden (für Phone), sonst erzwingen. + if [ "$size" = "native" ]; then + adb shell wm size reset + adb shell wm density reset + else + adb shell wm size "$size" + adb shell wm density "$density" + fi + + # App sicherstellen, frische (ausgeloggte) Session erzwingen und + # Notification-Permission erteilen, bevor drive läuft. pm clear setzt auch die + # Runtime-Permissions zurück, daher der Grant DANACH. + [ -f "$APK" ] && adb install -r "$APK" >/dev/null 2>&1 || true + adb shell pm clear "$APP_ID" >/dev/null 2>&1 || true + adb shell pm grant "$APP_ID" android.permission.POST_NOTIFICATIONS 2>/dev/null || true + sleep 2 + + SCREENSHOT_OUT_DIR="$materials_dir" flutter drive \ + --driver=test_driver/integration_test.dart \ + --target=integration_test/screenshot_test.dart \ + --"$BUILD_MODE" \ + --no-dds + + # Kanonische Bilder in den Fastlane-Upload-Ordner spiegeln. + cp "$materials_dir"/*.png "$fastlane_dir"/ + echo "Profil '$profile' fertig: $(ls -1 "$materials_dir"/*.png 2>/dev/null | wc -l) Screenshots" +done + +reset_display +trap - EXIT + +echo "" +echo "Kanonische Screenshots: $MATERIALS_BASE/{phone,7-inch,10-inch}/" +echo "Upload: cd android && fastlane upload_screenshots"