diff --git a/README.md b/README.md index fc690af..3ec857d 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ npm run serve ## Made with FlowOS is made with the following software: -* [Filer](https://github.com/filerjs/filer) +* [Kat21's HTML Library](https://github.com/datkat21/html) * [Prism Code Editor](https://github.com/FIameCaster/prism-code-editor) * [Vite](https://vitejs.dev) * [Ultraviolet](https://github.com/titaniumnetwork-dev/ultraviolet) diff --git a/src/builtin/apps/info.ts b/src/builtin/apps/info.ts index 1aff69a..b661b57 100644 --- a/src/builtin/apps/info.ts +++ b/src/builtin/apps/info.ts @@ -1,8 +1,9 @@ import icon from '../../assets/icons/userinfo.svg' import badge from '../../assets/badge.png' -import { App, PackageJSON } from '../../types' +import { App } from '../../types' import FlowWindow from '../../structures/FlowWindow' +import HTML from '../../lib' export default class InfoApp implements App { meta = { @@ -14,7 +15,6 @@ export default class InfoApp implements App { } async open (): Promise { - const packageJSON: PackageJSON = await import('../../../package.json') const win = window.wm.createWindow({ title: this.meta.name, icon: this.meta.icon, @@ -30,18 +30,29 @@ export default class InfoApp implements App { win.content.style.justifyContent = 'center' win.content.style.alignItems = 'center' win.content.style.background = 'var(--base)' - win.content.innerHTML = ` -
-

FlowOS

-

v${packageJSON.version}

-
-

Created by ThinLiquid, 1nspird_, proudparot2, systemless_

