From 8d3b629185be038cea757d9131f4dbf646531bd2 Mon Sep 17 00:00:00 2001 From: ThinLiquid Date: Thu, 18 Jan 2024 09:00:14 +0000 Subject: [PATCH 1/7] feat: added a button component --- src/system/lib/Components.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/system/lib/Components.ts b/src/system/lib/Components.ts index 0a5a430..f591378 100644 --- a/src/system/lib/Components.ts +++ b/src/system/lib/Components.ts @@ -37,6 +37,14 @@ const Components: Library = { }) return button } + }, + Icon: { + new: (icon: string) => { + const { HTML } = library + return new HTML('i') + .class('material-symbols-rounded') + .text(icon) + } } } } From f106776bee005f0abd4f808b5dfd20b9b35f521f Mon Sep 17 00:00:00 2001 From: ThinLiquid Date: Thu, 18 Jan 2024 09:00:48 +0000 Subject: [PATCH 2/7] feat: added a `prepend` function --- src/HTML.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/HTML.ts b/src/HTML.ts index 5c766cc..e92515f 100644 --- a/src/HTML.ts +++ b/src/HTML.ts @@ -196,6 +196,24 @@ export default class HTML { return this } + /** + * Prepend an element. Typically used as a `.prepend(new HTML(...))` call. + * @param elem The element to prepend. + * @returns HTML + */ + prepend (elem: string | HTMLElement | HTML): HTML { + if (elem instanceof HTMLElement) { + this.elm.prepend(elem) + } else if (elem instanceof HTML) { + this.elm.prepend(elem.elm) + } else if (typeof elem === 'string') { + const newElem = document.createElement(elem) + this.elm.prepend(newElem) + return new HTML(newElem.tagName) + } + return this + } + /** * Append multiple elements. Typically used as a `.appendMany(new HTML(...), new HTML(...)` call. * @param elements The elements to append. From 2dd5b5f88710edd83cbaadf5f5e4a374c30dcb91 Mon Sep 17 00:00:00 2001 From: ThinLiquid Date: Thu, 18 Jan 2024 11:04:22 +0000 Subject: [PATCH 3/7] feat: minor styling changes --- src/system/lib/Components.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/system/lib/Components.ts b/src/system/lib/Components.ts index f591378..3647015 100644 --- a/src/system/lib/Components.ts +++ b/src/system/lib/Components.ts @@ -31,19 +31,22 @@ const Components: Library = { const button = new HTML('button') button.style({ 'border-radius': '5px', - padding: '2.5px', - background: 'transparent', - border: '1px solid var(--surface-0)' + padding: '2.5px 5px', + background: 'var(--base)', + border: '1px solid var(--surface-1)' }) return button } }, Icon: { - new: (icon: string) => { + new: (icon: string, size = 'inherit') => { const { HTML } = library return new HTML('i') .class('material-symbols-rounded') .text(icon) + .style({ + 'font-size': size + }) } } } From acdba11691a5acd4f60b99613a47bd46925d0718 Mon Sep 17 00:00:00 2001 From: ThinLiquid Date: Thu, 18 Jan 2024 11:04:43 +0000 Subject: [PATCH 4/7] feat: added `prependMany` function --- src/HTML.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/HTML.ts b/src/HTML.ts index e92515f..90f6880 100644 --- a/src/HTML.ts +++ b/src/HTML.ts @@ -226,6 +226,18 @@ export default class HTML { return this } + /** + * Prepend multiple elements. Typically used as a `.prependMany(new HTML(...), new HTML(...)` call. + * @param elements The elements to prepend. + * @returns HTML + */ + prependMany (...elements: any[]): HTML { + for (const elem of elements) { + this.prepend(elem) + } + return this + } + /** * Clear the innerHTML of the element. * @returns HTML From 423dd46da0fff52770918c19eec41ad8856025f4 Mon Sep 17 00:00:00 2001 From: ThinLiquid Date: Thu, 18 Jan 2024 11:06:01 +0000 Subject: [PATCH 5/7] feat: added support for new `/apps/list` endpoint --- src/system/apps/Store.ts | 223 +++++++++++++++++++++++++++++---------- 1 file changed, 169 insertions(+), 54 deletions(-) diff --git a/src/system/apps/Store.ts b/src/system/apps/Store.ts index 7dd8176..3e4b308 100644 --- a/src/system/apps/Store.ts +++ b/src/system/apps/Store.ts @@ -1,7 +1,5 @@ import { Process, RepoData } from '../../types' import icon from '../../assets/icons/softwarecenter.svg' - -import { sanitize } from '../../utils' import nullIcon from '../../assets/icons/application-default-icon.svg' const Store: Process = { @@ -22,68 +20,175 @@ const Store: Process = { }) const fs = await process.loadLibrary('lib/VirtualFS') - - win.content.style.background = 'var(--base)' + const HTML = await process.loadLibrary('lib/HTML') + const { Button, Icon } = await process.loadLibrary('lib/Components') fetch(`${process.kernel.config.SERVER as string}/apps/list/`) .then(async (res) => await res.json()) .then(handle) .catch(e => console.error(e)) - document.addEventListener('fs_update', () => { - fetch(`${process.kernel.config.SERVER as string}/apps/list/`) - .then(async (res) => await res.json()) - .then(handle) - .catch(e => console.error(e)) - }) - function handle (repos: RepoData[]): void { - win.content.innerHTML = ` -
- ` + async function updateList (): Promise { + const res = fetch(`${process.kernel.config.SERVER as string}/apps/list/`) + const repos = await (await res).json() + const div = new HTML(win.content).qs('div') + repos.forEach(async (repo: string, index: number) => { + const repoDiv = div.qsa('div')[index] + repoDiv?.html('') + fetch(`${process.kernel.config.SERVER as string}/cors/?url=${repo}`) + .then(async res => await res.json()) + .then((repo: RepoData) => { + repo.apps.forEach((app) => { + fs.exists(`/home/Applications/${app.url.split('/').at(-1)?.replace('.js', '.app') as string}`) + .then((exists: boolean) => { + const button = Button.new().style({ + display: 'flex', + gap: '5px', + 'align-items': 'center' + }).text('Uninstall') + .prepend(Icon.new('delete')) + .on('click', () => uninstall(app.url)) + + if (exists) { + fetch(`${process.kernel.config.SERVER as string}/cors?url=${app.url}`) + .then(async (res) => await res.text()) + .then(async (data) => { + const local = Buffer.from(await fs.readFile(`/opt/apps/${app.url.split('/').at(-1) as string}`)).toString() + if (local !== data) { + button.text('Update') + .prepend(Icon.new('update')) + .on('click', () => install(app.url)) + } + }).catch(e => console.error(e)) + } else { + button.text('Install') + .prepend(Icon.new('download')) + .on('click', () => install(app.url)) + } + + new HTML('div') + .style({ + display: 'flex', + 'flex-direction': 'row', + gap: '10px', + padding: '10px', + background: 'var(--base)', + 'border-radius': '10px' + }) + .appendMany( + new HTML('img').attr({ + src: app.icon ?? nullIcon + }).style({ + 'aspect-ratio': '1 / 1', + width: '60px', + height: '60px' + }), + new HTML('div').appendMany( + new HTML('h3').style({ + margin: '0' + }).text(app.name), + button + ) + ) + .appendTo(repoDiv) + }) + }) + }) + .catch(e => console.error(e)) + }) + } + + function handle (repos: string[]): void { + win.content.innerHTML = '' + const div = new HTML('div').appendTo(win.content) repos.forEach((repo) => { - (win.content.querySelector('.repos') as HTMLElement).innerHTML += ` -
-
-

${sanitize(repo.name)}

- ${sanitize(repo.id)} -
-
-
-
- ` + fetch(`${process.kernel.config.SERVER as string}/cors/?url=${repo}`) + .then(async res => await res.json()) + .then((repo: RepoData) => { + const icon = Icon.new('arrow_drop_up') + new HTML('h2').text(repo.name).style({ + margin: '0', + padding: '10px', + display: 'flex', + gap: '5px', + 'align-items': 'center' + }) + .prepend(icon) + .appendTo(div) + .on('click', () => { + repoDiv.style({ + height: repoDiv.elm.style.height === '0px' ? 'max-content' : '0' + }) + icon.text(`arrow_drop_${repoDiv.elm.style.height === '0px' ? 'up' : 'down'}`) + }) + const repoDiv = new HTML('div').appendTo(div).style({ + height: '0', + display: 'flex', + 'flex-direction': 'column', + gap: '10px', + overflow: 'hidden', + padding: '0 10px' + }) + repo.apps.forEach((app) => { + fs.exists(`/home/Applications/${app.url.split('/').at(-1)?.replace('.js', '.app') as string}`) + .then((exists: boolean) => { + const button = Button.new().style({ + display: 'flex', + gap: '5px', + 'align-items': 'center' + }).text('Uninstall') + .prepend(Icon.new('delete')) + .on('click', () => uninstall(app.url)) - repo.apps.forEach((app) => { - (win.content.querySelector(`div[data-repo-id="${sanitize(repo.id)}"] > .apps`) as HTMLElement).innerHTML += ` -
- -
-

${sanitize(app.name)}

-
- ${sanitize(app.targetVer)} - download -
-
-
- ` + if (exists) { + fetch(`${process.kernel.config.SERVER as string}/cors?url=${app.url}`) + .then(async (res) => await res.text()) + .then(async (data) => { + const local = Buffer.from(await fs.readFile(`/opt/apps/${app.url.split('/').at(-1) as string}`)).toString() + if (local !== data) { + button.text('Update') + .prepend(Icon.new('update')) + .un('click', () => uninstall(app.url)) + .on('click', () => install(app.url)) + } + }).catch(e => console.error(e)) + } else { + button.text('Install') + .prepend(Icon.new('download')) + .un('click', () => uninstall(app.url)) + .on('click', () => install(app.url)) + } - fs.exists(`/opt/apps/${app.url.split('/').at(-1) as string}`).then((exists: boolean) => { - fs.exists(`/home/Applications/${app.url.split('/').at(-1)?.replace('.js', '.app') as string}`).then((exists2: boolean) => { - if (exists) { - (win.content.querySelector(`div[data-pkg="${sanitize(app.name)}"] div > .material-symbols-rounded`) as HTMLElement).innerHTML = 'delete'; - - (win.content.querySelector(`div[data-pkg="${sanitize(app.name)}"] div > .material-symbols-rounded`) as HTMLElement).onclick = async () => { - await fs.unlink(`/home/Applications/${app.url.split('/').at(-1)?.replace('.js', '.app') as string}`) - await fs.unlink(`/opt/apps/${app.url.split('/').at(-1) as string}`) - } - } else { - (win.content.querySelector(`div[data-pkg="${sanitize(app.name)}"] div > .material-symbols-rounded`) as HTMLElement).onclick = () => { - install(app.url) - } - } - }).catch((e: any) => console.error(e)) - }).catch((e: any) => console.error(e)) - }) + new HTML('div') + .style({ + display: 'flex', + 'flex-direction': 'row', + gap: '10px', + padding: '10px', + background: 'var(--base)', + 'border-radius': '10px' + }) + .appendMany( + new HTML('img').attr({ + src: app.icon ?? nullIcon + }).style({ + 'aspect-ratio': '1 / 1', + width: '60px', + height: '60px' + }), + new HTML('div').appendMany( + new HTML('h3').style({ + margin: '0' + }).text(app.name), + button + ) + ) + .appendTo(repoDiv) + }) + }) + }) + .catch(e => console.error(e)) }) } @@ -92,8 +197,18 @@ const Store: Process = { .then(async (data) => { await fs.writeFile(`/home/Applications/${url.split('/').at(-1)?.replace('.js', '.app') as string}`, `apps/${url.split('/').at(-1)?.split('.')[0] as string}`) await fs.writeFile(`/opt/apps/${url.split('/').at(-1) as string}`, data) + await updateList() }).catch(e => console.error(e)) } + + function uninstall (url: string): void { + fs.unlink(`/home/Applications/${url.split('/').at(-1)?.replace('.js', '.app') as string}`) + .then(async () => { + await fs.unlink(`/opt/apps/${url.split('/').at(-1) as string}`) + await updateList() + }) + .catch(e => console.error(e)) + } } } From bf23629905f62e45511f287648d91b324d90de61 Mon Sep 17 00:00:00 2001 From: ThinLiquid Date: Thu, 18 Jan 2024 11:10:08 +0000 Subject: [PATCH 6/7] style: applied ts-standard formatting --- src/system/apps/Store.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/system/apps/Store.ts b/src/system/apps/Store.ts index 3e4b308..9025f05 100644 --- a/src/system/apps/Store.ts +++ b/src/system/apps/Store.ts @@ -34,7 +34,7 @@ const Store: Process = { const div = new HTML(win.content).qs('div') repos.forEach(async (repo: string, index: number) => { - const repoDiv = div.qsa('div')[index] + const repoDiv = div?.qsa('div')?.[index] repoDiv?.html('') fetch(`${process.kernel.config.SERVER as string}/cors/?url=${repo}`) .then(async res => await res.json()) @@ -91,6 +91,7 @@ const Store: Process = { button ) ) + // @ts-expect-error .appendTo(repoDiv) }) }) From 77c0d577708f9d1dbb8decb07f34fa269319ceca Mon Sep 17 00:00:00 2001 From: ThinLiquid Date: Thu, 18 Jan 2024 11:41:33 +0000 Subject: [PATCH 7/7] fix: added missing error handling for `fs.exists` --- src/system/apps/Store.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/system/apps/Store.ts b/src/system/apps/Store.ts index 9025f05..8d1d64a 100644 --- a/src/system/apps/Store.ts +++ b/src/system/apps/Store.ts @@ -93,7 +93,7 @@ const Store: Process = { ) // @ts-expect-error .appendTo(repoDiv) - }) + }).catch(e => console.error(e)) }) }) .catch(e => console.error(e)) @@ -186,7 +186,7 @@ const Store: Process = { ) ) .appendTo(repoDiv) - }) + }).catch(e => console.error(e)) }) }) .catch(e => console.error(e))