diff --git a/docs/man_pages/project/configuration/resources/resources-generate-icons.md b/docs/man_pages/project/configuration/resources/resources-generate-icons.md index 60186a5d08..9f4ff21afa 100644 --- a/docs/man_pages/project/configuration/resources/resources-generate-icons.md +++ b/docs/man_pages/project/configuration/resources/resources-generate-icons.md @@ -15,6 +15,10 @@ Usage | Synopsis ------|------- `$ tns resources generate icons ` | Generate all icons for Android and iOS based on the specified image. +### Options + +* `--background` Sets the background color of the icon. When no color is specified, a default value of `transparent` is used. `` is a valid color and can be represented with string, like `white`, `black`, `blue`, or with HEX representation, for example `#FFFFFF`, `#000000`, `#0000FF`. NOTE: As the `#` is special symbol in some terminals, make sure to place the value in quotes, for example `$ tns resources generate icons ../myImage.png --background "#FF00FF"`. + ### Arguments * `` is a valid path to an image that will be used to generate all icons. diff --git a/lib/commands/generate-assets.ts b/lib/commands/generate-assets.ts index b7c9e9cff5..5133cea3e0 100644 --- a/lib/commands/generate-assets.ts +++ b/lib/commands/generate-assets.ts @@ -61,6 +61,7 @@ export class GenerateIconsCommand ): Promise { await this.$assetsGenerationService.generateIcons({ imagePath, + background, projectDir: this.$projectData.projectDir, }); } diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index 1e22ea542a..9de868d95c 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -1144,12 +1144,7 @@ interface IResourceGenerationData extends IProjectDir { * @param {string} platform Specify for which platform to generate assets. If not defined will generate for all platforms */ platform?: string; -} -/** - * Describes the data needed for splash screens generation - */ -interface ISplashesGenerationData extends IResourceGenerationData { /** * @param {string} background Background color that will be used for background. Defaults to #FFFFFF */ @@ -1169,11 +1164,11 @@ interface IAssetsGenerationService { /** * Generate splash screens for iOS and Android - * @param {ISplashesGenerationData} splashesGenerationData Provides the data needed for splash screens generation + * @param {IResourceGenerationData} splashesGenerationData Provides the data needed for splash screens generation * @returns {Promise} */ generateSplashScreens( - splashesGenerationData: ISplashesGenerationData + splashesGenerationData: IResourceGenerationData ): Promise; } diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index 1885afbe44..92b81934ad 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -407,6 +407,10 @@ interface IAssetItem { resizeOperation?: string; overlayImageScale?: number; rgba?: boolean; + + // additional operations for special cases + operation?: "delete" | "writeXMLColor"; + data?: any; } interface IAssetSubGroup { @@ -438,6 +442,7 @@ interface IImageDefinitionGroup { interface IImageDefinitionsStructure { ios: IImageDefinitionGroup; android: IImageDefinitionGroup; + android_legacy: IImageDefinitionGroup; } interface ITemplateData { diff --git a/lib/services/assets-generation/assets-generation-service.ts b/lib/services/assets-generation/assets-generation-service.ts index 898a79237c..56cab34623 100644 --- a/lib/services/assets-generation/assets-generation-service.ts +++ b/lib/services/assets-generation/assets-generation-service.ts @@ -5,21 +5,22 @@ import { AssetConstants } from "../../constants"; import { IAssetsGenerationService, IResourceGenerationData, - ISplashesGenerationData, } from "../../declarations"; import { IProjectDataService, IAssetGroup, IAssetSubGroup, } from "../../definitions/project"; -import { IDictionary } from "../../common/declarations"; +import { IDictionary, IFileSystem } from "../../common/declarations"; import * as _ from "lodash"; import { injector } from "../../common/yok"; +import { EOL } from "os"; export const enum Operations { OverlayWith = "overlayWith", Blank = "blank", Resize = "resize", + OuterScale = "outerScale", } export class AssetsGenerationService implements IAssetsGenerationService { @@ -32,7 +33,8 @@ export class AssetsGenerationService implements IAssetsGenerationService { constructor( private $logger: ILogger, - private $projectDataService: IProjectDataService + private $projectDataService: IProjectDataService, + private $fs: IFileSystem ) {} @exported("assetsGenerationService") @@ -49,7 +51,7 @@ export class AssetsGenerationService implements IAssetsGenerationService { @exported("assetsGenerationService") public async generateSplashScreens( - splashesGenerationData: ISplashesGenerationData + splashesGenerationData: IResourceGenerationData ): Promise { this.$logger.info("Generating splash screens ..."); await this.generateImagesForDefinitions( @@ -60,10 +62,10 @@ export class AssetsGenerationService implements IAssetsGenerationService { } private async generateImagesForDefinitions( - generationData: ISplashesGenerationData, + generationData: IResourceGenerationData, propertiesToEnumerate: string[] ): Promise { - generationData.background = generationData.background || "white"; + const background = generationData.background || "white"; const assetsStructure = await this.$projectDataService.getAssetsStructure( generationData ); @@ -88,6 +90,45 @@ export class AssetsGenerationService implements IAssetsGenerationService { .value(); for (const assetItem of assetItems) { + if (assetItem.operation === "delete") { + if (this.$fs.exists(assetItem.path)) { + this.$fs.deleteFile(assetItem.path); + } + continue; + } + + if (assetItem.operation === "writeXMLColor") { + const colorName = assetItem.data?.colorName; + if (!colorName) { + continue; + } + try { + const color = + (generationData as any)[assetItem.data?.fromKey] ?? + assetItem.data?.default ?? + "white"; + + const colorHEX: number = Jimp.cssColorToHex(color); + const hex = colorHEX?.toString(16).substring(0, 6) ?? "FFFFFF"; + + this.$fs.writeFile( + assetItem.path, + [ + ``, + ``, + ` #${hex.toUpperCase()}`, + ``, + ].join(EOL) + ); + } catch (err) { + this.$logger.info( + `Failed to write provided color to ${assetItem.path} -> ${colorName}. See --log trace for more info.` + ); + this.$logger.trace(err); + } + continue; + } + const operation = assetItem.resizeOperation || Operations.Resize; let tempScale: number = null; if (assetItem.scale) { @@ -133,7 +174,7 @@ export class AssetsGenerationService implements IAssetsGenerationService { imageResize ); image = this.generateImage( - generationData.background, + background, width, height, outputPath, @@ -141,16 +182,27 @@ export class AssetsGenerationService implements IAssetsGenerationService { ); break; case Operations.Blank: + image = this.generateImage(background, width, height, outputPath); + break; + case Operations.Resize: + image = await this.resize(generationData.imagePath, width, height); + break; + case Operations.OuterScale: + // Resize image without applying scale + image = await this.resize( + generationData.imagePath, + assetItem.width, + assetItem.height + ); + // The scale will apply to the underlying layer of the generated image image = this.generateImage( - generationData.background, + "#00000000", width, height, - outputPath + outputPath, + image ); break; - case Operations.Resize: - image = await this.resize(generationData.imagePath, width, height); - break; default: throw new Error(`Invalid image generation operation: ${operation}`); } diff --git a/lib/services/project-data-service.ts b/lib/services/project-data-service.ts index a734a2590e..abc917c424 100644 --- a/lib/services/project-data-service.ts +++ b/lib/services/project-data-service.ts @@ -236,18 +236,29 @@ export class ProjectDataService implements IProjectDataService { ? path.join(pathToAndroidDir, SRC_DIR, MAIN_DIR, RESOURCES_DIR) : pathToAndroidDir; - const currentStructure = this.$fs.enumerateFilesInDirectorySync(basePath); - const content = this.getImageDefinitions().android; + let useLegacy = false; + try { + const manifest = this.$fs.readText( + path.resolve(basePath, "../AndroidManifest.xml") + ); + useLegacy = !manifest.includes(`android:icon="@mipmap/ic_launcher"`); + } catch (err) { + // ignore + } + + const content = this.getImageDefinitions()[ + useLegacy ? "android_legacy" : "android" + ]; return { - icons: this.getAndroidAssetSubGroup(content.icons, currentStructure), + icons: this.getAndroidAssetSubGroup(content.icons, basePath), splashBackgrounds: this.getAndroidAssetSubGroup( content.splashBackgrounds, - currentStructure + basePath ), splashCenterImages: this.getAndroidAssetSubGroup( content.splashCenterImages, - currentStructure + basePath ), splashImages: null, }; @@ -448,23 +459,23 @@ export class ProjectDataService implements IProjectDataService { private getAndroidAssetSubGroup( assetItems: IAssetItem[], - realPaths: string[] + basePath: string ): IAssetSubGroup { const assetSubGroup: IAssetSubGroup = { images: [], }; - const normalizedPaths = _.map(realPaths, (p) => path.normalize(p)); _.each(assetItems, (assetItem) => { - _.each(normalizedPaths, (currentNormalizedPath) => { - const imagePath = path.join(assetItem.directory, assetItem.filename); - if (currentNormalizedPath.indexOf(path.normalize(imagePath)) !== -1) { - assetItem.path = currentNormalizedPath; - assetItem.size = `${assetItem.width}${AssetConstants.sizeDelimiter}${assetItem.height}`; - assetSubGroup.images.push(assetItem); - return false; - } - }); + const imagePath = path.join( + basePath, + assetItem.directory, + assetItem.filename + ); + assetItem.path = imagePath; + if (assetItem.width && assetItem.height) { + assetItem.size = `${assetItem.width}${AssetConstants.sizeDelimiter}${assetItem.height}`; + } + assetSubGroup.images.push(assetItem); }); return assetSubGroup; diff --git a/resources/assets/image-definitions.json b/resources/assets/image-definitions.json index 78e1e7eb5b..d3bc47cd6d 100644 --- a/resources/assets/image-definitions.json +++ b/resources/assets/image-definitions.json @@ -411,43 +411,162 @@ } ] }, - "android": { + "android_legacy": { "icons": [ { "width": 72, "height": 72, "directory": "drawable-hdpi", - "filename": "icon.png" + "filename": "icon.png", + "resizeOperation": "overlayWith", + "scale": "1x" }, { "width": 36, "height": 36, "directory": "drawable-ldpi", - "filename": "icon.png" + "filename": "icon.png", + "resizeOperation": "overlayWith", + "scale": "1x" }, { "width": 48, "height": 48, "directory": "drawable-mdpi", - "filename": "icon.png" + "filename": "icon.png", + "resizeOperation": "overlayWith", + "scale": "1x" }, { "width": 96, "height": 96, "directory": "drawable-xhdpi", - "filename": "icon.png" + "filename": "icon.png", + "resizeOperation": "overlayWith", + "scale": "1x" }, { "width": 144, "height": 144, "directory": "drawable-xxhdpi", - "filename": "icon.png" + "filename": "icon.png", + "resizeOperation": "overlayWith", + "scale": "1x" }, { "width": 345, "height": 345, "directory": "drawable-xxxhdpi", - "filename": "icon.png" + "filename": "icon.png", + "resizeOperation": "overlayWith", + "scale": "1x" + } + ] + }, + "android": { + "icons": [ + { + "directory": "drawable", + "filename": "ic_launcher_foreground.xml", + "operation": "delete" + }, + { + "directory": "values", + "filename": "ic_launcher_background.xml", + "operation": "writeXMLColor", + "data": { + "colorName": "ic_launcher_background", + "fromKey": "background", + "default": "#FFFFFF" + } + }, + { + "width": 108, + "height": 108, + "directory": "drawable-hdpi", + "filename": "ic_launcher_foreground.png", + "resizeOperation": "outerScale", + "scale": "1.5x" + }, + { + "width": 54, + "height": 54, + "directory": "drawable-ldpi", + "filename": "ic_launcher_foreground.png", + "resizeOperation": "outerScale", + "scale": "1.5x" + }, + { + "width": 72, + "height": 72, + "directory": "drawable-mdpi", + "filename": "ic_launcher_foreground.png", + "resizeOperation": "outerScale", + "scale": "1.5x" + }, + { + "width": 144, + "height": 144, + "directory": "drawable-xhdpi", + "filename": "ic_launcher_foreground.png", + "resizeOperation": "outerScale", + "scale": "1.5x" + }, + { + "width": 216, + "height": 216, + "directory": "drawable-xxhdpi", + "filename": "ic_launcher_foreground.png", + "resizeOperation": "outerScale", + "scale": "1.5x" + }, + { + "width": 288, + "height": 288, + "directory": "drawable-xxxhdpi", + "filename": "ic_launcher_foreground.png", + "resizeOperation": "outerScale", + "scale": "1.5x" + }, + { + "width": 72, + "height": 72, + "directory": "mipmap-hdpi", + "filename": "ic_launcher.png", + "resizeOperation": "overlayWith", + "scale": "1x" + }, + { + "width": 48, + "height": 48, + "directory": "mipmap-mdpi", + "filename": "ic_launcher.png", + "resizeOperation": "overlayWith", + "scale": "1x" + }, + { + "width": 96, + "height": 96, + "directory": "mipmap-xhdpi", + "filename": "ic_launcher.png", + "resizeOperation": "overlayWith", + "scale": "1x" + }, + { + "width": 144, + "height": 144, + "directory": "mipmap-xxhdpi", + "filename": "ic_launcher.png", + "resizeOperation": "overlayWith", + "scale": "1x" + }, + { + "width": 192, + "height": 192, + "directory": "mipmap-xxxhdpi", + "filename": "ic_launcher.png", + "resizeOperation": "overlayWith", + "scale": "1x" } ], "splashBackgrounds": [ @@ -533,4 +652,4 @@ } ] } -} \ No newline at end of file +}