diff --git a/README.md b/README.md index 8156c5f48e..ca0326b808 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,9 @@ flutter_icons: generate: true image_path: "path/to/image.png" icon_size: 48 # min:48, max:256, default: 48 + macos: + generate: true + image_path: "path/to/image.png" ``` If you name your configuration file something other than `flutter_launcher_icons.yaml` or `pubspec.yaml` you will need to specify @@ -61,42 +64,57 @@ with the icon located in the image path specified above and given the name "laun Shown below is the full list of attributes which you can specify within your Flutter Launcher Icons configuration. -- `android`/`ios` +### Global + +- `image_path`: The location of the icon image file which you want to use as the app launcher icon. + +### Android + +- `android` - `true`: Override the default existing Flutter launcher icon for the platform specified - `false`: Ignore making launcher icons for this platform - `icon/path/here.png`: This will generate a new launcher icons for the platform with the name you specify, without removing the old default existing Flutter launcher icon. - - `image_path`: The location of the icon image file which you want to use as the app launcher icon - - `image_path_android`: The location of the icon image file specific for Android platform (optional - if not defined then the image_path is used) +- `min_sdk_android`: Specify android min sdk value +**The next two attributes are only used when generating Android launcher icon** -- `image_path_ios`: The location of the icon image file specific for iOS platform (optional - if not defined then the image_path is used) +- `adaptive_icon_background`: The color (E.g. `"#ffffff"`) or image asset (E.g. `"assets/images/christmas-background.png"`) which will +be used to fill out the background of the adaptive icon. +- `adaptive_icon_foreground`: The image asset which will be used for the icon foreground of the adaptive icon +*Note: Adaptive Icons will only be generated when both adaptive_icon_background and adaptive_icon_foreground are specified. (the image_path is not automatically taken as foreground)* -- `min_sdk_android`: Specify android min sdk value +### IOS +- `ios` + - `true`: Override the default existing Flutter launcher icon for the platform specified + - `false`: Ignore making launcher icons for this platform + - `icon/path/here.png`: This will generate a new launcher icons for the platform with the name you specify, without removing the old default existing Flutter launcher icon. +- `image_path_ios`: The location of the icon image file specific for iOS platform (optional - if not defined then the image_path is used) - `remove_alpha_ios`: Removes alpha channel for IOS icons +### Web + - `web`: Add web related configs - `generate`: Specifies weather to generate icons for this platform or not - `image_path`: Path to web icon.png - `background_color`: Updates *background_color* in `web/manifest.json` - `theme_color`: Updates *theme_color* in `web/manifest.json` +### Windows + - `windows`: Add Windows related configs - `generate`: Specifies weather to generate icons for Windows platform or not - `image_path`: Path to web icon.png - `icon_size`: Windows app icon size. Icon size should be within this constrains *48<=icon_size<=256, defaults to 48* + +### MacOS -*Note: iOS icons should [fill the entire image](https://stackoverflow.com/questions/26014461/black-border-on-my-ios-icon) and not contain transparent borders.* - -The next two attributes are only used when generating Android launcher icon +- `macos`: Add MacOS related configs + - `generate`: Specifies weather to generate icons for MacOS platform or not + - `image_path`: Path to macos icon.png file -- `adaptive_icon_background`: The color (E.g. `"#ffffff"`) or image asset (E.g. `"assets/images/christmas-background.png"`) which will -be used to fill out the background of the adaptive icon. - -- `adaptive_icon_foreground`: The image asset which will be used for the icon foreground of the adaptive icon - -Note: Adaptive Icons will only be generated when both adaptive_icon_background and adaptive_icon_foreground are specified. (the image_path is not automatically taken as foreground) +*Note: iOS icons should [fill the entire image](https://stackoverflow.com/questions/26014461/black-border-on-my-ios-icon) and not contain transparent borders.* ## Flavor support diff --git a/example/default_example/pubspec.yaml b/example/default_example/pubspec.yaml index bd2c743759..d0e9d4d88b 100644 --- a/example/default_example/pubspec.yaml +++ b/example/default_example/pubspec.yaml @@ -25,13 +25,16 @@ flutter_icons: remove_alpha_ios: true web: generate: true - image_path: "path/to/image.png" + image_path: "assets/images/icon-1024x1024.png" background_color: "#hexcode" theme_color: "#hexcode" windows: generate: true - image_path: "path/to/image.png" + image_path: "assets/images/icon-1024x1024.png" icon_size: 48 # min:48, max:256, default: 48 + macos: + generate: true + image_path: "assets/images/icon-1024x1024.png" dev_dependencies: flutter_test: 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 ], ); diff --git a/lib/utils.dart b/lib/utils.dart index 5a5acfffe8..79069e5958 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -62,7 +62,7 @@ Directory createDirIfNotExist(String dirPath) { } /// Returns a prettified json string -String prettifyJsonEncode(Object? map) => JsonEncoder.withIndent(' ' * 2).convert(map); +String prettifyJsonEncode(Object? map) => JsonEncoder.withIndent(' ' * 4).convert(map); /// Check if give [File] or [Directory] exists at the give [paths], /// if not returns the failed [FileSystemEntity] path diff --git a/test/abs/icon_generator_test.mocks.dart b/test/abs/icon_generator_test.mocks.dart index c427ad651b..df330e0ce8 100644 --- a/test/abs/icon_generator_test.mocks.dart +++ b/test/abs/icon_generator_test.mocks.dart @@ -1,7 +1,8 @@ -// Mocks generated by Mockito 5.2.0 from annotations +// Mocks generated by Mockito 5.3.0 from annotations // in flutter_launcher_icons/test/abs/icon_generator_test.dart. // Do not manually edit this file. +// ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:flutter_launcher_icons/abs/icon_generator.dart' as _i2; import 'package:flutter_launcher_icons/flutter_launcher_icons_config.dart' as _i3; @@ -16,9 +17,13 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class -class _FakeIconGeneratorContext_0 extends _i1.Fake - implements _i2.IconGeneratorContext {} +class _FakeIconGeneratorContext_0 extends _i1.SmartFake + implements _i2.IconGeneratorContext { + _FakeIconGeneratorContext_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} /// A class which mocks [FlutterLauncherIconsConfig]. /// @@ -38,24 +43,24 @@ class MockFlutterLauncherIconsConfig extends _i1.Mock .noSuchMethod(Invocation.getter(#removeAlphaIOS), returnValue: false) as bool); @override - bool get isNeedingNewIOSIcon => - (super.noSuchMethod(Invocation.getter(#isNeedingNewIOSIcon), - returnValue: false) as bool); - @override - bool get isNeedingNewAndroidIcon => - (super.noSuchMethod(Invocation.getter(#isNeedingNewAndroidIcon), - returnValue: false) as bool); - @override bool get hasAndroidAdaptiveConfig => (super.noSuchMethod(Invocation.getter(#hasAndroidAdaptiveConfig), returnValue: false) as bool); @override + bool get hasPlatformConfig => + (super.noSuchMethod(Invocation.getter(#hasPlatformConfig), + returnValue: false) as bool); + @override bool get isCustomAndroidFile => (super.noSuchMethod(Invocation.getter(#isCustomAndroidFile), returnValue: false) as bool); @override - bool get hasPlatformConfig => - (super.noSuchMethod(Invocation.getter(#hasPlatformConfig), + bool get isNeedingNewAndroidIcon => + (super.noSuchMethod(Invocation.getter(#isNeedingNewAndroidIcon), + returnValue: false) as bool); + @override + bool get isNeedingNewIOSIcon => + (super.noSuchMethod(Invocation.getter(#isNeedingNewIOSIcon), returnValue: false) as bool); @override Map toJson() => @@ -73,8 +78,10 @@ class MockIconGenerator extends _i1.Mock implements _i2.IconGenerator { @override _i2.IconGeneratorContext get context => (super.noSuchMethod( - Invocation.getter(#context), - returnValue: _FakeIconGeneratorContext_0()) as _i2.IconGeneratorContext); + Invocation.getter(#context), + returnValue: + _FakeIconGeneratorContext_0(this, Invocation.getter(#context))) + as _i2.IconGeneratorContext); @override String get platformName => (super.noSuchMethod(Invocation.getter(#platformName), returnValue: '') diff --git a/test/all_tests.dart b/test/all_tests.dart index 04a1f2a01f..8867e8c645 100644 --- a/test/all_tests.dart +++ b/test/all_tests.dart @@ -3,6 +3,8 @@ import 'package:test/test.dart'; import 'abs/icon_generator_test.dart' as icon_generator_test; import 'android_test.dart' as android_test; import 'flutter_launcher_icons_config_test.dart' as fli_config; +import 'macos/macos_icon_generator_test.dart' as macos_icons_gen_test; +import 'macos/macos_icon_template_test.dart' as macos_template_test; import 'main_test.dart' as main_test; import 'utils_test.dart' as utils_test; import 'web/web_icon_generator_test.dart' as web_icon_gen_test; @@ -24,5 +26,8 @@ void main() { web_icon_gen_test.main(); // windows windows_icon_gen_test.main(); + // macos + macos_template_test.main(); + macos_icons_gen_test.main(); }); } diff --git a/test/flutter_launcher_icons_config_test.dart b/test/flutter_launcher_icons_config_test.dart index 2b99f52f3b..ace7d063ff 100644 --- a/test/flutter_launcher_icons_config_test.dart +++ b/test/flutter_launcher_icons_config_test.dart @@ -61,6 +61,17 @@ void main() { 'icon_size': 48, }), ); + // macos + expect(configs.macOSConfig, isNotNull); + expect(configs.macOSConfig!.generate, isNotNull); + expect(configs.macOSConfig!.imagePath, isNotNull); + expect( + configs.macOSConfig!.toJson(), + equals({ + 'generate': true, + 'image_path': 'app_icon.png', + }), + ); }); test('should return null when invalid filePath is given', () { @@ -98,6 +109,8 @@ void main() { expect(configs.webConfig, isNull); // windows expect(configs.windowsConfig, isNull); + // macos + expect(configs.macOSConfig, isNull); }); }); group('#loadConfigFromPubSpec', () { @@ -150,6 +163,17 @@ void main() { 'icon_size': 48, }), ); + // macos + expect(configs.macOSConfig, isNotNull); + expect(configs.macOSConfig!.generate, isNotNull); + expect(configs.macOSConfig!.imagePath, isNotNull); + expect( + configs.macOSConfig!.toJson(), + equals({ + 'generate': true, + 'image_path': 'app_icon.png', + }), + ); }); group('should throw', () { @@ -213,6 +237,17 @@ void main() { 'icon_size': 48, }), ); + // macos + expect(configs.macOSConfig, isNotNull); + expect(configs.macOSConfig!.generate, isNotNull); + expect(configs.macOSConfig!.imagePath, isNotNull); + expect( + configs.macOSConfig!.toJson(), + equals({ + 'generate': true, + 'image_path': 'app_icon.png', + }), + ); }); }); }); diff --git a/test/macos/macos_icon_generator_test.dart b/test/macos/macos_icon_generator_test.dart new file mode 100644 index 0000000000..36e5ef44d6 --- /dev/null +++ b/test/macos/macos_icon_generator_test.dart @@ -0,0 +1,158 @@ +import 'dart:io'; + +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/macos/macos_icon_generator.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../templates.dart' as templates; +import 'macos_icon_generator_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + group('MacOSIconGenerator', () { + late IconGeneratorContext context; + late IconGenerator generator; + late FlutterLauncherIconsConfig mockConfig; + late MacOSConfig mockMacOSConfig; + late String prefixPath; + late File testImageFile; + late MockFLILogger mockLogger; + final assetPath = path.join(Directory.current.path, 'test', 'assets'); + + group('#validateRequirments', () { + setUpAll(() { + testImageFile = File(path.join(assetPath, 'app_icon.png')); + expect(testImageFile.existsSync(), isTrue); + }); + setUp(() { + prefixPath = path.join(d.sandbox, 'fli_test'); + mockConfig = MockFlutterLauncherIconsConfig(); + mockMacOSConfig = MockMacOSConfig(); + mockLogger = MockFLILogger(); + context = IconGeneratorContext(config: mockConfig, prefixPath: prefixPath, logger: mockLogger); + generator = MacOSIconGenerator(context); + + // initilize mock defaults + when(mockLogger.error(argThat(anything))).thenReturn(anything); + when(mockLogger.verbose(argThat(anything))).thenReturn(anything); + when(mockLogger.isVerbose).thenReturn(false); + when(mockConfig.macOSConfig).thenReturn(mockMacOSConfig); + when(mockMacOSConfig.generate).thenReturn(true); + when(mockMacOSConfig.imagePath).thenReturn(path.join(prefixPath, 'app_icon.png')); + when(mockConfig.imagePath).thenReturn(path.join(prefixPath, 'app_icon.png')); + }); + + test('should return false when macos config is not provided', () { + when(mockConfig.macOSConfig).thenReturn(null); + expect(generator.validateRequirements(), isFalse); + verify(mockConfig.macOSConfig).called(equals(1)); + }); + + test('should return false when macosConfig is not null but macos.generate is false', () { + when(mockConfig.macOSConfig).thenReturn(mockMacOSConfig); + when(mockMacOSConfig.generate).thenReturn(false); + expect(generator.validateRequirements(), isFalse); + verify(mockConfig.macOSConfig).called(equals(1)); + verify(mockMacOSConfig.generate).called(equals(1)); + }); + + test('should return false when macos.image_path and imagePath is null', () { + when(mockMacOSConfig.imagePath).thenReturn(null); + when(mockConfig.imagePath).thenReturn(null); + expect(generator.validateRequirements(), isFalse); + + verifyInOrder([ + mockMacOSConfig.imagePath, + mockConfig.imagePath, + ]); + }); + + test('should return false when macos dir does not exist', () async { + await d.dir('fli_test', [ + d.file('app_icon.png', testImageFile.readAsBytesSync()), + ]).create(); + await expectLater( + d.dir('fli_test', [ + d.file('app_icon.png', anything), + ]).validate(), + completes, + ); + expect(generator.validateRequirements(), isFalse); + }); + + test('should return false when image file does not exist', () async { + await d.dir('fli_test', [d.dir('macos')]).create(); + await expectLater( + d.dir('fli_test', [d.dir('macos')]).validate(), + completes, + ); + expect(generator.validateRequirements(), isFalse); + }); + }); + }); + + group('MacOSIconGenerator end-to-end', () { + late IconGeneratorContext context; + late IconGenerator generator; + late FlutterLauncherIconsConfig config; + late String prefixPath; + final assetPath = path.join(Directory.current.path, 'test', 'assets'); + + setUp(() async { + final imageFile = File(path.join(assetPath, 'app_icon.png')); + expect(imageFile.existsSync(), isTrue); + await d.dir('fli_test', [ + d.dir('macos/Runner/Assets.xcassets/AppIcon.appiconset', [ + d.file('Contents.json', templates.macOSContentsJsonFile), + ]), + d.file('flutter_launcher_icons.yaml', templates.fliConfigTemplate), + d.file('app_icon.png', imageFile.readAsBytesSync()), + ]).create(); + prefixPath = path.join(d.sandbox, 'fli_test'); + config = FlutterLauncherIconsConfig.loadConfigFromPath('flutter_launcher_icons.yaml', prefixPath)!; + context = IconGeneratorContext(config: config, prefixPath: prefixPath, logger: FLILogger(false)); + generator = MacOSIconGenerator(context); + }); + + test('should generate valid icons & contents.json file', () async { + expect(generator.validateRequirements(), isTrue); + expect(() => generator.createIcons(), isNot(throwsException)); + + await expectLater( + d.dir('fli_test', [ + d.dir('macos/Runner/Assets.xcassets/AppIcon.appiconset', [ + d.file('app_icon_1024.png', anything), + d.file('app_icon_16.png', anything), + d.file('app_icon_32.png', anything), + d.file('app_icon_64.png', anything), + d.file('app_icon_128.png', anything), + d.file('app_icon_256.png', anything), + d.file('app_icon_512.png', anything), + ]), + ]).validate(), + completes, + reason: 'All icon files are not generated', + ); + + await expectLater( + d.dir('fli_test', [ + d.dir('macos/Runner/Assets.xcassets/AppIcon.appiconset', [ + d.file('Contents.json', equals(templates.macOSContentsJsonFile)), + ]), + ]).validate(), + completes, + reason: 'Contents.json file contents are not equal', + ); + }); + }); +} diff --git a/test/macos/macos_icon_generator_test.mocks.dart b/test/macos/macos_icon_generator_test.mocks.dart new file mode 100644 index 0000000000..cb3613de09 --- /dev/null +++ b/test/macos/macos_icon_generator_test.mocks.dart @@ -0,0 +1,119 @@ +// Mocks generated by Mockito 5.3.0 from annotations +// in flutter_launcher_icons/test/macos/macos_icon_generator_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:cli_util/cli_logging.dart' as _i2; +import 'package:flutter_launcher_icons/flutter_launcher_icons_config.dart' + as _i3; +import 'package:flutter_launcher_icons/logger.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeLogger_0 extends _i1.SmartFake implements _i2.Logger { + _FakeLogger_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeProgress_1 extends _i1.SmartFake implements _i2.Progress { + _FakeProgress_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [FlutterLauncherIconsConfig]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFlutterLauncherIconsConfig extends _i1.Mock + implements _i3.FlutterLauncherIconsConfig { + @override + int get minSdkAndroid => + (super.noSuchMethod(Invocation.getter(#minSdkAndroid), + returnValue: 0, returnValueForMissingStub: 0) as int); + @override + bool get removeAlphaIOS => + (super.noSuchMethod(Invocation.getter(#removeAlphaIOS), + returnValue: false, returnValueForMissingStub: false) as bool); + @override + bool get hasAndroidAdaptiveConfig => + (super.noSuchMethod(Invocation.getter(#hasAndroidAdaptiveConfig), + returnValue: false, returnValueForMissingStub: false) as bool); + @override + bool get hasPlatformConfig => + (super.noSuchMethod(Invocation.getter(#hasPlatformConfig), + returnValue: false, returnValueForMissingStub: false) as bool); + @override + bool get isCustomAndroidFile => + (super.noSuchMethod(Invocation.getter(#isCustomAndroidFile), + returnValue: false, returnValueForMissingStub: false) as bool); + @override + bool get isNeedingNewAndroidIcon => + (super.noSuchMethod(Invocation.getter(#isNeedingNewAndroidIcon), + returnValue: false, returnValueForMissingStub: false) as bool); + @override + bool get isNeedingNewIOSIcon => + (super.noSuchMethod(Invocation.getter(#isNeedingNewIOSIcon), + returnValue: false, returnValueForMissingStub: false) as bool); + @override + Map toJson() => (super.noSuchMethod( + Invocation.method(#toJson, []), + returnValue: {}, + returnValueForMissingStub: {}) as Map); +} + +/// A class which mocks [MacOSConfig]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockMacOSConfig extends _i1.Mock implements _i3.MacOSConfig { + @override + bool get generate => (super.noSuchMethod(Invocation.getter(#generate), + returnValue: false, returnValueForMissingStub: false) as bool); + @override + Map toJson() => (super.noSuchMethod( + Invocation.method(#toJson, []), + returnValue: {}, + returnValueForMissingStub: {}) as Map); +} + +/// A class which mocks [FLILogger]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFLILogger extends _i1.Mock implements _i4.FLILogger { + @override + bool get isVerbose => (super.noSuchMethod(Invocation.getter(#isVerbose), + returnValue: false, returnValueForMissingStub: false) as bool); + @override + _i2.Logger get rawLogger => (super.noSuchMethod(Invocation.getter(#rawLogger), + returnValue: _FakeLogger_0(this, Invocation.getter(#rawLogger)), + returnValueForMissingStub: + _FakeLogger_0(this, Invocation.getter(#rawLogger))) as _i2.Logger); + @override + void error(Object? message) => + super.noSuchMethod(Invocation.method(#error, [message]), + returnValueForMissingStub: null); + @override + void verbose(Object? message) => + super.noSuchMethod(Invocation.method(#verbose, [message]), + returnValueForMissingStub: null); + @override + void info(Object? message) => + super.noSuchMethod(Invocation.method(#info, [message]), + returnValueForMissingStub: null); + @override + _i2.Progress progress(String? message) => (super.noSuchMethod( + Invocation.method(#progress, [message]), + returnValue: + _FakeProgress_1(this, Invocation.method(#progress, [message])), + returnValueForMissingStub: + _FakeProgress_1(this, Invocation.method(#progress, [message]))) + as _i2.Progress); +} diff --git a/test/macos/macos_icon_template_test.dart b/test/macos/macos_icon_template_test.dart new file mode 100644 index 0000000000..c8cc33729a --- /dev/null +++ b/test/macos/macos_icon_template_test.dart @@ -0,0 +1,34 @@ +import 'package:flutter_launcher_icons/macos/macos_icon_template.dart'; +import 'package:test/test.dart'; + +void main() { + group('MacOSIconTemplate', () { + late int size; + late int scale; + late MacOSIconTemplate template; + + setUp(() { + size = 16; + scale = 1; + template = MacOSIconTemplate(size, scale); + }); + + test('should pass', () { + expect(template.size, equals(size)); + expect(template.scale, equals(scale)); + expect(template.scaledSize, equals(size * scale)); + expect(template.iconFile, equals('app_icon_${template.scaledSize}.png')); + expect( + template.iconContent, + equals( + { + 'size': '${size}x$size', + 'idiom': 'mac', + 'filename': 'app_icon_${template.scaledSize}.png', + 'scale': '1x', + }, + ), + ); + }); + }); +} diff --git a/test/templates.dart b/test/templates.dart index 7a0c59396f..4953d7f329 100644 --- a/test/templates.dart +++ b/test/templates.dart @@ -20,6 +20,9 @@ flutter_icons: generate: true image_path: "app_icon.png" icon_size: 48 + macos: + generate: true + image_path: "app_icon.png" '''; const flavorFLIConfigTemplate = fliConfigTemplate; @@ -138,6 +141,9 @@ flutter_icons: generate: true image_path: "app_icon.png" icon_size: 48 + macos: + generate: true + image_path: "app_icon.png" '''; const invalidPubspecTemplate = r''' @@ -299,3 +305,74 @@ const webIndexTemplate = r''' '''; + +// macos +const macOSContentsJsonFile = r''' +{ + "info": { + "version": 1, + "author": "xcode" + }, + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ] +}'''; diff --git a/test/utils_test.dart b/test/utils_test.dart index 1ff60f5e17..4880cddc6d 100644 --- a/test/utils_test.dart +++ b/test/utils_test.dart @@ -117,11 +117,11 @@ void main() { }); group('#prettifyJsonEncode', () { - test('should return prettiffed json string 2 indents', () { + test('should return prettiffed json string 4 indents', () { const expectedValue = r''' { - "key1": "value1", - "key2": "value2" + "key1": "value1", + "key2": "value2" }'''; final result = utils.prettifyJsonEncode({ 'key1': 'value1', diff --git a/test/windows/windows_icon_generator_test.mocks.dart b/test/windows/windows_icon_generator_test.mocks.dart index 9e38faefd7..07756ea200 100644 --- a/test/windows/windows_icon_generator_test.mocks.dart +++ b/test/windows/windows_icon_generator_test.mocks.dart @@ -1,7 +1,8 @@ -// Mocks generated by Mockito 5.2.0 from annotations +// Mocks generated by Mockito 5.3.0 from annotations // in flutter_launcher_icons/test/windows/windows_icon_generator_test.dart. // Do not manually edit this file. +// ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:cli_util/cli_logging.dart' as _i2; import 'package:flutter_launcher_icons/flutter_launcher_icons_config.dart' as _i3; @@ -17,10 +18,17 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class -class _FakeLogger_0 extends _i1.Fake implements _i2.Logger {} +class _FakeLogger_0 extends _i1.SmartFake implements _i2.Logger { + _FakeLogger_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} -class _FakeProgress_1 extends _i1.Fake implements _i2.Progress {} +class _FakeProgress_1 extends _i1.SmartFake implements _i2.Progress { + _FakeProgress_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} /// A class which mocks [FlutterLauncherIconsConfig]. /// @@ -40,24 +48,24 @@ class MockFlutterLauncherIconsConfig extends _i1.Mock .noSuchMethod(Invocation.getter(#removeAlphaIOS), returnValue: false) as bool); @override - bool get isNeedingNewIOSIcon => - (super.noSuchMethod(Invocation.getter(#isNeedingNewIOSIcon), - returnValue: false) as bool); - @override - bool get isNeedingNewAndroidIcon => - (super.noSuchMethod(Invocation.getter(#isNeedingNewAndroidIcon), - returnValue: false) as bool); - @override bool get hasAndroidAdaptiveConfig => (super.noSuchMethod(Invocation.getter(#hasAndroidAdaptiveConfig), returnValue: false) as bool); @override + bool get hasPlatformConfig => + (super.noSuchMethod(Invocation.getter(#hasPlatformConfig), + returnValue: false) as bool); + @override bool get isCustomAndroidFile => (super.noSuchMethod(Invocation.getter(#isCustomAndroidFile), returnValue: false) as bool); @override - bool get hasPlatformConfig => - (super.noSuchMethod(Invocation.getter(#hasPlatformConfig), + bool get isNeedingNewAndroidIcon => + (super.noSuchMethod(Invocation.getter(#isNeedingNewAndroidIcon), + returnValue: false) as bool); + @override + bool get isNeedingNewIOSIcon => + (super.noSuchMethod(Invocation.getter(#isNeedingNewIOSIcon), returnValue: false) as bool); @override Map toJson() => @@ -97,7 +105,8 @@ class MockFLILogger extends _i1.Mock implements _i4.FLILogger { as bool); @override _i2.Logger get rawLogger => (super.noSuchMethod(Invocation.getter(#rawLogger), - returnValue: _FakeLogger_0()) as _i2.Logger); + returnValue: _FakeLogger_0(this, Invocation.getter(#rawLogger))) + as _i2.Logger); @override void error(Object? message) => super.noSuchMethod(Invocation.method(#error, [message]), @@ -111,7 +120,9 @@ class MockFLILogger extends _i1.Mock implements _i4.FLILogger { super.noSuchMethod(Invocation.method(#info, [message]), returnValueForMissingStub: null); @override - _i2.Progress progress(String? message) => - (super.noSuchMethod(Invocation.method(#progress, [message]), - returnValue: _FakeProgress_1()) as _i2.Progress); + _i2.Progress progress(String? message) => (super.noSuchMethod( + Invocation.method(#progress, [message]), + returnValue: + _FakeProgress_1(this, Invocation.method(#progress, [message]))) + as _i2.Progress); }