From df10a7c332948fb724ae260c6608393f5b0701fa Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Fri, 20 Dec 2024 15:08:10 -0500 Subject: [PATCH] dev: add icon_size query for get-launch-apps --- src/backend/src/routers/get-launch-apps.js | 77 +++++++++++++++++-- .../src/routers/get-launch-apps.test.js | 2 + src/backend/src/services/AppIconService.js | 15 ++-- 3 files changed, 79 insertions(+), 15 deletions(-) diff --git a/src/backend/src/routers/get-launch-apps.js b/src/backend/src/routers/get-launch-apps.js index a9ff4c85..14acbc86 100644 --- a/src/backend/src/routers/get-launch-apps.js +++ b/src/backend/src/routers/get-launch-apps.js @@ -22,6 +22,41 @@ const router = express.Router(); const auth = require('../middleware/auth.js'); const { get_app } = require('../helpers.js'); const { DB_READ } = require('../services/database/consts.js'); +const { stream_to_buffer } = require('../util/streamutil.js'); + +const get_apps = async ({ specifiers }) => { + return await Promise.all(specifiers.map(async (specifier) => { + return await get_app(specifier); + })); +}; + +const iconify_apps = async (context, { apps, size }) => { + return await Promise.all(apps.map(async app => { + const data_url = app.icon; + if ( ! data_url ) return app; + + const metadata = data_url.split(',')[0]; + const input_mime = metadata.split(';')[0].split(':')[1]; + + // svg icons will be sent as-is + if (input_mime === 'image/svg+xml') { + return app; + } + + const svc_appIcon = context.services.get('app-icon'); + const { stream, mime } = await svc_appIcon.get_icon_stream({ + app_icon: app.icon, + app_uid: app.uid ?? app.uuid, + size: size, + }); + + const buffer = await stream_to_buffer(stream); + const resp_data_url = `data:image/png;base64,${buffer.toString('base64')}`; + + app.icon = resp_data_url; + return app; + })); +} // -----------------------------------------------------------------------// // GET /get-launch-apps @@ -29,10 +64,22 @@ const { DB_READ } = require('../services/database/consts.js'); module.exports = async (req, res) => { let result = {}; + // Verify query params + if ( req.query.icon_size ) { + const ALLOWED_SIZES = ['16', '32', '64', '128', '256', '512']; + + if ( ! ALLOWED_SIZES.includes(req.query.icon_size) ) { + res.status(400).send({ error: 'Invalid icon_size' }); + } + } + // -----------------------------------------------------------------------// // Recommended apps // -----------------------------------------------------------------------// - result.recommended = kv.get('global:recommended-apps'); + const recommended_cache_key = 'global:recommended-apps' + ( + req.query.icon_size ? `:icon-size:${req.query.icon_size}` : '' + ); + result.recommended = kv.get(recommended_cache_key); if ( ! result.recommended ) { let app_names = new Set([ 'app-center', @@ -62,12 +109,10 @@ module.exports = async (req, res) => { // Prepare each app for returning to user by only returning the necessary fields // and adding them to the retobj array - result.recommended = []; - for ( const name of app_names ) { - const app = await get_app({ name }); - if ( ! app ) continue; - - result.recommended.push({ + result.recommended = (await get_apps({ + specifiers: Array.from(app_names).map(name => ({ name })) + })).filter(app => !! app).map(app => { + return { uuid: app.uid, name: app.name, title: app.title, @@ -75,10 +120,18 @@ module.exports = async (req, res) => { godmode: app.godmode, maximize_on_start: app.maximize_on_start, index_url: app.index_url, + }; + }); + + // Iconify apps + if ( req.query.icon_size ) { + result.recommended = await iconify_apps({ services: req.services }, { + apps: result.recommended, + size: req.query.icon_size, }); } - kv.set('global:recommended-apps', result.recommended); + kv.set(recommended_cache_key, result.recommended); } // -----------------------------------------------------------------------// @@ -122,5 +175,13 @@ module.exports = async (req, res) => { }); } + // Iconify apps + if ( req.query.icon_size ) { + result.recent = await iconify_apps({ services: req.services }, { + apps: result.recent, + size: req.query.icon_size, + }); + } + return res.send(result); }; diff --git a/src/backend/src/routers/get-launch-apps.test.js b/src/backend/src/routers/get-launch-apps.test.js index b160f98f..1f9d04db 100644 --- a/src/backend/src/routers/get-launch-apps.test.js +++ b/src/backend/src/routers/get-launch-apps.test.js @@ -124,6 +124,7 @@ describe('GET /launch-apps', () => { // First call { const { get_launch_apps, req_mock, res_mock, spies } = get_mock_context(); + req_mock.query = {}; await get_launch_apps(req_mock, res_mock); expect(res_mock.send.calledOnce).to.equal(true, 'res.send should be called once'); @@ -177,6 +178,7 @@ describe('GET /launch-apps', () => { // Second call { const { get_launch_apps, req_mock, res_mock, spies } = get_mock_context(); + req_mock.query = {}; await get_launch_apps(req_mock, res_mock); expect(res_mock.send.calledOnce).to.equal(true, 'res.send should be called once'); diff --git a/src/backend/src/services/AppIconService.js b/src/backend/src/services/AppIconService.js index 3c282b97..002522f2 100644 --- a/src/backend/src/services/AppIconService.js +++ b/src/backend/src/services/AppIconService.js @@ -62,20 +62,21 @@ class AppIconService extends BaseService { }).attach(app); } - async get_icon_stream ({ app_uid, size }) { + async get_icon_stream ({ app_icon, app_uid, size }) { // Get icon file node const dir_app_icons = await this.get_app_icons(); + console.log('APP UID', app_uid); const node = await dir_app_icons.getChild(`${app_uid}-${size}.png`); if ( ! await node.exists() ) { // Use database-stored icon as a fallback - const app = await get_app({ uid: app_uid }); - if ( ! app.icon ) { - app.icon = DEFAULT_APP_ICON; - } - const [metadata, app_icon] = app.icon.split(','); + app_icon = app_icon ?? await (async () => { + const app = await get_app({ uid: app_uid }); + return app.icon ?? DEFAULT_APP_ICON; + })() + const [metadata, base64] = app_icon.split(','); console.log('METADATA', metadata); const mime = metadata.split(';')[0].split(':')[1]; - const img = Buffer.from(app_icon, 'base64'); + const img = Buffer.from(base64, 'base64'); return { mime, stream: buffer_to_stream(img),