Skip to content

Instantly share code, notes, and snippets.

@andrewpmoore
Created June 19, 2024 08:53
Show Gist options
  • Save andrewpmoore/64f9abd40a2497a04803fcba962372d0 to your computer and use it in GitHub Desktop.
Save andrewpmoore/64f9abd40a2497a04803fcba962372d0 to your computer and use it in GitHub Desktop.
App store and Play store flutter image creation
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:gusty/src/extensions/extensions.dart';
import 'package:path_provider/path_provider.dart';
import 'package:screenshot/screenshot.dart';
import 'package:window_manager/window_manager.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:image/image.dart' as img_lib;
import 'dart:typed_data';
import 'dart:io';
import '../../../generated/l10n.dart';
import '../../../main.dart';
/// Simple widget to wrap around the scaffold of a page that allows you to use keyboard shortcuts
/// and create a collection of framed images suitable for the play store and app store
/// Usage
/// Wrap StoreImages around a page's scaffold
/// Launch the app on desktop, go to the page press CTRL+1 (script handles CTRL+1 to CTRL+8)
/// Each is defined as a screen frame with a set of colours and promo text to show.
/// Then press CTRL+S
/// This will then go through all screen sizes defined in _sizes and all languages defined in _languages
/// and create a screenshot for each, both with and without the frame, change frameList, if you do/won't
/// want the frame
/// Files will be created in a format suitable for uploading with fastlane
/// Pressing CTRL+H will remove the frame from your view
/// Tweak as necessary
class StoreImages extends ConsumerStatefulWidget {
final Widget child;
const StoreImages({super.key, required this.child});
@override
ConsumerState<StoreImages> createState() => _StoreImagesState();
}
class _StoreImagesState extends ConsumerState<StoreImages> {
final ScreenshotController _screenshotController = ScreenshotController();
bool showScreenshot = false;
int imageNumber = 0;
final double heightFrame = 166;
final double variableWidthFrame = 120;
final List<(String, Size, double, String, String, bool)> _sizes = [
const ('APP_IPHONE_55_5', Size(1242, 2208), 3, 'ios','', false),
const ('APP_IPHONE_67_0', Size(1290, 2796), 3, 'ios','', false),
const ('APP_IPAD_PRO_3GEN_129_0', Size(2048, 2732), 2, 'ios','', true ),
const ('APP_IPAD_PRO_129_0', Size(2048, 2732), 2, 'ios','', true ),
const ('android_phone', Size(1080, 1920), 2, 'android','phoneScreenshots', false ),
const ('android_tablet_7', Size(1200, 1920), 2, 'android','sevenInchScreenshots', true ),
const ('android_tablet_10', Size(1600, 2560), 2, 'android','tenInchScreenshots', true ),
];
List<(String, String)> _storeStrings = [];
final _colorList = [
[const Color(0xffff8990), const Color(0xff174888), const Color(0xff0d8ab7)],
[const Color(0xffFAD7A1), const Color(0xff063f80), const Color(0xff021326)],
[const Color(0xff6DD5FA), const Color(0xff4286f4), const Color(0xff373B44)],
[const Color(0xffFFFFFF), const Color(0xff2684d0), const Color(0xff1A2980)],
[Colors.yellow, Colors.orange, Colors.red, Colors.purple, const Color(0xff1a225f), const Color(0xff2E0580), ], // Deep blue to light aqua
[const Color(0xffF0C27B), const Color(0xffF56217), const Color(0xff0B486B)],
[const Color(0xff802338), const Color(0xff4389A2), const Color(0xffEECDA3)],
[const Color(0xffEDB174), const Color(0xff859398), const Color(0xff283048)],
];
//create all images with (true) and without (false) the frames
List<bool> frameList = [true, false, ]; //true, false
//languages required, language code, and then the android, ios language codes for directory creation
final List<(String, String, String)> _languages = [
('de', 'de-DE', 'de-DE'),
('es', 'es-ES', 'es-ES'),
('fr', 'fr-FR', 'fr-FR'),
('it', 'it-IT', 'it'),
('ja', 'ja-JP', 'ja'),
('ko', 'ko-KR', 'ko'),
('nl', 'nl-NL', 'nl-NL'),
('no', 'no-NO', 'no'),
('pl', 'pl-PL', 'pl'),
('ru', 'ru-RU', 'ru'),
('zh', 'zh-CN', 'zh-Hans'), // Simplified Chinese
('pt', 'pt-PT', 'pt-PT'),
('en', 'en-US', 'en-US'),
('hi', 'hi-IN', 'hi'), // Hindi (India)
('ms', 'ms-MY', 'ms'), // Malay (Malaysia)
];
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
if (!kDebugMode) {
return widget.child;
}
_storeStrings = [
(S.of(context).theUltimateWeatherDashboard,'portrait'),
(S.of(context).personalWeatherStationConnectivity,'portrait'),
(S.of(context).selectionOfForecastProviders,'portrait'),
(S.of(context).monitorMultipleLocations,'portrait'),
(S.of(context).fortyPlusColorThemesIncludingDynamicWeatherThemes,'portrait'),
(S.of(context).customWidgetsForPersonalizedForecasts,'portrait'),
(S.of(context).seeRainMapForecasts,'landscape'),
(S.of(context).interactiveWeatherMaps,'landscape'),
];
return Screenshot(
controller: _screenshotController,
child: Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit1): const ActivateIntent(1),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit2): const ActivateIntent(2),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit3): const ActivateIntent(3),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit4): const ActivateIntent(4),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit5): const ActivateIntent(5),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit6): const ActivateIntent(6),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit7): const ActivateIntent(7),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit8): const ActivateIntent(8),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyH): const ActivateIntent(10),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyS): const ActivateIntent(20),
},
child: Actions(
actions: <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(
onInvoke: (ActivateIntent intent) {
_handleCaptureScreenIntent(intent.number);
return null;
},
),
},
child: Focus(
autofocus: true,
child: !showScreenshot ? widget.child : Container(
decoration: BoxDecoration(
gradient: LinearGradient(colors: _colorList[imageNumber - 1],
begin: imageNumber % 2 == 1 ? Alignment.bottomRight : Alignment.bottomCenter,
end: imageNumber % 2 == 1 ? Alignment.topLeft : Alignment.topCenter)
),
child: Stack(
children: [
Padding(
padding: EdgeInsets.only(top: 150.0, left: variableWidthFrame/2, right: variableWidthFrame/2),
child: Container(
decoration: const BoxDecoration(color: Colors.grey,
borderRadius: BorderRadius.only(topLeft: Radius.circular(26), topRight: Radius.circular(26)),
),
child: Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16, top: 16),
child: ClipRRect(
borderRadius: const BorderRadius.only(topLeft: Radius.circular(16), topRight: Radius.circular(16)),
child: widget.child),
),
),
),
Positioned(
top: 30,
left: 0,
right: 0,
child: Material(
color: Colors.transparent,
child: SizedBox(
height: 100,
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 68),
child: AutoSizeText(
_storeStrings[imageNumber - 1].$1,
maxLines: 2,
textAlign: TextAlign.center,
style: context.style.displayMedium!.copyWith(fontWeight: FontWeight.w800, fontFamily: GoogleFonts
.outfit()
.fontFamily),
),
))))),
],
),
),
),
),
),
);
}
void _handleCaptureScreenIntent(int number) async {
if (number < 10) {
setState(() {
imageNumber = number;
});
}
if (number == 10) {
//hide process ctrl-h
setState(() {
showScreenshot = false;
});
return;
}
if (number == 20) {
//save process ctrl-s
await windowManager.ensureInitialized();
for (var withFrame in frameList) {
setState(() {
showScreenshot = withFrame;
});
for (var (device, size, density, platform, appDir, tablet) in _sizes) {
//switch to landscape if set for that and the device is a tablet
if (_storeStrings[imageNumber - 1].$2=='landscape'&&tablet){
size = Size(size.height, size.width); //switch the dimensions around
}
for (var (lang, androidLang, iosLang) in _languages) {
String directory = '${(await getApplicationDocumentsDirectory()).path}/screenshots/${withFrame ? 'frame' : 'frameless'}';
if (platform=='android'){
directory = '$directory/android/metadata/android/$androidLang/images/$appDir';
} else if (platform=='ios'){
directory = '$directory/ios/screenshots/$iosLang';
}
//change the provider to the current locale (this is using riverpod, but any other state provider management that's listening for lang changes would work
ref.watch(settingsProvider).updateLocale(Locale(lang));
WindowOptions windowOptions = WindowOptions(
size: Size((size.width / density) + (withFrame ? variableWidthFrame+32 : 0), (size.height / density) + (withFrame ? heightFrame : 0)),
center: true,
alwaysOnTop: false,
);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
String fileName = '${imageNumber}_$device.jpg';
await _screenshotController.captureAndSave(directory, fileName: fileName, pixelRatio: density, delay: const Duration(milliseconds: 2000));
// print('resize $directory/$fileName');
await resizeAndCropImage('$directory/$fileName', size.width.toInt(), size.height.toInt()); //size.width
}
}
}
// print('Save complete');
return;
}
setState(() {
showScreenshot = true;
});
}
Future<void> resizeAndCropImage(String filePath, int newWidth, int newHeight) async {
// Read the image file
final File file = File(filePath);
final List<int> imageBytes = await file.readAsBytes();
// Convert List<int> to Uint8List
final Uint8List uint8list = Uint8List.fromList(imageBytes);
// Decode the image
img_lib.Image originalImage = img_lib.decodeImage(uint8list)!;
// Calculate the aspect ratios
double originalAspectRatio = originalImage.width / originalImage.height;
double newAspectRatio = newWidth / newHeight;
int resizeWidth;
int resizeHeight;
// Determine new dimensions that cover the area
if (originalAspectRatio > newAspectRatio) {
// Fit to height
resizeHeight = newHeight;
resizeWidth = (newHeight * originalAspectRatio).round();
} else {
// Fit to width
resizeWidth = newWidth;
resizeHeight = (newWidth / originalAspectRatio).round();
}
// Resize the image while maintaining aspect ratio
img_lib.Image resizedImage = img_lib.copyResize(
originalImage,
width: resizeWidth,
height: resizeHeight,
);
// Calculate crop coordinates
int cropX = (resizeWidth - newWidth) ~/ 2;
int cropY = (resizeHeight - newHeight) ~/ 2;
// Crop the image to the specified dimensions
img_lib.Image croppedImage = img_lib.copyCrop(
resizedImage,
x: cropX,
y: cropY,
width: newWidth,
height: newHeight,
);
// Encode the resized and cropped image
List<int> croppedImageBytes = img_lib.encodeJpg(croppedImage);
// Save the resized and cropped image to the same file
await file.writeAsBytes(croppedImageBytes);
}
}
class ActivateIntent extends Intent {
const ActivateIntent(this.number);
final int number;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment