diff --git a/src/HTML.ts b/src/HTML.ts index 5c766cc..90f6880 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. @@ -208,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 diff --git a/src/system/apps/Store.ts b/src/system/apps/Store.ts index 7dd8176..8d1d64a 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,176 @@ 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 + ) + ) + // @ts-expect-error + .appendTo(repoDiv) + }).catch(e => console.error(e)) + }) + }) + .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)) + }) + }) + .catch(e => console.error(e)) }) } @@ -92,8 +198,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)) + } } } diff --git a/src/system/lib/Components.ts b/src/system/lib/Components.ts index 0a5a430..3647015 100644 --- a/src/system/lib/Components.ts +++ b/src/system/lib/Components.ts @@ -31,12 +31,23 @@ 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, size = 'inherit') => { + const { HTML } = library + return new HTML('i') + .class('material-symbols-rounded') + .text(icon) + .style({ + 'font-size': size + }) + } } } }