Skip to content

Commit

Permalink
feat: Add icon handle in metadata to handle manifest icons in AdminCP
Browse files Browse the repository at this point in the history
  • Loading branch information
aXenDeveloper committed Nov 30, 2024
1 parent e531dca commit 027d280
Show file tree
Hide file tree
Showing 21 changed files with 355 additions and 187 deletions.
3 changes: 3 additions & 0 deletions apps/frontend/src/plugins/admin/langs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,9 @@
"label": "Start URL",
"desc": "When user launches the app, this is the page that will be loaded."
},
"icon": {
"label": "Icon"
},
"display": {
"label": "Display",
"desc": "The display property controls how the app is displayed.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"drizzle-orm": "^0.36.4",
"helmet": "^8.0.0",
"pg": "^8.13.1",
"rxjs": "^7.8.1",
"sharp": "^0.33.5",
"ua-parser-js": "^2.0.0",
"vitnode-shared": "workspace:*"
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const parseFrontendUrlFromEnv = () => {
};

const parseBackendUrlFromEnv = () => {
const envUrl = process.env.NEXT_PUBLIC_BACKEND_URL;
const envUrl = process.env.NEXT_PUBLIC_BACKEND_CLIENT_URL;
const frontendUrl = envUrl ? envUrl : 'http://localhost:8080';
const urlObj = new URL(frontendUrl);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Body, Put } from '@nestjs/common';
import { ApiOkResponse } from '@nestjs/swagger';
import { MainSettingsAdminBody } from 'vitnode-shared/admin/settings/main.dto';

import { EditMainSettingsAdminService } from './services/edit.main.service';
import { EditMainSettingsAdminService } from './services/edit.service';

@Controllers({
plugin_name: 'Core',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';

import { MainSettingsAdminController } from './main.controller';
import { EditMainSettingsAdminService } from './services/edit.main.service';
import { EditMainSettingsAdminService } from './services/edit.service';

@Module({
providers: [EditMainSettingsAdminService],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@ import { join } from 'path';
import { MainSettingsAdminBody } from 'vitnode-shared/admin/settings/main.dto';
import { ManifestWithLang } from 'vitnode-shared/manifest.dto';

import { getManifest, ManifestType } from '../../metadata/helpers';

@Injectable()
export class EditMainSettingsAdminService {
constructor(private readonly databaseService: InternalDatabaseService) {}

protected async updateDescription({
languages,
site_description,
site_name,
site_short_name,
}: {
languages: { code: string }[];
site_description: MainSettingsAdminBody['site_description'];
site_name: MainSettingsAdminBody['site_name'];
site_short_name: MainSettingsAdminBody['site_short_name'];
}) {
const update = await Promise.all(
(site_description ?? []).map(async el => {
Expand All @@ -32,22 +38,25 @@ export class EditMainSettingsAdminService {
throw new InternalServerErrorException();
}

const path = join(
ABSOLUTE_PATHS.uploads.public,
'assets',
item.language_code,
'manifest.webmanifest',
);
const manifest: ManifestWithLang = JSON.parse(
await readFile(path, 'utf8'),
);
const newData: ManifestWithLang = {
const manifest = await getManifest({ lang_code: item.language_code });
const newData: ManifestType = {
...manifest,
lang: el.language_code,
description: item.value,
name: site_name,
short_name: site_short_name,
};

await writeFile(path, JSON.stringify(newData, null, 2), 'utf8');
await writeFile(
join(
ABSOLUTE_PATHS.uploads.public,
'assets',
item.language_code,
'manifest.webmanifest',
),
JSON.stringify(newData, null, 2),
'utf8',
);

return el.language_code;
}),
Expand Down Expand Up @@ -112,6 +121,8 @@ export class EditMainSettingsAdminService {
await this.updateDescription({
languages,
site_description,
site_name,
site_short_name,
});

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ABSOLUTE_PATHS } from '@/app.module';
import { Injectable, NotFoundException } from '@nestjs/common';
import { NotFoundException } from '@nestjs/common';
import { existsSync } from 'fs';
import { readFile } from 'fs/promises';
import { join } from 'path';
import { ManifestDisplay } from 'vitnode-shared/admin/settings/metadata.enum';
import { FileObj } from 'vitnode-shared/utils/files.dto';

export interface ManifestType {
background_color: string;
Expand All @@ -16,12 +17,12 @@ export interface ManifestType {
| 'standalone'
| 'window-controls-overlay'
)[];
icons?: {
icons?: (FileObj & {
purpose?: 'any' | 'badge' | 'maskable' | 'monochrome';
sizes?: string;
src: string;
type?: string;
}[];
})[];
id: string;
lang: string;
name: string;
Expand Down Expand Up @@ -56,27 +57,24 @@ export interface ManifestType {
theme_color: string;
}

@Injectable()
export class HelpersShowMetadataAdminService {
async getManifest({
export const getManifest = async ({
lang_code,
}: {
lang_code: string;
}): Promise<ManifestType> => {
const path = join(
ABSOLUTE_PATHS.uploads.public,
'assets',
lang_code,
}: {
lang_code: string;
}): Promise<ManifestType> {
const path = join(
ABSOLUTE_PATHS.uploads.public,
'assets',
lang_code,
'manifest.webmanifest',
);
'manifest.webmanifest',
);

if (!existsSync(path)) {
throw new NotFoundException('MANIFEST_NOT_FOUND');
}
if (!existsSync(path)) {
throw new NotFoundException('MANIFEST_NOT_FOUND');
}

const file = await readFile(path, 'utf8');
const data: ManifestType = JSON.parse(file);
const file = await readFile(path, 'utf8');
const data: ManifestType = JSON.parse(file);

return data;
}
}
return data;
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Controllers } from '@/helpers/controller.decorator';
import { Body, Get, Put } from '@nestjs/common';
import { FilesValidationPipe } from '@/helpers/files/files.pipe';
import { UploadFilesMethod } from '@/helpers/upload-files.decorator';
import { Body, Get, Put, UploadedFiles } from '@nestjs/common';
import { ApiOkResponse } from '@nestjs/swagger';
import {
ShowMetadataAdminBody,
Expand All @@ -26,10 +28,21 @@ export class MetadataAdminController {
type: ShowMetadataAdminObj,
})
@Put()
@UploadFilesMethod({ fields: ['icon'] })
async edit(
@UploadedFiles(
new FilesValidationPipe({
icon: {
maxSize: 1024 * 1024, // 1 MB
acceptMimeType: ['image/png', 'image/jpeg', 'image/webp'],
maxCount: 1,
},
}),
)
files: Pick<ShowMetadataAdminBody, 'icon'>,
@Body() body: ShowMetadataAdminBody,
): Promise<ShowMetadataAdminObj> {
return this.editService.edit(body);
return this.editService.edit({ body, files });
}

@ApiOkResponse({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { Module } from '@nestjs/common';

import { HelpersShowMetadataAdminService } from './helpers.service';
import { MetadataAdminController } from './metadata.controller';
import { EditMetadataAdminService } from './services/edit.service';
import { ShowMetadataAdminService } from './services/show.service';

@Module({
providers: [
ShowMetadataAdminService,
HelpersShowMetadataAdminService,
EditMetadataAdminService,
],
providers: [ShowMetadataAdminService, EditMetadataAdminService],
controllers: [MetadataAdminController],
})
export class MetadataSettingsAdminModule {}
Original file line number Diff line number Diff line change
@@ -1,39 +1,125 @@
import { ABSOLUTE_PATHS } from '@/app.module';
import { FilesHelperService } from '@/helpers/files/files-helper.service';
import { InternalDatabaseService } from '@/utils/database/internal_database.service';
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { writeFile } from 'fs/promises';
import { join } from 'path';
import sharp from 'sharp';
import {
ShowMetadataAdminBody,
ShowMetadataAdminObj,
} from 'vitnode-shared/admin/settings/metadata.dto';

import { getManifest, ManifestType } from '../helpers';

@Injectable()
export class EditMetadataAdminService {
constructor(
private readonly configService: ConfigService,
private readonly databaseService: InternalDatabaseService,
private readonly filesHelper: FilesHelperService,
) {}

async edit(data: ShowMetadataAdminBody): Promise<ShowMetadataAdminObj> {
private async processIcon(icon: ShowMetadataAdminBody['icon']) {
if (!icon) return;
const resolutions = [512, 192, 144, 96, 72, 48, 36];

return await Promise.all(
resolutions.map(async resolution => {
const file = await sharp(Buffer.from(icon.buffer))
.resize(resolution, resolution, {
fit: 'contain',
kernel: 'lanczos3',
withoutEnlargement: true,
})
.png({ quality: 100, compressionLevel: 9 })
.toBuffer();
const fileSize = (await sharp(file).metadata()).size;
if (!fileSize) {
throw new InternalServerErrorException('File size not found');
}

const newFile: ShowMetadataAdminBody['icon'] = {
...icon,
buffer: file,
originalname: icon.originalname.replace(
'.png',
`-${resolution}x${resolution}.png`,
),
size: fileSize,
};

const uploadObj = await this.filesHelper.upload({
file: newFile,
folder: 'manifest',
plugin_code: 'core',
});

return {
...uploadObj,
resolution,
};
}),
);
}

async edit({
body: { remove_icon, ...body },
files,
}: {
body: Omit<ShowMetadataAdminBody, 'icon'>;
files: Pick<ShowMetadataAdminBody, 'icon'>;
}): Promise<ShowMetadataAdminObj> {
const frontendUrl: string = this.configService.getOrThrow('frontend_url');
const backendUrl: string = this.configService.getOrThrow('backend_url');
const langs = await this.databaseService.db.query.core_languages.findMany({
columns: {
code: true,
},
orderBy: (table, { desc }) => desc(table.default),
});

if (remove_icon || files.icon) {
const manifest = await getManifest({
lang_code: 'en',
});

if (manifest.icons) {
await Promise.all(
manifest.icons.map(async icon => {
await this.filesHelper.delete({
dir_folder: icon.dir_folder,
file_name: icon.file_name,
});
}),
);
}
}

const images = await this.processIcon(files.icon);

const updateManifests = await Promise.all(
langs.map(async ({ code }) => {
const dataToUpdate: ShowMetadataAdminObj = {
background_color: data.background_color,
start_url: `${frontendUrl}/${code}${data.start_url}`,
theme_color: data.theme_color,
display: data.display,
id: `${frontendUrl}/${code}${data.start_url}`,
const manifest = await getManifest({
lang_code: code,
});
const dataToUpdate: Omit<ManifestType, 'name' | 'short_name'> = {
...manifest,
background_color: body.background_color,
start_url: `${frontendUrl}/${code}${body.start_url}`,
theme_color: body.theme_color,
display: body.display,
id: `${frontendUrl}/${code}${body.start_url}`,
lang: code,
icons: images
? images.map(image => ({
sizes: `${image.resolution}x${image.resolution}`,
src: `${backendUrl}/public/${image.dir_folder}/${image.file_name}`,
type: 'image/png',
...image,
}))
: manifest.icons,
};

await writeFile(
Expand Down
Loading

0 comments on commit 027d280

Please sign in to comment.