commit
f06399516c
55 changed files with 3343 additions and 2875 deletions
|
|
@ -45,7 +45,7 @@ npm run serve
|
||||||
|
|
||||||
## Made with
|
## Made with
|
||||||
FlowOS is made with the following software:
|
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)
|
* [Prism Code Editor](https://github.com/FIameCaster/prism-code-editor)
|
||||||
* [Vite](https://vitejs.dev)
|
* [Vite](https://vitejs.dev)
|
||||||
* [Ultraviolet](https://github.com/titaniumnetwork-dev/ultraviolet)
|
* [Ultraviolet](https://github.com/titaniumnetwork-dev/ultraviolet)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
<link rel="shortcut icon" href="./src/assets/flow.png" type="image/png">
|
<link rel="shortcut icon" href="./src/assets/flow.png" type="image/png">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/filer"></script>
|
<script src="./src/kernel.ts" type="module"></script>
|
||||||
<script src="./src/index.ts" type="module"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
1967
package-lock.json
generated
1967
package-lock.json
generated
File diff suppressed because it is too large
Load diff
16
package.json
16
package.json
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "flowos",
|
"name": "flowos",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0-indev.0",
|
||||||
"description": "The most aesthetic webOS.",
|
"description": "The most aesthetic webOS.",
|
||||||
"main": "src/index.ts",
|
"main": "src/kernel.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"docs": "typedoc src/**",
|
"docs": "typedoc src/**",
|
||||||
"test": "ts-standard",
|
"test": "ts-standard",
|
||||||
|
|
@ -15,24 +15,26 @@
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.8.7",
|
"@types/node": "^20.11.3",
|
||||||
"@types/uuid": "^9.0.5",
|
"@types/uuid": "^9.0.5",
|
||||||
"@types/web": "^0.0.117",
|
"@types/web": "^0.0.117",
|
||||||
|
"less": "^4.2.0",
|
||||||
"ts-standard": "^12.0.2",
|
"ts-standard": "^12.0.2",
|
||||||
"typedoc": "^0.25.3",
|
"typedoc": "^0.25.3",
|
||||||
"typedoc-material-theme": "^1.0.0",
|
"typedoc-material-theme": "^1.0.2",
|
||||||
"typedoc-plugin-missing-exports": "^2.1.0",
|
"typedoc-plugin-missing-exports": "^2.1.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^4.4.12",
|
"vite": "^4.4.12",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-dynamic-import": "^1.5.0",
|
"vite-plugin-dynamic-import": "^1.5.0",
|
||||||
"vite-plugin-node-polyfills": "^0.15.0"
|
"vite-plugin-node-polyfills": "^0.15.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ptkdev/logger": "^1.8.0",
|
|
||||||
"eruda": "^3.0.1",
|
"eruda": "^3.0.1",
|
||||||
"filer": "^1.4.1",
|
"js-ini": "^1.6.0",
|
||||||
"prism-code-editor": "^2.3.0",
|
|
||||||
"material-symbols": "^0.14.3",
|
"material-symbols": "^0.14.3",
|
||||||
|
"prism-code-editor": "^2.3.0",
|
||||||
|
"semver": "^7.5.4",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"ts-standard": {
|
"ts-standard": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
importScripts('/uv/uv.sw.js');
|
importScripts('/uv/uv.sw.js');
|
||||||
|
|
||||||
const params = new URL(self.serviceWorker.scriptURL).searchParams
|
const params = new URL(self.location.href).searchParams
|
||||||
|
|
||||||
const serverURL = atob(params.get('url'));
|
const serverURL = atob(params.get('url'));
|
||||||
const sw = new UVServiceWorker(serverURL);
|
const sw = new UVServiceWorker(serverURL);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ self.xor = {
|
||||||
}
|
}
|
||||||
self.__uv$config = {
|
self.__uv$config = {
|
||||||
prefix: '/service/',
|
prefix: '/service/',
|
||||||
bare: 'https://server.flow-works.me' + '/bare/',
|
|
||||||
encodeUrl: self.xor.encode,
|
encodeUrl: self.xor.encode,
|
||||||
decodeUrl: self.xor.decode,
|
decodeUrl: self.xor.decode,
|
||||||
handler: '/uv/uv.handler.js',
|
handler: '/uv/uv.handler.js',
|
||||||
|
|
|
||||||
316
src/HTML.ts
Normal file
316
src/HTML.ts
Normal file
|
|
@ -0,0 +1,316 @@
|
||||||
|
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) {
|
||||||
|
this.elm = elm instanceof HTMLElement ? 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 {
|
||||||
|
return this.elm.querySelector(query) != null ? HTML.from(this.elm.querySelector(query) as HTMLElement) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An easier querySelectorAll method.
|
||||||
|
* @param query The string to query
|
||||||
|
* @returns a new HTML
|
||||||
|
*/
|
||||||
|
qsa (query: string): Array<HTML | null> | null {
|
||||||
|
return this.elm.querySelector(query) != null
|
||||||
|
? Array.from(this.elm.querySelectorAll(query)).map((e) =>
|
||||||
|
HTML.from(e as HTMLElement)
|
||||||
|
)
|
||||||
|
: 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 (const element of val) {
|
||||||
|
this.elm.classList.toggle(element)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles ON a class.
|
||||||
|
* @param val The class to enable.
|
||||||
|
* @returns HTML
|
||||||
|
*/
|
||||||
|
classOn (...val: string[]): HTML {
|
||||||
|
for (const element of val) {
|
||||||
|
this.elm.classList.add(element)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles OFF a class.
|
||||||
|
* @param val The class to disable.
|
||||||
|
* @returns HTML
|
||||||
|
*/
|
||||||
|
classOff (...val: string[]): HTML {
|
||||||
|
for (const element of val) {
|
||||||
|
this.elm.classList.remove(element)
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
this.elm.removeAttribute(key)
|
||||||
|
} else {
|
||||||
|
this.elm.setAttribute(key, obj[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)
|
||||||
|
return element === null ? null : 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 {
|
||||||
|
return document.querySelector(query) != null ? HTML.from(document.querySelector(query) as HTMLElement) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An easier querySelectorAll method.
|
||||||
|
* @param query The string to query
|
||||||
|
* @returns a new HTML
|
||||||
|
*/
|
||||||
|
static qsa (query: string): Array<HTML | null> | null {
|
||||||
|
return document.querySelector(query) != null
|
||||||
|
? Array.from(document.querySelectorAll(query)).map((e) =>
|
||||||
|
HTML.from(e as HTMLElement)
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/assets/icons/application-executable.svg
Normal file
35
src/assets/icons/application-executable.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 14 KiB |
36
src/assets/icons/icon-library.svg
Normal file
36
src/assets/icons/icon-library.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.8 KiB |
1
src/assets/icons/org.gnome.Loupe.svg
Normal file
1
src/assets/icons/org.gnome.Loupe.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.9 KiB |
1
src/assets/icons/utilities-system-monitor.svg
Normal file
1
src/assets/icons/utilities-system-monitor.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -1,148 +0,0 @@
|
||||||
import icon from '../../assets/icons/file-manager.svg'
|
|
||||||
import { App } from '../../types'
|
|
||||||
|
|
||||||
import FlowWindow from '../../structures/FlowWindow'
|
|
||||||
|
|
||||||
import { Stats } from 'fs'
|
|
||||||
|
|
||||||
export default class FilesApp implements App {
|
|
||||||
meta = {
|
|
||||||
name: 'Files',
|
|
||||||
description: 'A simple files app.',
|
|
||||||
pkg: 'flow.files',
|
|
||||||
version: '1.0.0',
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
|
|
||||||
async open (): Promise<FlowWindow> {
|
|
||||||
const win = window.wm.createWindow({
|
|
||||||
title: this.meta.name,
|
|
||||||
icon: this.meta.icon,
|
|
||||||
width: 500,
|
|
||||||
height: 400
|
|
||||||
})
|
|
||||||
|
|
||||||
win.content.style.display = 'flex'
|
|
||||||
win.content.style.flexDirection = 'column'
|
|
||||||
|
|
||||||
async function setDir (dir: string): Promise<void> {
|
|
||||||
await window.fs.readdir(dir, (e: NodeJS.ErrnoException, files: string[]) => {
|
|
||||||
const back = dir === '/' ? '<span class="material-symbols-rounded">first_page</span>' : '<span class="back material-symbols-rounded">chevron_left</span>'
|
|
||||||
|
|
||||||
win.content.innerHTML = `
|
|
||||||
<div style="padding: 5px;display: flex;align-items: center;gap: 5px;">
|
|
||||||
${back}${dir}
|
|
||||||
<div style="flex:1;"></div>
|
|
||||||
<i class='folder material-symbols-rounded' style="font-size: 17.5px;">create_new_folder</i><i class='file material-symbols-rounded' style="font-size: 17.5px;">note_add</i>
|
|
||||||
</div>
|
|
||||||
<div class="files" style="background: var(--base);flex: 1;border-radius: 10px;display: flex;flex-direction: column;"></div>
|
|
||||||
`
|
|
||||||
|
|
||||||
if (back !== '<span class="material-symbols-rounded">first_page</span>') {
|
|
||||||
(win.content.querySelector('.back') as HTMLElement).onclick = async () => {
|
|
||||||
if (dir.split('/')[1] === dir.replace('/', '')) {
|
|
||||||
await setDir(`/${dir.split('/')[0]}`)
|
|
||||||
} else {
|
|
||||||
await setDir(`/${dir.split('/')[1]}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(win.content.querySelector('.file') as HTMLElement).onclick = async () => {
|
|
||||||
const title: string = prompt('Enter file name') ?? 'new-file.txt'
|
|
||||||
await window.fs.promises.open(`${dir}/${title}`, 'w')
|
|
||||||
await setDir(dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
(win.content.querySelector('.folder') as HTMLElement).onclick = async () => {
|
|
||||||
const title: string = prompt('Enter folder name') ?? 'new-folder'
|
|
||||||
await window.fs.promises.mkdir(`${dir}/${title}`)
|
|
||||||
await setDir(dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const separator = dir === '/' ? '' : '/'
|
|
||||||
window.fs.stat(dir + separator + file, (e: NodeJS.ErrnoException, fileStat: Stats) => {
|
|
||||||
const element = document.createElement('div')
|
|
||||||
element.setAttribute('style', 'display: flex;gap: 5px;align-items:center;padding: 5px;border-bottom: 1px solid var(--text);display:flex;align-items:center;gap: 5px;')
|
|
||||||
|
|
||||||
const genIcon = (): string => {
|
|
||||||
switch (file.split('.').at(-1)) {
|
|
||||||
case 'js':
|
|
||||||
case 'mjs':
|
|
||||||
case 'cjs': {
|
|
||||||
return '<span class="material-symbols-rounded">javascript</span>'
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'html':
|
|
||||||
case 'htm': {
|
|
||||||
return '<span class="material-symbols-rounded">html</span>'
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'css': {
|
|
||||||
return '<span class="material-symbols-rounded">css</span>'
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'json': {
|
|
||||||
return '<span class="material-symbols-rounded">code</span>'
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'md': {
|
|
||||||
return '<span class="material-symbols-rounded">markdown</span>'
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'txt':
|
|
||||||
case 'text': {
|
|
||||||
return '<span class="material-symbols-rounded">description</span>'
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'png':
|
|
||||||
case 'apng':
|
|
||||||
case 'jpg':
|
|
||||||
case 'jpeg':
|
|
||||||
case 'gif': {
|
|
||||||
return '<span class="material-symbols-rounded">image</span>'
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return '<span class="material-symbols-rounded">draft</span>'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const icon = fileStat.isDirectory() ? '<span class="material-symbols-rounded">folder</span>' : genIcon()
|
|
||||||
|
|
||||||
element.innerHTML += `${icon} <span style="flex:1;">${file}</span><span class="material-symbols-rounded delete">delete_forever</span><span class="material-symbols-rounded rename">edit</span>`;
|
|
||||||
(element.querySelector('.rename') as HTMLElement).onclick = async () => {
|
|
||||||
const value = (prompt('Rename') as string)
|
|
||||||
if (value !== null || value !== undefined) {
|
|
||||||
await window.fs.promises.rename(dir + separator + file, dir + separator + value)
|
|
||||||
await setDir(dir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(element.querySelector('.delete') as HTMLElement).onclick = async () => {
|
|
||||||
if (fileStat.isDirectory()) {
|
|
||||||
await window.fs.rmdir(dir + separator + file, () => {})
|
|
||||||
} else {
|
|
||||||
await window.fs.promises.unlink(dir + separator + file)
|
|
||||||
}
|
|
||||||
await setDir(dir)
|
|
||||||
}
|
|
||||||
element.ondblclick = async () => {
|
|
||||||
if (fileStat.isDirectory()) {
|
|
||||||
await setDir(dir + separator + file)
|
|
||||||
} else {
|
|
||||||
await window.flow.openApp('flow.editor', { path: dir + separator + file })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
win.content.querySelector('.files')?.appendChild(element)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await setDir('/')
|
|
||||||
|
|
||||||
return win
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import icon from '../../assets/icons/userinfo.svg'
|
|
||||||
import badge from '../../assets/badge.png'
|
|
||||||
import { App, PackageJSON } from '../../types'
|
|
||||||
|
|
||||||
import FlowWindow from '../../structures/FlowWindow'
|
|
||||||
|
|
||||||
export default class InfoApp implements App {
|
|
||||||
meta = {
|
|
||||||
name: 'Info',
|
|
||||||
description: 'FlowOS Information.',
|
|
||||||
pkg: 'flow.info',
|
|
||||||
version: '1.0.0',
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
|
|
||||||
async open (): Promise<FlowWindow> {
|
|
||||||
const packageJSON: PackageJSON = await import('../../../package.json')
|
|
||||||
const win = window.wm.createWindow({
|
|
||||||
title: this.meta.name,
|
|
||||||
icon: this.meta.icon,
|
|
||||||
width: 300,
|
|
||||||
height: 400,
|
|
||||||
canResize: false
|
|
||||||
})
|
|
||||||
|
|
||||||
win.content.style.padding = '10px'
|
|
||||||
win.content.style.textAlign = 'center'
|
|
||||||
win.content.style.display = 'flex'
|
|
||||||
win.content.style.flexDirection = 'column'
|
|
||||||
win.content.style.justifyContent = 'center'
|
|
||||||
win.content.style.alignItems = 'center'
|
|
||||||
win.content.style.background = 'var(--base)'
|
|
||||||
win.content.innerHTML = `
|
|
||||||
<div>
|
|
||||||
<h1 style="margin:0;">FlowOS</h1>
|
|
||||||
<p style="margin:0;">v${packageJSON.version}</p>
|
|
||||||
<br/>
|
|
||||||
<p>Created by ThinLiquid, 1nspird_, proudparot2, systemless_</p>
|
|
||||||
<img src="${badge}" height="50"><br/>
|
|
||||||
<a class="discord" href="https://discord.gg/flowos">Discord</a>
|
|
||||||
-
|
|
||||||
<a class="github" href="https://github.com/Flow-Works/FlowOS-2.0">Github</a>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
|
|
||||||
return win
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
import icon from '../../assets/icons/software-properties.svg'
|
|
||||||
import { App, LoadedApp, LoadedPlugin } from '../../types'
|
|
||||||
import FlowWindow from '../../structures/FlowWindow'
|
|
||||||
import nullIcon from '../../assets/icons/application-default-icon.svg'
|
|
||||||
|
|
||||||
export default class ManagerApp implements App {
|
|
||||||
meta = {
|
|
||||||
name: 'Flow Manager',
|
|
||||||
description: 'A FlowOS utility app.',
|
|
||||||
pkg: 'flow.manager',
|
|
||||||
version: '1.0.0',
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
|
|
||||||
async open (): Promise<FlowWindow> {
|
|
||||||
const win = window.wm.createWindow({
|
|
||||||
title: this.meta.name,
|
|
||||||
icon: this.meta.icon,
|
|
||||||
width: 350,
|
|
||||||
height: 500
|
|
||||||
})
|
|
||||||
|
|
||||||
win.content.style.display = 'flex'
|
|
||||||
win.content.style.flexDirection = 'column'
|
|
||||||
win.content.style.gap = '10px'
|
|
||||||
win.content.style.padding = '10px'
|
|
||||||
win.content.style.background = 'var(--base)'
|
|
||||||
win.content.innerHTML = `
|
|
||||||
${window.flow.apps.map((app: LoadedApp) => {
|
|
||||||
return `
|
|
||||||
<div style="display:flex;gap: 10px;padding: 10px;background: var(--surface-0);border-radius: 10px;">
|
|
||||||
<img src="${app.meta.icon}" style="border-radius: 40%;aspect-ratio: 1 / 1;height: 50px;"/>
|
|
||||||
<div>
|
|
||||||
<h3 style="margin:0;">${app.meta.name} ${(app.builtin ?? false) ? '<code style="font-size: 0.75em;">(builtin)</code>' : ''}</h3>
|
|
||||||
<p style="margin:0;">${app.meta.pkg} (v${app.meta.version}) - App</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}).join('')}
|
|
||||||
${window.flow.plugins.map((plugin: LoadedPlugin) => {
|
|
||||||
return `
|
|
||||||
<div style="display:flex;gap: 10px;padding: 10px;background: var(--surface-0);border-radius: 10px;">
|
|
||||||
<img src="${plugin.meta.icon ?? nullIcon}" style="border-radius: 100%;aspect-ratio: 1 / 1;height: 50px;"/>
|
|
||||||
<div>
|
|
||||||
<h3 style="margin:0;">${plugin.meta.name} ${(plugin.builtin ?? false) ? '<code style="font-size: 0.75em;">(builtin)</code>' : ''}</h3>
|
|
||||||
<p style="margin:0;">${plugin.meta.pkg} (v${plugin.meta.version}) - Plugin</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}).join('')}
|
|
||||||
`
|
|
||||||
|
|
||||||
return win
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import icon from '../../assets/icons/gnome-music.svg'
|
|
||||||
import { App } from '../../types'
|
|
||||||
import FlowWindow from '../../structures/FlowWindow'
|
|
||||||
|
|
||||||
export default class MusicApp implements App {
|
|
||||||
meta = {
|
|
||||||
name: 'Music',
|
|
||||||
description: 'A simple music app.',
|
|
||||||
pkg: 'flow.music',
|
|
||||||
version: '1.0.0',
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
|
|
||||||
async open (): Promise<FlowWindow> {
|
|
||||||
const win = window.wm.createWindow({
|
|
||||||
title: this.meta.name,
|
|
||||||
icon: this.meta.icon,
|
|
||||||
width: 700,
|
|
||||||
height: 300
|
|
||||||
})
|
|
||||||
|
|
||||||
win.content.style.background = 'var(--base)'
|
|
||||||
win.content.innerHTML = 'hi'
|
|
||||||
|
|
||||||
return win
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
import icon from '../../assets/icons/preferences-system.svg'
|
|
||||||
import { App } from '../../types'
|
|
||||||
import FlowWindow from '../../structures/FlowWindow'
|
|
||||||
|
|
||||||
export default class SettingsApp implements App {
|
|
||||||
meta = {
|
|
||||||
name: 'Settings',
|
|
||||||
description: 'An easy-to-use configuration app.',
|
|
||||||
pkg: 'flow.settings',
|
|
||||||
icon,
|
|
||||||
version: '1.0.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
configFileLoc = '/.flow/config.json'
|
|
||||||
configFolderLoc = '/.flow/'
|
|
||||||
|
|
||||||
async open (): Promise<FlowWindow> {
|
|
||||||
const win = window.wm.createWindow({
|
|
||||||
title: this.meta.name,
|
|
||||||
icon: this.meta.icon,
|
|
||||||
width: 700,
|
|
||||||
height: 300,
|
|
||||||
canResize: true
|
|
||||||
})
|
|
||||||
|
|
||||||
win.content.style.padding = '10px'
|
|
||||||
|
|
||||||
win.content.innerHTML = `
|
|
||||||
<h1>Settings</h1>
|
|
||||||
<div class="settings"></div>
|
|
||||||
<button class="save">Save!</button>
|
|
||||||
<style>
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
input, button {
|
|
||||||
background: var(--mantle);
|
|
||||||
padding: 2.5px;
|
|
||||||
border: 1px solid var(--surface-0);
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 2.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
margin: 2.5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
`
|
|
||||||
|
|
||||||
const titles: {
|
|
||||||
[key: string]: string
|
|
||||||
} = {
|
|
||||||
SERVER_URL: 'FlowServer URL',
|
|
||||||
HOSTNAME: 'Device Hostname',
|
|
||||||
USERNAME: 'Username',
|
|
||||||
'24HR_CLOCK': '24hr Clock'
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = await window.config()
|
|
||||||
|
|
||||||
for (const key of Object.keys(config)) {
|
|
||||||
const container = document.createElement('div')
|
|
||||||
const label = document.createElement('label')
|
|
||||||
const input = document.createElement('input')
|
|
||||||
|
|
||||||
if (typeof (config as any)[key] === 'boolean') {
|
|
||||||
label.innerText = `${titles[key] ?? key}`
|
|
||||||
input.type = 'checkbox'
|
|
||||||
} else if (typeof (config as any)[key] === 'string') {
|
|
||||||
label.innerText = `${titles[key] ?? key}:\n`
|
|
||||||
input.type = 'text'
|
|
||||||
}
|
|
||||||
|
|
||||||
input.value = (config as any)[key]
|
|
||||||
|
|
||||||
container.appendChild(label)
|
|
||||||
container.appendChild(input)
|
|
||||||
|
|
||||||
win.content.querySelector('.settings')?.appendChild(container)
|
|
||||||
|
|
||||||
win.content.querySelector('.save')?.addEventListener('click', () => {
|
|
||||||
(config as any)[key] = input.value
|
|
||||||
|
|
||||||
window.location.reload()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
win.content.querySelector('.save')?.addEventListener('click', () => {
|
|
||||||
window.fs.promises.writeFile('/.config/flow.json', JSON.stringify(config))
|
|
||||||
.then(null)
|
|
||||||
.catch(e => console.error(e))
|
|
||||||
})
|
|
||||||
|
|
||||||
return win
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
import icon from '../../assets/icons/softwarecenter.svg'
|
|
||||||
import { App, RepoData } from '../../types'
|
|
||||||
import FlowWindow from '../../structures/FlowWindow'
|
|
||||||
import { sanitize } from '../../utils'
|
|
||||||
import nullIcon from '../../assets/icons/application-default-icon.svg'
|
|
||||||
|
|
||||||
export default class MusicApp implements App {
|
|
||||||
meta = {
|
|
||||||
name: 'Store',
|
|
||||||
description: 'A simple store app.',
|
|
||||||
pkg: 'flow.store',
|
|
||||||
version: '1.0.0',
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
|
|
||||||
async open (): Promise<FlowWindow> {
|
|
||||||
const win = window.wm.createWindow({
|
|
||||||
title: this.meta.name,
|
|
||||||
icon: this.meta.icon,
|
|
||||||
width: 500,
|
|
||||||
height: 700
|
|
||||||
})
|
|
||||||
|
|
||||||
win.content.style.background = 'var(--base)'
|
|
||||||
|
|
||||||
const config = await window.config()
|
|
||||||
|
|
||||||
fetch(config.SERVER_URL + '/apps/list/')
|
|
||||||
.then(async (res) => await res.json())
|
|
||||||
.then(handle)
|
|
||||||
.catch(e => console.error(e))
|
|
||||||
|
|
||||||
function handle (repos: RepoData[]): void {
|
|
||||||
win.content.innerHTML = `
|
|
||||||
<div class="repos" style="display: flex;flex-direction: column;gap: 10px;"></div>
|
|
||||||
`
|
|
||||||
|
|
||||||
repos.forEach((repo) => {
|
|
||||||
(win.content.querySelector('.repos') as HTMLElement).innerHTML += `
|
|
||||||
<div data-repo-id="${sanitize(repo.id)}" style="display: flex;flex-direction: column;gap: 10px;background: var(--surface-0);padding: 20px;margin: 10px;border-radius: 10px;">
|
|
||||||
<div style="flex: 1;">
|
|
||||||
<h2 style="margin: 0;margin-bottom: 10px;">${sanitize(repo.name)}</h2>
|
|
||||||
<code style="font-family: monospace;">${sanitize(repo.id)}</code>
|
|
||||||
</div>
|
|
||||||
<br/>
|
|
||||||
<div class="apps"></div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
|
|
||||||
repo.apps.forEach((app) => {
|
|
||||||
(win.content.querySelector(`div[data-repo-id="${sanitize(repo.id)}"] > .apps`) as HTMLElement).innerHTML += `
|
|
||||||
<div data-pkg="${sanitize(app.pkg)}" style="display: flex;gap: 20px;">
|
|
||||||
<img src="${sanitize(app.icon ?? nullIcon)}" height="59.5px" style="border-radius: var(--app-radius);">
|
|
||||||
<div>
|
|
||||||
<h3 style="margin: 0;margin-bottom: 10px;">${sanitize(app.name)}</h3>
|
|
||||||
<div style="display: flex;gap:5px;align-items: center;">
|
|
||||||
<code style="font-family: monospace;">${sanitize(app.pkg)}</code>
|
|
||||||
<span class="material-symbols-rounded">download</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
|
|
||||||
window.fs.exists(`/Applications/${app.url.split('/').at(-1) as string}`, (exists) => {
|
|
||||||
if (exists) {
|
|
||||||
(win.content.querySelector(`div[data-pkg="${sanitize(app.pkg)}"] div > .material-symbols-rounded`) as HTMLElement).innerHTML = 'delete';
|
|
||||||
|
|
||||||
(win.content.querySelector(`div[data-pkg="${sanitize(app.pkg)}"] div > .material-symbols-rounded`) as HTMLElement).onclick = () => {
|
|
||||||
window.fs.unlink(`/Applications/${app.url.split('/').at(-1) as string}`, () => {
|
|
||||||
window.location.reload()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(win.content.querySelector(`div[data-pkg="${sanitize(app.pkg)}"] div > .material-symbols-rounded`) as HTMLElement).onclick = () => {
|
|
||||||
install(app.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
(win.content.querySelector(`div[data-pkg="${sanitize(app.pkg)}"] div > .material-symbols-rounded`) as HTMLElement).onclick = () => {
|
|
||||||
install(app.url)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function install (url: string): void {
|
|
||||||
fetch(url).then(async (res) => await res.text())
|
|
||||||
.then((data) => {
|
|
||||||
window.fs.exists('/Applications', (exists) => {
|
|
||||||
if (!exists) window.fs.promises.mkdir('/Applications').catch(console.error)
|
|
||||||
|
|
||||||
window.fs.promises.writeFile(`/Applications/${url.split('/').at(-1) as string}`, data).then(() => window.location.reload()).catch(console.error)
|
|
||||||
})
|
|
||||||
}).catch(console.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
return win
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import { AppOpenedEvent, AppClosedEvent } from '../../types'
|
|
||||||
|
|
||||||
export const meta = {
|
|
||||||
name: 'Apps',
|
|
||||||
description: 'Displays the current apps open.',
|
|
||||||
pkg: 'flow.apps',
|
|
||||||
version: '1.0.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const run = (element: HTMLDivElement): void => {
|
|
||||||
element.style.display = 'flex'
|
|
||||||
element.style.alignItems = 'bottom'
|
|
||||||
element.style.flex = '1'
|
|
||||||
|
|
||||||
window.addEventListener('app_opened', (e: AppOpenedEvent): void => {
|
|
||||||
const appIcon = document.createElement('app')
|
|
||||||
const app = e.detail.app
|
|
||||||
const win = e.detail.win
|
|
||||||
appIcon.innerHTML = `<img data-id="${win.id}" src="${app.meta.icon}"/>`;
|
|
||||||
(appIcon.querySelector('img') as HTMLElement).style.borderBottom = '2px solid var(--text)'
|
|
||||||
appIcon.onclick = async () => {
|
|
||||||
const win = await e.detail.win
|
|
||||||
win.focus()
|
|
||||||
win.toggleMin()
|
|
||||||
}
|
|
||||||
element.appendChild(appIcon)
|
|
||||||
})
|
|
||||||
|
|
||||||
window.addEventListener('app_closed', (e: AppClosedEvent): void => {
|
|
||||||
const win = e.detail.win
|
|
||||||
element.querySelector(`img[data-id="${win.id}"]`)?.parentElement?.remove()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import { getTime } from '../../utils'
|
|
||||||
|
|
||||||
export const meta = {
|
|
||||||
name: 'Clock',
|
|
||||||
description: 'Displays the date & time.',
|
|
||||||
pkg: 'flow.clock',
|
|
||||||
version: '1.0.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const run = async (element: HTMLDivElement): Promise<void> => {
|
|
||||||
let date: Date = new Date()
|
|
||||||
|
|
||||||
element.style.display = 'flex'
|
|
||||||
element.style.flexDirection = 'column'
|
|
||||||
element.style.padding = '5px 10px'
|
|
||||||
element.style.fontSize = '12.5px'
|
|
||||||
element.style.justifyContent = 'center'
|
|
||||||
|
|
||||||
const refreshDate = (): string => {
|
|
||||||
const split = date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' }).split(',')
|
|
||||||
return `<i>${split[0]}</i>,${split[1]} `
|
|
||||||
}
|
|
||||||
|
|
||||||
let clock = await getTime()
|
|
||||||
let date_ = refreshDate()
|
|
||||||
element.innerHTML = `${clock}<div>${date_}</div>`
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
(async () => {
|
|
||||||
date = new Date()
|
|
||||||
clock = await getTime()
|
|
||||||
date_ = refreshDate()
|
|
||||||
element.innerHTML = `${clock}<div>${date_}</div>`
|
|
||||||
})().catch(e => console.error(e))
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
1
src/decs.d.ts
vendored
Normal file
1
src/decs.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
declare module '*?raw-hex'
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
import * as fs from 'fs'
|
|
||||||
import { PlatformPath } from 'path'
|
|
||||||
|
|
||||||
export interface Path extends PlatformPath {
|
|
||||||
basename: (path: string, suffix?: string | undefined) => string
|
|
||||||
normalize: (path: string) => string
|
|
||||||
isNull: (path: string) => boolean
|
|
||||||
addTrailing: (path: string) => string
|
|
||||||
removeTrailing: (path: string) => string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilerError extends Error {
|
|
||||||
code: string
|
|
||||||
errno: number
|
|
||||||
path?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Errors {
|
|
||||||
EACCES: FilerError
|
|
||||||
EBADF: FilerError
|
|
||||||
EBUSY: FilerError
|
|
||||||
EINVAL: FilerError
|
|
||||||
ENOTDIR: FilerError
|
|
||||||
EISDIR: FilerError
|
|
||||||
ENOENT: FilerError
|
|
||||||
EEXIST: FilerError
|
|
||||||
EPERM: FilerError
|
|
||||||
ELOOP: FilerError
|
|
||||||
ENOTEMPTY: FilerError
|
|
||||||
EIO: FilerError
|
|
||||||
ENOTMOUNTED: FilerError
|
|
||||||
EFILESYSTEMERROR: FilerError
|
|
||||||
ENOATTR: FilerError
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FileSystemOptionsFlags = 'FORMAT' | 'NOCTIME' | 'NOMTIME'
|
|
||||||
|
|
||||||
export interface FileSystemOptions {
|
|
||||||
name: string
|
|
||||||
flags: FileSystemOptionsFlags[]
|
|
||||||
/* TO-DO */
|
|
||||||
provider: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FileSystemCallback = (err: FilerError | null, fs: FileSystem) => {}
|
|
||||||
|
|
||||||
export interface FileSystem {
|
|
||||||
appendFile: typeof fs.appendFile
|
|
||||||
access: typeof fs.access
|
|
||||||
chown: typeof fs.chown
|
|
||||||
chmod: typeof fs.chmod
|
|
||||||
close: typeof fs.close
|
|
||||||
exists: typeof fs.exists
|
|
||||||
fchown: typeof fs.fchown
|
|
||||||
fchmod: typeof fs.fchmod
|
|
||||||
fstat: typeof fs.fstat
|
|
||||||
fsync: typeof fs.fsync
|
|
||||||
ftruncate: typeof fs.ftruncate
|
|
||||||
futimes: typeof fs.futimes
|
|
||||||
link: typeof fs.link
|
|
||||||
lstat: typeof fs.lstat
|
|
||||||
mkdir: typeof fs.mkdir
|
|
||||||
mkdtemp: typeof fs.mkdtemp
|
|
||||||
open: typeof fs.open
|
|
||||||
readdir: typeof fs.readdir
|
|
||||||
read: typeof fs.read
|
|
||||||
readFile: typeof fs.readFile
|
|
||||||
readlink: typeof fs.readlink
|
|
||||||
rename: typeof fs.rename
|
|
||||||
rmdir: typeof fs.rmdir
|
|
||||||
stat: typeof fs.stat
|
|
||||||
symlink: typeof fs.symlink
|
|
||||||
truncate: typeof fs.truncate
|
|
||||||
unlink: typeof fs.unlink
|
|
||||||
utimes: typeof fs.utimes
|
|
||||||
writeFile: typeof fs.writeFile
|
|
||||||
write: typeof fs.write
|
|
||||||
|
|
||||||
promises: FileSystemPromises
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileSystemPromises {
|
|
||||||
appendFile: typeof fs.promises.appendFile
|
|
||||||
access: typeof fs.promises.access
|
|
||||||
chown: typeof fs.promises.chown
|
|
||||||
chmod: typeof fs.promises.chmod
|
|
||||||
link: typeof fs.promises.link
|
|
||||||
lstat: typeof fs.promises.lstat
|
|
||||||
mkdir: typeof fs.promises.mkdir
|
|
||||||
mkdtemp: typeof fs.promises.mkdtemp
|
|
||||||
open: typeof fs.promises.open
|
|
||||||
readdir: typeof fs.promises.readdir
|
|
||||||
readFile: typeof fs.promises.readFile
|
|
||||||
readlink: typeof fs.promises.readlink
|
|
||||||
rename: typeof fs.promises.rename
|
|
||||||
rmdir: typeof fs.promises.rmdir
|
|
||||||
stat: typeof fs.promises.stat
|
|
||||||
symlink: typeof fs.promises.symlink
|
|
||||||
truncate: typeof fs.promises.truncate
|
|
||||||
unlink: typeof fs.promises.unlink
|
|
||||||
utimes: typeof fs.promises.utimes
|
|
||||||
writeFile: typeof fs.promises.writeFile
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileSystemShell {
|
|
||||||
cd: (path: string, callback: (err: FilerError | null) => void) => void
|
|
||||||
pwd: () => string
|
|
||||||
find: (
|
|
||||||
dir: string,
|
|
||||||
options: {
|
|
||||||
exec?: (path: string, next: () => void) => void
|
|
||||||
regex?: RegExp
|
|
||||||
name?: string
|
|
||||||
} | ((err: FilerError | null, found: any[]) => void) | undefined | null,
|
|
||||||
/* INCOMPLETE? */
|
|
||||||
callback: (err: FilerError | null, found: any[]) => void
|
|
||||||
) => void
|
|
||||||
ls: (
|
|
||||||
dir: string,
|
|
||||||
options: {
|
|
||||||
recursive?: boolean
|
|
||||||
} | ((err: FilerError | null) => void) | undefined | null,
|
|
||||||
callback: (err: FilerError | null) => void
|
|
||||||
) => void
|
|
||||||
exec: (
|
|
||||||
path: string,
|
|
||||||
args: Array<string | number> | ((err: FilerError | null, result: string) => void) | undefined | null,
|
|
||||||
callback: (err: FilerError | null, result: string) => void
|
|
||||||
) => void
|
|
||||||
touch: (
|
|
||||||
path: string,
|
|
||||||
options: {
|
|
||||||
updateOnly?: boolean
|
|
||||||
date: Date
|
|
||||||
} | ((err: FilerError | null) => void) | undefined | null,
|
|
||||||
callback: (err: FilerError | null) => void
|
|
||||||
) => void
|
|
||||||
cat: (
|
|
||||||
files: string[],
|
|
||||||
callback: (err: FilerError | null, data: string) => void
|
|
||||||
) => void
|
|
||||||
rm: (
|
|
||||||
path: string,
|
|
||||||
options: {
|
|
||||||
recursive?: boolean
|
|
||||||
} | ((err: FilerError | null) => void) | undefined | null,
|
|
||||||
callback: (err: FilerError | null) => void
|
|
||||||
) => void
|
|
||||||
tempDir: (
|
|
||||||
callback: (err: FilerError | null, tmp: string) => void
|
|
||||||
) => void
|
|
||||||
mkdirp: (
|
|
||||||
path: string,
|
|
||||||
callback: (err: FilerError | null) => void
|
|
||||||
) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileSystemShellOptions {
|
|
||||||
env: {
|
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default interface Filer {
|
|
||||||
FileSystem: (options: FileSystemOptions, callback: (err: FilerError | null, guid: string) => void) => FileSystem
|
|
||||||
Buffer: Buffer
|
|
||||||
Path: Path
|
|
||||||
path: Path
|
|
||||||
Errors: Errors
|
|
||||||
Shell: (fs: FileSystem, options: FileSystemShellOptions) => FileSystemShell
|
|
||||||
}
|
|
||||||
94
src/index.ts
94
src/index.ts
|
|
@ -1,94 +0,0 @@
|
||||||
import 'material-symbols'
|
|
||||||
import './assets/style.less'
|
|
||||||
|
|
||||||
import Preloader from './instances/Preloader'
|
|
||||||
import StatusBar from './instances/StatusBar'
|
|
||||||
import WindowManager from './instances/WindowManager'
|
|
||||||
import Flow from './instances/Flow'
|
|
||||||
|
|
||||||
import { FileSystem } from './filer-types'
|
|
||||||
import { FlowConfig } from './types'
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
preloader: Preloader
|
|
||||||
flow: Flow
|
|
||||||
fs: FileSystem
|
|
||||||
statusBar: StatusBar
|
|
||||||
wm: WindowManager
|
|
||||||
config: () => Promise<FlowConfig>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search)
|
|
||||||
|
|
||||||
async function enableDebug (): Promise<void> {
|
|
||||||
const { default: eruda } = await import('eruda')
|
|
||||||
eruda.init()
|
|
||||||
return await Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.get('debug') !== null && params.get('debug') !== undefined) {
|
|
||||||
enableDebug().catch(e => console.error(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
window.preloader = new Preloader()
|
|
||||||
window.flow = new Flow()
|
|
||||||
window.statusBar = new StatusBar()
|
|
||||||
window.wm = new WindowManager();
|
|
||||||
|
|
||||||
(async function () {
|
|
||||||
window.preloader.setPending('filesystem')
|
|
||||||
window.fs = new (window as any).Filer.FileSystem()
|
|
||||||
|
|
||||||
const defaultConfig = {
|
|
||||||
SERVER_URL: 'https://server.flow-works.me',
|
|
||||||
HOSTNAME: 'flow',
|
|
||||||
USERNAME: 'user',
|
|
||||||
'24HR_CLOCK': false
|
|
||||||
}
|
|
||||||
|
|
||||||
window.fs.exists('/.config', (exists) => {
|
|
||||||
if (!exists) window.fs.promises.mkdir('/.config').then(null).catch(e => console.error)
|
|
||||||
|
|
||||||
window.fs.exists('/.config/flow.json', (exists) => {
|
|
||||||
if (!exists) {
|
|
||||||
window.fs.promises.writeFile('/.config/flow.json', JSON.stringify(defaultConfig)).then(null).catch(e => console.error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current FlowOS config.
|
|
||||||
*
|
|
||||||
* @returns The current FlowOS config.
|
|
||||||
*/
|
|
||||||
window.config = async (): Promise<FlowConfig> => {
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
window.fs.exists('/.config/flow.json', (exists) => {
|
|
||||||
if (exists) {
|
|
||||||
window.fs.promises.readFile('/.config/flow.json')
|
|
||||||
.then(content => { resolve(JSON.parse(content.toString())) })
|
|
||||||
.catch(() => reject(new Error('Unable to read config file.')))
|
|
||||||
} else reject(new Error('Config file does not exist.'))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations()
|
|
||||||
for (const registration of registrations) {
|
|
||||||
await registration.unregister()
|
|
||||||
}
|
|
||||||
await navigator.serviceWorker.register('/uv-sw.js?url=' + encodeURIComponent(btoa((await window.config()).SERVER_URL)), {
|
|
||||||
scope: '/service/'
|
|
||||||
})
|
|
||||||
|
|
||||||
await window.preloader.setDone('filesystem')
|
|
||||||
|
|
||||||
await window.wm.init()
|
|
||||||
await window.flow.init()
|
|
||||||
await window.statusBar.init()
|
|
||||||
|
|
||||||
window.preloader.setStatus('')
|
|
||||||
window.preloader.finish()
|
|
||||||
})().catch(e => console.error)
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
import { LoadedApp, LoadedPlugin } from '../types'
|
|
||||||
import nullIcon from '../assets/icons/application-default-icon.svg'
|
|
||||||
|
|
||||||
class Flow {
|
|
||||||
apps: LoadedApp[] = []
|
|
||||||
defaultAppList: string[] = [
|
|
||||||
'settings',
|
|
||||||
'music',
|
|
||||||
'files',
|
|
||||||
'editor',
|
|
||||||
'info',
|
|
||||||
'manager',
|
|
||||||
'browser',
|
|
||||||
'store'
|
|
||||||
]
|
|
||||||
|
|
||||||
appList: string[] = []
|
|
||||||
|
|
||||||
plugins: LoadedPlugin[] = []
|
|
||||||
pluginList: string[] = []
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiates applications.
|
|
||||||
*/
|
|
||||||
private async initApps (): Promise<void> {
|
|
||||||
window.preloader.setPending('apps')
|
|
||||||
window.preloader.setStatus('importing apps...')
|
|
||||||
|
|
||||||
window.fs.exists('/Applications', (exists) => {
|
|
||||||
if (!exists) window.fs.promises.mkdir('/Applications').catch(e => console.error(e))
|
|
||||||
window.fs.promises.readdir('/Applications').then((list) => {
|
|
||||||
list.forEach((file) => {
|
|
||||||
window.fs.promises.readFile('/Applications/' + file).then(content => {
|
|
||||||
if (!file.endsWith('.js') && !file.endsWith('.mjs')) return
|
|
||||||
if (content.toString() === '') return
|
|
||||||
this.appList.push(`data:text/javascript;base64,${btoa(content.toString())}`)
|
|
||||||
}).catch((e) => console.error(e))
|
|
||||||
})
|
|
||||||
}).catch(e => console.error(e))
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const appPath of this.defaultAppList) {
|
|
||||||
window.preloader.setStatus(`importing default apps\n${appPath}`)
|
|
||||||
const { default: ImportedApp } = await import(`../builtin/apps/${appPath}.ts`).catch(async (e: Error) => {
|
|
||||||
console.error(e)
|
|
||||||
await window.preloader.setError('apps')
|
|
||||||
window.preloader.setStatus(`unable to import ${appPath}\n${e.name}: ${e.message}`)
|
|
||||||
})
|
|
||||||
const app = new ImportedApp()
|
|
||||||
app.builtin = true
|
|
||||||
app.meta.icon = app.meta.icon ?? nullIcon
|
|
||||||
|
|
||||||
this.addApp(app)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.appList.length > 0) {
|
|
||||||
for (const appPath of this.appList) {
|
|
||||||
window.preloader.setStatus(`importing appstore apps\n${appPath}`)
|
|
||||||
|
|
||||||
let error = false
|
|
||||||
const { default: ImportedApp } = await import(appPath).catch(async (e: Error) => {
|
|
||||||
console.error(e)
|
|
||||||
await window.preloader.setError('apps')
|
|
||||||
window.preloader.setStatus(`unable to import ${appPath}\n${e.name}: ${e.message}`)
|
|
||||||
error = true
|
|
||||||
})
|
|
||||||
if (error) break
|
|
||||||
const app = new ImportedApp()
|
|
||||||
app.builtin = false
|
|
||||||
app.meta.icon = app.meta.icon ?? nullIcon
|
|
||||||
|
|
||||||
this.addApp(app)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.wm.launcher.style.opacity = '0'
|
|
||||||
window.wm.launcher.style.filter = 'blur(0px)'
|
|
||||||
window.wm.launcher.style.pointerEvents = 'none'
|
|
||||||
|
|
||||||
window.preloader.setStatus('adding apps to app launcher...')
|
|
||||||
|
|
||||||
this.apps.forEach((app) => {
|
|
||||||
window.preloader.setStatus(`adding apps to app launcher\n${app.meta.name}`)
|
|
||||||
const appElement = document.createElement('app')
|
|
||||||
appElement.onclick = async () => {
|
|
||||||
await window.flow.openApp(app.meta.pkg)
|
|
||||||
window.wm.toggleLauncher()
|
|
||||||
}
|
|
||||||
appElement.innerHTML = `<img src="${app.meta.icon}"><div>${app.meta.name}</div>`
|
|
||||||
window.wm.launcher.querySelector('apps')?.appendChild(appElement)
|
|
||||||
})
|
|
||||||
|
|
||||||
document.body.appendChild(window.wm.windowArea)
|
|
||||||
document.body.appendChild(window.wm.launcher)
|
|
||||||
|
|
||||||
await window.preloader.setDone('apps')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiates plugins.
|
|
||||||
*/
|
|
||||||
private async initPlugins (): Promise<void> {
|
|
||||||
window.preloader.setPending('plugins')
|
|
||||||
window.preloader.setStatus('importing default plugins...')
|
|
||||||
|
|
||||||
for (const pluginPath of this.pluginList) {
|
|
||||||
window.preloader.setStatus(`importing default plugins\n${pluginPath}`)
|
|
||||||
const plugin = await import(`../builtin/plugins/${pluginPath}.ts`).catch((e: Error) => {
|
|
||||||
console.error(e)
|
|
||||||
window.preloader.setStatus(`unable to import ${pluginPath}\n${e.name}: ${e.message}`)
|
|
||||||
})
|
|
||||||
const loadedPlugin = {
|
|
||||||
...plugin,
|
|
||||||
builtin: true
|
|
||||||
}
|
|
||||||
this.addPlugin(loadedPlugin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiates the Flow session.
|
|
||||||
*/
|
|
||||||
async init (): Promise<void> {
|
|
||||||
await this.initApps()
|
|
||||||
await this.initPlugins()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers an app.
|
|
||||||
*
|
|
||||||
* @param app The app to be registered.
|
|
||||||
*/
|
|
||||||
addApp (app: LoadedApp): void {
|
|
||||||
if (this.apps.some(x => x.meta.pkg === app.meta.pkg)) {
|
|
||||||
console.error(`Unable to register app; ${app.meta.pkg} is already registered.`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.apps.push(app)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a plugin.
|
|
||||||
*
|
|
||||||
* @param plugin The plugin to be registered.
|
|
||||||
*/
|
|
||||||
addPlugin (plugin: LoadedPlugin): void {
|
|
||||||
if (window.flow.plugins.some(x => x.meta.pkg === plugin.meta.pkg)) {
|
|
||||||
console.error(`Unable to register tool; ${plugin.meta.pkg} is already registered.`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.plugins.push(plugin)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens a registered application.
|
|
||||||
*
|
|
||||||
* @param pkg The PKG ID of the application.
|
|
||||||
* @param data Payload info for app to recieve.
|
|
||||||
*/
|
|
||||||
async openApp (pkg: string, data?: any): Promise<void> {
|
|
||||||
const app = this.apps.find(x => x.meta.pkg === pkg)
|
|
||||||
const win = app?.open(data)
|
|
||||||
const event = new CustomEvent('app_opened', { detail: { app, win: await win } })
|
|
||||||
window.dispatchEvent(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Flow
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
import flowIcon from '../assets/flow.png'
|
|
||||||
|
|
||||||
class Preloader {
|
|
||||||
element: HTMLElement
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the preloader.
|
|
||||||
*/
|
|
||||||
constructor () {
|
|
||||||
this.element = document.createElement('preloader')
|
|
||||||
|
|
||||||
this.element.innerHTML = `
|
|
||||||
<img src="${flowIcon}" width="150px">
|
|
||||||
<div class="done"></div>
|
|
||||||
<div class="status"></div>
|
|
||||||
`
|
|
||||||
|
|
||||||
document.body.appendChild(this.element)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the status text of the preloader.
|
|
||||||
*
|
|
||||||
* @param value The preloader status text.
|
|
||||||
*/
|
|
||||||
setStatus (value: string): void {
|
|
||||||
(this.element.querySelector('.status') as HTMLElement).innerText = value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets loading state of an instance to pending.
|
|
||||||
*
|
|
||||||
* @param value The name of an instance.
|
|
||||||
*/
|
|
||||||
setPending (value: string): void {
|
|
||||||
(this.element.querySelector('.done') as HTMLElement).innerHTML += `<div class="${value.split(' ').join('-')}"><span class='material-symbols-rounded'>check_indeterminate_small</span>${value}</div>`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets loading state of an instance to done.
|
|
||||||
*
|
|
||||||
* @param value The name of an instance.
|
|
||||||
*/
|
|
||||||
async setDone (value: string): Promise<void> {
|
|
||||||
const icon = this.element.querySelector('.done')?.querySelector(`.${value.split(' ').join('-')}`)?.querySelector('.material-symbols-rounded') as HTMLElement
|
|
||||||
icon.innerHTML = 'check_small'
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 300))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets loading state of an instance to error.
|
|
||||||
*
|
|
||||||
* @param value The name of an instance.
|
|
||||||
*/
|
|
||||||
async setError (value: string): Promise<void> {
|
|
||||||
const icon = this.element.querySelector('.done')?.querySelector(`.${value.split(' ').join('-')}`)?.querySelector('.material-symbols-rounded') as HTMLElement
|
|
||||||
icon.innerHTML = 'close_small'
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 300))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the preloader.
|
|
||||||
*/
|
|
||||||
finish (): void {
|
|
||||||
this.element.style.opacity = '0'
|
|
||||||
this.element.style.pointerEvents = 'none'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Preloader
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
|
|
||||||
import { AppClosedEvent, AppOpenedEvent, Plugin } from '../types'
|
|
||||||
import { getTime } from '../utils'
|
|
||||||
|
|
||||||
class StatusBar {
|
|
||||||
element: HTMLElement
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the status bar.
|
|
||||||
*/
|
|
||||||
constructor () {
|
|
||||||
this.element = document.createElement('toolbar')
|
|
||||||
|
|
||||||
this.element.innerHTML = `
|
|
||||||
<div class="outlined" data-toolbar-id="start"><span class="material-symbols-rounded">space_dashboard</span></div>
|
|
||||||
${/* <div class="outlined" data-toolbar-id="widgets"><span class="material-symbols-rounded">widgets</span></div> */ ''}
|
|
||||||
<div data-toolbar-id="apps"></div>
|
|
||||||
<flex></flex>
|
|
||||||
<div class="outlined" data-toolbar-id="plugins"><span class="material-symbols-rounded">expand_less</span></div>
|
|
||||||
<div class="outlined" data-toolbar-id="controls">
|
|
||||||
<span class="material-symbols-rounded battery">battery_2_bar</span>
|
|
||||||
<span class="material-symbols-rounded signal">signal_cellular_4_bar</span>
|
|
||||||
</div>
|
|
||||||
<div class="outlined" data-toolbar-id="calendar"></div>
|
|
||||||
${/* <div class="outlined" data-toolbar-id="notifications">
|
|
||||||
<span class="material-symbols-rounded">notifications</span>
|
|
||||||
</div> */ ''}
|
|
||||||
`
|
|
||||||
|
|
||||||
setInterval((): any => {
|
|
||||||
getTime().then((time) => {
|
|
||||||
(this.element.querySelector('div[data-toolbar-id="calendar"]') as HTMLElement).innerText = time
|
|
||||||
}).catch(e => console.error)
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
this.element.querySelector('div[data-toolbar-id="start"]')?.addEventListener('click', () => {
|
|
||||||
window.wm.toggleLauncher()
|
|
||||||
})
|
|
||||||
|
|
||||||
function updateBatteryIcon (battery: any): void {
|
|
||||||
let iconHTML = ''
|
|
||||||
|
|
||||||
if (battery.charging === true) {
|
|
||||||
if (battery.level === 1) {
|
|
||||||
iconHTML = 'battery_charging_full'
|
|
||||||
} else if (battery.level >= 0.9) {
|
|
||||||
iconHTML = 'battery_charging_90'
|
|
||||||
} else if (battery.level >= 0.8) {
|
|
||||||
iconHTML = 'battery_charging_80'
|
|
||||||
} else if (battery.level >= 0.6) {
|
|
||||||
iconHTML = 'battery_charging_60'
|
|
||||||
} else if (battery.level >= 0.5) {
|
|
||||||
iconHTML = 'battery_charging_50'
|
|
||||||
} else if (battery.level >= 0.3) {
|
|
||||||
iconHTML = 'battery_charging_30'
|
|
||||||
} else if (battery.level >= 0) {
|
|
||||||
iconHTML = 'battery_charging_20'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (battery.level === 1) {
|
|
||||||
iconHTML = 'battery_full'
|
|
||||||
} else if (battery.level >= 0.6) {
|
|
||||||
iconHTML = 'battery_6_bar'
|
|
||||||
} else if (battery.level >= 0.5) {
|
|
||||||
iconHTML = 'battery_5_bar'
|
|
||||||
} else if (battery.level >= 0.4) {
|
|
||||||
iconHTML = 'battery_4_bar'
|
|
||||||
} else if (battery.level >= 0.3) {
|
|
||||||
iconHTML = 'battery_3_bar'
|
|
||||||
} else if (battery.level >= 0.2) {
|
|
||||||
iconHTML = 'battery_2_bar'
|
|
||||||
} else if (battery.level >= 0.1) {
|
|
||||||
iconHTML = 'battery_1_bar'
|
|
||||||
} else if (battery.level >= 0) {
|
|
||||||
iconHTML = 'battery_0_bar'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const batteryDiv = document.querySelector('div[data-toolbar-id="controls"] > .battery')
|
|
||||||
if (batteryDiv != null) {
|
|
||||||
batteryDiv.innerHTML = iconHTML
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('getBattery' in navigator) {
|
|
||||||
(navigator as any).getBattery().then(function (battery: any) {
|
|
||||||
updateBatteryIcon(battery)
|
|
||||||
|
|
||||||
battery.addEventListener('levelchange', function () {
|
|
||||||
updateBatteryIcon(battery)
|
|
||||||
})
|
|
||||||
|
|
||||||
battery.addEventListener('chargingchange', function () {
|
|
||||||
updateBatteryIcon(battery)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const batteryDiv = document.querySelector('div[data-toolbar-id="controls"] > .battery')
|
|
||||||
if (batteryDiv != null) {
|
|
||||||
batteryDiv.innerHTML = 'battery_unknown'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateIcon (ms: number): void {
|
|
||||||
let icon = ''
|
|
||||||
|
|
||||||
if (ms >= 200 && ms < 400) {
|
|
||||||
icon = 'signal_cellular_1_bar'
|
|
||||||
} else if (ms >= 400 && ms < 600) {
|
|
||||||
icon = 'signal_cellular_2_bar'
|
|
||||||
} else if (ms >= 600 && ms < 800) {
|
|
||||||
icon = 'signal_cellular_3_bar'
|
|
||||||
} else if (ms >= 800) {
|
|
||||||
icon = 'signal_cellular_4_bar'
|
|
||||||
} else {
|
|
||||||
icon = 'signal_cellular_0_bar'
|
|
||||||
}
|
|
||||||
|
|
||||||
(document.querySelector('div[data-toolbar-id="controls"] > .signal') as HTMLElement).innerHTML = icon
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ping (startTime: number): Promise<void> {
|
|
||||||
fetch((await window.config()).SERVER_URL + '/bare/')
|
|
||||||
.then(() => {
|
|
||||||
const endTime = performance.now()
|
|
||||||
const pingTime = endTime - startTime
|
|
||||||
updateIcon(pingTime)
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
(document.querySelector('div[data-toolbar-id="controls"] > .signal') as HTMLElement).innerHTML = 'signal_cellular_connected_no_internet_4_bar'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setInterval((): any => ping(performance.now()), 10000)
|
|
||||||
|
|
||||||
window.addEventListener('app_opened', (e: AppOpenedEvent): void => {
|
|
||||||
const appIcon = document.createElement('app')
|
|
||||||
const app = e.detail.app
|
|
||||||
const win = e.detail.win
|
|
||||||
appIcon.innerHTML = `<img data-id="${win.id}" src="${app.meta.icon}"/>`
|
|
||||||
appIcon.onclick = async () => {
|
|
||||||
const win = await e.detail.win
|
|
||||||
win.focus()
|
|
||||||
win.toggleMin()
|
|
||||||
}
|
|
||||||
this.element.querySelector('div[data-toolbar-id="apps"]')?.appendChild(appIcon)
|
|
||||||
})
|
|
||||||
|
|
||||||
window.addEventListener('app_closed', (e: AppClosedEvent): void => {
|
|
||||||
const win = e.detail.win
|
|
||||||
this.element.querySelector('div[data-toolbar-id="apps"]')?.querySelector(`img[data-id="${win.id}"]`)?.parentElement?.remove()
|
|
||||||
})
|
|
||||||
|
|
||||||
document.body.style.flexDirection = 'column-reverse'
|
|
||||||
document.body.appendChild(this.element)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a plugin to the status bar.
|
|
||||||
*
|
|
||||||
* @param item The plugin to be added to the status bar.
|
|
||||||
*/
|
|
||||||
async add (item: Plugin): Promise<void> {
|
|
||||||
const element = document.createElement('div')
|
|
||||||
element.setAttribute('data-toolbar-id', item.meta.pkg)
|
|
||||||
|
|
||||||
this.element.appendChild(element)
|
|
||||||
|
|
||||||
await item.run(element, await window.config())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiates the status bar.
|
|
||||||
*/
|
|
||||||
async init (): Promise<void> {
|
|
||||||
window.preloader.setStatus('adding plugins to statusbar...')
|
|
||||||
|
|
||||||
for (const plugin of window.flow.plugins) {
|
|
||||||
window.preloader.setStatus(`adding plugins to statusbar\n${plugin.meta.pkg}`)
|
|
||||||
await this.add(plugin)
|
|
||||||
}
|
|
||||||
|
|
||||||
await window.preloader.setDone('plugins')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StatusBar
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
|
|
||||||
import FlowWindow from '../structures/FlowWindow'
|
|
||||||
import { FlowWindowConfig } from '../types'
|
|
||||||
|
|
||||||
class WindowManager {
|
|
||||||
private isLauncherOpen = false
|
|
||||||
windowArea: HTMLElement
|
|
||||||
launcher: HTMLElement
|
|
||||||
windows: FlowWindow[] = []
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the window container.
|
|
||||||
*/
|
|
||||||
constructor () {
|
|
||||||
this.windowArea = document.createElement('window-area')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the highest window's z-index.
|
|
||||||
*
|
|
||||||
* @returns The heighest window's z-index.
|
|
||||||
*/
|
|
||||||
getHighestZIndex (): number {
|
|
||||||
const indexes = this.windows.map((win: FlowWindow) => {
|
|
||||||
return parseInt(win.element.style.zIndex)
|
|
||||||
})
|
|
||||||
|
|
||||||
const max = Math.max(...indexes)
|
|
||||||
|
|
||||||
return max === -Infinity ? 0 : max
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a window.
|
|
||||||
*
|
|
||||||
* @param config The config for the window to follow.
|
|
||||||
* @returns The created window.
|
|
||||||
*/
|
|
||||||
createWindow (config: FlowWindowConfig): FlowWindow {
|
|
||||||
const win = new FlowWindow(this, config)
|
|
||||||
this.windows.push(win)
|
|
||||||
this.windowArea.appendChild(win.element)
|
|
||||||
return win
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles the app launcher.
|
|
||||||
*/
|
|
||||||
toggleLauncher (): boolean {
|
|
||||||
if (this.isLauncherOpen) {
|
|
||||||
this.launcher.style.opacity = '0'
|
|
||||||
this.launcher.style.backdropFilter = 'blur(0px)'
|
|
||||||
this.launcher.style.pointerEvents = 'none'
|
|
||||||
} else {
|
|
||||||
this.launcher.style.opacity = '1'
|
|
||||||
this.launcher.style.backdropFilter = 'blur(20px)'
|
|
||||||
this.launcher.style.pointerEvents = 'all'
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isLauncherOpen = !this.isLauncherOpen
|
|
||||||
return this.isLauncherOpen
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiates the window manager.
|
|
||||||
*/
|
|
||||||
async init (): Promise<void> {
|
|
||||||
window.preloader.setPending('window manager')
|
|
||||||
window.preloader.setStatus('creating app launcher...')
|
|
||||||
this.launcher = document.createElement('launcher')
|
|
||||||
|
|
||||||
this.launcher.innerHTML = `
|
|
||||||
<input placeholder="Search"/>
|
|
||||||
<apps></apps>
|
|
||||||
`;
|
|
||||||
|
|
||||||
(this.launcher.querySelector('input') as HTMLInputElement).onkeyup = () => {
|
|
||||||
(this.launcher.querySelector('apps') as HTMLElement).innerHTML = ''
|
|
||||||
if ((this.launcher.querySelector('input') as HTMLInputElement).value !== '') {
|
|
||||||
window.flow.apps.filter(x => x.meta.name.toLowerCase().includes((this.launcher.querySelector('input') as HTMLInputElement).value.toLowerCase())).forEach((app) => {
|
|
||||||
const appElement = document.createElement('app')
|
|
||||||
appElement.onclick = async () => {
|
|
||||||
await window.flow.openApp(app.meta.pkg)
|
|
||||||
this.toggleLauncher()
|
|
||||||
}
|
|
||||||
appElement.innerHTML = `<img src="${app.meta.icon}"><div>${app.meta.name}</div>`
|
|
||||||
this.launcher.querySelector('apps')?.appendChild(appElement)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
window.flow.apps.forEach((app) => {
|
|
||||||
window.preloader.setStatus(`adding apps to app launcher\n${app.meta.name}`)
|
|
||||||
const appElement = document.createElement('app')
|
|
||||||
appElement.onclick = async () => {
|
|
||||||
await window.flow.openApp(app.meta.pkg)
|
|
||||||
window.wm.toggleLauncher()
|
|
||||||
}
|
|
||||||
appElement.innerHTML = `<img src="${app.meta.icon}"><div>${app.meta.name}</div>`
|
|
||||||
window.wm.launcher.querySelector('apps')?.appendChild(appElement)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.launcher.onclick = (e) => {
|
|
||||||
if (e.target !== e.currentTarget) return
|
|
||||||
this.toggleLauncher()
|
|
||||||
}
|
|
||||||
|
|
||||||
(this.launcher.querySelector('apps') as HTMLElement).onclick = (e) => {
|
|
||||||
if (e.target !== e.currentTarget) return
|
|
||||||
this.toggleLauncher()
|
|
||||||
}
|
|
||||||
|
|
||||||
await window.preloader.setDone('window manager')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default WindowManager
|
|
||||||
135
src/kernel.ts
Normal file
135
src/kernel.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
import './assets/style.less'
|
||||||
|
import { version } from '../package.json'
|
||||||
|
import { v4 as uuid } from 'uuid'
|
||||||
|
import { Permission } from './system/lib/VirtualFS'
|
||||||
|
import ProcessLib from './structures/ProcessLib'
|
||||||
|
import ProcLib from './structures/ProcLib'
|
||||||
|
import { Executable, Process, Package, ProcessInfo, KernelConfig } from './types'
|
||||||
|
import semver from 'semver'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
kernel: Kernel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
|
||||||
|
async function enableDebug (): Promise<void> {
|
||||||
|
const { default: eruda } = await import('eruda')
|
||||||
|
eruda.init()
|
||||||
|
return await Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.get('debug') != null) {
|
||||||
|
enableDebug().catch(e => console.error(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Kernel {
|
||||||
|
readonly version: string
|
||||||
|
readonly codename: string
|
||||||
|
processList: ProcessInfo[] = []
|
||||||
|
packageList: {
|
||||||
|
[key: string]: Package
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
fs: any
|
||||||
|
|
||||||
|
config: KernelConfig
|
||||||
|
lastPid: number = 0
|
||||||
|
|
||||||
|
constructor (version: string) {
|
||||||
|
this.codename = 'Mochi'
|
||||||
|
this.version = version
|
||||||
|
}
|
||||||
|
|
||||||
|
setFS (fs: any, process: ProcessLib): void {
|
||||||
|
if (process.permission === Permission.SYSTEM) {
|
||||||
|
this.fs = fs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig (data: any, process: ProcessLib): void {
|
||||||
|
if (process.permission === Permission.SYSTEM) {
|
||||||
|
this.config = data
|
||||||
|
document.dispatchEvent(new CustomEvent('config_update', {
|
||||||
|
detail: {
|
||||||
|
config: this.config
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startExecutable (url: string, permission = Permission.USER, data = {}): Promise<{ procLib: ProcessLib, executable: Process } | any> {
|
||||||
|
let executable: Executable
|
||||||
|
try {
|
||||||
|
const comps = import.meta.glob('./system/**/*.ts')
|
||||||
|
const module = comps[`./system/${url}.ts`]
|
||||||
|
const importedExecutable = (await module()) as any
|
||||||
|
executable = importedExecutable.default
|
||||||
|
} catch {
|
||||||
|
if (this.fs === undefined) throw new Error('Filesystem hasn\'t been initiated.')
|
||||||
|
const dataURL = `data:text/javascript;base64,${Buffer.from(await this.fs.readFile(`/opt/${url}.js`)).toString('base64')}`
|
||||||
|
const importedExecutable = await import(dataURL)
|
||||||
|
executable = importedExecutable.default
|
||||||
|
}
|
||||||
|
|
||||||
|
if (semver.gt(executable.config.targetVer, this.version)) throw new Error(`Executable requires a newer version of FlowOS: ${executable.config.targetVer}`)
|
||||||
|
if (executable === undefined) throw new Error(`No default export found for package: ${url}.`)
|
||||||
|
|
||||||
|
if (this.packageList[executable.config.name] === undefined) this.packageList[executable.config.name] = { url, executable }
|
||||||
|
else if (this.packageList[executable.config.name].url !== url) throw new Error(`Package name conflict: ${executable.config.name}`)
|
||||||
|
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
if (executable.config.type === 'process') {
|
||||||
|
const executableProcess = executable as Process
|
||||||
|
console.group(`Starting ${url}`)
|
||||||
|
const pid = ProcLib.findEmptyPID(this)
|
||||||
|
const token = uuid()
|
||||||
|
const procLib = new ProcessLib(url, pid, token, permission, data, executableProcess, this)
|
||||||
|
this.processList.push({
|
||||||
|
pid,
|
||||||
|
token,
|
||||||
|
name: executableProcess.config.name
|
||||||
|
})
|
||||||
|
document.dispatchEvent(new CustomEvent('update_process', {}))
|
||||||
|
executableProcess.run(procLib).then((value: any) => {
|
||||||
|
if (value !== undefined) procLib.kill().catch(e => console.error(e))
|
||||||
|
document.dispatchEvent(new CustomEvent('update_process', {}))
|
||||||
|
resolve({ procLib, value })
|
||||||
|
}).catch(e => console.error(e))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reject(new Error(`Unknown executable type: ${executable.config.type as string}`))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getExecutable (url: string): Promise<Executable> {
|
||||||
|
let executable: Executable
|
||||||
|
try {
|
||||||
|
const comps = import.meta.glob('./system/**/*.ts')
|
||||||
|
const module = comps[`./system/${url}.ts`]
|
||||||
|
const importedExecutable = (await module()) as any
|
||||||
|
executable = importedExecutable.default
|
||||||
|
} catch {
|
||||||
|
if (this.fs === undefined) throw new Error('Filesystem hasn\'t been initiated.')
|
||||||
|
const dataURL = `data:text/javascript;base64,${Buffer.from(await this.fs.readFile(`/opt/${url}.js`)).toString('base64')}`
|
||||||
|
const importedExecutable = await import(dataURL)
|
||||||
|
executable = importedExecutable.default
|
||||||
|
}
|
||||||
|
|
||||||
|
if (semver.gt(executable.config.targetVer, this.version)) throw new Error(`Executable requires a newer version of FlowOS: ${executable.config.targetVer}`)
|
||||||
|
if (executable === undefined) throw new Error(`No default export found for package: ${url}.`)
|
||||||
|
|
||||||
|
if (this.packageList[executable.config.name] === undefined) this.packageList[executable.config.name] = { url, executable }
|
||||||
|
else if (this.packageList[executable.config.name].url !== url) throw new Error(`Package name conflict: ${executable.config.name}`)
|
||||||
|
|
||||||
|
return executable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
import('material-symbols')
|
||||||
|
const kernel = new Kernel(version)
|
||||||
|
kernel.startExecutable('BootLoader', Permission.SYSTEM).catch(e => console.error(e))
|
||||||
|
})
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { v4 as uuid } from 'uuid'
|
|
||||||
import WindowManager from '../instances/WindowManager'
|
|
||||||
import { FlowWindowConfig } from '../types'
|
import { FlowWindowConfig } from '../types'
|
||||||
import { sanitize } from '../utils'
|
import { sanitize } from '../utils'
|
||||||
import nullIcon from '../assets/icons/application-default-icon.svg'
|
import nullIcon from '../assets/icons/application-default-icon.svg'
|
||||||
|
import ProcessLib from './ProcessLib'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes an element draggable.
|
* Makes an element draggable.
|
||||||
|
|
@ -78,9 +77,11 @@ class FlowWindow {
|
||||||
isMinimized = false
|
isMinimized = false
|
||||||
isMaximized = false
|
isMaximized = false
|
||||||
|
|
||||||
wm: WindowManager
|
wm: any
|
||||||
|
|
||||||
id = uuid()
|
readonly process: ProcessLib
|
||||||
|
|
||||||
|
onClose: () => void
|
||||||
|
|
||||||
config: FlowWindowConfig
|
config: FlowWindowConfig
|
||||||
|
|
||||||
|
|
@ -90,13 +91,16 @@ class FlowWindow {
|
||||||
* @param wm The current window manager session.
|
* @param wm The current window manager session.
|
||||||
* @param config The window's pre-set config.
|
* @param config The window's pre-set config.
|
||||||
*/
|
*/
|
||||||
constructor (wm: WindowManager, config: FlowWindowConfig) {
|
constructor (process: ProcessLib, wm: any, config: FlowWindowConfig, onClose = () => {}) {
|
||||||
|
this.process = process
|
||||||
this.wm = wm
|
this.wm = wm
|
||||||
this.config = config
|
this.config = config
|
||||||
|
|
||||||
|
this.onClose = onClose
|
||||||
|
|
||||||
this.element = document.createElement('window')
|
this.element = document.createElement('window')
|
||||||
|
|
||||||
this.element.style.zIndex = (window.wm.getHighestZIndex() + 1).toString()
|
this.element.style.zIndex = (wm.getHighestZIndex() as number + 1).toString()
|
||||||
this.element.style.position = 'absolute'
|
this.element.style.position = 'absolute'
|
||||||
this.focus()
|
this.focus()
|
||||||
|
|
||||||
|
|
@ -112,13 +116,13 @@ class FlowWindow {
|
||||||
this.element.style.height = `${config.height ?? 200}px`
|
this.element.style.height = `${config.height ?? 200}px`
|
||||||
|
|
||||||
this.header = document.createElement('window-header')
|
this.header = document.createElement('window-header')
|
||||||
this.header.innerHTML = `<img src="${sanitize(config.icon === '' ? nullIcon : config.icon)}"></img> <div class="title">${sanitize(config.title)}</div><div style="flex:1;"></div><i id="min" class='material-symbols-rounded' style="margin-bottom: 5px;">minimize</i><i id="close" class='material-symbols-rounded'>close</i>`
|
this.header.innerHTML = `<img alt="${sanitize(config.title)} icon" src="${sanitize(config.icon === '' ? nullIcon : config.icon)}"></img> <div class="title">${sanitize(config.title)}</div><div style="flex:1;"></div><i id="min" class='material-symbols-rounded' style="margin-bottom: 5px;">minimize</i><i id="close" class='material-symbols-rounded'>close</i>`
|
||||||
if (config.canResize) {
|
if (config.canResize) {
|
||||||
this.header.innerHTML = `<img src="${sanitize(config.icon === '' ? nullIcon : config.icon)}"></img> <div class="title">${sanitize(config.title)}</div><div style="flex:1;"></div><i id="min" class='material-symbols-rounded' style="margin-bottom: 5px;">minimize</i><i id="max" class='material-symbols-rounded' style="font-size: 20px;">square</i><i id="close" class='material-symbols-rounded'>close</i>`
|
this.header.innerHTML = `<img alt="${sanitize(config.title)} icon" src="${sanitize(config.icon === '' ? nullIcon : config.icon)}"></img> <div class="title">${sanitize(config.title)}</div><div style="flex:1;"></div><i id="min" class='material-symbols-rounded' style="margin-bottom: 5px;">minimize</i><i id="max" class='material-symbols-rounded' style="font-size: 20px;">square</i><i id="close" class='material-symbols-rounded'>close</i>`
|
||||||
}
|
}
|
||||||
|
|
||||||
(this.header.querySelector('#close') as HTMLElement).onclick = () => {
|
(this.header.querySelector('#close') as HTMLElement).onclick = () => {
|
||||||
this.close()
|
this.process.kill().catch((e: any) => console.error(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
(this.header.querySelector('#min') as HTMLElement).onclick = () => this.toggleMin()
|
(this.header.querySelector('#min') as HTMLElement).onclick = () => this.toggleMin()
|
||||||
|
|
@ -159,8 +163,6 @@ class FlowWindow {
|
||||||
shadow.appendChild(shadowBody)
|
shadow.appendChild(shadowBody)
|
||||||
this.content = shadowBody
|
this.content = shadowBody
|
||||||
|
|
||||||
console.log(this.content)
|
|
||||||
|
|
||||||
this.element.appendChild(this.header)
|
this.element.appendChild(this.header)
|
||||||
this.element.appendChild(this.realContent)
|
this.element.appendChild(this.realContent)
|
||||||
|
|
||||||
|
|
@ -224,7 +226,7 @@ class FlowWindow {
|
||||||
*/
|
*/
|
||||||
focus (): void {
|
focus (): void {
|
||||||
if (this.element.style.zIndex !== this.wm.getHighestZIndex().toString()) {
|
if (this.element.style.zIndex !== this.wm.getHighestZIndex().toString()) {
|
||||||
this.element.style.zIndex = (this.wm.getHighestZIndex() + 1).toString()
|
this.element.style.zIndex = (this.wm.getHighestZIndex() as number + 1).toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,8 +237,7 @@ class FlowWindow {
|
||||||
this.element.style.pointerEvents = 'none'
|
this.element.style.pointerEvents = 'none'
|
||||||
this.element.style.opacity = '0'
|
this.element.style.opacity = '0'
|
||||||
this.element.style.transform = 'translateY(10px)'
|
this.element.style.transform = 'translateY(10px)'
|
||||||
const event = new CustomEvent('app_closed', { detail: { win: this } })
|
this.onClose()
|
||||||
window.dispatchEvent(event)
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.element.remove()
|
this.element.remove()
|
||||||
}, 200)
|
}, 200)
|
||||||
|
|
|
||||||
5
src/structures/LibraryLib.ts
Normal file
5
src/structures/LibraryLib.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import HTML from '../HTML'
|
||||||
|
|
||||||
|
export default class LibraryLib {
|
||||||
|
HTML = HTML
|
||||||
|
}
|
||||||
26
src/structures/ProcLib.ts
Normal file
26
src/structures/ProcLib.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import Kernel from '../kernel'
|
||||||
|
|
||||||
|
const ProcLib = {
|
||||||
|
findEmptyPID: function (kernel: Kernel) {
|
||||||
|
const r = kernel.processList.findIndex((p) => p === null)
|
||||||
|
return r !== -1 ? r : kernel.processList.length
|
||||||
|
},
|
||||||
|
cleanupProcess: function (kernel: Kernel, pid: number) {
|
||||||
|
const proc = kernel.processList
|
||||||
|
.filter((p) => p !== null)
|
||||||
|
.find((p) => p.pid === pid)
|
||||||
|
if (proc === undefined) throw new Error(`Process ${pid} not found.`)
|
||||||
|
console.group(`Killing process ${pid} (${proc.name})`)
|
||||||
|
document.dispatchEvent(new CustomEvent('app_closed', {
|
||||||
|
detail: {
|
||||||
|
token: proc.token
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
kernel.processList.splice(pid, 1)
|
||||||
|
document.dispatchEvent(new CustomEvent('update_process', {}))
|
||||||
|
console.groupEnd()
|
||||||
|
console.groupEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProcLib
|
||||||
123
src/structures/ProcessLib.ts
Normal file
123
src/structures/ProcessLib.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
import semver from 'semver'
|
||||||
|
import Kernel from '../kernel'
|
||||||
|
import { Permission } from '../system/lib/VirtualFS'
|
||||||
|
import { Process, Executable, LibraryData, Package, Library } from '../types'
|
||||||
|
import FlowWindow from './FlowWindow'
|
||||||
|
import LibraryLib from './LibraryLib'
|
||||||
|
import ProcLib from './ProcLib'
|
||||||
|
|
||||||
|
export default class ProcessLib {
|
||||||
|
readonly pid: number
|
||||||
|
readonly token: string
|
||||||
|
process: Process
|
||||||
|
private readonly _kernel: Kernel
|
||||||
|
readonly kernel: {
|
||||||
|
getExecutable: (url: string) => Promise<Executable>
|
||||||
|
processList: Array<{ pid: number, name: string, token: string }>
|
||||||
|
packageList: {
|
||||||
|
[key: string]: Package
|
||||||
|
}
|
||||||
|
config: any
|
||||||
|
setConfig: (data: any) => void
|
||||||
|
setFS: (fs: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly permission: Permission
|
||||||
|
|
||||||
|
win: FlowWindow
|
||||||
|
readonly data: any
|
||||||
|
|
||||||
|
readonly sysInfo: {
|
||||||
|
codename: string
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (url: string, pid: number, token: string, permission = Permission.USER, data = {}, process: Process, kernel: Kernel) {
|
||||||
|
this.permission = permission
|
||||||
|
this.pid = pid
|
||||||
|
this.token = token
|
||||||
|
this._kernel = kernel
|
||||||
|
this.kernel = {
|
||||||
|
getExecutable: kernel.getExecutable.bind(kernel),
|
||||||
|
processList: kernel.processList,
|
||||||
|
packageList: kernel.packageList,
|
||||||
|
config: kernel.config,
|
||||||
|
setConfig: (data: any) => kernel.setConfig(data, this),
|
||||||
|
setFS: (fs: any) => kernel.setFS(fs, this)
|
||||||
|
}
|
||||||
|
this.process = process
|
||||||
|
this.data = data
|
||||||
|
|
||||||
|
this.sysInfo = {
|
||||||
|
codename: kernel.codename,
|
||||||
|
version: kernel.version
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('config_update', (e: CustomEvent) => {
|
||||||
|
this.kernel.config = e.detail.config
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadLibrary (url: string): Promise<LibraryData> {
|
||||||
|
let executable: Executable
|
||||||
|
try {
|
||||||
|
const comps = import.meta.glob('../system/**/*.ts')
|
||||||
|
const module = comps[`../system/${url}.ts`]
|
||||||
|
const importedExecutable = (await module()) as any
|
||||||
|
executable = importedExecutable.default
|
||||||
|
} catch {
|
||||||
|
if (this._kernel.fs === undefined) throw new Error('Filesystem hasn\'t been initiated.')
|
||||||
|
const dataURL = `data:text/javascript;base64,${Buffer.from(await this._kernel.fs.readFile(`/opt/${url}.js`)).toString('base64')}`
|
||||||
|
const importedExecutable = await import(dataURL)
|
||||||
|
executable = importedExecutable.default
|
||||||
|
}
|
||||||
|
|
||||||
|
if (semver.gt(executable.config.targetVer, this.sysInfo.version)) throw new Error(`Executable requires a newer version of FlowOS: ${executable.config.targetVer}`)
|
||||||
|
if (executable === undefined) throw new Error(`No default export found for package: ${url}.`)
|
||||||
|
|
||||||
|
if (this._kernel.packageList[executable.config.name] === undefined) this._kernel.packageList[executable.config.name] = { url, executable }
|
||||||
|
else if (this._kernel.packageList[executable.config.name].url !== url) throw new Error(`Package name conflict: ${executable.config.name}`)
|
||||||
|
|
||||||
|
if (executable.config.type !== 'library') throw new Error(`Executable is not a library: ${executable.config.name}`)
|
||||||
|
const executableLibrary = executable as Library
|
||||||
|
executableLibrary.init(new LibraryLib(), this._kernel, this)
|
||||||
|
|
||||||
|
return executableLibrary.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async launch (url: string, data = {}): Promise<{ procLib: ProcessLib, executable: Process } | null> {
|
||||||
|
const executable = await this._kernel.getExecutable(url)
|
||||||
|
if (this.permission === Permission.SYSTEM) {
|
||||||
|
return await this._kernel.startExecutable(url, Permission.USER, data)
|
||||||
|
} else {
|
||||||
|
const uac = await this._kernel.startExecutable('UserAccessControl', Permission.SYSTEM, {
|
||||||
|
type: 'launch',
|
||||||
|
executable,
|
||||||
|
process: this
|
||||||
|
}) as { value: boolean, procLib: ProcessLib }
|
||||||
|
if (uac.value) {
|
||||||
|
return await this._kernel.startExecutable(url, Permission.USER, data)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async killProcess (pid: number): Promise<void> {
|
||||||
|
const process = this._kernel.processList.find((p) => p.pid === pid)
|
||||||
|
if (process === undefined) throw new Error(`Process ${pid} not found.`)
|
||||||
|
const uac = await this._kernel.startExecutable('UserAccessControl', Permission.SYSTEM, {
|
||||||
|
type: 'kill',
|
||||||
|
name: process.name,
|
||||||
|
process: this
|
||||||
|
}) as { value: boolean, procLib: ProcessLib }
|
||||||
|
if (pid === 0) throw new Error('Cannot kill BootLoader process.')
|
||||||
|
if (uac.value) {
|
||||||
|
return ProcLib.cleanupProcess(this._kernel, pid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async kill (): Promise<void> {
|
||||||
|
ProcLib.cleanupProcess(this._kernel, this.pid)
|
||||||
|
}
|
||||||
|
}
|
||||||
191
src/system/BootLoader.ts
Normal file
191
src/system/BootLoader.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
import HTML from '../HTML'
|
||||||
|
import { AppClosedEvent, AppOpenedEvent, Process } from '../types'
|
||||||
|
import { getTime } from '../utils'
|
||||||
|
import { db, defaultFS, initializeDatabase, read, setFileSystem, write } from './lib/VirtualFS'
|
||||||
|
import nullIcon from '../assets/icons/application-default-icon.svg'
|
||||||
|
import { parse } from 'js-ini'
|
||||||
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
|
const BootLoader: Process = {
|
||||||
|
config: {
|
||||||
|
name: 'Bootloader',
|
||||||
|
type: 'process',
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
run: async (process) => {
|
||||||
|
const splashScreen = await process.loadLibrary('lib/SplashScreen')
|
||||||
|
const splashElement = splashScreen.getElement()
|
||||||
|
splashElement.appendTo(document.body)
|
||||||
|
|
||||||
|
const fs = await process.loadLibrary('lib/VirtualFS')
|
||||||
|
const wm = await process.loadLibrary('lib/WindowManager')
|
||||||
|
const launcher = await process.loadLibrary('lib/Launcher')
|
||||||
|
|
||||||
|
await initializeDatabase('virtualfs')
|
||||||
|
db.onerror = (event: Event) => {
|
||||||
|
const target = event.target as IDBRequest
|
||||||
|
const errorMessage = target.error !== null ? target.error.message : 'Unknown error'
|
||||||
|
throw new Error(`[VirtualFS] ${target.error?.name ?? 'Unknown Error'}: ${errorMessage}`)
|
||||||
|
}
|
||||||
|
if ('storage' in navigator) {
|
||||||
|
await navigator.storage?.persist()?.catch(e => console.error(e))
|
||||||
|
} else {
|
||||||
|
console.warn('Persistent storage is not supported.')
|
||||||
|
}
|
||||||
|
const fileSystem = await read()
|
||||||
|
if (fileSystem === undefined) {
|
||||||
|
await write(defaultFS)
|
||||||
|
} else {
|
||||||
|
await setFileSystem(fileSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = Buffer.from(await fs.readFile('/etc/flow')).toString()
|
||||||
|
process.kernel.setFS(fs)
|
||||||
|
process.kernel.setConfig(parse(config))
|
||||||
|
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
const registrations = await navigator.serviceWorker.getRegistrations()
|
||||||
|
for (const registration of registrations) {
|
||||||
|
await registration.unregister()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.serviceWorker.register(`/uv-sw.js?url=${encodeURIComponent(btoa(process.kernel.config.SERVER))}&e=${uuid()}`, {
|
||||||
|
scope: '/service/'
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('Service workers are not supported.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = new HTML('input').attr({
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'Search'
|
||||||
|
}).on('keyup', () => {
|
||||||
|
apps.elm.innerHTML = ''
|
||||||
|
renderApps().catch(e => console.error(e))
|
||||||
|
}).appendTo(launcher.element)
|
||||||
|
const apps = new HTML('apps').appendTo(launcher.element)
|
||||||
|
|
||||||
|
const renderApps = async (): Promise<void> => {
|
||||||
|
apps.html('')
|
||||||
|
const files = await fs.readdir('/home/Applications/')
|
||||||
|
files
|
||||||
|
.filter((x: string) => x.endsWith('.app') && ((input.elm as HTMLInputElement) !== null ? x.toLowerCase().includes((input.elm as HTMLInputElement).value.toLowerCase()) : true))
|
||||||
|
.forEach(async (file: string) => {
|
||||||
|
fs.readFile(`/home/Applications/${file}`).then(async (data: Uint8Array) => {
|
||||||
|
const path = Buffer.from(data).toString()
|
||||||
|
const executable = await process.kernel.getExecutable(path) as Process
|
||||||
|
|
||||||
|
const appElement = new HTML('app').on('click', () => {
|
||||||
|
process.launch(path).catch((e: any) => console.error(e))
|
||||||
|
launcher.toggle()
|
||||||
|
}).appendTo(apps)
|
||||||
|
new HTML('img').attr({
|
||||||
|
src: executable.config.icon ?? nullIcon,
|
||||||
|
alt: `${executable.config.name} icon`
|
||||||
|
}).appendTo(appElement)
|
||||||
|
new HTML('div').text(executable.config.name).appendTo(appElement)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await renderApps()
|
||||||
|
document.addEventListener('fs_update', () => {
|
||||||
|
renderApps().catch(e => console.error(e))
|
||||||
|
})
|
||||||
|
|
||||||
|
launcher.element.on('click', (e: Event) => {
|
||||||
|
if (e.target !== e.currentTarget) return
|
||||||
|
launcher.toggle()
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusBar = await process.loadLibrary('lib/StatusBar')
|
||||||
|
|
||||||
|
statusBar.element.html(`
|
||||||
|
<div class="outlined" data-toolbar-id="start"><span class="material-symbols-rounded">space_dashboard</span></div>
|
||||||
|
|
||||||
|
<div data-toolbar-id="apps"></div>
|
||||||
|
<flex></flex>
|
||||||
|
<div class="outlined" data-toolbar-id="plugins"><span class="material-symbols-rounded">expand_less</span></div>
|
||||||
|
<div class="outlined" data-toolbar-id="controls">
|
||||||
|
<span class="material-symbols-rounded battery">battery_2_bar</span>
|
||||||
|
<span class="material-symbols-rounded signal">signal_cellular_4_bar</span>
|
||||||
|
</div>
|
||||||
|
<div class="outlined" data-toolbar-id="calendar"></div>
|
||||||
|
|
||||||
|
`)
|
||||||
|
|
||||||
|
setInterval((): any => {
|
||||||
|
getTime().then((time) => {
|
||||||
|
statusBar.element.qs('div[data-toolbar-id="calendar"]').text(time)
|
||||||
|
}).catch(e => console.error)
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
statusBar.element.qs('div[data-toolbar-id="start"]').on('click', () => {
|
||||||
|
launcher.toggle()
|
||||||
|
})
|
||||||
|
|
||||||
|
if ('getBattery' in navigator) {
|
||||||
|
(navigator as any).getBattery().then((battery: any) => {
|
||||||
|
statusBar.updateBatteryIcon(battery)
|
||||||
|
|
||||||
|
battery.addEventListener('levelchange', () => {
|
||||||
|
statusBar.updateBatteryIcon(battery)
|
||||||
|
})
|
||||||
|
|
||||||
|
battery.addEventListener('chargingchange', () => {
|
||||||
|
statusBar.updateBatteryIcon(battery)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const batteryDiv = document.querySelector('div[data-toolbar-id="controls"] > .battery')
|
||||||
|
if (batteryDiv != null) {
|
||||||
|
batteryDiv.innerHTML = 'battery_unknown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ping (startTime: number): Promise<void> {
|
||||||
|
fetch(`${process.kernel.config.SERVER as string}/bare/`)
|
||||||
|
.then(() => {
|
||||||
|
const endTime = performance.now()
|
||||||
|
const pingTime = endTime - startTime
|
||||||
|
statusBar.updateIcon(pingTime)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
(document.querySelector('div[data-toolbar-id="controls"] > .signal') as HTMLElement).innerHTML = 'signal_cellular_connected_no_internet_4_bar'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval((): any => ping(performance.now()), 10_000)
|
||||||
|
|
||||||
|
document.addEventListener('app_opened', (e: AppOpenedEvent): void => {
|
||||||
|
new HTML('app').appendMany(
|
||||||
|
new HTML('img').attr({
|
||||||
|
alt: `${e.detail.proc.config.name} icon`,
|
||||||
|
'data-id': e.detail.token,
|
||||||
|
src: e.detail.proc.config.icon ?? nullIcon
|
||||||
|
}).on('click', () => {
|
||||||
|
e.detail.win.focus()
|
||||||
|
e.detail.win.toggleMin()
|
||||||
|
})
|
||||||
|
).appendTo(statusBar.element.qs('div[data-toolbar-id="apps"]'))
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('app_closed', (e: AppClosedEvent): void => {
|
||||||
|
statusBar.element.qs('div[data-toolbar-id="apps"]').qs(`img[data-id="${e.detail.token}"]`).elm.parentElement.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
document.body.style.flexDirection = 'column-reverse'
|
||||||
|
|
||||||
|
await statusBar.element.appendTo(document.body)
|
||||||
|
await launcher.element.appendTo(document.body)
|
||||||
|
await wm.windowArea.appendTo(document.body)
|
||||||
|
|
||||||
|
splashElement.cleanup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BootLoader
|
||||||
51
src/system/UserAccessControl.ts
Normal file
51
src/system/UserAccessControl.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
|
||||||
|
import FlowWindow from '../structures/FlowWindow'
|
||||||
|
import { Process } from '../types'
|
||||||
|
|
||||||
|
const UserAccessControl: Process = {
|
||||||
|
config: {
|
||||||
|
name: 'User Access Control',
|
||||||
|
type: 'process',
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
run: async (process) => {
|
||||||
|
if (Object.keys(process.data).length <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const initiator = process.data.process.process
|
||||||
|
const target = process.data.executable
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
process.loadLibrary('lib/WindowManager').then(async wm => {
|
||||||
|
let message
|
||||||
|
switch (process.data.type) {
|
||||||
|
case 'launch': {
|
||||||
|
message = `${initiator.config.name as string} wants to launch ${target.config.name as string}`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'kill': {
|
||||||
|
message = `${initiator.config.name as string} wants to kill ${process.data.name as string}`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'fs': {
|
||||||
|
message = `${initiator.config.name as string} wants to modify/access ${process.data.path as string}`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wm.createModal('User Account Control', message, process)
|
||||||
|
.then(async ({ value, win }: {
|
||||||
|
value: boolean
|
||||||
|
win: FlowWindow
|
||||||
|
}) => {
|
||||||
|
if (value) {
|
||||||
|
resolve(true)
|
||||||
|
} else {
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).catch((e) => console.error(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserAccessControl
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
import icon from '../../assets/icons/web-browser.svg'
|
import icon from '../../assets/icons/web-browser.svg'
|
||||||
import { App } from '../../types'
|
import { Process } from '../../types'
|
||||||
|
|
||||||
import FlowWindow from '../../structures/FlowWindow'
|
const BrowserApp: Process = {
|
||||||
|
config: {
|
||||||
export default class BrowserApp implements App {
|
|
||||||
meta = {
|
|
||||||
name: 'Browser',
|
name: 'Browser',
|
||||||
description: 'A simple browser app.',
|
type: 'process',
|
||||||
pkg: 'flow.browser',
|
icon,
|
||||||
version: '1.0.0',
|
targetVer: '1.0.0-indev.0'
|
||||||
icon
|
},
|
||||||
}
|
run: async (process) => {
|
||||||
|
const win = await process.loadLibrary('lib/WindowManager').then((wm: any) => {
|
||||||
async open (): Promise<FlowWindow> {
|
return wm.createWindow({
|
||||||
const win = window.wm.createWindow({
|
title: 'Browser',
|
||||||
title: this.meta.name,
|
icon,
|
||||||
icon: this.meta.icon,
|
width: 500,
|
||||||
width: 400,
|
height: 700
|
||||||
height: 300
|
}, process)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const xor = await process.loadLibrary('lib/XOR')
|
||||||
|
|
||||||
win.content.style.height = '100%'
|
win.content.style.height = '100%'
|
||||||
win.content.style.display = 'flex'
|
win.content.style.display = 'flex'
|
||||||
win.content.style.flexDirection = 'column'
|
win.content.style.flexDirection = 'column'
|
||||||
|
|
@ -46,6 +46,7 @@ export default class BrowserApp implements App {
|
||||||
|
|
||||||
#content-container {
|
#content-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
background: white;
|
||||||
}
|
}
|
||||||
.add {
|
.add {
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -74,7 +75,7 @@ export default class BrowserApp implements App {
|
||||||
iframe: HTMLIFrameElement = document.createElement('iframe')
|
iframe: HTMLIFrameElement = document.createElement('iframe')
|
||||||
|
|
||||||
constructor (url: string) {
|
constructor (url: string) {
|
||||||
this.iframe.src = `/service/${xor.encode(url)}`
|
this.iframe.src = `/service/${xor.encode(url) as string}`
|
||||||
this.iframe.style.display = 'none'
|
this.iframe.style.display = 'none'
|
||||||
|
|
||||||
this.header.innerHTML = `
|
this.header.innerHTML = `
|
||||||
|
|
@ -91,12 +92,12 @@ export default class BrowserApp implements App {
|
||||||
}
|
}
|
||||||
(this.header.querySelector('.title') as HTMLElement).innerText = 'Tab'
|
(this.header.querySelector('.title') as HTMLElement).innerText = 'Tab'
|
||||||
this.iframe.src = (win.content.querySelector('input')?.value as string)
|
this.iframe.src = (win.content.querySelector('input')?.value as string)
|
||||||
} else {
|
return
|
||||||
|
}
|
||||||
if (this === tabManager.activeTab) {
|
if (this === tabManager.activeTab) {
|
||||||
(win.content.querySelector('.toggle') as HTMLElement).innerHTML = 'toggle_on'
|
(win.content.querySelector('.toggle') as HTMLElement).innerHTML = 'toggle_on'
|
||||||
}
|
}
|
||||||
this.iframe.src = `/service/${xor.encode(win.content.querySelector('input')?.value as string)}`
|
this.iframe.src = `/service/${xor.encode(win.content.querySelector('input').value) as string}`
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,20 +137,21 @@ export default class BrowserApp implements App {
|
||||||
|
|
||||||
setActiveTab (tab: Tab): void {
|
setActiveTab (tab: Tab): void {
|
||||||
this.tabs.forEach((tab) => {
|
this.tabs.forEach((tab) => {
|
||||||
if (tab.active) {
|
if (!tab.active) {
|
||||||
|
return
|
||||||
|
}
|
||||||
tab.active = false
|
tab.active = false
|
||||||
tab.iframe.style.display = 'none'
|
tab.iframe.style.display = 'none'
|
||||||
tab.header.classList.remove('active')
|
tab.header.classList.remove('active')
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!tab.proxy) {
|
if (tab.proxy) {
|
||||||
|
try { (win.content.querySelector('.inp') as HTMLInputElement).value = xor.decode((tab.iframe.contentWindow as Window).location.href.split('/service/')[1]) } catch (e) { (win.content.querySelector('.inp') as HTMLInputElement).value = 'about:blank' }
|
||||||
|
(win.content.querySelector('.toggle') as HTMLElement).innerHTML = 'toggle_on'
|
||||||
|
} else {
|
||||||
(tab.header.querySelector('.title') as HTMLElement).textContent = 'Tab'
|
(tab.header.querySelector('.title') as HTMLElement).textContent = 'Tab'
|
||||||
try { (win.content.querySelector('.inp') as HTMLInputElement).value = (tab.iframe.contentWindow as Window).location.href } catch (e) { (win.content.querySelector('.inp') as HTMLInputElement).value = 'about:blank' }
|
try { (win.content.querySelector('.inp') as HTMLInputElement).value = (tab.iframe.contentWindow as Window).location.href } catch (e) { (win.content.querySelector('.inp') as HTMLInputElement).value = 'about:blank' }
|
||||||
(win.content.querySelector('.toggle') as HTMLElement).innerHTML = 'toggle_off'
|
(win.content.querySelector('.toggle') as HTMLElement).innerHTML = 'toggle_off'
|
||||||
} else {
|
|
||||||
try { (win.content.querySelector('.inp') as HTMLInputElement).value = xor.decode((tab.iframe.contentWindow as Window).location.href.split('/service/')[1]) } catch (e) { (win.content.querySelector('.inp') as HTMLInputElement).value = 'about:blank' }
|
|
||||||
(win.content.querySelector('.toggle') as HTMLElement).innerHTML = 'toggle_on'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tab.active = true
|
tab.active = true
|
||||||
|
|
@ -165,55 +167,9 @@ export default class BrowserApp implements App {
|
||||||
|
|
||||||
win.content.querySelector('.inp')?.addEventListener('keydown', (event: KeyboardEvent) => {
|
win.content.querySelector('.inp')?.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
if (tabManager.activeTab.proxy) {
|
tabManager.activeTab.iframe.src = tabManager.activeTab.proxy ? `/service/${xor.encode((win.content.querySelector('.inp') as HTMLInputElement).value) as string}` : (win.content.querySelector('.inp') as HTMLInputElement).value
|
||||||
tabManager.activeTab.iframe.src = `/service/${xor.encode((win.content.querySelector('.inp') as HTMLInputElement).value)}`
|
|
||||||
} else {
|
|
||||||
tabManager.activeTab.iframe.src = (win.content.querySelector('.inp') as HTMLInputElement).value
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
})
|
|
||||||
|
|
||||||
interface XOR {
|
|
||||||
randomMax: number
|
|
||||||
randomMin: number
|
|
||||||
encode: (str: string) => string
|
|
||||||
decode: (str: string) => string
|
|
||||||
}
|
|
||||||
|
|
||||||
const xor: XOR = {
|
|
||||||
randomMax: 100,
|
|
||||||
randomMin: -100,
|
|
||||||
|
|
||||||
encode: (str: string): string => {
|
|
||||||
return encodeURIComponent(
|
|
||||||
str
|
|
||||||
.toString()
|
|
||||||
.split('')
|
|
||||||
.map((char, ind): string => {
|
|
||||||
let indCheck
|
|
||||||
if (ind % 2 === 0) { indCheck = false } else { indCheck = true }
|
|
||||||
|
|
||||||
return indCheck ? String.fromCharCode(char.charCodeAt(0) ^ 2) : char
|
|
||||||
})
|
|
||||||
.join('')
|
|
||||||
)
|
|
||||||
},
|
|
||||||
decode: (str: string): string => {
|
|
||||||
const [input, ...search] = str.split('?')
|
|
||||||
|
|
||||||
return (
|
|
||||||
decodeURIComponent(input)
|
|
||||||
.split('')
|
|
||||||
.map((char, ind): string => {
|
|
||||||
let indCheck
|
|
||||||
if (ind % 2 === 0) { indCheck = false } else { indCheck = true }
|
|
||||||
|
|
||||||
return indCheck ? String.fromCharCode(char.charCodeAt(0) ^ 2) : char
|
|
||||||
})
|
|
||||||
.join('') + ((search.length > 0) ? '?' + search.join('?') : '')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
(win.content.querySelector('button') as HTMLElement).onclick = () => {
|
(win.content.querySelector('button') as HTMLElement).onclick = () => {
|
||||||
tabManager.addTab(new Tab('https://google.com'))
|
tabManager.addTab(new Tab('https://google.com'))
|
||||||
|
|
@ -236,23 +192,19 @@ export default class BrowserApp implements App {
|
||||||
}
|
}
|
||||||
|
|
||||||
win.content.onfullscreenchange = () => {
|
win.content.onfullscreenchange = () => {
|
||||||
if (document.fullscreenElement !== null) {
|
(win.content.querySelector('.fullscreen') as HTMLElement).innerHTML = document.fullscreenElement !== null ? 'fullscreen_exit' : 'fullscreen'
|
||||||
(win.content.querySelector('.fullscreen') as HTMLElement).innerHTML = 'fullscreen_exit'
|
|
||||||
} else {
|
|
||||||
(win.content.querySelector('.fullscreen') as HTMLElement).innerHTML = 'fullscreen'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(win.content.querySelector('.fullscreen') as HTMLElement).onclick = async () => {
|
(win.content.querySelector('.fullscreen') as HTMLElement).onclick = async () => {
|
||||||
if (document.fullscreenElement !== null) {
|
if (document.fullscreenElement === null) {
|
||||||
await document.exitFullscreen().catch(e => console.error)
|
await win.content.requestFullscreen().catch((e: any) => console.error)
|
||||||
} else {
|
} else {
|
||||||
await win.content.requestFullscreen().catch(e => console.error)
|
await document.exitFullscreen().catch(e => console.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tabManager.addTab(new Tab('https://google.com'))
|
tabManager.addTab(new Tab('https://google.com'))
|
||||||
|
|
||||||
return win
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default BrowserApp
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import icon from '../../assets/icons/text-editor.svg'
|
import icon from '../../assets/icons/text-editor.svg'
|
||||||
import { App } from '../../types'
|
|
||||||
|
|
||||||
import { fullEditor } from 'prism-code-editor/setups'
|
import { fullEditor } from 'prism-code-editor/setups'
|
||||||
// this will also import markup, clike, javascript, typescript and jsx
|
// this will also import markup, clike, javascript, typescript and jsx
|
||||||
import 'prism-code-editor/grammars/tsx'
|
import 'prism-code-editor/grammars/tsx'
|
||||||
|
|
@ -8,7 +6,7 @@ import 'prism-code-editor/grammars/css-extras'
|
||||||
import 'prism-code-editor/grammars/markdown'
|
import 'prism-code-editor/grammars/markdown'
|
||||||
import 'prism-code-editor/grammars/python'
|
import 'prism-code-editor/grammars/python'
|
||||||
|
|
||||||
import FlowWindow from '../../structures/FlowWindow'
|
import { Process } from '../../types'
|
||||||
|
|
||||||
interface EditorConfig {
|
interface EditorConfig {
|
||||||
path: string
|
path: string
|
||||||
|
|
@ -34,28 +32,40 @@ const fileLanguageMap: {
|
||||||
py: 'python'
|
py: 'python'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class EditorApp implements App {
|
const Editor: Process = {
|
||||||
meta = {
|
config: {
|
||||||
name: 'Editor',
|
name: 'Editor',
|
||||||
description: 'A simple editor app.',
|
type: 'process',
|
||||||
pkg: 'flow.editor',
|
icon,
|
||||||
version: '1.0.0',
|
targetVer: '1.0.0-indev.0'
|
||||||
icon
|
},
|
||||||
}
|
run: async (process) => {
|
||||||
|
if (Object.keys(process.data).length > 0) {
|
||||||
async open (data?: EditorConfig): Promise<FlowWindow> {
|
const win = await process.loadLibrary('lib/WindowManager').then((wm: any) => {
|
||||||
const win = window.wm.createWindow({
|
return wm.createWindow({
|
||||||
title: this.meta.name,
|
title: 'Editor',
|
||||||
icon: this.meta.icon,
|
icon,
|
||||||
width: 500,
|
width: 350,
|
||||||
height: 400
|
height: 500,
|
||||||
|
canResize: false
|
||||||
|
}, process)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (data != null) {
|
const fs = await process.loadLibrary('lib/VirtualFS')
|
||||||
win.setTitle(`Editor - ${data.path}`)
|
|
||||||
|
|
||||||
|
const data = process.data as EditorConfig
|
||||||
|
|
||||||
|
win.setTitle(`Editor - ${data.path.split('/').at(-1) as string}`)
|
||||||
win.content.style.display = 'flex'
|
win.content.style.display = 'flex'
|
||||||
win.content.style.flexDirection = 'column'
|
win.content.style.flexDirection = 'column'
|
||||||
|
|
||||||
|
if (data == null) {
|
||||||
|
await process.launch('lib/FileManager')
|
||||||
|
setTimeout(() => {
|
||||||
|
win.close()
|
||||||
|
}, 10)
|
||||||
|
} else {
|
||||||
|
const render = async (): Promise<void> => {
|
||||||
win.content.innerHTML = `
|
win.content.innerHTML = `
|
||||||
<div style="padding: 5px;display: flex;align-items: center;gap: 5px;">
|
<div style="padding: 5px;display: flex;align-items: center;gap: 5px;">
|
||||||
<div id="file-open">File</div>
|
<div id="file-open">File</div>
|
||||||
|
|
@ -122,12 +132,12 @@ export default class EditorApp implements App {
|
||||||
el?.classList.toggle('show')
|
el?.classList.toggle('show')
|
||||||
}
|
}
|
||||||
|
|
||||||
fileBtn?.addEventListener('click', (e) => {
|
fileBtn?.addEventListener('click', (e: Event) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
toggleDropdown('file')
|
toggleDropdown('file')
|
||||||
})
|
})
|
||||||
|
|
||||||
editBtn?.addEventListener('click', (e) => {
|
editBtn?.addEventListener('click', (e: Event) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
toggleDropdown('edit')
|
toggleDropdown('edit')
|
||||||
})
|
})
|
||||||
|
|
@ -146,7 +156,7 @@ export default class EditorApp implements App {
|
||||||
const fileExtension = data.path.split('.').pop()?.toLowerCase() as string
|
const fileExtension = data.path.split('.').pop()?.toLowerCase() as string
|
||||||
const language = fileLanguageMap[fileExtension] ?? 'text'
|
const language = fileLanguageMap[fileExtension] ?? 'text'
|
||||||
|
|
||||||
const value = (await window.fs.promises.readFile(data.path)).toString()
|
const value = Buffer.from(await fs.readFile(data.path)).toString()
|
||||||
const editor = fullEditor(
|
const editor = fullEditor(
|
||||||
win.content.querySelector('.editor') as HTMLElement,
|
win.content.querySelector('.editor') as HTMLElement,
|
||||||
{
|
{
|
||||||
|
|
@ -191,15 +201,19 @@ export default class EditorApp implements App {
|
||||||
editor.extensions.searchWidget?.open()
|
editor.extensions.searchWidget?.open()
|
||||||
}
|
}
|
||||||
(win.content.querySelector('#save') as HTMLElement).onclick = async () => {
|
(win.content.querySelector('#save') as HTMLElement).onclick = async () => {
|
||||||
await window.fs.promises.writeFile(data.path, editor.value)
|
await fs.writeFile(data.path, editor.value)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
await window.flow.openApp('flow.files')
|
|
||||||
setTimeout(() => {
|
|
||||||
win.close()
|
|
||||||
}, 10)
|
|
||||||
}
|
}
|
||||||
|
await render()
|
||||||
return win
|
document.addEventListener('fs_update', () => {
|
||||||
|
render().catch(e => console.error(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await process.kill()
|
||||||
|
await process.launch('apps/Files')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Editor
|
||||||
121
src/system/apps/Files.ts
Normal file
121
src/system/apps/Files.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
import icon from '../../assets/icons/file-manager.svg'
|
||||||
|
import { Process } from '../../types'
|
||||||
|
|
||||||
|
const Files: Process = {
|
||||||
|
config: {
|
||||||
|
name: 'Files',
|
||||||
|
type: 'process',
|
||||||
|
icon,
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
run: async (process) => {
|
||||||
|
const win = await process.loadLibrary('lib/WindowManager').then((wm: any) => {
|
||||||
|
return wm.createWindow({
|
||||||
|
title: 'Files',
|
||||||
|
icon,
|
||||||
|
width: 500,
|
||||||
|
height: 400
|
||||||
|
}, process)
|
||||||
|
})
|
||||||
|
|
||||||
|
const fs = await process.loadLibrary('lib/VirtualFS')
|
||||||
|
const MIMETypes = await process.loadLibrary('lib/MIMETypes')
|
||||||
|
|
||||||
|
win.content.style.display = 'flex'
|
||||||
|
win.content.style.flexDirection = 'column'
|
||||||
|
|
||||||
|
let currentDir = '/home'
|
||||||
|
|
||||||
|
async function setDir (dir: string): Promise<void> {
|
||||||
|
currentDir = dir
|
||||||
|
const files: string[] = await fs.readdir(dir)
|
||||||
|
const back = dir === '/' ? '<span class="material-symbols-rounded">first_page</span>' : '<span class="back material-symbols-rounded">chevron_left</span>'
|
||||||
|
|
||||||
|
win.content.innerHTML = `
|
||||||
|
<div style="padding: 5px;display: flex;align-items: center;gap: 5px;">
|
||||||
|
${back}${dir}
|
||||||
|
<div style="flex:1;"></div>
|
||||||
|
<i class='folder material-symbols-rounded' style="font-size: 17.5px;">create_new_folder</i><i class='file material-symbols-rounded' style="font-size: 17.5px;">note_add</i>
|
||||||
|
</div>
|
||||||
|
<div class="files" style="background: var(--base);flex: 1;border-radius: 10px;display: flex;flex-direction: column;"></div>
|
||||||
|
`
|
||||||
|
|
||||||
|
if (back !== '<span class="material-symbols-rounded">first_page</span>') {
|
||||||
|
(win.content.querySelector('.back') as HTMLElement).onclick = async () => {
|
||||||
|
await setDir(dir.split('/').slice(0, -1).join('/'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(win.content.querySelector('.file') as HTMLElement).onclick = async () => {
|
||||||
|
const title = prompt('Enter file name')
|
||||||
|
if (title != null) {
|
||||||
|
await fs.writeFile(`${dir}/${title}`, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(win.content.querySelector('.folder') as HTMLElement).onclick = async () => {
|
||||||
|
const title = prompt('Enter folder name')
|
||||||
|
if (title != null) {
|
||||||
|
await fs.mkdir(`${dir}/${title}`, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const seperator = dir === '/' ? '' : '/'
|
||||||
|
const fileStat = await fs.stat(dir + seperator + file)
|
||||||
|
const element = document.createElement('div')
|
||||||
|
element.setAttribute('style', 'display: flex;gap: 5px;align-items:center;padding: 5px;border-bottom: 1px solid var(--text);display:flex;align-items:center;gap: 5px;')
|
||||||
|
|
||||||
|
const genIcon = (): string => {
|
||||||
|
return `<span class="material-symbols-rounded">${(MIMETypes[file.split('.')[1]] === undefined ? 'draft' : MIMETypes[file.split('.')[1]].icon) as string}</span>`
|
||||||
|
}
|
||||||
|
const icon = fileStat.isDirectory() as boolean ? '<span class="material-symbols-rounded">folder</span>' : genIcon()
|
||||||
|
|
||||||
|
element.innerHTML += `${icon} <span style="flex:1;">${file}</span><span class="material-symbols-rounded delete">delete_forever</span><span class="material-symbols-rounded rename">edit</span>`;
|
||||||
|
(element.querySelector('.rename') as HTMLElement).onclick = async () => {
|
||||||
|
const value = prompt('Rename')
|
||||||
|
if (value != null) {
|
||||||
|
await fs.rename(dir + seperator + file, dir + seperator + value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(element.querySelector('.delete') as HTMLElement).onclick = async () => {
|
||||||
|
if (fileStat.isDirectory() as boolean) {
|
||||||
|
await fs.rmdir(dir + seperator + file)
|
||||||
|
} else {
|
||||||
|
await fs.unlink(dir + seperator + file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const run = async (file: string): Promise<void> => {
|
||||||
|
if (file.split('.').at(-1) === 'lnk') {
|
||||||
|
await fs.readFile(file).then(async (data: Uint8Array) => {
|
||||||
|
await run(Buffer.from(data).toString())
|
||||||
|
})
|
||||||
|
} else if (file.split('.').at(-1) === 'app') {
|
||||||
|
await fs.readFile(file).then(async (data: Uint8Array) => {
|
||||||
|
await process.launch(Buffer.from(data).toString())
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await process.launch(MIMETypes[file.split('.').at(-1) as string] === undefined ? 'apps/Editor' : MIMETypes[file.split('.')[1]].opensWith[0], { path: file })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
element.ondblclick = async () => {
|
||||||
|
if (fileStat.isDirectory() as boolean) {
|
||||||
|
await setDir(dir + seperator + file)
|
||||||
|
} else {
|
||||||
|
await run(dir + seperator + file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
win.content.querySelector('.files')?.appendChild(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await setDir(currentDir)
|
||||||
|
document.addEventListener('fs_update', () => {
|
||||||
|
setDir(currentDir).catch(e => console.error(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Files
|
||||||
53
src/system/apps/ImageViewer.ts
Normal file
53
src/system/apps/ImageViewer.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Process } from '../../types'
|
||||||
|
import icon from '../../assets/icons/org.gnome.Loupe.svg'
|
||||||
|
|
||||||
|
const ImageViewer: Process = {
|
||||||
|
config: {
|
||||||
|
name: 'Image Viewer',
|
||||||
|
type: 'process',
|
||||||
|
icon,
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
run: async (process) => {
|
||||||
|
if (Object.keys(process.data).length > 0) {
|
||||||
|
const win = await process.loadLibrary('lib/WindowManager').then((wm: any) => {
|
||||||
|
return wm.createWindow({
|
||||||
|
title: 'Image Viewer',
|
||||||
|
icon,
|
||||||
|
width: 500,
|
||||||
|
height: 500
|
||||||
|
}, process)
|
||||||
|
})
|
||||||
|
|
||||||
|
const fs = await process.loadLibrary('lib/VirtualFS')
|
||||||
|
const MIMETypes: Record<string, { type: string }> = await process.loadLibrary('lib/MIMETypes')
|
||||||
|
const HTML = await process.loadLibrary('lib/HTML')
|
||||||
|
|
||||||
|
const render = async (): Promise<void> => {
|
||||||
|
win.content.innerHTML = ''
|
||||||
|
const fileData = await fs.readFile(process.data.path)
|
||||||
|
const url = `data:${MIMETypes[(process.data.path.split('.').at(-1) as string)].type};base64,${encodeURIComponent(Buffer.from(fileData).toString('base64'))}`
|
||||||
|
|
||||||
|
new HTML('div').style({
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
background: `url(${url})`,
|
||||||
|
'background-size': 'contain',
|
||||||
|
'background-position': 'center',
|
||||||
|
'background-repeat': 'no-repeat',
|
||||||
|
'aspect-ratio': '1 / 1'
|
||||||
|
}).appendTo(win.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
await render()
|
||||||
|
document.addEventListener('fs_update', () => {
|
||||||
|
render().catch(e => console.error(e))
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await process.kill()
|
||||||
|
await process.launch('apps/Files')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageViewer
|
||||||
67
src/system/apps/Info.ts
Normal file
67
src/system/apps/Info.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import icon from '../../assets/icons/userinfo.svg'
|
||||||
|
import badge from '../../assets/badge.png'
|
||||||
|
|
||||||
|
import HTML from '../../HTML'
|
||||||
|
import { Process } from '../../types'
|
||||||
|
|
||||||
|
const Info: Process = {
|
||||||
|
config: {
|
||||||
|
name: 'Info',
|
||||||
|
type: 'process',
|
||||||
|
icon,
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
run: async (process) => {
|
||||||
|
const win = await process.loadLibrary('lib/WindowManager').then((wm: any) => {
|
||||||
|
return wm.createWindow({
|
||||||
|
title: 'Info',
|
||||||
|
icon,
|
||||||
|
width: 300,
|
||||||
|
height: 400,
|
||||||
|
canResize: false
|
||||||
|
}, process)
|
||||||
|
})
|
||||||
|
|
||||||
|
win.content.style.padding = '10px'
|
||||||
|
win.content.style.textAlign = 'center'
|
||||||
|
win.content.style.display = 'flex'
|
||||||
|
win.content.style.flexDirection = 'column'
|
||||||
|
win.content.style.justifyContent = 'center'
|
||||||
|
win.content.style.alignItems = 'center'
|
||||||
|
win.content.style.background = 'var(--base)'
|
||||||
|
|
||||||
|
new HTML('div').appendTo(win.content)
|
||||||
|
.appendMany(
|
||||||
|
new HTML('h1').style({
|
||||||
|
margin: '0'
|
||||||
|
}).text('FlowOS')
|
||||||
|
.append(new HTML('sup').text(`${process.sysInfo.codename}`).style({
|
||||||
|
'font-size': '0.5em'
|
||||||
|
})),
|
||||||
|
new HTML('p').style({
|
||||||
|
margin: '0'
|
||||||
|
}).text(`v${String(process.sysInfo.version)}`),
|
||||||
|
new HTML('br'),
|
||||||
|
new HTML('a').attr({
|
||||||
|
href: ''
|
||||||
|
}).append(
|
||||||
|
new HTML('img').attr({
|
||||||
|
src: badge,
|
||||||
|
height: '50'
|
||||||
|
})
|
||||||
|
),
|
||||||
|
new HTML('br'),
|
||||||
|
new HTML('a').text('Discord').attr({
|
||||||
|
href: 'https://discord.gg/86F8dK9vfn',
|
||||||
|
class: 'discord'
|
||||||
|
}),
|
||||||
|
new HTML('span').text(' - '),
|
||||||
|
new HTML('a').text('Github').attr({
|
||||||
|
href: 'https://github.com/Flow-Works/FlowOS',
|
||||||
|
class: 'github'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Info
|
||||||
53
src/system/apps/Manager.ts
Normal file
53
src/system/apps/Manager.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import icon from '../../assets/icons/software-properties.svg'
|
||||||
|
import nullIcon from '../../assets/icons/application-executable.svg'
|
||||||
|
import libraryIcon from '../../assets/icons/icon-library.svg'
|
||||||
|
import { Process } from '../../types'
|
||||||
|
|
||||||
|
const Manager: Process = {
|
||||||
|
config: {
|
||||||
|
name: 'Manager',
|
||||||
|
type: 'process',
|
||||||
|
icon,
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
run: async (process) => {
|
||||||
|
const win = await process.loadLibrary('lib/WindowManager').then((wm: any) => {
|
||||||
|
return wm.createWindow({
|
||||||
|
title: 'Manager',
|
||||||
|
icon,
|
||||||
|
width: 350,
|
||||||
|
height: 500,
|
||||||
|
canResize: false
|
||||||
|
}, process)
|
||||||
|
})
|
||||||
|
|
||||||
|
const HTML = await process.loadLibrary('lib/HTML')
|
||||||
|
|
||||||
|
win.content.style.display = 'flex'
|
||||||
|
win.content.style.flexDirection = 'column'
|
||||||
|
win.content.style.gap = '10px'
|
||||||
|
win.content.style.padding = '10px'
|
||||||
|
win.content.style.background = 'var(--base)'
|
||||||
|
|
||||||
|
for (const pkg in process.kernel.packageList) {
|
||||||
|
const container = new HTML('div').style({
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
padding: '10px',
|
||||||
|
background: 'var(--surface-0)',
|
||||||
|
borderRadius: '10px'
|
||||||
|
}).appendTo(win.content)
|
||||||
|
new HTML('img').attr({
|
||||||
|
src: process.kernel.packageList[pkg].executable.config.icon ?? (process.kernel.packageList[pkg].executable.config.type === 'library' ? libraryIcon : nullIcon),
|
||||||
|
style: 'border-radius: 40%;aspect-ratio: 1 / 1;height: 50px;'
|
||||||
|
}).appendTo(container)
|
||||||
|
const div = new HTML('div').appendTo(container)
|
||||||
|
new HTML('h3').style({
|
||||||
|
margin: '0'
|
||||||
|
}).text(process.kernel.packageList[pkg].executable.config.name).appendTo(div)
|
||||||
|
new HTML('code').text(process.kernel.packageList[pkg].executable.config.type as string).appendTo(div)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Manager
|
||||||
78
src/system/apps/Settings.ts
Normal file
78
src/system/apps/Settings.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { Process } from '../../types'
|
||||||
|
import icon from '../../assets/icons/preferences-system.svg'
|
||||||
|
import { stringify } from 'js-ini'
|
||||||
|
|
||||||
|
const Settings: Process = {
|
||||||
|
config: {
|
||||||
|
name: 'Settings',
|
||||||
|
type: 'process',
|
||||||
|
icon,
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
run: async process => {
|
||||||
|
const win = await process
|
||||||
|
.loadLibrary('lib/WindowManager')
|
||||||
|
.then((wm: any) => {
|
||||||
|
return wm.createWindow(
|
||||||
|
{
|
||||||
|
title: 'Settings',
|
||||||
|
icon,
|
||||||
|
width: 500,
|
||||||
|
height: 500
|
||||||
|
},
|
||||||
|
process
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const fs = await process.loadLibrary('lib/VirtualFS')
|
||||||
|
const HTML = await process.loadLibrary('lib/HTML')
|
||||||
|
|
||||||
|
const { Input, Button } = await process.loadLibrary('lib/Components')
|
||||||
|
|
||||||
|
const render = async (config: any): Promise<void> => {
|
||||||
|
win.content.innerHTML = ''
|
||||||
|
for (const item in config) {
|
||||||
|
const input = Input.new().attr({
|
||||||
|
value: config[item]
|
||||||
|
})
|
||||||
|
new HTML('div')
|
||||||
|
.appendMany(
|
||||||
|
new HTML('label')
|
||||||
|
.style({
|
||||||
|
'text-transform': 'capitalize'
|
||||||
|
})
|
||||||
|
.text(`${item.toLowerCase().replaceAll('_', ' ')}:`),
|
||||||
|
new HTML('br'),
|
||||||
|
new HTML('div')
|
||||||
|
.style({
|
||||||
|
display: 'flex',
|
||||||
|
gap: '5px'
|
||||||
|
})
|
||||||
|
.appendMany(
|
||||||
|
input,
|
||||||
|
Button.new().text('Save').on('click', async () => {
|
||||||
|
config[item] = input.getValue()
|
||||||
|
process.kernel.setConfig(config)
|
||||||
|
await fs.writeFile('/etc/flow', stringify(config))
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent('config_update', {
|
||||||
|
detail: {
|
||||||
|
config
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.appendTo(win.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await render(process.kernel.config)
|
||||||
|
document.addEventListener('config_update', (e: CustomEvent) => {
|
||||||
|
render(e.detail.config).catch(e => console.error(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Settings
|
||||||
100
src/system/apps/Store.ts
Normal file
100
src/system/apps/Store.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
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 = {
|
||||||
|
config: {
|
||||||
|
name: 'Store',
|
||||||
|
type: 'process',
|
||||||
|
icon,
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
run: async (process) => {
|
||||||
|
const win = await process.loadLibrary('lib/WindowManager').then((wm: any) => {
|
||||||
|
return wm.createWindow({
|
||||||
|
title: 'Store',
|
||||||
|
icon,
|
||||||
|
width: 500,
|
||||||
|
height: 700
|
||||||
|
}, process)
|
||||||
|
})
|
||||||
|
|
||||||
|
const fs = await process.loadLibrary('lib/VirtualFS')
|
||||||
|
|
||||||
|
win.content.style.background = 'var(--base)'
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<div class="repos" style="display: flex;flex-direction: column;gap: 10px;"></div>
|
||||||
|
`
|
||||||
|
|
||||||
|
repos.forEach((repo) => {
|
||||||
|
(win.content.querySelector('.repos') as HTMLElement).innerHTML += `
|
||||||
|
<div data-repo-id="${sanitize(repo.id)}" style="display: flex;flex-direction: column;gap: 10px;background: var(--surface-0);padding: 20px;margin: 10px;border-radius: 10px;">
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<h2 style="margin: 0;margin-bottom: 10px;">${sanitize(repo.name)}</h2>
|
||||||
|
<code style="font-family: monospace;">${sanitize(repo.id)}</code>
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
<div class="apps"></div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
repo.apps.forEach((app) => {
|
||||||
|
(win.content.querySelector(`div[data-repo-id="${sanitize(repo.id)}"] > .apps`) as HTMLElement).innerHTML += `
|
||||||
|
<div data-pkg="${sanitize(app.name)}" style="display: flex;gap: 20px;">
|
||||||
|
<img src="${sanitize(app.icon ?? nullIcon)}" height="59.5px" style="border-radius: var(--app-radius);">
|
||||||
|
<div>
|
||||||
|
<h3 style="margin: 0;margin-bottom: 10px;">${sanitize(app.name)}</h3>
|
||||||
|
<div style="display: flex;gap:5px;align-items: center;">
|
||||||
|
<code style="font-family: monospace;">${sanitize(app.targetVer)}</code>
|
||||||
|
<span class="material-symbols-rounded">download</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function install (url: string): void {
|
||||||
|
fetch(url).then(async (res) => await res.text())
|
||||||
|
.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)
|
||||||
|
}).catch(e => console.error(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Store
|
||||||
91
src/system/apps/TaskManager.ts
Normal file
91
src/system/apps/TaskManager.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { Process } from '../../types'
|
||||||
|
import icon from '../../assets/icons/utilities-system-monitor.svg'
|
||||||
|
|
||||||
|
const TaskManager: Process = {
|
||||||
|
config: {
|
||||||
|
name: 'Task Manager',
|
||||||
|
type: 'process',
|
||||||
|
icon,
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
run: async (process) => {
|
||||||
|
const win = await process.loadLibrary('lib/WindowManager').then((wm: any) => {
|
||||||
|
return wm.createWindow({
|
||||||
|
title: 'Task Manager',
|
||||||
|
icon,
|
||||||
|
width: 600,
|
||||||
|
height: 200
|
||||||
|
}, process)
|
||||||
|
})
|
||||||
|
|
||||||
|
const HTML = await process.loadLibrary('lib/HTML')
|
||||||
|
|
||||||
|
win.content.style.display = 'flex'
|
||||||
|
win.content.style.flexDirection = 'column'
|
||||||
|
win.content.style.gap = '10px'
|
||||||
|
win.content.style.padding = '10px'
|
||||||
|
win.content.style.background = 'var(--base)'
|
||||||
|
|
||||||
|
new HTML('style').html(
|
||||||
|
`tbody tr:hover {
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:first-child td:first-child { border-top-left-radius: 10px; }
|
||||||
|
tr:first-child td:last-child { border-top-right-radius: 10px; }
|
||||||
|
|
||||||
|
tr:last-child td:first-child { border-bottom-left-radius: 10px; }
|
||||||
|
tr:last-child td:last-child { border-bottom-right-radius: 10px; }
|
||||||
|
|
||||||
|
table, table td, table th {
|
||||||
|
border: none!important;
|
||||||
|
border-collapse:collapse;
|
||||||
|
}`
|
||||||
|
).appendTo(win.content)
|
||||||
|
|
||||||
|
const table = new HTML('table').style({
|
||||||
|
width: '100%'
|
||||||
|
}).appendTo(win.content)
|
||||||
|
|
||||||
|
const render = (): void => {
|
||||||
|
const { processList } = process.kernel
|
||||||
|
table.html('')
|
||||||
|
|
||||||
|
new HTML('thead').appendTo(table)
|
||||||
|
.append(
|
||||||
|
new HTML('tr').style({
|
||||||
|
padding: '5px',
|
||||||
|
'border-radius': '10px'
|
||||||
|
}).appendMany(
|
||||||
|
new HTML('th').style({ 'text-align': 'center', width: '10%' }).text('PID'),
|
||||||
|
new HTML('th').style({ 'text-align': 'left', width: '45%' }).text('Process Name'),
|
||||||
|
new HTML('th').style({ 'text-align': 'left', width: '45%' }).text('Session Token')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const tbody = new HTML('tbody').appendTo(table)
|
||||||
|
|
||||||
|
for (const proc of processList) {
|
||||||
|
new HTML('tr').style({
|
||||||
|
padding: '5px',
|
||||||
|
'border-radius': '10px'
|
||||||
|
}).appendTo(tbody)
|
||||||
|
.appendMany(
|
||||||
|
new HTML('td').style({ 'text-align': 'center' }).text(proc.pid.toString()),
|
||||||
|
new HTML('td').style({ 'text-align': 'left' }).text(proc.name),
|
||||||
|
new HTML('td').style({ 'text-align': 'left' }).text(proc.token)
|
||||||
|
)
|
||||||
|
.on('click', () => {
|
||||||
|
process.killProcess(proc.pid).catch((e: any) => console.error(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
|
||||||
|
document.addEventListener('update_process', () => render())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TaskManager
|
||||||
44
src/system/lib/Components.ts
Normal file
44
src/system/lib/Components.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import LibraryLib from '../../structures/LibraryLib'
|
||||||
|
import { Library } from '../../types'
|
||||||
|
|
||||||
|
let library: LibraryLib
|
||||||
|
|
||||||
|
const Components: Library = {
|
||||||
|
config: {
|
||||||
|
name: 'Components',
|
||||||
|
type: 'library',
|
||||||
|
targetVer: '0.0.1'
|
||||||
|
},
|
||||||
|
init: (l, k, p) => { library = l },
|
||||||
|
data: {
|
||||||
|
Input: {
|
||||||
|
new: () => {
|
||||||
|
const { HTML } = library
|
||||||
|
const input = new HTML('input')
|
||||||
|
input.style({
|
||||||
|
'border-radius': '5px',
|
||||||
|
padding: '2.5px',
|
||||||
|
outline: 'none',
|
||||||
|
background: 'transparent',
|
||||||
|
border: '1px solid var(--surface-0)'
|
||||||
|
})
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Button: {
|
||||||
|
new: () => {
|
||||||
|
const { HTML } = library
|
||||||
|
const button = new HTML('button')
|
||||||
|
button.style({
|
||||||
|
'border-radius': '5px',
|
||||||
|
padding: '2.5px',
|
||||||
|
background: 'transparent',
|
||||||
|
border: '1px solid var(--surface-0)'
|
||||||
|
})
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Components
|
||||||
14
src/system/lib/HTML.ts
Normal file
14
src/system/lib/HTML.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import HTMLClass from '../../HTML'
|
||||||
|
import { Library } from '../../types'
|
||||||
|
|
||||||
|
const HTML: Library = {
|
||||||
|
config: {
|
||||||
|
name: 'HTML',
|
||||||
|
type: 'library',
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
init: (l, k, p) => {},
|
||||||
|
data: HTMLClass
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HTML
|
||||||
40
src/system/lib/Launcher.ts
Normal file
40
src/system/lib/Launcher.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { Library } from '../../types'
|
||||||
|
|
||||||
|
let isLauncherOpen = false
|
||||||
|
|
||||||
|
const Launcher: Library = {
|
||||||
|
config: {
|
||||||
|
name: 'Launcher',
|
||||||
|
type: 'library',
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
init: (l, k) => {
|
||||||
|
Launcher.data.element = new l.HTML('launcher').style({
|
||||||
|
opacity: '0',
|
||||||
|
'backdrop-filter': 'blur(0px)',
|
||||||
|
'pointer-events': 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
toggle: () => {
|
||||||
|
if (isLauncherOpen) {
|
||||||
|
Launcher.data.element.style({
|
||||||
|
opacity: '0',
|
||||||
|
'backdrop-filter': 'blur(0px)',
|
||||||
|
'pointer-events': 'none'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Launcher.data.element.style({
|
||||||
|
opacity: '1',
|
||||||
|
'backdrop-filter': 'blur(20px)',
|
||||||
|
'pointer-events': 'all'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
isLauncherOpen = !isLauncherOpen
|
||||||
|
return isLauncherOpen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Launcher
|
||||||
86
src/system/lib/MIMETypes.ts
Normal file
86
src/system/lib/MIMETypes.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { Library } from '../../types'
|
||||||
|
|
||||||
|
const MIME: Library = {
|
||||||
|
config: {
|
||||||
|
name: 'MIMETypes',
|
||||||
|
type: 'library',
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
init: (l, k, p) => {},
|
||||||
|
data: {
|
||||||
|
aac: {
|
||||||
|
type: 'audio/aac',
|
||||||
|
description: 'AAC Audio',
|
||||||
|
opensWith: ['apps/MusicPlayer'],
|
||||||
|
icon: 'audio_file'
|
||||||
|
},
|
||||||
|
abw: {
|
||||||
|
type: 'application/x-abiword',
|
||||||
|
description: 'AbiWord Document',
|
||||||
|
opensWith: ['apps/Editor'],
|
||||||
|
icon: 'description'
|
||||||
|
},
|
||||||
|
apng: {
|
||||||
|
type: 'image/apng',
|
||||||
|
description: 'Animated PNG File',
|
||||||
|
opensWith: ['apps/ImageViewer'],
|
||||||
|
icon: 'image'
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
type: 'application/x-flow-executable',
|
||||||
|
description: 'FlowOS Application',
|
||||||
|
opensWith: [],
|
||||||
|
icon: 'deployed_code'
|
||||||
|
},
|
||||||
|
/* arc: {
|
||||||
|
type: 'application/x-freearc',
|
||||||
|
description: 'Archive Document',
|
||||||
|
opensWith: ['apps/ArchiveViewer'],
|
||||||
|
icon: 'archive'
|
||||||
|
}, */
|
||||||
|
avif: {
|
||||||
|
type: 'image/avif',
|
||||||
|
description: 'AVIF Image',
|
||||||
|
opensWith: ['apps/ImageViewer'],
|
||||||
|
icon: 'image'
|
||||||
|
},
|
||||||
|
lnk: {
|
||||||
|
type: 'application/x-ms-shortcut',
|
||||||
|
description: 'Windows Shortcut',
|
||||||
|
opensWith: [],
|
||||||
|
icon: 'file_present'
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
type: 'text/markdown',
|
||||||
|
description: 'Markdown Document',
|
||||||
|
opensWith: ['apps/Editor'],
|
||||||
|
icon: 'markdown'
|
||||||
|
},
|
||||||
|
mp4: {
|
||||||
|
type: 'video/mp4',
|
||||||
|
description: 'MP4 Video',
|
||||||
|
opensWith: ['apps/VideoPlayer'],
|
||||||
|
icon: 'video_file'
|
||||||
|
},
|
||||||
|
png: {
|
||||||
|
type: 'image/png',
|
||||||
|
description: 'PNG File',
|
||||||
|
opensWith: ['apps/ImageViewer'],
|
||||||
|
icon: 'image'
|
||||||
|
},
|
||||||
|
svg: {
|
||||||
|
type: 'image/svg+xml',
|
||||||
|
description: 'SVG File',
|
||||||
|
opensWith: ['apps/ImageViewer'],
|
||||||
|
icon: 'image'
|
||||||
|
},
|
||||||
|
txt: {
|
||||||
|
type: 'text/plain',
|
||||||
|
description: 'Text Document',
|
||||||
|
opensWith: ['apps/Editor'],
|
||||||
|
icon: 'description'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MIME
|
||||||
48
src/system/lib/SplashScreen.ts
Normal file
48
src/system/lib/SplashScreen.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import Kernel from '../../kernel'
|
||||||
|
|
||||||
|
import FlowLogo from '../../assets/flow.png'
|
||||||
|
import { Library } from '../../types'
|
||||||
|
import LibraryLib from '../../structures/LibraryLib'
|
||||||
|
|
||||||
|
let library: LibraryLib
|
||||||
|
let kernel: Kernel
|
||||||
|
|
||||||
|
const SplashScreen: Library = {
|
||||||
|
config: {
|
||||||
|
name: 'SplashScreen',
|
||||||
|
type: 'library',
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
init: (l, k, p) => {
|
||||||
|
library = l
|
||||||
|
kernel = k
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
getElement: () => {
|
||||||
|
const { HTML } = library
|
||||||
|
const div = new HTML('div').style({
|
||||||
|
position: 'absolute',
|
||||||
|
top: '0',
|
||||||
|
left: '0',
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
'flex-direction': 'column',
|
||||||
|
'align-items': 'center',
|
||||||
|
'justify-content': 'center',
|
||||||
|
'z-index': '1000000'
|
||||||
|
})
|
||||||
|
new HTML('img').attr({
|
||||||
|
src: FlowLogo,
|
||||||
|
width: 128
|
||||||
|
}).appendTo(div)
|
||||||
|
const h1 = new HTML('h1').style({ margin: '0' }).text('FlowOS').appendTo(div)
|
||||||
|
new HTML('sup').style({ 'font-size': '0.5em' }).text(kernel.codename).appendTo(h1)
|
||||||
|
new HTML('p').style({ margin: '0' }).text('loading...').appendTo(div)
|
||||||
|
|
||||||
|
return div
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SplashScreen
|
||||||
75
src/system/lib/StatusBar.ts
Normal file
75
src/system/lib/StatusBar.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { Library } from '../../types'
|
||||||
|
|
||||||
|
const StatusBar: Library = {
|
||||||
|
config: {
|
||||||
|
name: 'StatusBar',
|
||||||
|
type: 'library',
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
init: (l, k, p) => {
|
||||||
|
StatusBar.data.element = new l.HTML('toolbar')
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
updateBatteryIcon (battery: any) {
|
||||||
|
let iconHTML = ''
|
||||||
|
|
||||||
|
if (battery.charging === true) {
|
||||||
|
if (battery.level === 1) {
|
||||||
|
iconHTML = 'battery_charging_full'
|
||||||
|
} else if (battery.level >= 0.9) {
|
||||||
|
iconHTML = 'battery_charging_90'
|
||||||
|
} else if (battery.level >= 0.8) {
|
||||||
|
iconHTML = 'battery_charging_80'
|
||||||
|
} else if (battery.level >= 0.6) {
|
||||||
|
iconHTML = 'battery_charging_60'
|
||||||
|
} else if (battery.level >= 0.5) {
|
||||||
|
iconHTML = 'battery_charging_50'
|
||||||
|
} else if (battery.level >= 0.3) {
|
||||||
|
iconHTML = 'battery_charging_30'
|
||||||
|
} else if (battery.level >= 0) {
|
||||||
|
iconHTML = 'battery_charging_20'
|
||||||
|
}
|
||||||
|
} else if (battery.level === 1) {
|
||||||
|
iconHTML = 'battery_full'
|
||||||
|
} else if (battery.level >= 0.6) {
|
||||||
|
iconHTML = 'battery_6_bar'
|
||||||
|
} else if (battery.level >= 0.5) {
|
||||||
|
iconHTML = 'battery_5_bar'
|
||||||
|
} else if (battery.level >= 0.4) {
|
||||||
|
iconHTML = 'battery_4_bar'
|
||||||
|
} else if (battery.level >= 0.3) {
|
||||||
|
iconHTML = 'battery_3_bar'
|
||||||
|
} else if (battery.level >= 0.2) {
|
||||||
|
iconHTML = 'battery_2_bar'
|
||||||
|
} else if (battery.level >= 0.1) {
|
||||||
|
iconHTML = 'battery_1_bar'
|
||||||
|
} else if (battery.level >= 0) {
|
||||||
|
iconHTML = 'battery_0_bar'
|
||||||
|
}
|
||||||
|
|
||||||
|
const batteryDiv = document.querySelector('div[data-toolbar-id="controls"] > .battery')
|
||||||
|
if (batteryDiv != null) {
|
||||||
|
batteryDiv.innerHTML = iconHTML
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateIcon (ms: number) {
|
||||||
|
let icon = ''
|
||||||
|
|
||||||
|
if (ms >= 200 && ms < 400) {
|
||||||
|
icon = 'signal_cellular_1_bar'
|
||||||
|
} else if (ms >= 400 && ms < 600) {
|
||||||
|
icon = 'signal_cellular_2_bar'
|
||||||
|
} else if (ms >= 600 && ms < 800) {
|
||||||
|
icon = 'signal_cellular_3_bar'
|
||||||
|
} else if (ms >= 800) {
|
||||||
|
icon = 'signal_cellular_4_bar'
|
||||||
|
} else {
|
||||||
|
icon = 'signal_cellular_0_bar'
|
||||||
|
}
|
||||||
|
|
||||||
|
(document.querySelector('div[data-toolbar-id="controls"] > .signal') as HTMLElement).innerHTML = icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StatusBar
|
||||||
492
src/system/lib/VirtualFS.ts
Normal file
492
src/system/lib/VirtualFS.ts
Normal file
|
|
@ -0,0 +1,492 @@
|
||||||
|
import Kernel from '../../kernel'
|
||||||
|
import ProcessLib from '../../structures/ProcessLib'
|
||||||
|
import { Library } from '../../types'
|
||||||
|
|
||||||
|
console.debug = (...args: any[]) => {
|
||||||
|
console.log('[VirtualFS]', ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Errors {
|
||||||
|
ENOENT = 'ENOENT',
|
||||||
|
EISDIR = 'EISDIR',
|
||||||
|
EEXIST = 'EEXIST',
|
||||||
|
EPERM = 'EPERM',
|
||||||
|
ENOTDIR = 'ENOTDIR',
|
||||||
|
EACCES = 'EACCES'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Permission {
|
||||||
|
USER,
|
||||||
|
ELEVATED,
|
||||||
|
SYSTEM
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Directory {
|
||||||
|
type: 'directory'
|
||||||
|
permission: Permission
|
||||||
|
deleteable: boolean
|
||||||
|
children: {
|
||||||
|
[key: string]: Directory | File
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface File {
|
||||||
|
type: 'file'
|
||||||
|
permission: Permission
|
||||||
|
deleteable: boolean
|
||||||
|
content: Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultFS: { root: Directory } = {
|
||||||
|
root: {
|
||||||
|
type: 'directory',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.SYSTEM,
|
||||||
|
children: {
|
||||||
|
home: {
|
||||||
|
type: 'directory',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.SYSTEM,
|
||||||
|
children: {
|
||||||
|
Downloads: {
|
||||||
|
type: 'directory',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.USER,
|
||||||
|
children: {}
|
||||||
|
},
|
||||||
|
Applications: {
|
||||||
|
type: 'directory',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.USER,
|
||||||
|
children: {
|
||||||
|
'Info.app': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('apps/Info')
|
||||||
|
},
|
||||||
|
'Manager.app': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('apps/Manager')
|
||||||
|
},
|
||||||
|
'Store.app': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('apps/Store')
|
||||||
|
},
|
||||||
|
'TaskManager.app': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('apps/TaskManager')
|
||||||
|
},
|
||||||
|
'Browser.app': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('apps/Browser')
|
||||||
|
},
|
||||||
|
'ImageViewer.app': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('apps/ImageViewer')
|
||||||
|
},
|
||||||
|
'Files.app': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('apps/Files')
|
||||||
|
},
|
||||||
|
'Editor.app': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('apps/Editor')
|
||||||
|
},
|
||||||
|
'Settings.app': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('apps/Settings')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Desktop: {
|
||||||
|
type: 'directory',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.USER,
|
||||||
|
children: {
|
||||||
|
'README.md': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('# Welcome to FlowOS!')
|
||||||
|
},
|
||||||
|
'Info.lnk': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('/home/Applications/Info.app')
|
||||||
|
},
|
||||||
|
'Manager.lnk': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('/home/Applications/Manager.app')
|
||||||
|
},
|
||||||
|
'Store.lnk': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('/home/Applications/Store.app')
|
||||||
|
},
|
||||||
|
'TaskManager.lnk': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('/home/Applications/TaskManager.app')
|
||||||
|
},
|
||||||
|
'Browser.lnk': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('/home/Applications/Browser.app')
|
||||||
|
},
|
||||||
|
'ImageViewer.lnk': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('/home/Applications/ImageViewer.app')
|
||||||
|
},
|
||||||
|
'Files.lnk': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('/home/Applications/Files.app')
|
||||||
|
},
|
||||||
|
'Editor.lnk': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('/home/Applications/Editor.app')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Pictures: {
|
||||||
|
type: 'directory',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.USER,
|
||||||
|
children: {}
|
||||||
|
},
|
||||||
|
Videos: {
|
||||||
|
type: 'directory',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.USER,
|
||||||
|
children: {}
|
||||||
|
},
|
||||||
|
Documents: {
|
||||||
|
type: 'directory',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.USER,
|
||||||
|
children: {}
|
||||||
|
},
|
||||||
|
Music: {
|
||||||
|
type: 'directory',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.USER,
|
||||||
|
children: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
var: {
|
||||||
|
type: 'directory',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.SYSTEM,
|
||||||
|
children: {}
|
||||||
|
},
|
||||||
|
etc: {
|
||||||
|
type: 'directory',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.SYSTEM,
|
||||||
|
children: {
|
||||||
|
flow: {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.ELEVATED,
|
||||||
|
content: Buffer.from([
|
||||||
|
'SERVER=https://server.flow-works.me',
|
||||||
|
'24HOUR=FALSE'
|
||||||
|
].join('\n'))
|
||||||
|
},
|
||||||
|
hostname: {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.ELEVATED,
|
||||||
|
content: Buffer.from('flow')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
opt: {
|
||||||
|
type: 'directory',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.SYSTEM,
|
||||||
|
children: {
|
||||||
|
apps: {
|
||||||
|
type: 'directory',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.SYSTEM,
|
||||||
|
children: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setFileSystem = async (fileSystemObject: { root: Directory }): Promise<void> => {
|
||||||
|
fileSystem = fileSystemObject
|
||||||
|
}
|
||||||
|
|
||||||
|
export let db: IDBDatabase
|
||||||
|
let fileSystem: { root: Directory }
|
||||||
|
let kernel: Kernel
|
||||||
|
let process: ProcessLib
|
||||||
|
|
||||||
|
export const initializeDatabase = async (dbName: string): Promise<boolean> => {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const request = window.indexedDB.open(dbName)
|
||||||
|
|
||||||
|
request.onupgradeneeded = (event: Event) => {
|
||||||
|
const target = event.target as IDBRequest
|
||||||
|
const db = target.result
|
||||||
|
db.createObjectStore('fs')
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onerror = (event: Event) => {
|
||||||
|
reject(new Error('[VirtualFS] Error opening database.'))
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
db = request.result
|
||||||
|
resolve(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const read = async (): Promise<any> => {
|
||||||
|
const transaction = db.transaction(['fs'], 'readonly')
|
||||||
|
const store = transaction.objectStore('fs')
|
||||||
|
const getRequest = store.get('fs')
|
||||||
|
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
getRequest.onsuccess = () => {
|
||||||
|
resolve(getRequest.result)
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequest.onerror = () => {
|
||||||
|
reject(getRequest.error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const write = async (fileSystemObject: { root: Directory }): Promise<void> => {
|
||||||
|
fileSystem = fileSystemObject
|
||||||
|
await save()
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = async (): Promise<void> => {
|
||||||
|
const transaction = db.transaction(['fs'], 'readwrite')
|
||||||
|
const store = transaction.objectStore('fs')
|
||||||
|
const putRequest = store.put(fileSystem, 'fs')
|
||||||
|
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
putRequest.onsuccess = () => {
|
||||||
|
document.dispatchEvent(new CustomEvent('fs_update', {}))
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
putRequest.onerror = () => {
|
||||||
|
reject(putRequest.error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePermissions = async (path: string): Promise<void> => {
|
||||||
|
let { current } = (await navigatePath(path))
|
||||||
|
|
||||||
|
if (current === undefined) current = (await navigatePathParent(path)).current
|
||||||
|
|
||||||
|
if (current.permission === Permission.USER && current.permission > process.permission) {
|
||||||
|
const uac = await kernel.startExecutable('UserAccessControl', Permission.SYSTEM, { type: 'fs', process, path })
|
||||||
|
if (uac.value === false) {
|
||||||
|
throw new Error(Errors.EACCES)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current.permission === Permission.ELEVATED && current.permission > process.permission) {
|
||||||
|
const uac = await kernel.startExecutable('UserAccessControl', Permission.SYSTEM, { type: 'fs', process, path })
|
||||||
|
if (uac.value === false) {
|
||||||
|
throw new Error(Errors.EACCES)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current.permission === Permission.SYSTEM && current.permission > process.permission) throw new Error(Errors.EPERM)
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigatePath = async (path: string): Promise<{ current: Directory | File, parts: string[] }> => {
|
||||||
|
const parts = path.split('/').filter(x => x !== '')
|
||||||
|
let current = fileSystem.root
|
||||||
|
for (const part of parts) {
|
||||||
|
current = current.children[part] as Directory
|
||||||
|
}
|
||||||
|
return { current, parts }
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigatePathParent = async (path: string): Promise<{ current: Directory, parts: string[], filename: string }> => {
|
||||||
|
const parts = path.split('/').filter(x => x !== '')
|
||||||
|
const filename = parts.pop() as string
|
||||||
|
let current = fileSystem.root
|
||||||
|
for (const part of parts) {
|
||||||
|
current = current.children[part] as Directory
|
||||||
|
}
|
||||||
|
return { current, parts, filename }
|
||||||
|
}
|
||||||
|
|
||||||
|
const VirtualFS: Library = {
|
||||||
|
config: {
|
||||||
|
name: 'VirtualFS',
|
||||||
|
type: 'library',
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
init: (l, k, p) => {
|
||||||
|
kernel = k
|
||||||
|
process = p
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
unlink: async (path: string): Promise<void> => {
|
||||||
|
const { current, filename } = await navigatePathParent(path)
|
||||||
|
|
||||||
|
if (!current.children[filename].deleteable) throw new Error(Errors.EPERM)
|
||||||
|
await handlePermissions(path)
|
||||||
|
|
||||||
|
Reflect.deleteProperty(current.children, filename)
|
||||||
|
|
||||||
|
console.debug(`unlink ${path}`)
|
||||||
|
await save()
|
||||||
|
},
|
||||||
|
readFile: async (path: string): Promise<Buffer> => {
|
||||||
|
const { current } = await navigatePath(path)
|
||||||
|
|
||||||
|
await handlePermissions(path)
|
||||||
|
|
||||||
|
if (current.type !== 'file') throw new Error(Errors.EISDIR)
|
||||||
|
|
||||||
|
console.debug(`read ${path}`)
|
||||||
|
return current.content
|
||||||
|
},
|
||||||
|
writeFile: async (path: string, content: string | Buffer): Promise<void> => {
|
||||||
|
const { current, filename } = await navigatePathParent(path)
|
||||||
|
|
||||||
|
let permission
|
||||||
|
|
||||||
|
if (typeof current.children[filename] === 'undefined') {
|
||||||
|
permission = Permission.USER
|
||||||
|
} else {
|
||||||
|
await handlePermissions(path)
|
||||||
|
permission = current.children[filename].permission
|
||||||
|
}
|
||||||
|
|
||||||
|
current.children[filename] = {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission,
|
||||||
|
content: Buffer.from(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(`write ${path}`)
|
||||||
|
await save()
|
||||||
|
},
|
||||||
|
mkdir: async (path: string): Promise<void> => {
|
||||||
|
const { current, filename } = await navigatePathParent(path)
|
||||||
|
|
||||||
|
let permission
|
||||||
|
|
||||||
|
if (typeof current.children[filename] === 'undefined') {
|
||||||
|
permission = Permission.USER
|
||||||
|
} else {
|
||||||
|
await handlePermissions(path)
|
||||||
|
permission = current.children[filename].permission
|
||||||
|
}
|
||||||
|
|
||||||
|
current.children[filename] = {
|
||||||
|
type: 'directory',
|
||||||
|
deleteable: true,
|
||||||
|
permission,
|
||||||
|
children: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(`mkdir ${path}`)
|
||||||
|
await save()
|
||||||
|
},
|
||||||
|
rmdir: async (path: string): Promise<void> => {
|
||||||
|
const { current, filename } = await navigatePathParent(path)
|
||||||
|
|
||||||
|
if (!current.deleteable) throw new Error(Errors.EPERM)
|
||||||
|
await handlePermissions(path)
|
||||||
|
|
||||||
|
if (current.children[filename].type !== 'directory') throw new Error(Errors.ENOTDIR)
|
||||||
|
|
||||||
|
Reflect.deleteProperty(current.children, filename)
|
||||||
|
|
||||||
|
console.debug(`rmdir ${path}`)
|
||||||
|
await save()
|
||||||
|
},
|
||||||
|
readdir: async (path: string): Promise<string[]> => {
|
||||||
|
const { current } = await navigatePath(path)
|
||||||
|
|
||||||
|
if (current.type === 'file') throw new Error(Errors.ENOTDIR)
|
||||||
|
const result = await Promise.all(Object.keys(current.children ?? {}))
|
||||||
|
|
||||||
|
console.debug(`readdir ${path}`)
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
stat: async (path: string): Promise<{ isDirectory: () => boolean, isFile: () => boolean }> => {
|
||||||
|
const { current } = await navigatePath(path)
|
||||||
|
|
||||||
|
console.debug(`stat ${path}`)
|
||||||
|
return {
|
||||||
|
isDirectory: () => current.type === 'directory',
|
||||||
|
isFile: () => current.type === 'file'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rename: async (oldPath: string, newPath: string): Promise<void> => {
|
||||||
|
const { current: oldCurrent, filename: oldFilename } = await navigatePathParent(oldPath)
|
||||||
|
const { current: newCurrent, filename: newFilename } = await navigatePathParent(newPath)
|
||||||
|
|
||||||
|
if (!oldCurrent.deleteable) throw new Error(Errors.EPERM)
|
||||||
|
if (!newCurrent.deleteable) throw new Error(Errors.EPERM)
|
||||||
|
|
||||||
|
await handlePermissions(oldPath)
|
||||||
|
await handlePermissions(newPath)
|
||||||
|
|
||||||
|
newCurrent.children[newFilename] = oldCurrent.children[oldFilename]
|
||||||
|
Reflect.deleteProperty(oldCurrent.children, oldFilename)
|
||||||
|
|
||||||
|
console.debug(`rename ${oldPath} -> ${newPath}`)
|
||||||
|
await save()
|
||||||
|
},
|
||||||
|
exists: async (path: string): Promise<boolean> => {
|
||||||
|
console.debug(`exists ${path}`)
|
||||||
|
try {
|
||||||
|
const { current } = await navigatePath(path)
|
||||||
|
return current !== undefined
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VirtualFS
|
||||||
90
src/system/lib/WindowManager.ts
Normal file
90
src/system/lib/WindowManager.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import HTML from '../../HTML'
|
||||||
|
import FlowWindow from '../../structures/FlowWindow'
|
||||||
|
import ProcessLib from '../../structures/ProcessLib'
|
||||||
|
import { FlowWindowConfig, Library } from '../../types'
|
||||||
|
|
||||||
|
const WindowManager: Library = {
|
||||||
|
config: {
|
||||||
|
name: 'WindowManager',
|
||||||
|
type: 'library',
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
init: (l, k, p) => {
|
||||||
|
document.addEventListener('app_closed', (e: any) => {
|
||||||
|
WindowManager.data.windows.find((win: FlowWindow) => {
|
||||||
|
if (win.process.token === e.detail.token) {
|
||||||
|
win.close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
windowArea: new HTML('window-area'),
|
||||||
|
windows: [],
|
||||||
|
getHighestZIndex: () => {
|
||||||
|
const indexes = WindowManager.data.windows.map((win: FlowWindow) => {
|
||||||
|
return parseInt(win.element.style.zIndex)
|
||||||
|
})
|
||||||
|
|
||||||
|
const max = Math.max(...indexes)
|
||||||
|
|
||||||
|
return max === -Infinity ? 0 : max
|
||||||
|
},
|
||||||
|
createWindow: (config: FlowWindowConfig, process: ProcessLib) => {
|
||||||
|
const win = new FlowWindow(process, WindowManager.data, config)
|
||||||
|
const appOpenedEvent = {
|
||||||
|
detail: {
|
||||||
|
token: process.token,
|
||||||
|
proc: process.process,
|
||||||
|
win
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.dispatchEvent(new CustomEvent('app_opened', appOpenedEvent))
|
||||||
|
WindowManager.data.windows.push(win)
|
||||||
|
WindowManager.data.windowArea.elm.appendChild(win.element)
|
||||||
|
return win
|
||||||
|
},
|
||||||
|
createModal: async (title: string, text: string, process: ProcessLib) => {
|
||||||
|
const win = new FlowWindow(process, WindowManager.data, {
|
||||||
|
title,
|
||||||
|
icon: '',
|
||||||
|
width: 300,
|
||||||
|
height: 200,
|
||||||
|
canResize: false
|
||||||
|
})
|
||||||
|
const appOpenedEvent = {
|
||||||
|
detail: {
|
||||||
|
token: process.token,
|
||||||
|
proc: process.process,
|
||||||
|
win
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.dispatchEvent(new CustomEvent('app_opened', appOpenedEvent))
|
||||||
|
|
||||||
|
const { Button } = await process.loadLibrary('lib/Components')
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: await new Promise((resolve) => {
|
||||||
|
new HTML('h3').text(title).appendTo(win.content)
|
||||||
|
new HTML('p').text(text).appendTo(win.content)
|
||||||
|
Button.new().text('Allow').appendTo(win.content).on('click', () => {
|
||||||
|
resolve(true)
|
||||||
|
win.close()
|
||||||
|
})
|
||||||
|
Button.new().text('Deny').appendTo(win.content).on('click', () => {
|
||||||
|
resolve(false)
|
||||||
|
win.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
WindowManager.data.windows.push(win)
|
||||||
|
WindowManager.data.windowArea.elm.appendChild(win.element)
|
||||||
|
}),
|
||||||
|
win
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WindowManager
|
||||||
46
src/system/lib/XOR.ts
Normal file
46
src/system/lib/XOR.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Library } from '../../types'
|
||||||
|
|
||||||
|
const XOR: Library = {
|
||||||
|
config: {
|
||||||
|
name: 'XOR',
|
||||||
|
type: 'library',
|
||||||
|
targetVer: '1.0.0-indev.0'
|
||||||
|
},
|
||||||
|
init: (l, k, p) => {},
|
||||||
|
data: {
|
||||||
|
randomMax: 100,
|
||||||
|
randomMin: -100,
|
||||||
|
|
||||||
|
encode: (str: string): string => {
|
||||||
|
return encodeURIComponent(
|
||||||
|
str
|
||||||
|
.toString()
|
||||||
|
.split('')
|
||||||
|
.map((char, ind): string => {
|
||||||
|
let indCheck
|
||||||
|
if (ind % 2 === 0) { indCheck = false } else { indCheck = true }
|
||||||
|
|
||||||
|
return indCheck ? String.fromCharCode(char.charCodeAt(0) ^ 2) : char
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
decode: (str: string): string => {
|
||||||
|
const [input, ...search] = str.split('?')
|
||||||
|
|
||||||
|
return (
|
||||||
|
decodeURIComponent(input)
|
||||||
|
.split('')
|
||||||
|
.map((char, ind): string => {
|
||||||
|
let indCheck
|
||||||
|
if (ind % 2 === 0) { indCheck = false } else { indCheck = true }
|
||||||
|
|
||||||
|
return indCheck ? String.fromCharCode(char.charCodeAt(0) ^ 2) : char
|
||||||
|
})
|
||||||
|
.join('') + ((search.length > 0) ? `?${search.join('?')}` : '')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default XOR
|
||||||
101
src/types.ts
101
src/types.ts
|
|
@ -1,56 +1,64 @@
|
||||||
|
import Kernel from './kernel'
|
||||||
import FlowWindow from './structures/FlowWindow'
|
import FlowWindow from './structures/FlowWindow'
|
||||||
|
import LibraryLib from './structures/LibraryLib'
|
||||||
export type AppOpenFunction = (data: any) => Promise<FlowWindow>
|
import ProcessLib from './structures/ProcessLib'
|
||||||
export type PluginRunFunction = (element: HTMLDivElement, config: any) => void | Promise<void>
|
|
||||||
|
|
||||||
export interface AppClosedEvent extends CustomEvent {
|
export interface AppClosedEvent extends CustomEvent {
|
||||||
detail: {
|
detail: {
|
||||||
win: FlowWindow
|
token: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppOpenedEvent extends CustomEvent {
|
export interface AppOpenedEvent extends CustomEvent {
|
||||||
detail: {
|
detail: {
|
||||||
app: App
|
proc: Process
|
||||||
|
token: string
|
||||||
win: FlowWindow
|
win: FlowWindow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export interface BaseMeta {
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
pkg: string
|
|
||||||
version: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppMeta extends BaseMeta {
|
export interface Package {
|
||||||
icon: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PluginMeta extends BaseMeta {
|
|
||||||
icon?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RepoAppMeta extends BaseMeta {
|
|
||||||
icon?: string
|
|
||||||
url: string
|
url: string
|
||||||
|
executable: Executable
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Apps {
|
export interface Executable {
|
||||||
[key: string]: App
|
config: {
|
||||||
|
name: string
|
||||||
|
type: 'process' | 'library'
|
||||||
|
icon?: string
|
||||||
|
targetVer: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Plugins {
|
export type LibraryData = any
|
||||||
[key: string]: Plugin
|
export interface Library extends Executable {
|
||||||
|
config: {
|
||||||
|
name: string
|
||||||
|
type: 'library'
|
||||||
|
targetVer: string
|
||||||
|
}
|
||||||
|
|
||||||
|
init: (library: LibraryLib, kernel: Kernel, process: ProcessLib) => void
|
||||||
|
data: LibraryData
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface App {
|
export interface Process extends Executable {
|
||||||
meta: AppMeta
|
config: {
|
||||||
open: AppOpenFunction
|
name: string
|
||||||
|
type: 'process'
|
||||||
|
icon?: string
|
||||||
|
targetVer: string
|
||||||
|
}
|
||||||
|
|
||||||
|
run: (process: ProcessLib) => Promise<any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Plugin {
|
export interface RepoAppMeta {
|
||||||
meta: PluginMeta
|
name: string
|
||||||
run: PluginRunFunction
|
icon?: string
|
||||||
|
targetVer: string
|
||||||
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FlowWindowConfig {
|
export interface FlowWindowConfig {
|
||||||
|
|
@ -65,28 +73,19 @@ export interface FlowWindowConfig {
|
||||||
minWidth?: number
|
minWidth?: number
|
||||||
minHeight?: number
|
minHeight?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadedApp extends App {
|
|
||||||
builtin: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LoadedPlugin extends Plugin {
|
|
||||||
builtin: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PackageJSON {
|
|
||||||
version: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FlowConfig {
|
|
||||||
SERVER_URL: string
|
|
||||||
HOSTNAME: string
|
|
||||||
USERNAME: string
|
|
||||||
'24HOUR_CLOCK': boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RepoData {
|
export interface RepoData {
|
||||||
name: string
|
name: string
|
||||||
id: string
|
id: string
|
||||||
apps: RepoAppMeta[]
|
apps: RepoAppMeta[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProcessInfo {
|
||||||
|
pid: number
|
||||||
|
name: string
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KernelConfig {
|
||||||
|
SERVER: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@
|
||||||
* @returns The time.
|
* @returns The time.
|
||||||
*/
|
*/
|
||||||
export const getTime = async (): Promise<string> => {
|
export const getTime = async (): Promise<string> => {
|
||||||
const config = await window.config()
|
const use24hrs = false
|
||||||
const use24hrs = config['24HOUR_CLOCK']
|
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
let hours: string | number = now.getHours()
|
let hours: string | number = now.getHours()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
|
/** @type {import('typedoc').TypeDocOptions} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: 'FlowOS',
|
name: 'FlowOS',
|
||||||
plugin: ['typedoc-material-theme', 'typedoc-plugin-missing-exports'],
|
plugin: ['typedoc-material-theme'],
|
||||||
themeColor: '#1e1e2e'
|
themeColor: '#1e1e2e',
|
||||||
|
entryPoints: ['src/kernel.ts'],
|
||||||
|
entryPointStrategy: 'expand'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,33 @@
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||||
|
import viteCompression from 'vite-plugin-compression'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
|
/** @type {import('vite').Plugin} */
|
||||||
|
const hexLoader = {
|
||||||
|
name: 'hex-loader',
|
||||||
|
transform (code, id) {
|
||||||
|
const [path, query] = id.split('?')
|
||||||
|
if (query !== 'raw-hex') { return null }
|
||||||
|
|
||||||
|
const data = fs.readFileSync(path)
|
||||||
|
const hex = data.toString('hex')
|
||||||
|
|
||||||
|
return `export default '${hex}';`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
nodePolyfills()
|
hexLoader,
|
||||||
|
nodePolyfills(),
|
||||||
|
viteCompression({
|
||||||
|
algorithm: 'gzip',
|
||||||
|
ext: '.gz',
|
||||||
|
deleteOriginFile: false,
|
||||||
|
threshold: 10240,
|
||||||
|
disable: false,
|
||||||
|
verbose: true
|
||||||
|
})
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue