Automated release screenshots generation (#177)
* added system overlays for iPhone 8 Plus & iPhone 13 Pro * add device frame (wip) * scale device frame (wip) * add text to screenshots (wip) * added screenshots config json * reorganized screenshot models * cleanup * added fonts for dark screenshots * typo * store raw screenshots * added standalone script to update screenshots * wip * refined screenshots naming * skip metering layout dialog screenshot * parse ipad name * added assets for Pixel 6 * typo * added text for incident light metering * reorganized store script * typo * wip * synced outlined icons * added timer screen to screenshot generator * constrained timer screen timeline for tablets * added timer screenshot title * typo * revised scripts * track release screenshots * Update README.md * iphone 6.5" -> iphone 6.7" * Update google_play_resources.md * softened screenshot font colors * cleanup
2
.gitignore
vendored
|
@ -63,4 +63,4 @@ ios/Runner/GoogleService-Info.plist
|
||||||
coverage/
|
coverage/
|
||||||
test/coverage_helper_test.dart
|
test/coverage_helper_test.dart
|
||||||
**/failures/*.png
|
**/failures/*.png
|
||||||
screenshots/generated/
|
screenshots/generated/raw/
|
10
README.md
|
@ -23,11 +23,11 @@ Without further delay behold my new Lightmeter app inspired by Material You (a.k
|
||||||
# Screenshots
|
# Screenshots
|
||||||
|
|
||||||
<p float="center">
|
<p float="center">
|
||||||
<img src="https://lh3.googleusercontent.com/8Sd-pmNcQ0xAr5opuTeJKWr2OXeQvCoFSdVDSoKQSHHKeNmqF71hqeAdm3yjunY12zY" width="18.8%" />
|
<img src="screenshots/generated/android/android/light_metering-reflected.png" width="18.8%" />
|
||||||
<img src="https://lh3.googleusercontent.com/rqBv8pT0AdcBy0xEgQY2unV-YEQ5KfUkandAxJ62yYCiSF72HClA_tkb4JT_3UPaIfFP" width="18.8%" />
|
<img src="screenshots/generated/android/android/light_settings.png" width="18.8%" />
|
||||||
<img src="https://lh3.googleusercontent.com/-SnYbYSugVfdwYi6m_rd9CzpCZMCIfudhnq0zRIlzEtLSXhrwziWVd2hotygfqiSofI" width="18.8%" />
|
<img src="screenshots/generated/android/android/light_timer.png" width="18.8%" />
|
||||||
<img src="https://lh3.googleusercontent.com/UXxptL_dAIJDtrmpEZuSz39Iq4HuPb3ZPeuANfE9XH0De0uZQT83LNdu1AObBPobpg" width="18.8%" />
|
<img src="screenshots/generated/android/android/light_equipment-profiles.png" width="18.8%" />
|
||||||
<img src="https://lh3.googleusercontent.com/15g_SPV8knDLFbz1_-wGNJFsJeyVWZ_y--TGHpk75MaaIdMDyTXY2_TL-Aw8bpOhpw" width="18.8%" />
|
<img src="screenshots/generated/android/android/dark_metering-reflected.png" width="18.8%" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
|
@ -6,26 +6,19 @@ Lightmeter
|
||||||
|
|
||||||
## Short description
|
## 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
|
## Long description
|
||||||
|
|
||||||
<b>Material Design</b>
|
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:
|
||||||
The user interface matches every single detail of the material design guidelines to ensure Lightmeter is an eye candy for you.
|
|
||||||
|
|
||||||
<b>Easy to Use</b>
|
- A reflected light meter with spot metering (using the device's camera)
|
||||||
No complicated or overblown menus but a familiar and clean interface.
|
- 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
|
||||||
|
|
||||||
<b>Customizability</b>
|
and many more!
|
||||||
There is an inbuilt theme engine with many different colors to choose from.
|
|
||||||
|
|
||||||
<b>Features</b>
|
|
||||||
• 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
|
|
||||||
|
|
||||||
<b>NOTE</b>
|
<b>NOTE</b>
|
||||||
The accuracy of the measurements depends on your decice's hardware.
|
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
|
## Graphics
|
||||||
|
|
||||||
[Figma](https://www.figma.com/file/X7pUAsxtjx19Rj8VtBV3Ft/Material-lightmeter?node-id=501%3A1586&t=cWNHaXm024KM4KYn-1)
|
[Figma](https://www.figma.com/file/X7pUAsxtjx19Rj8VtBV3Ft/Material-lightmeter?node-id=501%3A1586&t=cWNHaXm024KM4KYn-1)
|
||||||
|
|
|
@ -49,7 +49,7 @@ void testE2E(String description) {
|
||||||
/// Create Praktica + Zenitar profile from scratch
|
/// Create Praktica + Zenitar profile from scratch
|
||||||
await tester.openSettings();
|
await tester.openSettings();
|
||||||
await tester.tapDescendantTextOf<SettingsScreen>(S.current.equipmentProfiles);
|
await tester.tapDescendantTextOf<SettingsScreen>(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.pumpAndSettle();
|
||||||
await tester.setProfileName(mockEquipmentProfiles[0].name);
|
await tester.setProfileName(mockEquipmentProfiles[0].name);
|
||||||
await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[0].name);
|
await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[0].name);
|
||||||
|
@ -63,7 +63,7 @@ void testE2E(String description) {
|
||||||
expect(find.text('1/1000 - B'), findsOneWidget);
|
expect(find.text('1/1000 - B'), findsOneWidget);
|
||||||
|
|
||||||
/// Create Praktica + Jupiter profile from Zenitar profile
|
/// 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.pumpAndSettle();
|
||||||
await tester.setProfileName(mockEquipmentProfiles[1].name);
|
await tester.setProfileName(mockEquipmentProfiles[1].name);
|
||||||
await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[1].name);
|
await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[1].name);
|
||||||
|
@ -193,7 +193,7 @@ extension on WidgetTester {
|
||||||
bool deselectAll = true,
|
bool deselectAll = true,
|
||||||
}) async {
|
}) async {
|
||||||
if (deselectAll) {
|
if (deselectAll) {
|
||||||
await tap(find.byIcon(Icons.deselect));
|
await tap(find.byIcon(Icons.deselect_outlined));
|
||||||
await pump();
|
await pump();
|
||||||
}
|
}
|
||||||
for (final value in valuesToSelect) {
|
for (final value in valuesToSelect) {
|
||||||
|
|
|
@ -68,4 +68,7 @@ class Dimens {
|
||||||
paddingL,
|
paddingL,
|
||||||
);
|
);
|
||||||
static const EdgeInsets dialogMargin = EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0);
|
static const EdgeInsets dialogMargin = EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0);
|
||||||
|
|
||||||
|
// TODO(@vodemn) constrain dialogs with this value
|
||||||
|
static const double tabletMaxWidth = 600;
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,15 +79,18 @@ class TimerScreenState extends State<TimerScreen> with TickerProviderStateMixin
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(Dimens.paddingL),
|
padding: const EdgeInsets.all(Dimens.paddingL),
|
||||||
child: SizedBox.fromSize(
|
child: ConstrainedBox(
|
||||||
size: Size.square(MediaQuery.sizeOf(context).width - Dimens.paddingL * 4),
|
constraints: const BoxConstraints(maxHeight: Dimens.tabletMaxWidth, maxWidth: Dimens.tabletMaxWidth),
|
||||||
child: ValueListenableBuilder(
|
child: SizedBox.fromSize(
|
||||||
valueListenable: timelineAnimation,
|
size: Size.square(MediaQuery.sizeOf(context).width - Dimens.paddingL * 4),
|
||||||
builder: (_, value, child) => TimerTimeline(
|
child: ValueListenableBuilder(
|
||||||
progress: value,
|
valueListenable: timelineAnimation,
|
||||||
child: TimerText(
|
builder: (_, value, child) => TimerTimeline(
|
||||||
timeLeft: Duration(milliseconds: (widget.duration.inMilliseconds * value).toInt()),
|
progress: value,
|
||||||
duration: widget.duration,
|
child: TimerText(
|
||||||
|
timeLeft: Duration(milliseconds: (widget.duration.inMilliseconds * value).toInt()),
|
||||||
|
duration: widget.duration,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -44,6 +44,7 @@ dependencies:
|
||||||
vibration: 1.8.1
|
vibration: 1.8.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
args: 2.5.0
|
||||||
bloc_test: 9.1.3
|
bloc_test: 9.1.3
|
||||||
build_runner: 2.4.6
|
build_runner: 2.4.6
|
||||||
flutter_native_splash: 2.3.5
|
flutter_native_splash: 2.3.5
|
||||||
|
@ -51,9 +52,11 @@ dev_dependencies:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
golden_toolkit: 0.15.0
|
golden_toolkit: 0.15.0
|
||||||
google_fonts: 3.0.1
|
google_fonts: 3.0.1
|
||||||
|
image: 4.1.7
|
||||||
integration_test:
|
integration_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
lint: 2.1.2
|
lint: 2.1.2
|
||||||
|
logging: 1.2.0
|
||||||
meta: 1.9.1
|
meta: 1.9.1
|
||||||
mocktail: 0.3.0
|
mocktail: 0.3.0
|
||||||
test: 1.24.3
|
test: 1.24.3
|
||||||
|
|
|
@ -17,28 +17,35 @@ As a user I want to see the most relevant screenshots in the store, so that I ca
|
||||||
- Settings screen
|
- Settings screen
|
||||||
|
|
||||||
1. Just the screen
|
1. Just the screen
|
||||||
2. Opened metering screen layout features dialog
|
|
||||||
|
|
||||||
- Equipment profiles screen
|
- Equipment profiles screen
|
||||||
|
|
||||||
1. Just the screen
|
1. Just the screen
|
||||||
2. Opened equipment profile ISO picker
|
2. Opened equipment profile ISO picker
|
||||||
|
|
||||||
|
- Timer screen
|
||||||
|
|
||||||
|
1. Just the screen
|
||||||
|
|
||||||
> \*also in dark mode
|
> \*also in dark mode
|
||||||
|
|
||||||
> \*\*Android only
|
> \*\*Android only
|
||||||
|
|
||||||
## Run the generator
|
## Run the generator
|
||||||
|
|
||||||
Screenshots will be stored in the _screenshots/generated/\<platform\>/_ folder.
|
Release screenshots will be stored in the _screenshots/generated/\<platform\>/_ folder.
|
||||||
|
|
||||||
### Android
|
Raw screenshots will be stored in the _screenshots/generated/raw/\<platform\>/_ folder.
|
||||||
|
|
||||||
|
### Generate raw screenshots
|
||||||
|
|
||||||
|
#### Android
|
||||||
|
|
||||||
```console
|
```console
|
||||||
sh screenshots/generate_screenshots.sh <deviceName>
|
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.
|
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
|
sh screenshots/generate_ios_screenshots.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Apply store constraints and text data
|
||||||
|
|
||||||
|
```console
|
||||||
|
sh screenshots/scripts/convert_to_store_screenshots.sh
|
||||||
|
```
|
||||||
|
|
||||||
## List of devices
|
## List of devices
|
||||||
|
|
||||||
### Android
|
### Android
|
||||||
|
@ -56,9 +69,5 @@ sh screenshots/generate_ios_screenshots.sh
|
||||||
|
|
||||||
### iOS
|
### iOS
|
||||||
|
|
||||||
- iPhone 8 Plus
|
|
||||||
- iPhone 13 Pro
|
- iPhone 13 Pro
|
||||||
- iPhone 13 Pro Max
|
|
||||||
- iPhone 15 Pro
|
|
||||||
- iPhone 15 Pro Max
|
|
||||||
- iPad Pro (12.9-inch) (6th generation)
|
- iPad Pro (12.9-inch) (6th generation)
|
||||||
|
|
44
screenshots/assets/content/screenshot_titles_en.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
screenshots/assets/fonts/SF-Pro-Display-Bold-dark.zip
Normal file
BIN
screenshots/assets/fonts/SF-Pro-Display-Bold.zip
Normal file
BIN
screenshots/assets/fonts/SF-Pro-Display-Regular-dark.zip
Normal file
BIN
screenshots/assets/fonts/SF-Pro-Display-Regular.zip
Normal file
BIN
screenshots/assets/frames/android/pixel_6_frame.png
Normal file
After Width: | Height: | Size: 198 KiB |
BIN
screenshots/assets/frames/ios/iphone_13_pro_frame.png
Normal file
After Width: | Height: | Size: 219 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 16 KiB |
189
screenshots/convert_to_store_screenshots.dart
Normal file
|
@ -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<int> main(List<String> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
@ -5,44 +7,58 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
import 'package:lightmeter/data/models/ev_source_type.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/metering_screen_layout_config.dart';
|
||||||
import 'package:lightmeter/data/models/theme_type.dart';
|
import 'package:lightmeter/data/models/theme_type.dart';
|
||||||
import 'package:lightmeter/data/models/volume_action.dart';
|
import 'package:lightmeter/data/models/volume_action.dart';
|
||||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
import 'package:lightmeter/res/theme.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/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/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/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:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../integration_test/mocks/paid_features_mock.dart';
|
import '../integration_test/mocks/paid_features_mock.dart';
|
||||||
import '../integration_test/utils/widget_tester_actions.dart';
|
import '../integration_test/utils/widget_tester_actions.dart';
|
||||||
|
import 'models/screenshot_args.dart';
|
||||||
|
|
||||||
//https://stackoverflow.com/a/67186625/13167574
|
//https://stackoverflow.com/a/67186625/13167574
|
||||||
|
|
||||||
const _mockFilm = Film('Ilford HP5+', 400);
|
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.
|
/// Just a screenshot generator. No expectations here.
|
||||||
void main() {
|
void main() {
|
||||||
final binding = IntegrationTestWidgetsFlutterBinding();
|
final binding = IntegrationTestWidgetsFlutterBinding();
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
final Color lightThemeColor = primaryColorsList[5];
|
|
||||||
final Color darkThemeColor = primaryColorsList[3];
|
|
||||||
|
|
||||||
void mockSharedPrefs(ThemeType theme, Color color) {
|
void mockSharedPrefs({
|
||||||
// ignore: invalid_use_of_visible_for_testing_member
|
int iso = 400,
|
||||||
|
int nd = 0,
|
||||||
|
double calibration = 0.0,
|
||||||
|
required ThemeType theme,
|
||||||
|
required Color color,
|
||||||
|
}) {
|
||||||
SharedPreferences.setMockInitialValues({
|
SharedPreferences.setMockInitialValues({
|
||||||
/// Metering values
|
/// Metering values
|
||||||
UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index,
|
UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index,
|
||||||
UserPreferencesService.isoKey: 400,
|
UserPreferencesService.isoKey: iso,
|
||||||
UserPreferencesService.ndFilterKey: 0,
|
UserPreferencesService.ndFilterKey: nd,
|
||||||
|
|
||||||
/// Metering settings
|
/// Metering settings
|
||||||
UserPreferencesService.stopTypeKey: StopType.third.index,
|
UserPreferencesService.stopTypeKey: StopType.third.index,
|
||||||
UserPreferencesService.cameraEvCalibrationKey: 0.0,
|
UserPreferencesService.cameraEvCalibrationKey: calibration,
|
||||||
UserPreferencesService.lightSensorEvCalibrationKey: 0.0,
|
UserPreferencesService.lightSensorEvCalibrationKey: calibration,
|
||||||
UserPreferencesService.meteringScreenLayoutKey: json.encode(
|
UserPreferencesService.meteringScreenLayoutKey: json.encode(
|
||||||
{
|
{
|
||||||
MeteringScreenLayoutFeature.equipmentProfiles: true,
|
MeteringScreenLayoutFeature.equipmentProfiles: true,
|
||||||
|
@ -52,6 +68,7 @@ void main() {
|
||||||
),
|
),
|
||||||
|
|
||||||
/// General settings
|
/// General settings
|
||||||
|
UserPreferencesService.autostartTimerKey: false,
|
||||||
UserPreferencesService.caffeineKey: true,
|
UserPreferencesService.caffeineKey: true,
|
||||||
UserPreferencesService.hapticsKey: true,
|
UserPreferencesService.hapticsKey: true,
|
||||||
UserPreferencesService.volumeActionKey: VolumeAction.shutter.toString(),
|
UserPreferencesService.volumeActionKey: VolumeAction.shutter.toString(),
|
||||||
|
@ -70,7 +87,7 @@ void main() {
|
||||||
|
|
||||||
/// Generates several screenshots with the light theme
|
/// Generates several screenshots with the light theme
|
||||||
testWidgets('Generate light theme screenshots', (tester) async {
|
testWidgets('Generate light theme screenshots', (tester) async {
|
||||||
mockSharedPrefs(ThemeType.light, lightThemeColor);
|
mockSharedPrefs(theme: ThemeType.light, color: _lightThemeColor);
|
||||||
await tester.pumpApplication(
|
await tester.pumpApplication(
|
||||||
availableFilms: [_mockFilm],
|
availableFilms: [_mockFilm],
|
||||||
filmsInUse: [_mockFilm],
|
filmsInUse: [_mockFilm],
|
||||||
|
@ -78,43 +95,39 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.takePhoto();
|
await tester.takePhoto();
|
||||||
await tester.takeScreenshot(binding, 'light-metering_reflected');
|
await tester.takeScreenshotLight(binding, 'metering-reflected');
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
|
await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.toggleIncidentMetering(7.3);
|
await tester.toggleIncidentMetering(7.3);
|
||||||
await tester.takeScreenshot(binding, 'light-metering_incident');
|
await tester.takeScreenshotLight(binding, 'metering-incident');
|
||||||
}
|
}
|
||||||
|
|
||||||
await tester.openAnimatedPicker<IsoValuePicker>();
|
await tester.openAnimatedPicker<IsoValuePicker>();
|
||||||
await tester.takeScreenshot(binding, 'light-metering_iso_picker');
|
await tester.takeScreenshotLight(binding, 'metering-iso-picker');
|
||||||
|
|
||||||
await tester.tapCancelButton();
|
await tester.tapCancelButton();
|
||||||
await tester.tap(find.byTooltip(S.current.tooltipOpenSettings));
|
await tester.tap(find.byTooltip(S.current.tooltipOpenSettings));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.takeScreenshot(binding, 'light-settings');
|
await tester.takeScreenshotLight(binding, 'settings');
|
||||||
|
|
||||||
await tester.tapDescendantTextOf<SettingsScreen>(S.current.meteringScreenLayout);
|
|
||||||
await tester.takeScreenshot(binding, 'light-settings_metering_screen_layout');
|
|
||||||
|
|
||||||
await tester.tapCancelButton();
|
|
||||||
await tester.tapDescendantTextOf<SettingsScreen>(S.current.equipmentProfiles);
|
await tester.tapDescendantTextOf<SettingsScreen>(S.current.equipmentProfiles);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tapDescendantTextOf<EquipmentProfilesScreen>(mockEquipmentProfiles.first.name);
|
await tester.tapDescendantTextOf<EquipmentProfilesScreen>(mockEquipmentProfiles.first.name);
|
||||||
await tester.pumpAndSettle();
|
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.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
|
/// and the additionally the first one with the dark theme
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'Generate dark theme screenshots',
|
'Generate dark theme screenshots',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
mockSharedPrefs(ThemeType.dark, darkThemeColor);
|
mockSharedPrefs(theme: ThemeType.dark, color: _darkThemeColor);
|
||||||
await tester.pumpApplication(
|
await tester.pumpApplication(
|
||||||
availableFilms: [_mockFilm],
|
availableFilms: [_mockFilm],
|
||||||
filmsInUse: [_mockFilm],
|
filmsInUse: [_mockFilm],
|
||||||
|
@ -122,14 +135,39 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.takePhoto();
|
await tester.takePhoto();
|
||||||
await tester.takeScreenshot(binding, 'dark-metering_reflected');
|
await tester.takeScreenshotDark(binding, 'metering-reflected');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
testWidgets(
|
||||||
await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
|
'Generate timer screenshot',
|
||||||
await tester.pumpAndSettle();
|
(tester) async {
|
||||||
await tester.toggleIncidentMetering(7.3);
|
const timerExposurePair = ExposurePair(
|
||||||
await tester.takeScreenshot(binding, 'dark-metering_incident');
|
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';
|
final String _platformFolder = Platform.isAndroid ? 'android' : 'ios';
|
||||||
|
|
||||||
extension on WidgetTester {
|
extension on WidgetTester {
|
||||||
Future<void> takeScreenshot(IntegrationTestWidgetsFlutterBinding binding, String name) async {
|
Future<void> takeScreenshotLight(IntegrationTestWidgetsFlutterBinding binding, String name) =>
|
||||||
await binding.takeScreenshot("$_platformFolder/${const String.fromEnvironment('deviceName')}/$name");
|
_takeScreenshot(binding, name, _themeLight);
|
||||||
|
Future<void> takeScreenshotDark(IntegrationTestWidgetsFlutterBinding binding, String name) =>
|
||||||
|
_takeScreenshot(binding, name, _themeDark);
|
||||||
|
|
||||||
|
Future<void> _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();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension on WidgetTester {
|
||||||
|
Future<void> 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<void> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
After Width: | Height: | Size: 492 KiB |
After Width: | Height: | Size: 221 KiB |
After Width: | Height: | Size: 221 KiB |
After Width: | Height: | Size: 235 KiB |
After Width: | Height: | Size: 220 KiB |
After Width: | Height: | Size: 500 KiB |
BIN
screenshots/generated/android/android/light_settings.png
Normal file
After Width: | Height: | Size: 235 KiB |
BIN
screenshots/generated/android/android/light_timer.png
Normal file
After Width: | Height: | Size: 219 KiB |
After Width: | Height: | Size: 346 KiB |
After Width: | Height: | Size: 159 KiB |
After Width: | Height: | Size: 163 KiB |
After Width: | Height: | Size: 177 KiB |
After Width: | Height: | Size: 345 KiB |
BIN
screenshots/generated/ios/iphone55inch/light_settings.png
Normal file
After Width: | Height: | Size: 167 KiB |
BIN
screenshots/generated/ios/iphone55inch/light_timer.png
Normal file
After Width: | Height: | Size: 148 KiB |
After Width: | Height: | Size: 507 KiB |
After Width: | Height: | Size: 223 KiB |
After Width: | Height: | Size: 227 KiB |
After Width: | Height: | Size: 248 KiB |
After Width: | Height: | Size: 501 KiB |
BIN
screenshots/generated/ios/iphone65inch/light_settings.png
Normal file
After Width: | Height: | Size: 235 KiB |
BIN
screenshots/generated/ios/iphone65inch/light_timer.png
Normal file
After Width: | Height: | Size: 206 KiB |
60
screenshots/models/screenshot_args.dart
Normal file
|
@ -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<String, dynamic>);
|
||||||
|
|
||||||
|
factory ScreenshotArgs._fromJson(Map<String, dynamic> 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<String, dynamic> _toJson() {
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"deviceName": deviceName,
|
||||||
|
"platformFolder": platformFolder,
|
||||||
|
"backgroundColor": backgroundColor,
|
||||||
|
"isDark": isDark,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
19
screenshots/models/screenshot_config.dart
Normal file
|
@ -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<String, dynamic> data) {
|
||||||
|
return ScreenshotConfig(
|
||||||
|
title: data['title'] as String,
|
||||||
|
subtitle: data['subtitle'] as String,
|
||||||
|
screenshotName: data['screenshotName'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
49
screenshots/models/screenshot_device.dart
Normal file
|
@ -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 = <String, ScreenshotDevice>{
|
||||||
|
for (final d in _screenshotDevicesAndroid + _screenshotDevicesIos) d.name: d,
|
||||||
|
};
|
||||||
|
|
||||||
|
final List<ScreenshotDevice> _screenshotDevicesAndroid = [
|
||||||
|
ScreenshotDevice.fromDisplayName(
|
||||||
|
displayName: 'Pixel 6',
|
||||||
|
platform: ScreenshotDevicePlatform.android,
|
||||||
|
screenshotFrameOffset: (dx: 67, dy: 66),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final List<ScreenshotDevice> _screenshotDevicesIos = [
|
||||||
|
ScreenshotDevice.fromDisplayName(
|
||||||
|
displayName: 'iPhone 8 Plus',
|
||||||
|
platform: ScreenshotDevicePlatform.ios,
|
||||||
|
),
|
||||||
|
ScreenshotDevice.fromDisplayName(
|
||||||
|
displayName: 'iPhone 13 Pro',
|
||||||
|
platform: ScreenshotDevicePlatform.ios,
|
||||||
|
screenshotFrameOffset: (dx: 72, dy: 60),
|
||||||
|
),
|
||||||
|
];
|
35
screenshots/models/screenshot_layout.dart
Normal file
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
4
screenshots/scripts/convert_to_store_screenshots.sh
Normal file
|
@ -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
|
1
screenshots/scripts/generate_android_screenshots.sh
Normal file
|
@ -0,0 +1 @@
|
||||||
|
sh screenshots/scripts/generate_screenshots.sh "Pixel 6"
|
|
@ -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
|
open -a Simulator
|
||||||
|
for i in "${simulators_array[@]}"; do # https://www.baeldung.com/linux/shell-script-iterate-over-string-list#2-understanding--and--special-indices
|
||||||
for i in "${devices_array[@]}"; do # https://www.baeldung.com/linux/shell-script-iterate-over-string-list#2-understanding--and--special-indices
|
|
||||||
echo "$i"
|
echo "$i"
|
||||||
xcrun simctl boot "$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"
|
sh screenshots/scripts/generate_screenshots.sh "$i"
|
||||||
done
|
done
|
||||||
|
|
||||||
killall 'Simulator'
|
killall 'Simulator'
|
||||||
|
|
14
screenshots/utils/parse_configs.dart
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../models/screenshot_config.dart';
|
||||||
|
|
||||||
|
Map<String, ScreenshotConfig> parseScreenshotConfigs([String locale = 'en']) {
|
||||||
|
final configPath = 'screenshots/assets/content/screenshot_titles_$locale.json';
|
||||||
|
final data = jsonDecode(File(configPath).readAsStringSync()) as Map<String, dynamic>;
|
||||||
|
final entries = (data['screenshots'] as List).map((value) {
|
||||||
|
final config = ScreenshotConfig.fromJson(value as Map<String, dynamic>);
|
||||||
|
return MapEntry(config.screenshotName, config);
|
||||||
|
});
|
||||||
|
return Map.fromEntries(entries);
|
||||||
|
}
|
|
@ -79,7 +79,7 @@ extension WidgetTesterActions on WidgetTester {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AnimatedDialogPicker<int>(
|
child: AnimatedDialogPicker<int>(
|
||||||
icon: Icons.iso,
|
icon: Icons.iso_outlined,
|
||||||
title: '',
|
title: '',
|
||||||
subtitle: '',
|
subtitle: '',
|
||||||
selectedValue: 0,
|
selectedValue: 0,
|
||||||
|
|
|
@ -51,7 +51,7 @@ extension WidgetTesterActions on WidgetTester {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AnimatedDialogPicker<int>(
|
child: AnimatedDialogPicker<int>(
|
||||||
icon: Icons.iso,
|
icon: Icons.iso_outlined,
|
||||||
title: '',
|
title: '',
|
||||||
subtitle: '',
|
subtitle: '',
|
||||||
selectedValue: 0,
|
selectedValue: 0,
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:integration_test/integration_test_driver_extended.dart';
|
import 'package:integration_test/integration_test_driver_extended.dart';
|
||||||
|
|
||||||
import 'utils/grant_camera_permission.dart';
|
import '../screenshots/models/screenshot_args.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
await grantCameraPermission();
|
|
||||||
await integrationDriver(
|
await integrationDriver(
|
||||||
onScreenshot: (name, bytes, [args]) async {
|
onScreenshot: (name, bytes, [_]) async {
|
||||||
final File image = await File('screenshots/generated/$name.png').create(recursive: true);
|
final screenshotArgs = ScreenshotArgs.fromString(name);
|
||||||
image.writeAsBytesSync(bytes);
|
final file = await File(screenshotArgs.toPathRaw()).create(recursive: true);
|
||||||
|
file.writeAsBytesSync(bytes);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|