From c22a950c2533c439f2d6fb190d01a991dfb2f071 Mon Sep 17 00:00:00 2001 From: Ratakondala Arun Date: Sat, 23 Jul 2022 19:58:18 +0530 Subject: [PATCH] feat(windows): add support for windows closes #380 --- lib/abs/icon_generator.dart | 3 + lib/constants.dart | 14 +++++ lib/flutter_launcher_icons_config.dart | 39 ++++++++++++ lib/flutter_launcher_icons_config.g.dart | 27 ++++++++- lib/main.dart | 2 + lib/utils.dart | 2 + lib/windows/windows_icon_generator.dart | 75 ++++++++++++++++++++++++ 7 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 lib/windows/windows_icon_generator.dart diff --git a/lib/abs/icon_generator.dart b/lib/abs/icon_generator.dart index 1576ddb939..10f3086268 100644 --- a/lib/abs/icon_generator.dart +++ b/lib/abs/icon_generator.dart @@ -55,6 +55,9 @@ class IconGeneratorContext { /// Shortcut for `config.webConfig` WebConfig? get webConfig => config.webConfig; + + /// Shortcut for `config.windowsConfig` + WindowsConfig? get windowsConfig => config.windowsConfig; } /// Generates Icon for given platforms diff --git a/lib/constants.dart b/lib/constants.dart index 638249f4cf..97efa0466b 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -42,6 +42,20 @@ String webIndexFilePath = path.join(webDirPath, 'index.html'); /// Relative pubspec.yaml path String pubspecFilePath = path.join('pubspec.yaml'); +// Windows +/// Relative path to windows directory +String windowsDirPath = path.join('windows'); + +/// Relative path to windows resources directory +String windowsResourcesDirPath = path.join(windowsDirPath, 'runner', 'resources'); + +/// Relative path to windows icon file path +String windowsIconFilePath = path.join(windowsResourcesDirPath, 'app_icon.ico'); + +/// Default windows icon size for flutter +/// +const int kWindowsIconSize = 48; + const String errorMissingImagePath = 'Missing "image_path" or "image_path_android" + "image_path_ios" within configuration'; const String errorMissingPlatform = 'No platform specified within config to generate icons for.'; diff --git a/lib/flutter_launcher_icons_config.dart b/lib/flutter_launcher_icons_config.dart index 1f5d0bc895..f386bee11f 100644 --- a/lib/flutter_launcher_icons_config.dart +++ b/lib/flutter_launcher_icons_config.dart @@ -46,6 +46,10 @@ class FlutterLauncherIconsConfig { @JsonKey(name: 'web') final WebConfig? webConfig; + /// Windows platform config + @JsonKey(name: 'windows') + final WindowsConfig? windowsConfig; + /// Creates an instance of [FlutterLauncherIconsConfig] const FlutterLauncherIconsConfig({ this.imagePath, @@ -56,6 +60,7 @@ class FlutterLauncherIconsConfig { this.adaptiveIconForeground, this.adaptiveIconBackground, this.webConfig, + this.windowsConfig, }); /// Creates [FlutterLauncherIconsConfig] icons from [json] @@ -161,3 +166,37 @@ class WebConfig { @override String toString() => 'WebConfig: ${toJson()}'; } + +/// A Configs for Windows +@JsonSerializable( + anyMap: true, + checked: true, +) +class WindowsConfig { + /// Specifies weather to generate icons for web + final bool generate; + + /// Image path for web + @JsonKey(name: 'image_path') + final String? imagePath; + + /// Size of the icon to generate + @JsonKey(name: 'icon_size') + final int? iconSize; + + /// Creates a instance of [WindowsConfig] + const WindowsConfig({ + this.generate = false, + this.imagePath, + this.iconSize, + }); + + /// Creates [WindowsConfig] from [json] + factory WindowsConfig.fromJson(Map json) => _$WindowsConfigFromJson(json); + + /// Creates [Map] from [WindowsConfig] + Map toJson() => _$WindowsConfigToJson(this); + + @override + String toString() => 'WindowsConfig: ${toJson()}'; +} diff --git a/lib/flutter_launcher_icons_config.g.dart b/lib/flutter_launcher_icons_config.g.dart index c3d85f33a5..7e9c9eb621 100644 --- a/lib/flutter_launcher_icons_config.g.dart +++ b/lib/flutter_launcher_icons_config.g.dart @@ -24,6 +24,8 @@ FlutterLauncherIconsConfig _$FlutterLauncherIconsConfigFromJson(Map json) => $checkedConvert('adaptive_icon_background', (v) => v as String?), webConfig: $checkedConvert( 'web', (v) => v == null ? null : WebConfig.fromJson(v as Map)), + windowsConfig: $checkedConvert('windows', + (v) => v == null ? null : WindowsConfig.fromJson(v as Map)), ); return val; }, @@ -33,7 +35,8 @@ FlutterLauncherIconsConfig _$FlutterLauncherIconsConfigFromJson(Map json) => 'imagePathIOS': 'image_path_ios', 'adaptiveIconForeground': 'adaptive_icon_foreground', 'adaptiveIconBackground': 'adaptive_icon_background', - 'webConfig': 'web' + 'webConfig': 'web', + 'windowsConfig': 'windows' }, ); @@ -48,6 +51,7 @@ Map _$FlutterLauncherIconsConfigToJson( 'adaptive_icon_foreground': instance.adaptiveIconForeground, 'adaptive_icon_background': instance.adaptiveIconBackground, 'web': instance.webConfig, + 'windows': instance.windowsConfig, }; WebConfig _$WebConfigFromJson(Map json) => $checkedCreate( @@ -76,3 +80,24 @@ Map _$WebConfigToJson(WebConfig instance) => { 'background_color': instance.backgroundColor, 'theme_color': instance.themeColor, }; + +WindowsConfig _$WindowsConfigFromJson(Map json) => $checkedCreate( + 'WindowsConfig', + json, + ($checkedConvert) { + final val = WindowsConfig( + generate: $checkedConvert('generate', (v) => v as bool? ?? false), + imagePath: $checkedConvert('image_path', (v) => v as String?), + iconSize: $checkedConvert('icon_size', (v) => v as int?), + ); + return val; + }, + fieldKeyMap: const {'imagePath': 'image_path', 'iconSize': 'icon_size'}, + ); + +Map _$WindowsConfigToJson(WindowsConfig instance) => + { + 'generate': instance.generate, + 'image_path': instance.imagePath, + 'icon_size': instance.iconSize, + }; diff --git a/lib/main.dart b/lib/main.dart index 5cffcc4bfb..16d7fc02b9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:flutter_launcher_icons/flutter_launcher_icons_config.dart'; import 'package:flutter_launcher_icons/ios.dart' as ios_launcher_icons; import 'package:flutter_launcher_icons/logger.dart'; import 'package:flutter_launcher_icons/web/web_icon_generator.dart'; +import 'package:flutter_launcher_icons/windows/windows_icon_generator.dart'; import 'package:path/path.dart' as path; import 'package:yaml/yaml.dart'; @@ -151,6 +152,7 @@ Future createIconsFromConfig( flavor: flavor, platforms: (context) => [ WebIconGenerator(context), + WindowsIconGenerator(context), // todo: add other platforms ], ); diff --git a/lib/utils.dart b/lib/utils.dart index d87e9d0601..5a5acfffe8 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -33,6 +33,8 @@ String generateError(Exception e, String? error) { return '\n✗ ERROR: ${(e).runtimeType.toString()}$errorOutput'; } +// TODO(RatakondalaArun): Remove nullable return type +// this can never return null value since it already throws exception Image? decodeImageFile(String filePath) { final image = decodeImage(File(filePath).readAsBytesSync()); if (image == null) { diff --git a/lib/windows/windows_icon_generator.dart b/lib/windows/windows_icon_generator.dart new file mode 100644 index 0000000000..663fa63cd7 --- /dev/null +++ b/lib/windows/windows_icon_generator.dart @@ -0,0 +1,75 @@ +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; + +/// A Implementation of [IconGenerator] for Windows +class WindowsIconGenerator extends IconGenerator { + /// Creates a instance of [WindowsIconGenerator] + WindowsIconGenerator(IconGeneratorContext context) : super(context, 'Windows'); + + @override + void createIcons() { + final imgFilePath = path.join( + context.prefixPath, + context.windowsConfig!.imagePath ?? context.config.imagePath, + ); + + context.logger.verbose('Decoding and loading image file from $imgFilePath...'); + final imgFile = utils.decodeImageFile(imgFilePath); + // TODO(RatakondalaArun): remove null check + // #utils.decodeImageFile never returns null instead it throws Exception + if (imgFile == null) { + context.logger.error('Image File not found at given path $imgFilePath...'); + throw FileNotFoundException(imgFilePath); + } + + context.logger.verbose('Generating icon from $imgFilePath...'); + _generateIcon(imgFile); + } + + @override + bool validateRequirements() { + context.logger.verbose('Validating windows config...'); + final windowsConfig = context.windowsConfig; + if (windowsConfig == null || !windowsConfig.generate) { + context.logger.error('Windows config is not provided or windows.generate is false. Skipped...'); + return false; + } + + if (windowsConfig.imagePath == null && context.config.imagePath == null) { + context.logger.error('Invalid config. Either provide windows.image_path or image_path'); + return false; + } + + // if icon_size is given it should be between 48<=icon_size<=256 + // because .ico only supports this size + if (windowsConfig.iconSize != null && (windowsConfig.iconSize! < 48 || windowsConfig.iconSize! > 256)) { + context.logger.error( + 'Invalid windows.icon_size=${windowsConfig.iconSize}. Icon size should be between 48<=icon_size<=256', + ); + return false; + } + final entitesToCheck = [ + path.join(context.prefixPath, constants.windowsDirPath), + path.join(context.prefixPath, windowsConfig.imagePath ?? context.config.imagePath), + ]; + + final failedEntityPath = utils.areFSEntiesExist(entitesToCheck); + if (failedEntityPath != null) { + context.logger.error('$failedEntityPath this file or folder is required to generate web icons'); + return false; + } + + return true; + } + + void _generateIcon(Image image) { + final favIcon = utils.createResizedImage(context.windowsConfig!.iconSize ?? constants.kWindowsIconSize, image); + final favIconFile = utils.createFileIfNotExist(path.join(context.prefixPath, constants.windowsIconFilePath)); + favIconFile.writeAsBytesSync(encodeIco(favIcon)); + } +}