diff --git a/.gitignore b/.gitignore index 7d5f96c..f8353b3 100644 --- a/.gitignore +++ b/.gitignore @@ -63,4 +63,4 @@ ios/Runner/GoogleService-Info.plist coverage/ test/coverage_helper_test.dart **/failures/*.png -screenshots/generated/ \ No newline at end of file +screenshots/generated/raw/ \ No newline at end of file diff --git a/README.md b/README.md index f3a57d1..37b9fdb 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,11 @@ Without further delay behold my new Lightmeter app inspired by Material You (a.k # Screenshots

- - - - - + + + + +

# Development diff --git a/doc/google_play_resources.md b/doc/google_play_resources.md index 9b098cf..1c8ea53 100644 --- a/doc/google_play_resources.md +++ b/doc/google_play_resources.md @@ -6,26 +6,19 @@ Lightmeter ## Short description -Simple and powerful metering app inspired by Google's Material Design 3. +A simple and powerful metering app that can be used for any type of camera from film SLR to pinhole to cinematographic. ## Long description -Material Design -The user interface matches every single detail of the material design guidelines to ensure Lightmeter is an eye candy for you. +A simple and easy to use metering app that can be used for any type of camera from film SLR to pinhole to cinematographic. The app contains the following features: -Easy to Use -No complicated or overblown menus but a familiar and clean interface. +- A reflected light meter with spot metering (using the device's camera) +- An incident light meter (using the device's light sensor) +- An in-built timer for shooting long exposures +- A wide range of ISO values sutable even for solarphotograpy +- Reciprocity calculations for a variety of films -Customizability -There is an inbuilt theme engine with many different colors to choose from. - -Features -• Incident light metering (uses lightsensor) -• Reflected light metering (needs camera) -• ISO range from 3 to 6400 -• Pre-built reciprocity for some films -• Calibration & ND filters -and many more +and many more! NOTE The accuracy of the measurements depends on your decice's hardware. @@ -34,4 +27,4 @@ Email me, if you need help or detected bugs ## Graphics -[Figma](https://www.figma.com/file/X7pUAsxtjx19Rj8VtBV3Ft/Material-lightmeter?node-id=501%3A1586&t=cWNHaXm024KM4KYn-1) \ No newline at end of file +[Figma](https://www.figma.com/file/X7pUAsxtjx19Rj8VtBV3Ft/Material-lightmeter?node-id=501%3A1586&t=cWNHaXm024KM4KYn-1) diff --git a/integration_test/e2e_test.dart b/integration_test/e2e_test.dart index 419db8e..5d32074 100644 --- a/integration_test/e2e_test.dart +++ b/integration_test/e2e_test.dart @@ -49,7 +49,7 @@ void testE2E(String description) { /// Create Praktica + Zenitar profile from scratch await tester.openSettings(); await tester.tapDescendantTextOf(S.current.equipmentProfiles); - await tester.tap(find.byIcon(Icons.add).first); + await tester.tap(find.byIcon(Icons.add_outlined).first); await tester.pumpAndSettle(); await tester.setProfileName(mockEquipmentProfiles[0].name); await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[0].name); @@ -63,7 +63,7 @@ void testE2E(String description) { expect(find.text('1/1000 - B'), findsOneWidget); /// Create Praktica + Jupiter profile from Zenitar profile - await tester.tap(find.byIcon(Icons.copy).first); + await tester.tap(find.byIcon(Icons.copy_outlined).first); await tester.pumpAndSettle(); await tester.setProfileName(mockEquipmentProfiles[1].name); await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[1].name); @@ -193,7 +193,7 @@ extension on WidgetTester { bool deselectAll = true, }) async { if (deselectAll) { - await tap(find.byIcon(Icons.deselect)); + await tap(find.byIcon(Icons.deselect_outlined)); await pump(); } for (final value in valuesToSelect) { diff --git a/lib/res/dimens.dart b/lib/res/dimens.dart index 0e03b22..30d064d 100644 --- a/lib/res/dimens.dart +++ b/lib/res/dimens.dart @@ -68,4 +68,7 @@ class Dimens { paddingL, ); static const EdgeInsets dialogMargin = EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0); + + // TODO(@vodemn) constrain dialogs with this value + static const double tabletMaxWidth = 600; } diff --git a/lib/screens/timer/screen_timer.dart b/lib/screens/timer/screen_timer.dart index ca42577..065c064 100644 --- a/lib/screens/timer/screen_timer.dart +++ b/lib/screens/timer/screen_timer.dart @@ -79,15 +79,18 @@ class TimerScreenState extends State with TickerProviderStateMixin const Spacer(), Padding( padding: const EdgeInsets.all(Dimens.paddingL), - child: SizedBox.fromSize( - size: Size.square(MediaQuery.sizeOf(context).width - Dimens.paddingL * 4), - child: ValueListenableBuilder( - valueListenable: timelineAnimation, - builder: (_, value, child) => TimerTimeline( - progress: value, - child: TimerText( - timeLeft: Duration(milliseconds: (widget.duration.inMilliseconds * value).toInt()), - duration: widget.duration, + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: Dimens.tabletMaxWidth, maxWidth: Dimens.tabletMaxWidth), + child: SizedBox.fromSize( + size: Size.square(MediaQuery.sizeOf(context).width - Dimens.paddingL * 4), + child: ValueListenableBuilder( + valueListenable: timelineAnimation, + builder: (_, value, child) => TimerTimeline( + progress: value, + child: TimerText( + timeLeft: Duration(milliseconds: (widget.duration.inMilliseconds * value).toInt()), + duration: widget.duration, + ), ), ), ), diff --git a/pubspec.yaml b/pubspec.yaml index b25745f..391e525 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: vibration: 1.8.1 dev_dependencies: + args: 2.5.0 bloc_test: 9.1.3 build_runner: 2.4.6 flutter_native_splash: 2.3.5 @@ -51,9 +52,11 @@ dev_dependencies: sdk: flutter golden_toolkit: 0.15.0 google_fonts: 3.0.1 + image: 4.1.7 integration_test: sdk: flutter lint: 2.1.2 + logging: 1.2.0 meta: 1.9.1 mocktail: 0.3.0 test: 1.24.3 diff --git a/screenshots/README.md b/screenshots/README.md index ab8c20a..8c6555e 100644 --- a/screenshots/README.md +++ b/screenshots/README.md @@ -17,28 +17,35 @@ As a user I want to see the most relevant screenshots in the store, so that I ca - Settings screen 1. Just the screen - 2. Opened metering screen layout features dialog - Equipment profiles screen 1. Just the screen 2. Opened equipment profile ISO picker +- Timer screen + + 1. Just the screen + > \*also in dark mode > \*\*Android only ## Run the generator -Screenshots will be stored in the _screenshots/generated/\/_ folder. +Release screenshots will be stored in the _screenshots/generated/\/_ folder. -### Android +Raw screenshots will be stored in the _screenshots/generated/raw/\/_ folder. + +### Generate raw screenshots + +#### Android ```console -sh screenshots/generate_screenshots.sh +sh screenshots/generate_android_screenshots.sh ``` -### iOS +#### iOS Apple requires screenshots a specific list of devices, so we can implement a custom generator to cover all those devices. @@ -48,6 +55,12 @@ Can be run on Simulator. sh screenshots/generate_ios_screenshots.sh ``` +### Apply store constraints and text data + +```console +sh screenshots/scripts/convert_to_store_screenshots.sh +``` + ## List of devices ### Android @@ -56,9 +69,5 @@ sh screenshots/generate_ios_screenshots.sh ### iOS -- iPhone 8 Plus - iPhone 13 Pro -- iPhone 13 Pro Max -- iPhone 15 Pro -- iPhone 15 Pro Max - iPad Pro (12.9-inch) (6th generation) diff --git a/screenshots/assets/content/screenshot_titles_en.json b/screenshots/assets/content/screenshot_titles_en.json new file mode 100644 index 0000000..13b14bf --- /dev/null +++ b/screenshots/assets/content/screenshot_titles_en.json @@ -0,0 +1,44 @@ +{ + "screenshots": [ + { + "screenshotName": "light_metering-reflected", + "title": "Quick & easy to use", + "subtitle": "with all the necessary controls\nunder your thumb" + }, + { + "screenshotName": "light_metering-incident", + "title": "Incident light metering", + "subtitle": "using the light sensor\nof your device" + }, + { + "screenshotName": "light_metering-iso-picker", + "title": "Lots of ISO values", + "subtitle": "from 3 and up to 6400" + }, + { + "screenshotName": "light_timer", + "title": "In-built timer", + "subtitle": "for the ease of shooting\nlong exposures" + }, + { + "screenshotName": "light_settings", + "title": "Useful settings", + "subtitle": "to get the most accurate\nmetering results" + }, + { + "screenshotName": "light_equipment-profiles", + "title": "Create multiple profiles", + "subtitle": "to match your\ncamera & lens setups" + }, + { + "screenshotName": "light_equipment-profiles-iso-picker", + "title": "Fine-tune results", + "subtitle": "by selecting the values\nthat you use the most" + }, + { + "screenshotName": "dark_metering-reflected", + "title": "Match your style", + "subtitle": "with various theme types and colors" + } + ] +} \ No newline at end of file diff --git a/screenshots/assets/fonts/SF-Pro-Display-Bold-dark.zip b/screenshots/assets/fonts/SF-Pro-Display-Bold-dark.zip new file mode 100644 index 0000000..d5ec9ae Binary files /dev/null and b/screenshots/assets/fonts/SF-Pro-Display-Bold-dark.zip differ diff --git a/screenshots/assets/fonts/SF-Pro-Display-Bold.zip b/screenshots/assets/fonts/SF-Pro-Display-Bold.zip new file mode 100644 index 0000000..3931967 Binary files /dev/null and b/screenshots/assets/fonts/SF-Pro-Display-Bold.zip differ diff --git a/screenshots/assets/fonts/SF-Pro-Display-Regular-dark.zip b/screenshots/assets/fonts/SF-Pro-Display-Regular-dark.zip new file mode 100644 index 0000000..757b8fb Binary files /dev/null and b/screenshots/assets/fonts/SF-Pro-Display-Regular-dark.zip differ diff --git a/screenshots/assets/fonts/SF-Pro-Display-Regular.zip b/screenshots/assets/fonts/SF-Pro-Display-Regular.zip new file mode 100644 index 0000000..a3404db Binary files /dev/null and b/screenshots/assets/fonts/SF-Pro-Display-Regular.zip differ diff --git a/screenshots/assets/frames/android/pixel_6_frame.png b/screenshots/assets/frames/android/pixel_6_frame.png new file mode 100644 index 0000000..9e27dab Binary files /dev/null and b/screenshots/assets/frames/android/pixel_6_frame.png differ diff --git a/screenshots/assets/frames/ios/iphone_13_pro_frame.png b/screenshots/assets/frames/ios/iphone_13_pro_frame.png new file mode 100644 index 0000000..69d871f Binary files /dev/null and b/screenshots/assets/frames/ios/iphone_13_pro_frame.png differ diff --git a/screenshots/assets/system_overlays/android/pixel_6_system_overlay_dark.png b/screenshots/assets/system_overlays/android/pixel_6_system_overlay_dark.png new file mode 100644 index 0000000..45f11de Binary files /dev/null and b/screenshots/assets/system_overlays/android/pixel_6_system_overlay_dark.png differ diff --git a/screenshots/assets/system_overlays/android/pixel_6_system_overlay_light.png b/screenshots/assets/system_overlays/android/pixel_6_system_overlay_light.png new file mode 100644 index 0000000..cb97d05 Binary files /dev/null and b/screenshots/assets/system_overlays/android/pixel_6_system_overlay_light.png differ diff --git a/screenshots/assets/system_overlays/ios/iphone_13_pro_system_overlay_dark.png b/screenshots/assets/system_overlays/ios/iphone_13_pro_system_overlay_dark.png new file mode 100644 index 0000000..7b2086f Binary files /dev/null and b/screenshots/assets/system_overlays/ios/iphone_13_pro_system_overlay_dark.png differ diff --git a/screenshots/assets/system_overlays/ios/iphone_13_pro_system_overlay_light.png b/screenshots/assets/system_overlays/ios/iphone_13_pro_system_overlay_light.png new file mode 100644 index 0000000..43b0878 Binary files /dev/null and b/screenshots/assets/system_overlays/ios/iphone_13_pro_system_overlay_light.png differ diff --git a/screenshots/assets/system_overlays/ios/iphone_8_plus_system_overlay_dark.png b/screenshots/assets/system_overlays/ios/iphone_8_plus_system_overlay_dark.png new file mode 100644 index 0000000..c3407a0 Binary files /dev/null and b/screenshots/assets/system_overlays/ios/iphone_8_plus_system_overlay_dark.png differ diff --git a/screenshots/assets/system_overlays/ios/iphone_8_plus_system_overlay_light.png b/screenshots/assets/system_overlays/ios/iphone_8_plus_system_overlay_light.png new file mode 100644 index 0000000..f5b0abf Binary files /dev/null and b/screenshots/assets/system_overlays/ios/iphone_8_plus_system_overlay_light.png differ diff --git a/screenshots/convert_to_store_screenshots.dart b/screenshots/convert_to_store_screenshots.dart new file mode 100644 index 0000000..5736cf0 --- /dev/null +++ b/screenshots/convert_to_store_screenshots.dart @@ -0,0 +1,189 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:args/args.dart'; +import 'package:image/image.dart'; +import 'package:logging/logging.dart'; + +import 'models/screenshot_args.dart'; +import 'models/screenshot_device.dart'; +import 'models/screenshot_layout.dart'; +import 'utils/parse_configs.dart'; + +final _configs = parseScreenshotConfigs(); + +Future main(List args) async { + final parser = ArgParser() + ..addFlag('verbose', abbr: 'v', help: 'Verbose output') + ..addOption('platform', abbr: 'p', help: 'Device platform', mandatory: true) + ..addOption('device', abbr: 'd', help: 'device_snake_name', mandatory: true) + ..addOption('layout', abbr: 'l', help: 'Device layout', mandatory: true); + final ArgResults argResults = parser.parse(args); + + if (argResults['verbose'] as bool) { + Logger.root.level = Level.ALL; + } else { + Logger.root.level = Level.INFO; + } + + final platform = argResults["platform"] as String; + final device = argResults["device"] as String; + final layout = ScreenshotLayout.values.firstWhere((e) => e.name == argResults["layout"] as String); + + Directory('screenshots/generated/raw/$platform/$device').listSync().forEach((filePath) async { + final screenshotName = filePath.path.split('/').last.replaceAll('.png', ''); + final screenshotBytes = File(filePath.path).readAsBytesSync(); + final screenshot = decodePng(Uint8List.fromList(screenshotBytes))!; + + final screenshotArgs = ScreenshotArgs.fromRawName( + name: screenshotName, + deviceName: device, + platformFolder: platform, + ); + + final file = await File(screenshotArgs.toPath(layout.name)).create(recursive: true); + file.writeAsBytesSync( + encodePng( + screenshot.convertToStoreScreenshot( + args: screenshotArgs, + layout: layout, + ), + ), + ); + }); + + return 0; +} + +extension ScreenshotImage on Image { + Image convertToStoreScreenshot({ + required ScreenshotArgs args, + required ScreenshotLayout layout, + }) { + if (_configs[args.nameWithTheme] == null) { + return this; + } + return _addSystemOverlay( + screenshotDevices[args.deviceName]!, + isDark: args.isDark, + ) + ._addDeviceFrame( + screenshotDevices[args.deviceName]!, + args.backgroundColor, + ) + ._applyLayout( + layout, + _configs[args.nameWithTheme]!.title, + _configs[args.nameWithTheme]!.subtitle, + isDark: args.isDark, + ); + } + + Image _addSystemOverlay(ScreenshotDevice device, {required bool isDark}) { + final path = isDark ? device.systemOverlayPathDark : device.systemOverlayPathLight; + final statusBar = copyResize( + decodePng(File(path).readAsBytesSync())!, + width: width, + ); + return compositeImage(this, statusBar); + } + + Image _addDeviceFrame(ScreenshotDevice device, String color) { + final backgroundColor = ColorRgba8( + int.parse(color.substring(2, 4), radix: 16), + int.parse(color.substring(4, 6), radix: 16), + int.parse(color.substring(6, 8), radix: 16), + int.parse(color.substring(0, 2), radix: 16), + ); + final screenshotRounded = copyCrop( + this, + x: 0, + y: 0, + width: width, + height: height, + ); + + final frame = decodePng(File(device.deviceFramePath).readAsBytesSync())!; + final expandedScreenshot = copyExpandCanvas( + copyExpandCanvas( + screenshotRounded, + newWidth: screenshotRounded.width + device.screenshotFrameOffset.dx, + newHeight: screenshotRounded.height + device.screenshotFrameOffset.dy, + position: ExpandCanvasPosition.bottomRight, + backgroundColor: backgroundColor, + ), + newWidth: frame.width, + newHeight: frame.height, + position: ExpandCanvasPosition.topLeft, + backgroundColor: backgroundColor, + ); + + return compositeImage(expandedScreenshot, frame); + } + + Image _applyLayout(ScreenshotLayout layout, String title, String subtitle, {required bool isDark}) { + final textImage = _drawTitles(layout, title, subtitle, isDark: isDark); + final maxFrameHeight = + layout.size.height - (layout.contentPadding.top + textImage.height + 84 + layout.contentPadding.bottom); + int maxFrameWidth = layout.size.width - (layout.contentPadding.left + layout.contentPadding.right); + if (maxFrameWidth * height / width > maxFrameHeight) { + maxFrameWidth = maxFrameHeight * width ~/ height; + } + final scaledScreenshot = copyResize(this, width: maxFrameWidth); + + final draft = copyExpandCanvas( + copyExpandCanvas( + scaledScreenshot, + newWidth: scaledScreenshot.width + (layout.size.width - scaledScreenshot.width) ~/ 2, + newHeight: scaledScreenshot.height + layout.contentPadding.bottom, + position: ExpandCanvasPosition.topLeft, + backgroundColor: getPixel(0, 0), + ), + newWidth: layout.size.width, + newHeight: layout.size.height, + position: ExpandCanvasPosition.bottomRight, + backgroundColor: getPixel(0, 0), + ); + + return compositeImage( + draft, + textImage, + dstX: layout.contentPadding.left, + dstY: layout.contentPadding.top, + ); + } + + Image _drawTitles(ScreenshotLayout layout, String title, String subtitle, {required bool isDark}) { + final titleFont = + BitmapFont.fromZip(File(isDark ? layout.titleFontDarkPath : layout.titleFontPath).readAsBytesSync()); + final subtitleFont = + BitmapFont.fromZip(File(isDark ? layout.subtitleFontDarkPath : layout.subtitleFontPath).readAsBytesSync()); + final textImage = fill( + Image( + height: titleFont.lineHeight + 36 + subtitleFont.lineHeight * 2, + width: layout.size.width - (layout.contentPadding.left + layout.contentPadding.right), + ), + color: getPixel(0, 0), + ); + + drawString( + textImage, + title, + font: titleFont, + y: 0, + ); + + int subtitleDy = titleFont.lineHeight + 36; + subtitle.split('\n').forEach((line) { + drawString( + textImage, + line, + font: subtitleFont, + y: subtitleDy, + ); + subtitleDy += subtitleFont.lineHeight; + }); + + return textImage; + } +} diff --git a/screenshots/generate_screenshots.dart b/screenshots/generate_screenshots.dart index fae038a..307c368 100644 --- a/screenshots/generate_screenshots.dart +++ b/screenshots/generate_screenshots.dart @@ -1,3 +1,5 @@ +// ignore_for_file: invalid_use_of_visible_for_testing_member + import 'dart:convert'; import 'dart:io'; @@ -5,44 +7,58 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:lightmeter/data/models/ev_source_type.dart'; +import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/data/models/volume_action.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/theme.dart'; +import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart'; +import 'package:lightmeter/screens/metering/screen_metering.dart'; import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart'; import 'package:lightmeter/screens/settings/screen_settings.dart'; +import 'package:lightmeter/screens/shared/animated_circular_button/widget_button_circular_animated.dart'; +import 'package:lightmeter/screens/timer/screen_timer.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../integration_test/mocks/paid_features_mock.dart'; import '../integration_test/utils/widget_tester_actions.dart'; +import 'models/screenshot_args.dart'; //https://stackoverflow.com/a/67186625/13167574 const _mockFilm = Film('Ilford HP5+', 400); +final Color _lightThemeColor = primaryColorsList[5]; +final Color _darkThemeColor = primaryColorsList[3]; +final ThemeData _themeLight = themeFrom(_lightThemeColor, Brightness.light); +final ThemeData _themeDark = themeFrom(_darkThemeColor, Brightness.dark); /// Just a screenshot generator. No expectations here. void main() { final binding = IntegrationTestWidgetsFlutterBinding(); IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - final Color lightThemeColor = primaryColorsList[5]; - final Color darkThemeColor = primaryColorsList[3]; - void mockSharedPrefs(ThemeType theme, Color color) { - // ignore: invalid_use_of_visible_for_testing_member + void mockSharedPrefs({ + int iso = 400, + int nd = 0, + double calibration = 0.0, + required ThemeType theme, + required Color color, + }) { SharedPreferences.setMockInitialValues({ /// Metering values UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index, - UserPreferencesService.isoKey: 400, - UserPreferencesService.ndFilterKey: 0, + UserPreferencesService.isoKey: iso, + UserPreferencesService.ndFilterKey: nd, /// Metering settings UserPreferencesService.stopTypeKey: StopType.third.index, - UserPreferencesService.cameraEvCalibrationKey: 0.0, - UserPreferencesService.lightSensorEvCalibrationKey: 0.0, + UserPreferencesService.cameraEvCalibrationKey: calibration, + UserPreferencesService.lightSensorEvCalibrationKey: calibration, UserPreferencesService.meteringScreenLayoutKey: json.encode( { MeteringScreenLayoutFeature.equipmentProfiles: true, @@ -52,6 +68,7 @@ void main() { ), /// General settings + UserPreferencesService.autostartTimerKey: false, UserPreferencesService.caffeineKey: true, UserPreferencesService.hapticsKey: true, UserPreferencesService.volumeActionKey: VolumeAction.shutter.toString(), @@ -70,7 +87,7 @@ void main() { /// Generates several screenshots with the light theme testWidgets('Generate light theme screenshots', (tester) async { - mockSharedPrefs(ThemeType.light, lightThemeColor); + mockSharedPrefs(theme: ThemeType.light, color: _lightThemeColor); await tester.pumpApplication( availableFilms: [_mockFilm], filmsInUse: [_mockFilm], @@ -78,43 +95,39 @@ void main() { ); await tester.takePhoto(); - await tester.takeScreenshot(binding, 'light-metering_reflected'); + await tester.takeScreenshotLight(binding, 'metering-reflected'); if (Platform.isAndroid) { await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor)); await tester.pumpAndSettle(); await tester.toggleIncidentMetering(7.3); - await tester.takeScreenshot(binding, 'light-metering_incident'); + await tester.takeScreenshotLight(binding, 'metering-incident'); } await tester.openAnimatedPicker(); - await tester.takeScreenshot(binding, 'light-metering_iso_picker'); + await tester.takeScreenshotLight(binding, 'metering-iso-picker'); await tester.tapCancelButton(); await tester.tap(find.byTooltip(S.current.tooltipOpenSettings)); await tester.pumpAndSettle(); - await tester.takeScreenshot(binding, 'light-settings'); + await tester.takeScreenshotLight(binding, 'settings'); - await tester.tapDescendantTextOf(S.current.meteringScreenLayout); - await tester.takeScreenshot(binding, 'light-settings_metering_screen_layout'); - - await tester.tapCancelButton(); await tester.tapDescendantTextOf(S.current.equipmentProfiles); await tester.pumpAndSettle(); await tester.tapDescendantTextOf(mockEquipmentProfiles.first.name); await tester.pumpAndSettle(); - await tester.takeScreenshot(binding, 'light-equipment_profiles'); + await tester.takeScreenshotLight(binding, 'equipment-profiles'); - await tester.tap(find.byIcon(Icons.iso).first); + await tester.tap(find.byIcon(Icons.iso_outlined).first); await tester.pumpAndSettle(); - await tester.takeScreenshot(binding, 'light-equipment_profiles_iso_picker'); + await tester.takeScreenshotLight(binding, 'equipment-profiles-iso-picker'); }); /// and the additionally the first one with the dark theme testWidgets( 'Generate dark theme screenshots', (tester) async { - mockSharedPrefs(ThemeType.dark, darkThemeColor); + mockSharedPrefs(theme: ThemeType.dark, color: _darkThemeColor); await tester.pumpApplication( availableFilms: [_mockFilm], filmsInUse: [_mockFilm], @@ -122,14 +135,39 @@ void main() { ); await tester.takePhoto(); - await tester.takeScreenshot(binding, 'dark-metering_reflected'); + await tester.takeScreenshotDark(binding, 'metering-reflected'); + }, + ); - if (Platform.isAndroid) { - await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor)); - await tester.pumpAndSettle(); - await tester.toggleIncidentMetering(7.3); - await tester.takeScreenshot(binding, 'dark-metering_incident'); - } + testWidgets( + 'Generate timer screenshot', + (tester) async { + const timerExposurePair = ExposurePair( + ApertureValue(16, StopType.full), + ShutterSpeedValue(8, false, StopType.full), + ); + mockSharedPrefs( + iso: 100, + nd: 8, + calibration: -0.3, + theme: ThemeType.light, + color: _lightThemeColor, + ); + await tester.pumpApplication( + availableFilms: [_mockFilm], + filmsInUse: [_mockFilm], + selectedFilm: _mockFilm, + ); + + await tester.takePhoto(); + await tester.scrollToExposurePair( + ev: 5, + exposurePair: timerExposurePair, + ); + await tester.tap(find.text(timerExposurePair.shutterSpeed.toString())); + await tester.pumpAndSettle(); + await tester.mockTimerResumedState(timerExposurePair.shutterSpeed); + await tester.takeScreenshotLight(binding, 'timer'); }, ); } @@ -137,8 +175,56 @@ void main() { final String _platformFolder = Platform.isAndroid ? 'android' : 'ios'; extension on WidgetTester { - Future takeScreenshot(IntegrationTestWidgetsFlutterBinding binding, String name) async { - await binding.takeScreenshot("$_platformFolder/${const String.fromEnvironment('deviceName')}/$name"); + Future takeScreenshotLight(IntegrationTestWidgetsFlutterBinding binding, String name) => + _takeScreenshot(binding, name, _themeLight); + Future takeScreenshotDark(IntegrationTestWidgetsFlutterBinding binding, String name) => + _takeScreenshot(binding, name, _themeDark); + + Future _takeScreenshot(IntegrationTestWidgetsFlutterBinding binding, String name, ThemeData theme) async { + final Color backgroundColor = theme.colorScheme.surface; + await binding.takeScreenshot( + ScreenshotArgs( + name: name, + deviceName: const String.fromEnvironment('deviceName'), + platformFolder: _platformFolder, + backgroundColor: backgroundColor.value.toRadixString(16), + isDark: theme.brightness == Brightness.dark, + ).toString(), + ); await pumpAndSettle(); } } + +extension on WidgetTester { + Future scrollToExposurePair({ + double ev = mockPhotoEv100, + EquipmentProfile equipmentProfile = defaultEquipmentProfile, + required ExposurePair exposurePair, + }) async { + final exposurePairs = MeteringContainerBuidler.buildExposureValues( + ev, + StopType.third, + equipmentProfile, + ); + + await scrollUntilVisible( + find.byWidgetPredicate((widget) => widget is Row && widget.key == ValueKey(exposurePairs.indexOf(exposurePair))), + 56, + scrollable: find.descendant(of: find.byType(ExposurePairsList), matching: find.byType(Scrollable)), + ); + } + + Future mockTimerResumedState(ShutterSpeedValue shutterSpeedValue) async { + await tap(find.byType(AnimatedCircluarButton)); + await pump(Dimens.durationS); + + late final skipTimerDuration = + Duration(milliseconds: (shutterSpeedValue.value * 0.35 * Duration.millisecondsPerSecond).toInt()); + await pump(skipTimerDuration); + + final TimerScreenState state = this.state(find.byType(TimerScreen)); + state.startStopIconController.stop(); + state.timelineController.stop(); + await pump(); + } +} diff --git a/screenshots/generated/android/android/dark_metering-reflected.png b/screenshots/generated/android/android/dark_metering-reflected.png new file mode 100644 index 0000000..cc3dd89 Binary files /dev/null and b/screenshots/generated/android/android/dark_metering-reflected.png differ diff --git a/screenshots/generated/android/android/light_equipment-profiles-iso-picker.png b/screenshots/generated/android/android/light_equipment-profiles-iso-picker.png new file mode 100644 index 0000000..743dc68 Binary files /dev/null and b/screenshots/generated/android/android/light_equipment-profiles-iso-picker.png differ diff --git a/screenshots/generated/android/android/light_equipment-profiles.png b/screenshots/generated/android/android/light_equipment-profiles.png new file mode 100644 index 0000000..e4c7345 Binary files /dev/null and b/screenshots/generated/android/android/light_equipment-profiles.png differ diff --git a/screenshots/generated/android/android/light_metering-incident.png b/screenshots/generated/android/android/light_metering-incident.png new file mode 100644 index 0000000..61168b4 Binary files /dev/null and b/screenshots/generated/android/android/light_metering-incident.png differ diff --git a/screenshots/generated/android/android/light_metering-iso-picker.png b/screenshots/generated/android/android/light_metering-iso-picker.png new file mode 100644 index 0000000..6ed2c57 Binary files /dev/null and b/screenshots/generated/android/android/light_metering-iso-picker.png differ diff --git a/screenshots/generated/android/android/light_metering-reflected.png b/screenshots/generated/android/android/light_metering-reflected.png new file mode 100644 index 0000000..3efe588 Binary files /dev/null and b/screenshots/generated/android/android/light_metering-reflected.png differ diff --git a/screenshots/generated/android/android/light_settings.png b/screenshots/generated/android/android/light_settings.png new file mode 100644 index 0000000..4205ca3 Binary files /dev/null and b/screenshots/generated/android/android/light_settings.png differ diff --git a/screenshots/generated/android/android/light_timer.png b/screenshots/generated/android/android/light_timer.png new file mode 100644 index 0000000..262cc0b Binary files /dev/null and b/screenshots/generated/android/android/light_timer.png differ diff --git a/screenshots/generated/ios/iphone55inch/dark_metering-reflected.png b/screenshots/generated/ios/iphone55inch/dark_metering-reflected.png new file mode 100644 index 0000000..b47f725 Binary files /dev/null and b/screenshots/generated/ios/iphone55inch/dark_metering-reflected.png differ diff --git a/screenshots/generated/ios/iphone55inch/light_equipment-profiles-iso-picker.png b/screenshots/generated/ios/iphone55inch/light_equipment-profiles-iso-picker.png new file mode 100644 index 0000000..7c11358 Binary files /dev/null and b/screenshots/generated/ios/iphone55inch/light_equipment-profiles-iso-picker.png differ diff --git a/screenshots/generated/ios/iphone55inch/light_equipment-profiles.png b/screenshots/generated/ios/iphone55inch/light_equipment-profiles.png new file mode 100644 index 0000000..7d30267 Binary files /dev/null and b/screenshots/generated/ios/iphone55inch/light_equipment-profiles.png differ diff --git a/screenshots/generated/ios/iphone55inch/light_metering-iso-picker.png b/screenshots/generated/ios/iphone55inch/light_metering-iso-picker.png new file mode 100644 index 0000000..53e869c Binary files /dev/null and b/screenshots/generated/ios/iphone55inch/light_metering-iso-picker.png differ diff --git a/screenshots/generated/ios/iphone55inch/light_metering-reflected.png b/screenshots/generated/ios/iphone55inch/light_metering-reflected.png new file mode 100644 index 0000000..b2aa51b Binary files /dev/null and b/screenshots/generated/ios/iphone55inch/light_metering-reflected.png differ diff --git a/screenshots/generated/ios/iphone55inch/light_settings.png b/screenshots/generated/ios/iphone55inch/light_settings.png new file mode 100644 index 0000000..b65508d Binary files /dev/null and b/screenshots/generated/ios/iphone55inch/light_settings.png differ diff --git a/screenshots/generated/ios/iphone55inch/light_timer.png b/screenshots/generated/ios/iphone55inch/light_timer.png new file mode 100644 index 0000000..ac8ff0e Binary files /dev/null and b/screenshots/generated/ios/iphone55inch/light_timer.png differ diff --git a/screenshots/generated/ios/iphone65inch/dark_metering-reflected.png b/screenshots/generated/ios/iphone65inch/dark_metering-reflected.png new file mode 100644 index 0000000..60ad820 Binary files /dev/null and b/screenshots/generated/ios/iphone65inch/dark_metering-reflected.png differ diff --git a/screenshots/generated/ios/iphone65inch/light_equipment-profiles-iso-picker.png b/screenshots/generated/ios/iphone65inch/light_equipment-profiles-iso-picker.png new file mode 100644 index 0000000..d137e16 Binary files /dev/null and b/screenshots/generated/ios/iphone65inch/light_equipment-profiles-iso-picker.png differ diff --git a/screenshots/generated/ios/iphone65inch/light_equipment-profiles.png b/screenshots/generated/ios/iphone65inch/light_equipment-profiles.png new file mode 100644 index 0000000..a747ab5 Binary files /dev/null and b/screenshots/generated/ios/iphone65inch/light_equipment-profiles.png differ diff --git a/screenshots/generated/ios/iphone65inch/light_metering-iso-picker.png b/screenshots/generated/ios/iphone65inch/light_metering-iso-picker.png new file mode 100644 index 0000000..676c07c Binary files /dev/null and b/screenshots/generated/ios/iphone65inch/light_metering-iso-picker.png differ diff --git a/screenshots/generated/ios/iphone65inch/light_metering-reflected.png b/screenshots/generated/ios/iphone65inch/light_metering-reflected.png new file mode 100644 index 0000000..8fb5ffe Binary files /dev/null and b/screenshots/generated/ios/iphone65inch/light_metering-reflected.png differ diff --git a/screenshots/generated/ios/iphone65inch/light_settings.png b/screenshots/generated/ios/iphone65inch/light_settings.png new file mode 100644 index 0000000..31be0c0 Binary files /dev/null and b/screenshots/generated/ios/iphone65inch/light_settings.png differ diff --git a/screenshots/generated/ios/iphone65inch/light_timer.png b/screenshots/generated/ios/iphone65inch/light_timer.png new file mode 100644 index 0000000..ad3d00a Binary files /dev/null and b/screenshots/generated/ios/iphone65inch/light_timer.png differ diff --git a/screenshots/models/screenshot_args.dart b/screenshots/models/screenshot_args.dart new file mode 100644 index 0000000..80efa61 --- /dev/null +++ b/screenshots/models/screenshot_args.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; + +class ScreenshotArgs { + final String name; + final String deviceName; + final String platformFolder; + final String backgroundColor; + final bool isDark; + + static const _pathArgsDelimited = '_'; + + ScreenshotArgs({ + required this.name, + required String deviceName, + required this.platformFolder, + required this.backgroundColor, + required this.isDark, + }) : deviceName = deviceName.replaceAll(' ', _pathArgsDelimited).replaceAll(RegExp('[(|)]'), '').toLowerCase(); + + ScreenshotArgs.fromRawName({ + required String name, + required String deviceName, + required this.platformFolder, + }) : name = name.split(_pathArgsDelimited)[1], + deviceName = deviceName.replaceAll(' ', _pathArgsDelimited).replaceAll(RegExp('[(|)]'), '').toLowerCase(), + backgroundColor = name.split(_pathArgsDelimited)[2], + isDark = name.contains('dark'); + + static const _folderPrefix = 'screenshots/generated'; + String get nameWithTheme => '${isDark ? 'dark' : 'light'}$_pathArgsDelimited$name'; + + String toPathRaw() => + '$_folderPrefix/raw/$platformFolder/$deviceName/$nameWithTheme$_pathArgsDelimited$backgroundColor.png'; + String toPath(String layoutName) => '$_folderPrefix/$platformFolder/$layoutName/$nameWithTheme.png'; + + @override + String toString() => jsonEncode(_toJson()); + + factory ScreenshotArgs.fromString(String data) => ScreenshotArgs._fromJson(jsonDecode(data) as Map); + + factory ScreenshotArgs._fromJson(Map data) { + return ScreenshotArgs( + name: data['name'] as String, + deviceName: data['deviceName'] as String, + platformFolder: data['platformFolder'] as String, + backgroundColor: data['backgroundColor'] as String, + isDark: data['isDark'] as bool, + ); + } + + Map _toJson() { + return { + "name": name, + "deviceName": deviceName, + "platformFolder": platformFolder, + "backgroundColor": backgroundColor, + "isDark": isDark, + }; + } +} diff --git a/screenshots/models/screenshot_config.dart b/screenshots/models/screenshot_config.dart new file mode 100644 index 0000000..c281de6 --- /dev/null +++ b/screenshots/models/screenshot_config.dart @@ -0,0 +1,19 @@ +class ScreenshotConfig { + final String title; + final String subtitle; + final String screenshotName; + + const ScreenshotConfig({ + required this.title, + required this.subtitle, + required this.screenshotName, + }); + + factory ScreenshotConfig.fromJson(Map data) { + return ScreenshotConfig( + title: data['title'] as String, + subtitle: data['subtitle'] as String, + screenshotName: data['screenshotName'] as String, + ); + } +} diff --git a/screenshots/models/screenshot_device.dart b/screenshots/models/screenshot_device.dart new file mode 100644 index 0000000..c24f326 --- /dev/null +++ b/screenshots/models/screenshot_device.dart @@ -0,0 +1,49 @@ +enum ScreenshotDevicePlatform { android, ios } + +class ScreenshotDevice { + final String name; + final ScreenshotDevicePlatform platform; + final ({int dx, int dy}) screenshotFrameOffset; + + const ScreenshotDevice({ + required this.name, + required this.platform, + this.screenshotFrameOffset = (dx: 0, dy: 0), + }); + + ScreenshotDevice.fromDisplayName({ + required String displayName, + required this.platform, + this.screenshotFrameOffset = (dx: 0, dy: 0), + }) : name = displayName.replaceAll(' ', '_').toLowerCase(); + + String get systemOverlayPathLight => + 'screenshots/assets/system_overlays/${platform.name}/${name}_system_overlay_light.png'; + String get systemOverlayPathDark => + 'screenshots/assets/system_overlays/${platform.name}/${name}_system_overlay_dark.png'; + String get deviceFramePath => 'screenshots/assets/frames/${platform.name}/${name}_frame.png'; +} + +final screenshotDevices = { + for (final d in _screenshotDevicesAndroid + _screenshotDevicesIos) d.name: d, +}; + +final List _screenshotDevicesAndroid = [ + ScreenshotDevice.fromDisplayName( + displayName: 'Pixel 6', + platform: ScreenshotDevicePlatform.android, + screenshotFrameOffset: (dx: 67, dy: 66), + ), +]; + +final List _screenshotDevicesIos = [ + ScreenshotDevice.fromDisplayName( + displayName: 'iPhone 8 Plus', + platform: ScreenshotDevicePlatform.ios, + ), + ScreenshotDevice.fromDisplayName( + displayName: 'iPhone 13 Pro', + platform: ScreenshotDevicePlatform.ios, + screenshotFrameOffset: (dx: 72, dy: 60), + ), +]; diff --git a/screenshots/models/screenshot_layout.dart b/screenshots/models/screenshot_layout.dart new file mode 100644 index 0000000..e941bca --- /dev/null +++ b/screenshots/models/screenshot_layout.dart @@ -0,0 +1,35 @@ +enum ScreenshotLayout { + android( + size: (width: 1440, height: 2560), + contentPadding: (left: 144, top: 132, right: 144, bottom: 132), + titleFontPath: 'screenshots/assets/fonts/SF-Pro-Display-Bold.zip', + subtitleFontPath: 'screenshots/assets/fonts/SF-Pro-Display-Regular.zip', + ), + iphone65inch( + size: (width: 1290, height: 2796), + contentPadding: (left: 144, top: 184, right: 144, bottom: 184), + titleFontPath: 'screenshots/assets/fonts/SF-Pro-Display-Bold.zip', + subtitleFontPath: 'screenshots/assets/fonts/SF-Pro-Display-Regular.zip', + ), + iphone55inch( + size: (width: 1242, height: 2208), + contentPadding: (left: 144, top: 144, right: 144, bottom: 144), + titleFontPath: 'screenshots/assets/fonts/SF-Pro-Display-Bold.zip', + subtitleFontPath: 'screenshots/assets/fonts/SF-Pro-Display-Regular.zip', + ); + + final ({int height, int width}) size; + final ({int left, int top, int right, int bottom}) contentPadding; + final String titleFontPath; + final String subtitleFontPath; + + String get titleFontDarkPath => '${titleFontPath.split('.').first}-dark.zip'; + String get subtitleFontDarkPath => '${subtitleFontPath.split('.').first}-dark.zip'; + + const ScreenshotLayout({ + required this.size, + required this.contentPadding, + required this.titleFontPath, + required this.subtitleFontPath, + }); +} diff --git a/screenshots/scripts/convert_to_store_screenshots.sh b/screenshots/scripts/convert_to_store_screenshots.sh new file mode 100644 index 0000000..1688c5e --- /dev/null +++ b/screenshots/scripts/convert_to_store_screenshots.sh @@ -0,0 +1,4 @@ +dart run screenshots/convert_to_store_screenshots.dart -p android -d pixel_6 -l android +dart run screenshots/convert_to_store_screenshots.dart -p ios -d iphone_13_pro -l iphone55inch +dart run screenshots/convert_to_store_screenshots.dart -p ios -d iphone_13_pro -l iphone65inch +#dart run screenshots/convert_to_store_screenshots.dart -p ios -d ipad_pro_12.9-inch_6th_generation -l ipad13inch diff --git a/screenshots/scripts/generate_android_screenshots.sh b/screenshots/scripts/generate_android_screenshots.sh new file mode 100644 index 0000000..7601ea2 --- /dev/null +++ b/screenshots/scripts/generate_android_screenshots.sh @@ -0,0 +1 @@ +sh screenshots/scripts/generate_screenshots.sh "Pixel 6" \ No newline at end of file diff --git a/screenshots/scripts/generate_ios_screenshots.sh b/screenshots/scripts/generate_ios_screenshots.sh index 5befaae..f0cd882 100644 --- a/screenshots/scripts/generate_ios_screenshots.sh +++ b/screenshots/scripts/generate_ios_screenshots.sh @@ -1,13 +1,8 @@ -devices_array=("iPhone 8 Plus" "iPhone 13 Pro" "iPhone 13 Pro Max" "iPhone 15 Pro" "iPhone 15 Pro Max" "iPad Pro (12.9-inch) (6th generation)") - +simulators_array=("iPhone 13 Pro" "iPad Pro (12.9-inch) (6th generation)") open -a Simulator - -for i in "${devices_array[@]}"; do # https://www.baeldung.com/linux/shell-script-iterate-over-string-list#2-understanding--and--special-indices +for i in "${simulators_array[@]}"; do # https://www.baeldung.com/linux/shell-script-iterate-over-string-list#2-understanding--and--special-indices echo "$i" xcrun simctl boot "$i" - #uid=$(echo "$(fvm flutter devices)" | sed -n -r "s/$i \(mobile\) • (.*) • .* • .*\(simulator\)/\1/p") - #echo $uid sh screenshots/scripts/generate_screenshots.sh "$i" done - killall 'Simulator' diff --git a/screenshots/utils/parse_configs.dart b/screenshots/utils/parse_configs.dart new file mode 100644 index 0000000..d082b77 --- /dev/null +++ b/screenshots/utils/parse_configs.dart @@ -0,0 +1,14 @@ +import 'dart:convert'; +import 'dart:io'; + +import '../models/screenshot_config.dart'; + +Map parseScreenshotConfigs([String locale = 'en']) { + final configPath = 'screenshots/assets/content/screenshot_titles_$locale.json'; + final data = jsonDecode(File(configPath).readAsStringSync()) as Map; + final entries = (data['screenshots'] as List).map((value) { + final config = ScreenshotConfig.fromJson(value as Map); + return MapEntry(config.screenshotName, config); + }); + return Map.fromEntries(entries); +} diff --git a/test/screens/metering/components/shared/readings_container/shared/animated_dialog_test.dart b/test/screens/metering/components/shared/readings_container/shared/animated_dialog_test.dart index d4b83c6..f3ec1fa 100644 --- a/test/screens/metering/components/shared/readings_container/shared/animated_dialog_test.dart +++ b/test/screens/metering/components/shared/readings_container/shared/animated_dialog_test.dart @@ -79,7 +79,7 @@ extension WidgetTesterActions on WidgetTester { children: [ Expanded( child: AnimatedDialogPicker( - icon: Icons.iso, + icon: Icons.iso_outlined, title: '', subtitle: '', selectedValue: 0, diff --git a/test/screens/metering/components/shared/readings_container/shared/dialog_picker_test.dart b/test/screens/metering/components/shared/readings_container/shared/dialog_picker_test.dart index bd037af..9c69ead 100644 --- a/test/screens/metering/components/shared/readings_container/shared/dialog_picker_test.dart +++ b/test/screens/metering/components/shared/readings_container/shared/dialog_picker_test.dart @@ -51,7 +51,7 @@ extension WidgetTesterActions on WidgetTester { children: [ Expanded( child: AnimatedDialogPicker( - icon: Icons.iso, + icon: Icons.iso_outlined, title: '', subtitle: '', selectedValue: 0, diff --git a/test_driver/screenshot_driver.dart b/test_driver/screenshot_driver.dart index fe49e36..ae1c85e 100644 --- a/test_driver/screenshot_driver.dart +++ b/test_driver/screenshot_driver.dart @@ -1,14 +1,15 @@ import 'dart:io'; + import 'package:integration_test/integration_test_driver_extended.dart'; -import 'utils/grant_camera_permission.dart'; +import '../screenshots/models/screenshot_args.dart'; Future main() async { - await grantCameraPermission(); await integrationDriver( - onScreenshot: (name, bytes, [args]) async { - final File image = await File('screenshots/generated/$name.png').create(recursive: true); - image.writeAsBytesSync(bytes); + onScreenshot: (name, bytes, [_]) async { + final screenshotArgs = ScreenshotArgs.fromString(name); + final file = await File(screenshotArgs.toPathRaw()).create(recursive: true); + file.writeAsBytesSync(bytes); return true; }, );