From 4a49ff8344db741bf55008207c44d5ed1521c836 Mon Sep 17 00:00:00 2001 From: Ratakondala Arun Date: Fri, 8 Jul 2022 21:49:11 +0530 Subject: [PATCH] feat(web): support for web icons --- lib/abs/icon_generator.dart | 75 ++++++++++++ lib/custom_exceptions.dart | 10 ++ lib/flutter_launcher_icons_config.dart | 145 ++++++++++++++++++++++ lib/flutter_launcher_icons_config.g.dart | 78 ++++++++++++ lib/main.dart | 84 +++++++++---- lib/utils.dart | 37 ++++++ lib/web/web_icon_generator.dart | 148 +++++++++++++++++++++++ lib/web/web_template.dart | 31 +++++ 8 files changed, 583 insertions(+), 25 deletions(-) create mode 100644 lib/abs/icon_generator.dart create mode 100644 lib/flutter_launcher_icons_config.dart create mode 100644 lib/flutter_launcher_icons_config.g.dart create mode 100644 lib/web/web_icon_generator.dart create mode 100644 lib/web/web_template.dart diff --git a/lib/abs/icon_generator.dart b/lib/abs/icon_generator.dart new file mode 100644 index 0000000000..dcefb01e5c --- /dev/null +++ b/lib/abs/icon_generator.dart @@ -0,0 +1,75 @@ +import 'dart:io'; + +import 'package:flutter_launcher_icons/flutter_launcher_icons_config.dart'; +import 'package:flutter_launcher_icons/logger.dart'; + +///A base class to generate icons +abstract class IconGenerator { + final IconGeneratorContext context; + final String platformName; + + IconGenerator(this.context, this.platformName); + + /// Creates icons for this platform. + void createIcons(); + + /// Should return `true` if this platform + /// has all the requirments to create icons. + /// This runs before to [createIcons] + bool validateRequirments(); +} + +/// Provides easy access to user arguments and configuration +class IconGeneratorContext { + /// Contains configuration from configuration file + final FlutterLauncherIconsConfig config; + final FLILogger logger; + final String? flavor; + + IconGeneratorContext({required this.config, this.flavor, required this.logger}); + + /// Shortcut for `config.webConfig` + WebConfig? get webConfig => config.webConfig; +} + +/// Generates Icon for given platforms +void generateIconsFor({ + required FlutterLauncherIconsConfig config, + required String? flavor, + required FLILogger logger, + required List Function(IconGeneratorContext context) platforms, +}) { + try { + final platformList = platforms(IconGeneratorContext(config: config, logger: logger, flavor: flavor)); + if (platformList.isEmpty) { + // ? maybe we can print help + logger.info('No platform provided'); + } + + for (final platform in platformList) { + final progress = logger.progress('Creating Icons for ${platform.platformName}'); + logger.verbose('Validating platform requirments for ${platform.platformName}'); + // in case a platform throws an exception it should not effect other platforms + try { + if (!platform.validateRequirments()) { + logger.error('Requirments failed for platform ${platform.platformName}. Skipped'); + progress.cancel(); + continue; + } + platform.createIcons(); + progress.finish(message: 'done', showTiming: true); + } catch (e) { + progress.cancel(); + continue; + } + } + } catch (e, st) { + // todo: better error handling + // stacktrace should only print when verbose is turned on + // else a normal help line + logger + ..error(e.toString()) + ..verbose(st); + exit(1); + } +} diff --git a/lib/custom_exceptions.dart b/lib/custom_exceptions.dart index 3a297b0cd2..4ade63ab05 100644 --- a/lib/custom_exceptions.dart +++ b/lib/custom_exceptions.dart @@ -39,3 +39,13 @@ class NoDecoderForImageFormatException implements Exception { return generateError(this, message); } } + +class FileNotFoundException implements Exception { + const FileNotFoundException(this.fileName); + + final String fileName; + @override + String toString() { + return generateError(this, '$fileName file not found'); + } +} diff --git a/lib/flutter_launcher_icons_config.dart b/lib/flutter_launcher_icons_config.dart new file mode 100644 index 0000000000..75e6417e50 --- /dev/null +++ b/lib/flutter_launcher_icons_config.dart @@ -0,0 +1,145 @@ +import 'dart:io'; + +import 'package:checked_yaml/checked_yaml.dart' as yaml; +import 'package:json_annotation/json_annotation.dart'; + +import 'constants.dart' as constants; +import 'custom_exceptions.dart'; +import 'utils.dart' as utils; + +part 'flutter_launcher_icons_config.g.dart'; + +@JsonSerializable( + anyMap: true, + checked: true, +) +class FlutterLauncherIconsConfig { + /// Generic imagepath + @JsonKey(name: 'image_path') + final String? imagePath; + + /// Returns true or path if android config is enabled + final dynamic android; // path or bool + + /// Returns true or path if ios config is enabled + final dynamic ios; // path or bool + + /// Image path specific to android + @JsonKey(name: 'image_path_android') + final String? imagePathAndroid; + + /// Image path specific to ios + @JsonKey(name: 'image_path_ios') + final String? imagePathIOS; + + /// android adaptive icon foreground image + @JsonKey(name: 'adaptive_icon_foreground') + final String? adaptiveIconForeground; + + /// android adaptive_icon_background image + @JsonKey(name: 'adaptive_icon_background') + final String? adaptiveIconBackground; + + /// Web platform config + @JsonKey(name: 'web') + final WebConfig? webConfig; + + const FlutterLauncherIconsConfig({ + this.imagePath, + this.android = false, + this.ios = false, + this.imagePathAndroid, + this.imagePathIOS, + this.adaptiveIconForeground, + this.adaptiveIconBackground, + this.webConfig, + }); + + factory FlutterLauncherIconsConfig.fromJson(Map json) => _$FlutterLauncherIconsConfigFromJson(json); + + /// Loads flutter launcher icons configs from given [filePath] + static FlutterLauncherIconsConfig? loadConfigFromPath(String filePath) { + final configFile = File(filePath); + if (!configFile.existsSync()) { + return null; + } + final configContent = configFile.readAsStringSync(); + try { + return yaml.checkedYamlDecode( + configContent, + (json) => FlutterLauncherIconsConfig.fromJson(json!['flutter_icons']), + ); + } on yaml.ParsedYamlException catch (e) { + throw InvalidConfigException(e.formattedMessage); + } catch (e) { + rethrow; + } + } + + /// Loads flutter launcher icons config from `pubspec.yaml` file + static FlutterLauncherIconsConfig? loadConfigFromPubSpec() { + try { + final pubspecFile = File(constants.pubspecFilePath); + if (!pubspecFile.existsSync()) { + return null; + } + final pubspecContent = pubspecFile.readAsStringSync(); + return yaml.checkedYamlDecode( + pubspecContent, + (json) { + if (json!['flutter_icons'] == null) { + return null; + } + return FlutterLauncherIconsConfig.fromJson(json['flutter_icons']); + }, + ); + } on yaml.ParsedYamlException catch (e) { + throw InvalidConfigException(e.formattedMessage); + } catch (e) { + rethrow; + } + } + + static FlutterLauncherIconsConfig? loadConfigFromFlavor(String flavor) { + return FlutterLauncherIconsConfig.loadConfigFromPath(utils.flavorConfigFile(flavor)); + } + + Map toJson() => _$FlutterLauncherIconsConfigToJson(this); + + @override + String toString() => 'FlutterLauncherIconsConfig: ${toJson()}'; +} + +@JsonSerializable( + anyMap: true, + checked: true, +) +class WebConfig { + final bool generate; + + /// Image path for web + @JsonKey(name: 'image_path') + final String? imagePath; + + /// manifest.json's background_color + @JsonKey(name: 'background_color') + final String? backgroundColor; + + /// manifest.json's theme_color + @JsonKey(name: 'theme_color') + final String? themeColor; + + const WebConfig({ + this.generate = false, + this.imagePath, + this.backgroundColor, + this.themeColor, + }); + + factory WebConfig.fromJson(Map json) => _$WebConfigFromJson(json); + + Map toJson() => _$WebConfigToJson(this); + + @override + String toString() => 'WebConfig: ${toJson()}'; +} diff --git a/lib/flutter_launcher_icons_config.g.dart b/lib/flutter_launcher_icons_config.g.dart new file mode 100644 index 0000000000..c3d85f33a5 --- /dev/null +++ b/lib/flutter_launcher_icons_config.g.dart @@ -0,0 +1,78 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'flutter_launcher_icons_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +FlutterLauncherIconsConfig _$FlutterLauncherIconsConfigFromJson(Map json) => + $checkedCreate( + 'FlutterLauncherIconsConfig', + json, + ($checkedConvert) { + final val = FlutterLauncherIconsConfig( + imagePath: $checkedConvert('image_path', (v) => v as String?), + android: $checkedConvert('android', (v) => v ?? false), + ios: $checkedConvert('ios', (v) => v ?? false), + imagePathAndroid: + $checkedConvert('image_path_android', (v) => v as String?), + imagePathIOS: $checkedConvert('image_path_ios', (v) => v as String?), + adaptiveIconForeground: + $checkedConvert('adaptive_icon_foreground', (v) => v as String?), + adaptiveIconBackground: + $checkedConvert('adaptive_icon_background', (v) => v as String?), + webConfig: $checkedConvert( + 'web', (v) => v == null ? null : WebConfig.fromJson(v as Map)), + ); + return val; + }, + fieldKeyMap: const { + 'imagePath': 'image_path', + 'imagePathAndroid': 'image_path_android', + 'imagePathIOS': 'image_path_ios', + 'adaptiveIconForeground': 'adaptive_icon_foreground', + 'adaptiveIconBackground': 'adaptive_icon_background', + 'webConfig': 'web' + }, + ); + +Map _$FlutterLauncherIconsConfigToJson( + FlutterLauncherIconsConfig instance) => + { + 'image_path': instance.imagePath, + 'android': instance.android, + 'ios': instance.ios, + 'image_path_android': instance.imagePathAndroid, + 'image_path_ios': instance.imagePathIOS, + 'adaptive_icon_foreground': instance.adaptiveIconForeground, + 'adaptive_icon_background': instance.adaptiveIconBackground, + 'web': instance.webConfig, + }; + +WebConfig _$WebConfigFromJson(Map json) => $checkedCreate( + 'WebConfig', + json, + ($checkedConvert) { + final val = WebConfig( + generate: $checkedConvert('generate', (v) => v as bool? ?? false), + imagePath: $checkedConvert('image_path', (v) => v as String?), + backgroundColor: + $checkedConvert('background_color', (v) => v as String?), + themeColor: $checkedConvert('theme_color', (v) => v as String?), + ); + return val; + }, + fieldKeyMap: const { + 'imagePath': 'image_path', + 'backgroundColor': 'background_color', + 'themeColor': 'theme_color' + }, + ); + +Map _$WebConfigToJson(WebConfig instance) => { + 'generate': instance.generate, + 'image_path': instance.imagePath, + 'background_color': instance.backgroundColor, + 'theme_color': instance.themeColor, + }; diff --git a/lib/main.dart b/lib/main.dart index d7176af4c6..785bdb0abe 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,17 +1,25 @@ import 'dart:io'; import 'package:args/args.dart'; +import 'package:flutter_launcher_icons/abs/icon_generator.dart'; +import 'package:flutter_launcher_icons/flutter_launcher_icons_config.dart'; +import 'package:flutter_launcher_icons/logger.dart'; +import 'package:flutter_launcher_icons/web/web_icon_generator.dart'; import 'package:path/path.dart' as path; import 'package:yaml/yaml.dart'; import 'package:flutter_launcher_icons/android.dart' as android_launcher_icons; import 'package:flutter_launcher_icons/ios.dart' as ios_launcher_icons; import 'package:flutter_launcher_icons/constants.dart'; +import 'package:flutter_launcher_icons/constants.dart' as constants; import 'package:flutter_launcher_icons/custom_exceptions.dart'; const String fileOption = 'file'; const String helpFlag = 'help'; +const String verboseFlag = 'verbose'; const String defaultConfigFile = 'flutter_launcher_icons.yaml'; const String flavorConfigFilePattern = r'^flutter_launcher_icons-(.*).yaml$'; + +/// todo: remove this as it is moved to utils.dart String flavorConfigFile(String flavor) => 'flutter_launcher_icons-$flavor.yaml'; List getFlavors() { @@ -32,9 +40,13 @@ Future createIconsFromArguments(List arguments) async { final ArgParser parser = ArgParser(allowTrailingOptions: true); parser.addFlag(helpFlag, abbr: 'h', help: 'Usage help', negatable: false); // Make default null to differentiate when it is explicitly set - parser.addOption(fileOption, - abbr: 'f', help: 'Config file (default: $defaultConfigFile)'); + parser.addOption(fileOption, abbr: 'f', help: 'Path to config file', defaultsTo: defaultConfigFile); + parser.addFlag(verboseFlag, abbr: 'v', help: 'Verbose output', defaultsTo: false); final ArgResults argResults = parser.parse(arguments); + // creating logger based on -v flag + final logger = FLILogger(argResults[verboseFlag]); + + logger.verbose('Recieved args ${argResults.arguments}'); if (argResults[helpFlag]) { stdout.writeln('Generates icons for iOS and Android'); @@ -47,17 +59,23 @@ Future createIconsFromArguments(List arguments) async { final hasFlavors = flavors.isNotEmpty; // Load the config file - final Map? yamlConfig = - loadConfigFileFromArgResults(argResults, verbose: true); + final Map? yamlConfig = loadConfigFileFromArgResults(argResults, verbose: true); - if (yamlConfig == null) { - throw const NoConfigFoundException(); + // Load configs from given file(defaults to ./flutter_launcher_icons.yaml) or from ./pubspec.yaml + + final flutterLauncherIconsConfigs = FlutterLauncherIconsConfig.loadConfigFromPath(argResults[fileOption]) ?? + FlutterLauncherIconsConfig.loadConfigFromPubSpec(); + if (yamlConfig == null || flutterLauncherIconsConfigs == null) { + throw NoConfigFoundException( + 'No configuration found in $defaultConfigFile or in ${constants.pubspecFilePath}. ' + 'In case file exists in different directory use --file option', + ); } // Create icons if (!hasFlavors) { try { - createIconsFromConfig(yamlConfig); + await createIconsFromConfig(yamlConfig, flutterLauncherIconsConfigs, logger); print('\n✓ Successfully generated launcher icons'); } catch (e) { stderr.writeln('\n✕ Could not generate launcher icons'); @@ -68,9 +86,8 @@ Future createIconsFromArguments(List arguments) async { try { for (String flavor in flavors) { print('\nFlavor: $flavor'); - final Map yamlConfig = - loadConfigFile(flavorConfigFile(flavor), flavorConfigFile(flavor)); - await createIconsFromConfig(yamlConfig, flavor); + final Map yamlConfig = loadConfigFile(flavorConfigFile(flavor), flavorConfigFile(flavor)); + await createIconsFromConfig(yamlConfig, flutterLauncherIconsConfigs, logger, flavor); } print('\n✓ Successfully generated launcher icons for flavors'); } catch (e) { @@ -81,8 +98,12 @@ Future createIconsFromArguments(List arguments) async { } } -Future createIconsFromConfig(Map config, - [String? flavor]) async { +Future createIconsFromConfig( + Map config, + FlutterLauncherIconsConfig flutterConfigs, + FLILogger logger, [ + String? flavor, +]) async { if (!isImagePathInConfig(config)) { throw const InvalidConfigException(errorMissingImagePath); } @@ -95,9 +116,7 @@ Future createIconsFromConfig(Map config, if (minSdk == 0) { throw const InvalidConfigException(errorMissingMinSdk); } - if (minSdk < 26 && - hasAndroidAdaptiveConfig(config) && - !hasAndroidConfig(config)) { + if (minSdk < 26 && hasAndroidAdaptiveConfig(config) && !hasAndroidConfig(config)) { throw const InvalidConfigException(errorMissingRegularAndroid); } } @@ -111,10 +130,20 @@ Future createIconsFromConfig(Map config, if (isNeedingNewIOSIcon(config)) { ios_launcher_icons.createIcons(config, flavor); } + + // Generates Icons for given platform + generateIconsFor( + config: flutterConfigs, + logger: logger, + flavor: flavor, + platforms: (context) => [ + WebIconGenerator(context), + // todo: add other platforms + ], + ); } -Map? loadConfigFileFromArgResults(ArgResults argResults, - {bool verbose = false}) { +Map? loadConfigFileFromArgResults(ArgResults argResults, {bool verbose = false}) { final String? configFile = argResults[fileOption]; final String? fileOptionResult = argResults[fileOption]; @@ -176,13 +205,12 @@ Map loadConfigFile(String path, String? fileOptionResult) { bool isImagePathInConfig(Map flutterIconsConfig) { return flutterIconsConfig.containsKey('image_path') || - (flutterIconsConfig.containsKey('image_path_android') && - flutterIconsConfig.containsKey('image_path_ios')); + (flutterIconsConfig.containsKey('image_path_android') && flutterIconsConfig.containsKey('image_path_ios')) || + flutterIconsConfig.containsKey('web'); } bool hasPlatformConfig(Map flutterIconsConfig) { - return hasAndroidConfig(flutterIconsConfig) || - hasIOSConfig(flutterIconsConfig); + return hasAndroidConfig(flutterIconsConfig) || hasIOSConfig(flutterIconsConfig) || hasWebConfig(flutterIconsConfig); } bool hasAndroidConfig(Map flutterLauncherIcons) { @@ -190,8 +218,7 @@ bool hasAndroidConfig(Map flutterLauncherIcons) { } bool isNeedingNewAndroidIcon(Map flutterLauncherIconsConfig) { - return hasAndroidConfig(flutterLauncherIconsConfig) && - flutterLauncherIconsConfig['android'] != false; + return hasAndroidConfig(flutterLauncherIconsConfig) && flutterLauncherIconsConfig['android'] != false; } bool hasAndroidAdaptiveConfig(Map flutterLauncherIconsConfig) { @@ -205,6 +232,13 @@ bool hasIOSConfig(Map flutterLauncherIconsConfig) { } bool isNeedingNewIOSIcon(Map flutterLauncherIconsConfig) { - return hasIOSConfig(flutterLauncherIconsConfig) && - flutterLauncherIconsConfig['ios'] != false; + return hasIOSConfig(flutterLauncherIconsConfig) && flutterLauncherIconsConfig['ios'] != false; +} + +bool hasWebConfig(Map flutterLauncherIconsConfig) { + return flutterLauncherIconsConfig.containsKey('web'); +} + +bool isNeddingNewWebIcons(Map flutterLauncherIconsConfig) { + return hasWebConfig(flutterLauncherIconsConfig) && flutterLauncherIconsConfig['web'] != false; } diff --git a/lib/utils.dart b/lib/utils.dart index 71491bfad5..d87e9d0601 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1,6 +1,8 @@ +import 'dart:convert'; import 'dart:io'; import 'package:image/image.dart'; +import 'package:path/path.dart' as path; import 'custom_exceptions.dart'; @@ -38,3 +40,38 @@ Image? decodeImageFile(String filePath) { } return image; } + +/// Creates [File] in the given [filePath] if not exists +File createFileIfNotExist(String filePath) { + final file = File(path.joinAll(path.split(filePath))); + if (!file.existsSync()) { + file.createSync(recursive: true); + } + return file; +} + +/// Creates [Directory] in the given [dirPath] if not exists +Directory createDirIfNotExist(String dirPath) { + final dir = Directory(path.joinAll(path.split(dirPath))); + if (!dir.existsSync()) { + dir.createSync(recursive: true); + } + return dir; +} + +/// Returns a prettified json string +String prettifyJsonEncode(Object? map) => JsonEncoder.withIndent(' ' * 2).convert(map); + +/// Check if give [File] or [Directory] exists at the give [paths], +/// if not returns the failed [FileSystemEntity] path +String? areFSEntiesExist(List paths) { + for (final path in paths) { + final fsType = FileSystemEntity.typeSync(path); + if (![FileSystemEntityType.directory, FileSystemEntityType.file].contains(fsType)) { + return path; + } + } + return null; +} + +String flavorConfigFile(String flavor) => 'flutter_launcher_icons-$flavor.yaml'; diff --git a/lib/web/web_icon_generator.dart b/lib/web/web_icon_generator.dart new file mode 100644 index 0000000000..6346c418e9 --- /dev/null +++ b/lib/web/web_icon_generator.dart @@ -0,0 +1,148 @@ +import 'dart:convert'; + +import 'package:image/image.dart'; +import 'package:path/path.dart' as path; + +import '../abs/icon_generator.dart'; +import '../constants.dart' as constants; +import '../custom_exceptions.dart'; +import '../utils.dart' as utils; +import 'web_template.dart'; + +final metaTagsTemplate = ( + String appleMobileWebAppTitle, + String appleMobileWebAppStatusBarStyle, { + bool shouldInsertFLIString = false, +}) => + ''' + + + + + + + + + + + + + +'''; + +/// Generates Web icons for flutter +class WebIconGenerator extends IconGenerator { + static const _webIconSizeTemplates = [ + WebIconTemplate(size: 192), + WebIconTemplate(size: 512), + WebIconTemplate(size: 192, maskable: true), + WebIconTemplate(size: 512, maskable: true), + ]; + + WebIconGenerator(IconGeneratorContext context) : super(context, 'Web'); + + @override + void createIcons() { + final imgFilePath = context.webConfig!.imagePath ?? context.config.imagePath!; + + context.logger.verbose('Decoding and loading image file at $imgFilePath...'); + final imgFile = utils.decodeImageFile(imgFilePath); + if (imgFile == null) { + context.logger.error('Image File not found at give path $imgFilePath...'); + throw FileNotFoundException(imgFilePath); + } + + // generate favicon in web/favicon.png + context.logger.verbose('Generating favicon from $imgFilePath...'); + _generateFavicon(imgFile); + + // generate icons in web/icons/ + context.logger.verbose('Generating icons from $imgFilePath...'); + _generateIcons(imgFile); + + // update manifest.json in web/mainfest.json + context.logger.verbose('Updating ${constants.webManifestFilePath}...'); + _updateManifestFile(); + + // todo: update index.html in web/index.html + // as we are using flutter default config we no need + // to update index.html for now + // _updateIndexFile(); + } + + @override + bool validateRequirments() { + // check if web config exists + context.logger.verbose('Checking webconfig...'); + final webConfig = context.webConfig; + if (webConfig == null || !webConfig.generate) { + context.logger.verbose('Web config is not provided or generate is false. Skipped...'); + return false; + } + if (webConfig.imagePath == null && context.config.imagePath == null) { + context.logger.verbose('Invalid config. Either provide web.imagePath or imagePath'); + return false; + } + + // verify web platform related files and directories exists + final entitesToCheck = [ + constants.webDirPath, + constants.webManifestFilePath, + constants.webIndexFilePath, + ]; + + // web platform related files must exist to continue + final failedEntityPath = utils.areFSEntiesExist(entitesToCheck); + if (failedEntityPath != null) { + context.logger.error('$failedEntityPath this file or folder is required to generate web icons'); + } + + return true; + } + + void _generateFavicon(Image image) { + final favIcon = utils.createResizedImage(constants.kFaviconSize, image); + final favIconFile = utils.createFileIfNotExist(constants.webFaviconFilePath); + favIconFile.writeAsBytesSync(encodePng(favIcon)); + } + + void _generateIcons(Image image) { + final iconsDir = utils.createDirIfNotExist(constants.webIconsDirPath); + // generate icons + for (final template in _webIconSizeTemplates) { + final resizedImg = utils.createResizedImage(template.size, image); + final iconFile = utils.createFileIfNotExist(path.join(iconsDir.path, template.iconFile)); + iconFile.writeAsBytesSync(encodePng(resizedImg)); + } + } + + // void _updateIndexFile() { + // todo + // final indexFile = File(constants.webIndexFilePath); + // if (!indexFile.existsSync()) { + // throw FileNotFoundException(constants.webFaviconFilePath); + // } + // } + + void _updateManifestFile() { + final manifestFile = utils.createFileIfNotExist(constants.webManifestFilePath); + final manifestConfig = jsonDecode(manifestFile.readAsStringSync()) as Map; + + // update background_color + if (context.webConfig?.backgroundColor != null) { + manifestConfig['background_color'] = context.webConfig?.backgroundColor; + } + + // update theme_color + if (context.webConfig?.themeColor != null) { + manifestConfig['theme_color'] = context.webConfig?.themeColor; + } + + // replace existing icons to eliminate conflicts + manifestConfig + ..remove('icons') + ..['icons'] = _webIconSizeTemplates.map>((e) => e.iconManifest).toList(); + + manifestFile.writeAsStringSync(utils.prettifyJsonEncode(manifestConfig)); + } +} diff --git a/lib/web/web_template.dart b/lib/web/web_template.dart new file mode 100644 index 0000000000..4d4b8ab1bd --- /dev/null +++ b/lib/web/web_template.dart @@ -0,0 +1,31 @@ +class WebIconTemplate { + const WebIconTemplate({ + required this.size, + this.maskable = false, + }); + + final int size; + final bool maskable; + + /// Icon file name + String get iconFile => 'Icon${maskable ? '-maskable' : ''}-$size.png'; + + /// Icon config for manifest.json + /// + /// ```json + /// { + /// "src": "icons/Icon-maskable-192.png", + /// "sizes": "192x192", + /// "type": "image/png", + /// "purpose": "maskable" + /// }, + /// ``` + Map get iconManifest { + return { + 'src': 'icons/$iconFile', + 'sizes': '${size}x$size', + 'type': 'image/png', + if (maskable) 'purpose': 'maskable', + }; + } +}