diff --git a/lib/abs/icon_generator.dart b/lib/abs/icon_generator.dart index 8dcd89cdb8..094b5f76b8 100644 --- a/lib/abs/icon_generator.dart +++ b/lib/abs/icon_generator.dart @@ -58,6 +58,9 @@ class IconGeneratorContext { /// Shortcut for `config.windowsConfig` WindowsConfig? get windowsConfig => config.windowsConfig; + + /// Shortcut for `config.macOSConfig` + MacOSConfig? get macOSConfig => config.macOSConfig; } /// Generates Icon for given platforms diff --git a/lib/constants.dart b/lib/constants.dart index 2214ecc07f..4201b1d8a7 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -63,6 +63,17 @@ String windowsIconFilePath = path.join(windowsResourcesDirPath, 'app_icon.ico'); /// const int windowsDefaultIconSize = 48; +// MacOS + +/// Relative path to macos folder +final macOSDirPath = path.join('macos'); + +/// Relative path to macos icons folder +final macOSIconsDirPath = path.join(macOSDirPath, 'Runner', 'Assets.xcassets', 'AppIcon.appiconset'); + +/// Relative path to macos contents.json +final macOSContentsFilePath = path.join(macOSIconsDirPath, 'Contents.json'); + 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 92c5084e23..d1be2be226 100644 --- a/lib/flutter_launcher_icons_config.dart +++ b/lib/flutter_launcher_icons_config.dart @@ -43,7 +43,7 @@ class FlutterLauncherIconsConfig { final String? adaptiveIconBackground; /// Android min_sdk_android - @JsonKey(name: 'min_sdk_android', defaultValue: constants.androidDefaultAndroidMinSDK) + @JsonKey(name: 'min_sdk_android') final int minSdkAndroid; /// IOS remove_alpha_ios @@ -58,6 +58,10 @@ class FlutterLauncherIconsConfig { @JsonKey(name: 'windows') final WindowsConfig? windowsConfig; + /// MacOS platform config + @JsonKey(name: 'macos') + final MacOSConfig? macOSConfig; + /// Creates an instance of [FlutterLauncherIconsConfig] const FlutterLauncherIconsConfig({ this.imagePath, @@ -71,11 +75,48 @@ class FlutterLauncherIconsConfig { this.removeAlphaIOS = false, this.webConfig, this.windowsConfig, + this.macOSConfig, }); /// Creates [FlutterLauncherIconsConfig] icons from [json] factory FlutterLauncherIconsConfig.fromJson(Map json) => _$FlutterLauncherIconsConfigFromJson(json); + bool get hasAndroidAdaptiveConfig => + isNeedingNewAndroidIcon && adaptiveIconForeground != null && adaptiveIconBackground != null; + + /// Checks if contains any platform config + bool get hasPlatformConfig { + return ios != false || android != false || webConfig != null || windowsConfig != null || macOSConfig != null; + } + + /// Check to see if specified Android config is a string or bool + /// String - Generate new launcher icon with the string specified + /// bool - override the default flutter project icon + bool get isCustomAndroidFile => android is String; + + bool get isNeedingNewAndroidIcon => android != false; + + bool get isNeedingNewIOSIcon => ios != false; + + /// Method for the retrieval of the Android icon path + /// If image_path_android is found, this will be prioritised over the image_path + /// value. + String? getImagePathAndroid() => imagePathAndroid ?? imagePath; + // todo: refactor after Android & iOS configs will be refactored to the new schema + // https://github.com/fluttercommunity/flutter_launcher_icons/issues/394 + String? getImagePathIOS() => imagePathIOS ?? imagePath; + + /// Converts config to [Map] + Map toJson() => _$FlutterLauncherIconsConfigToJson(this); + + @override + String toString() => 'FlutterLauncherIconsConfig: ${toJson()}'; + + /// Creates [FlutterLauncherIconsConfig] for given [flavor] and [prefixPath] + static FlutterLauncherIconsConfig? loadConfigFromFlavor(String flavor, String prefixPath) { + return FlutterLauncherIconsConfig.loadConfigFromPath(utils.flavorConfigFile(flavor), prefixPath); + } + /// Loads flutter launcher icons configs from given [filePath] static FlutterLauncherIconsConfig? loadConfigFromPath(String filePath, String prefixPath) { final configFile = File(path.join(prefixPath, filePath)); @@ -125,45 +166,36 @@ class FlutterLauncherIconsConfig { rethrow; } } +} - /// Creates [FlutterLauncherIconsConfig] for given [flavor] and [prefixPath] - static FlutterLauncherIconsConfig? loadConfigFromFlavor(String flavor, String prefixPath) { - return FlutterLauncherIconsConfig.loadConfigFromPath(utils.flavorConfigFile(flavor), prefixPath); - } - - /// Converts config to [Map] - Map toJson() => _$FlutterLauncherIconsConfigToJson(this); - - @override - String toString() => 'FlutterLauncherIconsConfig: ${toJson()}'; +/// A Configs for Windows +@JsonSerializable( + anyMap: true, + checked: true, +) +class MacOSConfig { + /// Specifies weather to generate icons for macos + @JsonKey() + final bool generate; - bool get isNeedingNewIOSIcon => ios != false; - bool get isNeedingNewAndroidIcon => android != false; - bool get hasAndroidAdaptiveConfig => - isNeedingNewAndroidIcon && - adaptiveIconForeground != null && - adaptiveIconBackground != null; + /// Image path for macos + @JsonKey(name: 'image_path') + final String? imagePath; - // todo: refactor after Android & iOS configs will be refactored to the new schema - // https://github.com/fluttercommunity/flutter_launcher_icons/issues/394 - String? getImagePathIOS() => imagePathIOS ?? imagePath; + /// Creates a instance of [MacOSConfig] + const MacOSConfig({ + this.generate = false, + this.imagePath, + }); - /// Method for the retrieval of the Android icon path - /// If image_path_android is found, this will be prioritised over the image_path - /// value. - String? getImagePathAndroid() => imagePathAndroid ?? imagePath; + /// Creates [WebConfig] from [json] + factory MacOSConfig.fromJson(Map json) => _$MacOSConfigFromJson(json); - /// Check to see if specified Android config is a string or bool - /// String - Generate new launcher icon with the string specified - /// bool - override the default flutter project icon - bool get isCustomAndroidFile => android is String; + /// Creates [Map] from [WebConfig] + Map toJson() => _$MacOSConfigToJson(this); - /// Checks if contains any platform config - bool get hasPlatformConfig => - ios != false || - android != false || - webConfig != null || - windowsConfig != null; + @override + String toString() => '$runtimeType: ${toJson()}'; } /// Parse `web` config from `flutter_launcher_icons.yaml` diff --git a/lib/flutter_launcher_icons_config.g.dart b/lib/flutter_launcher_icons_config.g.dart index 4c70f82115..5636badb01 100644 --- a/lib/flutter_launcher_icons_config.g.dart +++ b/lib/flutter_launcher_icons_config.g.dart @@ -22,14 +22,16 @@ FlutterLauncherIconsConfig _$FlutterLauncherIconsConfigFromJson(Map json) => $checkedConvert('adaptive_icon_foreground', (v) => v as String?), adaptiveIconBackground: $checkedConvert('adaptive_icon_background', (v) => v as String?), - minSdkAndroid: - $checkedConvert('min_sdk_android', (v) => v as int? ?? 21), + minSdkAndroid: $checkedConvert('min_sdk_android', + (v) => v as int? ?? constants.androidDefaultAndroidMinSDK), removeAlphaIOS: $checkedConvert('remove_alpha_ios', (v) => v as bool? ?? false), webConfig: $checkedConvert( 'web', (v) => v == null ? null : WebConfig.fromJson(v as Map)), windowsConfig: $checkedConvert('windows', (v) => v == null ? null : WindowsConfig.fromJson(v as Map)), + macOSConfig: $checkedConvert('macos', + (v) => v == null ? null : MacOSConfig.fromJson(v as Map)), ); return val; }, @@ -42,7 +44,8 @@ FlutterLauncherIconsConfig _$FlutterLauncherIconsConfigFromJson(Map json) => 'minSdkAndroid': 'min_sdk_android', 'removeAlphaIOS': 'remove_alpha_ios', 'webConfig': 'web', - 'windowsConfig': 'windows' + 'windowsConfig': 'windows', + 'macOSConfig': 'macos' }, ); @@ -60,6 +63,26 @@ Map _$FlutterLauncherIconsConfigToJson( 'remove_alpha_ios': instance.removeAlphaIOS, 'web': instance.webConfig, 'windows': instance.windowsConfig, + 'macos': instance.macOSConfig, + }; + +MacOSConfig _$MacOSConfigFromJson(Map json) => $checkedCreate( + 'MacOSConfig', + json, + ($checkedConvert) { + final val = MacOSConfig( + generate: $checkedConvert('generate', (v) => v as bool? ?? false), + imagePath: $checkedConvert('image_path', (v) => v as String?), + ); + return val; + }, + fieldKeyMap: const {'imagePath': 'image_path'}, + ); + +Map _$MacOSConfigToJson(MacOSConfig instance) => + { + 'generate': instance.generate, + 'image_path': instance.imagePath, }; WebConfig _$WebConfigFromJson(Map json) => $checkedCreate( diff --git a/lib/macos/macos_icon_generator.dart b/lib/macos/macos_icon_generator.dart new file mode 100644 index 0000000000..50d82a5720 --- /dev/null +++ b/lib/macos/macos_icon_generator.dart @@ -0,0 +1,110 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_launcher_icons/abs/icon_generator.dart'; +import 'package:flutter_launcher_icons/constants.dart' as constants; +import 'package:flutter_launcher_icons/custom_exceptions.dart'; +import 'package:flutter_launcher_icons/macos/macos_icon_template.dart'; +import 'package:flutter_launcher_icons/utils.dart' as utils; +import 'package:image/image.dart'; +import 'package:path/path.dart' as path; + +/// A [IconGenerator] implementation for macos +class MacOSIconGenerator extends IconGenerator { + static const _iconSizeTemplates = [ + MacOSIconTemplate(16, 1), + MacOSIconTemplate(16, 2), + MacOSIconTemplate(32, 1), + MacOSIconTemplate(32, 2), + MacOSIconTemplate(128, 1), + MacOSIconTemplate(128, 2), + MacOSIconTemplate(256, 1), + MacOSIconTemplate(256, 2), + MacOSIconTemplate(512, 1), + MacOSIconTemplate(512, 2), + ]; + + /// Creates a instance of [MacOSIconGenerator] + MacOSIconGenerator(IconGeneratorContext context) : super(context, 'MacOS'); + + @override + void createIcons() { + final imgFilePath = path.join( + context.prefixPath, + context.config.macOSConfig!.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); + } + + context.logger.verbose('Generating icons $imgFilePath...'); + _generateIcons(imgFile); + context.logger.verbose('Updating contents.json'); + _updateContentsFile(); + } + + @override + bool validateRequirements() { + context.logger.verbose('Checking $platformName config...'); + final macOSConfig = context.macOSConfig; + + if (macOSConfig == null || !macOSConfig.generate) { + context.logger + ..verbose('$platformName config is missing or "flutter_icons.macos.generate" is false. Skipped...') + ..verbose(macOSConfig); + return false; + } + + if (macOSConfig.imagePath == null && context.config.imagePath == null) { + context.logger + ..verbose({ + 'flutter_icons.macos.image_path': macOSConfig.imagePath, + 'flutter_icons.image_path': context.config.imagePath, + }) + ..error( + 'Missing image_path. Either provide "flutter_icons.macos.image_path" or "flutter_icons.image_path"', + ); + + return false; + } + + // this files and folders should exist to create macos icons + final enitiesToCheck = [ + path.join(context.prefixPath, constants.macOSDirPath), + path.join(context.prefixPath, constants.macOSIconsDirPath), + path.join(context.prefixPath, constants.macOSContentsFilePath), + ]; + + final failedEntityPath = utils.areFSEntiesExist(enitiesToCheck); + if (failedEntityPath != null) { + context.logger.error('$failedEntityPath this file or folder is required to generate $platformName icons'); + return false; + } + + return true; + } + + void _generateIcons(Image image) { + final iconsDir = utils.createDirIfNotExist(path.join(context.prefixPath, constants.macOSIconsDirPath)); + + for (final template in _iconSizeTemplates) { + final resizedImg = utils.createResizedImage(template.scaledSize, image); + final iconFile = utils.createFileIfNotExist(path.join(context.prefixPath, iconsDir.path, template.iconFile)); + iconFile.writeAsBytesSync(encodePng(resizedImg)); + } + } + + void _updateContentsFile() { + final contentsFilePath = File(path.join(context.prefixPath, constants.macOSContentsFilePath)); + final contentsConfig = jsonDecode(contentsFilePath.readAsStringSync()) as Map; + contentsConfig + ..remove('images') + ..['images'] = _iconSizeTemplates.map>((e) => e.iconContent).toList(); + + contentsFilePath.writeAsStringSync(utils.prettifyJsonEncode(contentsConfig)); + } +} diff --git a/lib/macos/macos_icon_template.dart b/lib/macos/macos_icon_template.dart new file mode 100644 index 0000000000..03dbd19a58 --- /dev/null +++ b/lib/macos/macos_icon_template.dart @@ -0,0 +1,39 @@ +/// A macOS icon template +class MacOSIconTemplate { + /// Icon size + final int size; + + /// Icon scale + final int scale; + + /// Creates an instance of [MacOSIconTemplate] + /// + const MacOSIconTemplate(this.size, this.scale); + + /// Icon content for contents.json' s images + /// + /// ```json + /// { + /// "size" : "16x16", + /// "idiom" : "mac", + /// "filename" : "app_icon_16.png", + /// "scale" : "1x" + /// } + /// ``` + Map get iconContent { + return { + 'size': '${size}x$size', + 'idiom': 'mac', + 'filename': iconFile, + 'scale': '${scale}x', + }; + } + + /// Icon file name with extension + /// + /// `app_icon_16.png` + String get iconFile => 'app_icon_$scaledSize.png'; + + /// Image size after computing scale + int get scaledSize => size * scale; +} diff --git a/lib/main.dart b/lib/main.dart index 31db89907d..0b195bf920 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'package:flutter_launcher_icons/custom_exceptions.dart'; 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/macos/macos_icon_generator.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; @@ -74,7 +75,7 @@ Future createIconsFromArguments(List arguments) async { if (flutterLauncherIconsConfigs == null) { throw NoConfigFoundException( 'No configuration found in $defaultConfigFile or in ${constants.pubspecFilePath}. ' - 'In case file exists in different directory use --file option', + 'In case file exists in different directory use --file option', ); } try { @@ -135,6 +136,7 @@ Future createIconsFromConfig( platforms: (context) => [ WebIconGenerator(context), WindowsIconGenerator(context), + MacOSIconGenerator(context), // todo: add other platforms ], );