diff --git a/.changeset/proud-ways-compare.md b/.changeset/proud-ways-compare.md new file mode 100644 index 0000000..810784f --- /dev/null +++ b/.changeset/proud-ways-compare.md @@ -0,0 +1,5 @@ +--- +'@capacitor/assets': minor +--- + +Added support for Android notification icons diff --git a/src/definitions.ts b/src/definitions.ts index 42160dd..c746a17 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -144,10 +144,18 @@ export interface PwaOutputAssetTemplate extends OutputAssetTemplate { export interface AndroidOutputAssetTemplate extends OutputAssetTemplate { density: AndroidDensity; } + +export interface AndroidNotificationTemplate extends AndroidOutputAssetTemplate { + kind: AssetKind.NotificationIcon; + width: number; + height: number; +} + export interface AndroidOutputAssetTemplateSplash extends OutputAssetTemplate { density: AndroidDensity; orientation: Orientation; } + export interface AndroidOutputAssetTemplateAdaptiveIcon extends OutputAssetTemplate { density: AndroidDensity; } diff --git a/src/platforms/android/assets.ts b/src/platforms/android/assets.ts index 37592d6..f47b17b 100644 --- a/src/platforms/android/assets.ts +++ b/src/platforms/android/assets.ts @@ -59,6 +59,54 @@ export const ANDROID_XXXHDPI_ICON: AndroidOutputAssetTemplate = { density: AndroidDensity.Xxxhdpi, }; +/** + * Notification icons + */ +export const ANDROID_NOTIFICATION_MDPI_ICON: AndroidOutputAssetTemplate = { + platform: Platform.Android, + kind: AssetKind.NotificationIcon, + format: Format.Png, + width: 24, + height: 24, + density: AndroidDensity.Mdpi, +}; + +export const ANDROID_NOTIFICATION_HDPI_ICON: AndroidOutputAssetTemplate = { + platform: Platform.Android, + kind: AssetKind.NotificationIcon, + format: Format.Png, + width: 36, + height: 36, + density: AndroidDensity.Hdpi, +}; + +export const ANDROID_NOTIFICATION_XHDPI_ICON: AndroidOutputAssetTemplate = { + platform: Platform.Android, + kind: AssetKind.NotificationIcon, + format: Format.Png, + width: 48, + height: 48, + density: AndroidDensity.Xhdpi, +}; + +export const ANDROID_NOTIFICATION_XXHDPI_ICON: AndroidOutputAssetTemplate = { + platform: Platform.Android, + kind: AssetKind.NotificationIcon, + format: Format.Png, + width: 72, + height: 72, + density: AndroidDensity.Xxhdpi, +}; + +export const ANDROID_NOTIFICATION_XXXHDPI_ICON: AndroidOutputAssetTemplate = { + platform: Platform.Android, + kind: AssetKind.NotificationIcon, + format: Format.Png, + width: 144, + height: 144, + density: AndroidDensity.Xxxhdpi, +}; + /** * Adaptive icons */ diff --git a/src/platforms/android/index.ts b/src/platforms/android/index.ts index 8d1b7c3..a84eb49 100644 --- a/src/platforms/android/index.ts +++ b/src/platforms/android/index.ts @@ -10,13 +10,14 @@ import type { AndroidOutputAssetTemplate, AndroidOutputAssetTemplateAdaptiveIcon, AndroidOutputAssetTemplateSplash, + AndroidNotificationTemplate, } from '../../definitions'; -import { AssetKind, Platform } from '../../definitions'; +import { AssetKind, Format, Platform } from '../../definitions'; import { BadPipelineError, BadProjectError } from '../../error'; import type { InputAsset } from '../../input-asset'; import { OutputAsset } from '../../output-asset'; import type { Project } from '../../project'; -import { warn } from '../../util/log'; +import { warn, error } from '../../util/log'; import * as AndroidAssetTemplates from './assets'; @@ -35,7 +36,6 @@ export class AndroidAssetGenerator extends AssetGenerator { if (asset.platform !== Platform.Any && asset.platform !== Platform.Android) { return []; } - switch (asset.kind) { case AssetKind.Logo: case AssetKind.LogoDark: @@ -49,6 +49,8 @@ export class AndroidAssetGenerator extends AssetGenerator { case AssetKind.Splash: case AssetKind.SplashDark: return this.generateSplashes(asset, project); + case AssetKind.NotificationIcon: + return this.generateNotificationIcons(asset, project); } return []; @@ -525,4 +527,72 @@ export class AndroidAssetGenerator extends AssetGenerator { private getResPath(project: Project): string { return join(project.config.android!.path!, 'app', 'src', this.options.androidFlavor ?? 'main', 'res'); } + + private async generateNotificationIcons(asset: InputAsset, project: Project): Promise { + const pipe = asset.pipeline(); + if (!pipe) { + throw new BadPipelineError('Sharp instance not created'); + } + + const notificationTemplates = Object.values(AndroidAssetTemplates).filter( + (a) => a.kind === AssetKind.NotificationIcon, + ) as AndroidNotificationTemplate[]; + const resPath = this.getResPath(project); + const generated: OutputAsset[] = []; + + for (const template of notificationTemplates) { + try { + const drawablePath = join(resPath, `drawable-${template.density}`); + if (!(await pathExists(drawablePath))) { + await mkdirp(drawablePath); + } + + const destFile = join(drawablePath, 'ic_stat_notification.png'); + const outputInfo = await pipe.resize(template.width, template.height).png().toFile(destFile); + + const relPath = relative(resPath, destFile); + generated.push(new OutputAsset(template, asset, project, { [relPath]: destFile }, { [relPath]: outputInfo })); + } catch (err) { + error(`Failed to generate ${template.density} notification icon:`, err); + } + } + + // Generate for main drawable folder + try { + const mainDrawablePath = join(resPath, 'drawable'); + if (!(await pathExists(mainDrawablePath))) { + await mkdirp(mainDrawablePath); + } + + const mainDestFile = join(mainDrawablePath, 'ic_stat_notification.png'); + const outputInfo = await pipe + .resize( + AndroidAssetTemplates.ANDROID_NOTIFICATION_XXXHDPI_ICON.width, + AndroidAssetTemplates.ANDROID_NOTIFICATION_XXXHDPI_ICON.height, + ) + .png() + .toFile(mainDestFile); + + const relPath = relative(resPath, mainDestFile); + generated.push( + new OutputAsset( + { + platform: Platform.Android, + kind: AssetKind.NotificationIcon, + format: Format.Png, + width: AndroidAssetTemplates.ANDROID_NOTIFICATION_XXXHDPI_ICON.width, + height: AndroidAssetTemplates.ANDROID_NOTIFICATION_XXXHDPI_ICON.height, + }, + asset, + project, + { [relPath]: mainDestFile }, + { [relPath]: outputInfo }, + ), + ); + } catch (err) { + error('Failed to generate main notification icon:', err); + } + + return generated; + } } diff --git a/src/platforms/ios/index.ts b/src/platforms/ios/index.ts index 7006f51..2db4e82 100644 --- a/src/platforms/ios/index.ts +++ b/src/platforms/ios/index.ts @@ -63,7 +63,7 @@ export class IosAssetGenerator extends AssetGenerator { throw new BadPipelineError('Sharp instance not created'); } - const iosDir = project.config.ios!.path!; + const iosDir = project.config.ios?.path ?? 'App'; // Generate logos let logos: OutputAsset[] = []; @@ -179,7 +179,7 @@ export class IosAssetGenerator extends AssetGenerator { throw new BadPipelineError('Sharp instance not created'); } - const iosDir = project.config.ios!.path!; + const iosDir = project.config.ios?.path ?? 'App'; const lightDefaultBackground = '#ffffff'; const generated = await Promise.all( icons.map(async (icon) => { @@ -242,7 +242,7 @@ export class IosAssetGenerator extends AssetGenerator { const generated: OutputAsset[] = []; for (const assetMeta of assetMetas) { - const iosDir = project.config.ios!.path!; + const iosDir = project.config.ios?.path ?? 'App'; const dest = join(iosDir, IOS_SPLASH_IMAGE_SET_PATH, assetMeta.name); const outputInfo = await pipe.resize(assetMeta.width, assetMeta.height).png().toFile(dest); @@ -273,7 +273,7 @@ export class IosAssetGenerator extends AssetGenerator { } private async updateIconsContentsJson(generated: OutputAsset[], project: Project) { - const assetsPath = join(project.config.ios!.path!, IOS_APP_ICON_SET_PATH); + const assetsPath = join(project.config.ios?.path ?? 'App', IOS_APP_ICON_SET_PATH); const contentsJsonPath = join(assetsPath, 'Contents.json'); const json = await readFile(contentsJsonPath, { encoding: 'utf-8' }); @@ -304,7 +304,7 @@ export class IosAssetGenerator extends AssetGenerator { } private async updateSplashContentsJson(generated: OutputAsset[], project: Project) { - const contentsJsonPath = join(project.config.ios!.path!, IOS_SPLASH_IMAGE_SET_PATH, 'Contents.json'); + const contentsJsonPath = join(project.config.ios?.path ?? 'App', IOS_SPLASH_IMAGE_SET_PATH, 'Contents.json'); const json = await readFile(contentsJsonPath, { encoding: 'utf-8' }); const parsed = JSON.parse(json); @@ -334,7 +334,7 @@ export class IosAssetGenerator extends AssetGenerator { } private async updateSplashContentsJsonDark(generated: OutputAsset[], project: Project) { - const contentsJsonPath = join(project.config.ios!.path!, IOS_SPLASH_IMAGE_SET_PATH, 'Contents.json'); + const contentsJsonPath = join(project.config.ios?.path ?? 'App', IOS_SPLASH_IMAGE_SET_PATH, 'Contents.json'); const json = await readFile(contentsJsonPath, { encoding: 'utf-8' }); const parsed = JSON.parse(json); diff --git a/src/tasks/generate.ts b/src/tasks/generate.ts index 37b3498..2f20499 100644 --- a/src/tasks/generate.ts +++ b/src/tasks/generate.ts @@ -93,8 +93,8 @@ async function generateAssets(assets: Assets, generators: AssetGenerator[], proj const generated: OutputAsset[] = []; async function generateAndCollect(asset: InputAsset) { - const g = await Promise.all(generators.map((g) => asset.generate(g, project))); - generated.push(...(g.flat().filter((f) => !!f) as OutputAsset[])); + const output = await Promise.all(generators.map((g) => asset.generate(g, project))); + generated.push(...(output.flat().filter((f) => !!f) as OutputAsset[])); } const assetTypes = Object.values(assets).filter((v) => !!v);