-
- Discord - - - Github -
- ` + + const div = new HTML('div').appendTo(win.content) + new HTML('h1').style({ + margin: '0' + }).text(`FlowOS ${window.flowDetails.codename}`).appendTo(div) + new HTML('p').style({ + margin: '0' + }).text(`v${window.flowDetails.version}`).appendTo(div) + new HTML('br').appendTo(div) + new HTML('img').attr({ + src: badge, + height: '50' + }).appendTo(div) + new HTML('br').appendTo(div) + new HTML('a').text('Discord').attr({ + href: 'https://discord.gg/86F8dK9vfn', + class: 'discord' + }).appendTo(div) + new HTML('span').text(' - ').appendTo(div) + new HTML('a').text('Github').attr({ + href: 'https://github.com/Flow-Works/FlowOS', + class: 'github' + }).appendTo(div) return win } diff --git a/src/fs.ts b/src/fs.ts index 3f82d1f..54dfb1a 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -8,14 +8,15 @@ export enum Errors { } export enum Permission { - ROOT, - ADMIN, - USER + USER, + ELEVATED, + SYSTEM } export interface Directory { type: 'directory' permission: Permission + deleteable: boolean children: { [key: string]: Directory | File } @@ -24,34 +25,41 @@ export interface Directory { export interface File { type: 'file' permission: Permission + deleteable: boolean content: Buffer } const defaultFS: { root: Directory } = { root: { type: 'directory', - permission: Permission.ROOT, + deleteable: false, + permission: Permission.SYSTEM, children: { home: { type: 'directory', - permission: Permission.ROOT, + deleteable: false, + permission: Permission.SYSTEM, children: { Downloads: { type: 'directory', - permission: Permission.ADMIN, + deleteable: false, + permission: Permission.USER, children: {} }, Applications: { type: 'directory', - permission: Permission.ADMIN, + deleteable: false, + permission: Permission.USER, children: {} }, Desktop: { type: 'directory', - permission: Permission.ADMIN, + deleteable: false, + permission: Permission.USER, children: { 'README.md': { type: 'file', + deleteable: true, permission: Permission.USER, content: Buffer.from('# Welcome to FlowOS!') } @@ -59,39 +67,53 @@ const defaultFS: { root: Directory } = { }, Pictures: { type: 'directory', - permission: Permission.ADMIN, + deleteable: false, + permission: Permission.USER, children: {} }, Videos: { type: 'directory', - permission: Permission.ADMIN, + deleteable: false, + permission: Permission.USER, children: {} }, Documents: { type: 'directory', - permission: Permission.ADMIN, + deleteable: false, + permission: Permission.USER, children: {} }, Music: { type: 'directory', - permission: Permission.ADMIN, + deleteable: false, + permission: Permission.USER, children: {} } } }, var: { type: 'directory', - permission: Permission.ROOT, + deleteable: false, + permission: Permission.SYSTEM, children: {} }, etc: { type: 'directory', - permission: Permission.ROOT, - children: {} + deleteable: false, + permission: Permission.SYSTEM, + children: { + hostname: { + type: 'file', + deleteable: false, + permission: Permission.ELEVATED, + content: Buffer.from('flow') + } + } }, boot: { type: 'directory', - permission: Permission.ROOT, + deleteable: false, + permission: Permission.SYSTEM, children: {} } } @@ -203,10 +225,28 @@ export class VirtualFS { * sufficient permissions to access a certain path. */ private async handlePermissions (path: string, permission: Permission): Promise { - const { current } = await this.navigatePath(path) + let current - if (current.permission === Permission.ADMIN && current.permission <= permission) throw new Error(Errors.EACCES) - if (current.permission === Permission.ROOT && current.permission <= permission) throw new Error(Errors.EPERM) + current = (await this.navigatePath(path)).current + console.log(current) + if (current === undefined) current = (await this.navigatePathParent(path)).current + console.log(current) + + console.log(current.permission, permission) + + if (current.permission === Permission.USER && current.permission < permission) { + const uac = await window.wm.createModal('Elevated Permissions Required', 'You need elevated permissions to perform this operation.') + if (!uac) { + throw new Error(Errors.EACCES) + } + } + if (current.permission === Permission.ELEVATED && current.permission < permission) { + const uac = await window.wm.createModal('Elevated Permissions Required', 'You need elevated permissions to perform this operation.') + if (!uac) { + throw new Error(Errors.EACCES) + } + } + if (current.permission === Permission.SYSTEM && current.permission < permission) throw new Error(Errors.EPERM) } /** @@ -254,6 +294,7 @@ export class VirtualFS { async unlink (path: string, permission = Permission.USER): Promise { const { current, filename } = await this.navigatePathParent(path) + if (!current.children[filename].deleteable) throw new Error(Errors.EPERM) await this.handlePermissions(path, permission) Reflect.deleteProperty(current.children, filename) @@ -296,7 +337,8 @@ export class VirtualFS { current.children[filename] = { type: 'file', - permission: Permission.USER, + deleteable: true, + permission, content: Buffer.from(content) } await this.save() @@ -318,7 +360,8 @@ export class VirtualFS { current.children[filename] = { type: 'directory', - permission: Permission.USER, + deleteable: true, + permission, children: {} } await this.save() @@ -336,6 +379,7 @@ export class VirtualFS { async rmdir (path: string, permission = Permission.USER): Promise { const { current, filename } = await this.navigatePathParent(path) + if (!current.deleteable) throw new Error(Errors.EPERM) await this.handlePermissions(path, permission) if (current.children[filename].type !== 'directory') throw new Error(Errors.ENOTDIR) @@ -391,6 +435,9 @@ export class VirtualFS { const { current: oldCurrent, filename: oldFilename } = await this.navigatePathParent(oldPath) const { current: newCurrent, filename: newFilename } = await this.navigatePathParent(newPath) + if (!oldCurrent.deleteable) throw new Error(Errors.EPERM) + if (!newCurrent.deleteable) throw new Error(Errors.EPERM) + await this.handlePermissions(oldPath, permission) await this.handlePermissions(newPath, permission) diff --git a/src/index.ts b/src/index.ts index 89b059d..bba77f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,6 @@ const flowDetails = { version, codename: 'Mochi' } - declare global { interface Window { flowDetails: typeof flowDetails @@ -38,6 +37,7 @@ if (params.get('debug') !== null && params.get('debug') !== undefined) { enableDebug().catch(e => console.error(e)) } +window.flowDetails = flowDetails window.preloader = new Preloader() window.flow = new Flow() window.statusBar = new StatusBar() diff --git a/src/instances/WindowManager.ts b/src/instances/WindowManager.ts index 37a9094..e7b0524 100644 --- a/src/instances/WindowManager.ts +++ b/src/instances/WindowManager.ts @@ -1,4 +1,5 @@ +import HTML from '../lib' import FlowWindow from '../structures/FlowWindow' import { FlowWindowConfig } from '../types' @@ -43,6 +44,40 @@ class WindowManager { return win } + /** + * Creates a modal window. + * + * @param {string} title - A string representing the title of the modal window. + * @param {string} text - The `text` parameter is a string that represents the content or message to + * be displayed in the modal window. + * @returns The function `createModal` is returning a `FlowWindow` object. + */ + async createModal (title: string, text: string): Promise { + const win = new FlowWindow(this, { + title, + icon: '', + width: 300, + height: 200, + canResize: false + }) + + return await new Promise((resolve) => { + new HTML('h3').text(text).appendTo(win.content) + new HTML('p').text(text).appendTo(win.content) + new HTML('button').text('Allow').appendTo(win.content).on('click', () => { + resolve(true) + win.close() + }) + new HTML('button').text('Allow').appendTo(win.content).on('click', () => { + resolve(false) + win.close() + }) + + this.windows.push(win) + this.windowArea.appendChild(win.element) + }) + } + /** * Toggles the app launcher. */ diff --git a/src/lib.ts b/src/lib.ts new file mode 100644 index 0000000..8a35f69 --- /dev/null +++ b/src/lib.ts @@ -0,0 +1,333 @@ +export default class HTML { + /** The HTML element referenced in this instance. Change using `.swapRef()`, or remove using `.cleanup()`. */ + elm: HTMLInputElement | HTMLElement + /** + * Create a new instance of the HTML class. + * @param elm The HTML element to be created or classified from. + */ + constructor (elm: string | HTMLElement) { + if (elm instanceof HTMLElement) { + this.elm = elm + } else { + this.elm = document.createElement(elm === '' ? 'div' : elm) + } + } + + /** + * Sets the text of the current element. + * @param val The text to set to. + * @returns HTML + */ + text (val: string): HTML { + this.elm.innerText = val + return this + } + + /** + * Sets the text of the current element. + * @param val The text to set to. + * @returns HTML + */ + html (val: string): HTML { + this.elm.innerHTML = val + return this + } + + /** + * Safely remove the element. Can be used in combination with a `.swapRef()` to achieve a "delete & swap" result. + * @returns HTML + */ + cleanup (): HTML { + this.elm.remove() + return this + } + + /** + * querySelector something. + * @param selector The query selector. + * @returns The HTML element (not as HTML) + */ + query (selector: string): HTMLElement | null { + return this.elm.querySelector(selector) + } + + /** + * An easier querySelector method. + * @param query The string to query + * @returns a new HTML + */ + qs (query: string): HTML | null { + if (this.elm.querySelector(query) != null) { + return HTML.from(this.elm.querySelector(query) as HTMLElement) + } else { + return null + } + } + + /** + * An easier querySelectorAll method. + * @param query The string to query + * @returns a new HTML + */ + qsa (query: string): Array | null { + if (this.elm.querySelector(query) != null) { + return Array.from(this.elm.querySelectorAll(query)).map((e) => + HTML.from(e as HTMLElement) + ) + } else { + return null + } + } + + /** + * Sets the ID of the element. + * @param val The ID to set. + * @returns HTML + */ + id (val: string): HTML { + this.elm.id = val + return this + } + + /** + * Toggle on/off a class. + * @param val The class to toggle. + * @returns HTML + */ + class (...val: string[]): HTML { + for (let i = 0; i < val.length; i++) { + this.elm.classList.toggle(val[i]) + } + return this + } + + /** + * Toggles ON a class. + * @param val The class to enable. + * @returns HTML + */ + classOn (...val: string[]): HTML { + for (let i = 0; i < val.length; i++) { + this.elm.classList.add(val[i]) + } + return this + } + + /** + * Toggles OFF a class. + * @param val The class to disable. + * @returns HTML + */ + classOff (...val: string[]): HTML { + for (let i = 0; i < val.length; i++) { + this.elm.classList.remove(val[i]) + } + return this + } + + /** + * Apply CSS styles (dashed method.) Keys use CSS syntax, e.g. `background-color`. + * @param obj The styles to apply (as an object of `key: value;`.) + * @returns HTML + */ + style (obj: { [x: string]: string | null }): HTML { + for (const key of Object.keys(obj)) { + this.elm.style.setProperty(key, obj[key]) + } + return this + } + + /** + * Apply CSS styles (JS method.) Keys use JS syntax, e.g. `backgroundColor`. + * @param obj The styles to apply (as an object of `key: value;`) + * @returns HTML + */ + styleJs (obj: { [key: string]: string | null }): HTML { + for (const key of Object.keys(obj)) { + // @ts-expect-error No other workaround I could find. + this.elm.style[key] = obj[key] + } + return this + } + + /** + * Apply an event listener. + * @param ev The event listener type to add. + * @param cb The event listener callback to add. + * @returns HTML + */ + on (ev: string, cb: EventListenerOrEventListenerObject): HTML { + this.elm.addEventListener(ev, cb) + return this + } + + /** + * Remove an event listener. + * @param ev The event listener type to remove. + * @param cb The event listener callback to remove. + * @returns HTML + */ + un (ev: string, cb: EventListenerOrEventListenerObject): HTML { + this.elm.removeEventListener(ev, cb) + return this + } + + /** + * Append this element to another element. Uses `appendChild()` on the parent. + * @param parent Element to append to. HTMLElement, HTML, and string (as querySelector) are supported. + * @returns HTML + */ + appendTo (parent: HTMLElement | HTML | string): HTML { + if (parent instanceof HTMLElement) { + parent.appendChild(this.elm) + } else if (parent instanceof HTML) { + parent.elm.appendChild(this.elm) + } else if (typeof parent === 'string') { + document.querySelector(parent)?.appendChild(this.elm) + } + return this + } + + /** + * Append an element. Typically used as a `.append(new HTML(...))` call. + * @param elem The element to append. + * @returns HTML + */ + append (elem: string | HTMLElement | HTML): HTML { + if (elem instanceof HTMLElement) { + this.elm.appendChild(elem) + } else if (elem instanceof HTML) { + this.elm.appendChild(elem.elm) + } else if (typeof elem === 'string') { + const newElem = document.createElement(elem) + this.elm.appendChild(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. + * @returns HTML + */ + appendMany (...elements: any[]): HTML { + for (const elem of elements) { + this.append(elem) + } + return this + } + + /** + * Clear the innerHTML of the element. + * @returns HTML + */ + clear (): HTML { + this.elm.innerHTML = '' + return this + } + + /** + * Set attributes (object method.) + * @param obj The attributes to set (as an object of `key: value;`) + * @returns HTML + */ + attr (obj: { [x: string]: any }): HTML { + for (const key in obj) { + if (obj[key] !== null && obj[key] !== undefined) { + this.elm.setAttribute(key, obj[key]) + } else { + this.elm.removeAttribute(key) + } + } + return this + } + + /** + * Set the text value of the element. Only works if element is `input` or `textarea`. + * @param str The value to set. + * @returns HTML + */ + val (str: any): HTML { + const x = this.elm as HTMLInputElement + x.value = str + return this + } + + /** + * Retrieve text content from the element. (as innerText, not trimmed) + * @returns string + */ + getText (): string { + return (this.elm as HTMLInputElement).innerText + } + + /** + * Retrieve HTML content from the element. + * @returns string + */ + getHTML (): string { + return (this.elm as HTMLInputElement).innerHTML + } + + /** + * Retrieve the value of the element. Only applicable if it is an `input` or `textarea`. + * @returns string + */ + getValue (): string { + return (this.elm as HTMLInputElement).value + } + + /** + * Swap the local `elm` with a new HTMLElement. + * @param elm The element to swap with. + * @returns HTML + */ + swapRef (elm: HTMLElement): HTML { + this.elm = elm + return this + } + + /** + * An alternative method to create an HTML instance. + * @param elm Element to create from. + * @returns HTML + */ + static from (elm: HTMLElement | string): HTML | null { + if (typeof elm === 'string') { + const element = HTML.qs(elm) + if (element === null) return null + else return element + } else { + return new HTML(elm) + } + } + + /** + * An easier querySelector method. + * @param query The string to query + * @returns a new HTML + */ + static qs (query: string): HTML | null { + if (document.querySelector(query) != null) { + return HTML.from(document.querySelector(query) as HTMLElement) + } else { + return null + } + } + + /** + * An easier querySelectorAll method. + * @param query The string to query + * @returns a new HTML + */ + static qsa (query: string): Array | null { + if (document.querySelector(query) != null) { + return Array.from(document.querySelectorAll(query)).map((e) => + HTML.from(e as HTMLElement) + ) + } else { + return null + } + } +}