Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/proud-ways-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@capacitor/assets': minor
---

Added support for Android notification icons
8 changes: 8 additions & 0 deletions src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
48 changes: 48 additions & 0 deletions src/platforms/android/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
76 changes: 73 additions & 3 deletions src/platforms/android/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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:
Expand All @@ -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 [];
Expand Down Expand Up @@ -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<OutputAsset[]> {
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;
}
}
12 changes: 6 additions & 6 deletions src/platforms/ios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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' });

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/tasks/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down