Merge pull request #127 from Flow-Works/dev

FlowOS Mochi
This commit is contained in:
ThinLiquid 2024-01-16 06:09:39 +00:00 committed by GitHub
commit f06399516c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 3343 additions and 2875 deletions

View file

@ -45,7 +45,7 @@ npm run serve
## Made with
FlowOS is made with the following software:
* [Filer](https://github.com/filerjs/filer)
* [Kat21's HTML Library](https://github.com/datkat21/html)
* [Prism Code Editor](https://github.com/FIameCaster/prism-code-editor)
* [Vite](https://vitejs.dev)
* [Ultraviolet](https://github.com/titaniumnetwork-dev/ultraviolet)

View file

@ -7,7 +7,6 @@
<link rel="shortcut icon" href="./src/assets/flow.png" type="image/png">
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/filer"></script>
<script src="./src/index.ts" type="module"></script>
<script src="./src/kernel.ts" type="module"></script>
</body>
</html>

1969
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
{
"name": "flowos",
"version": "1.0.0",
"version": "1.0.0-indev.0",
"description": "The most aesthetic webOS.",
"main": "src/index.ts",
"main": "src/kernel.ts",
"scripts": {
"docs": "typedoc src/**",
"test": "ts-standard",
@ -15,24 +15,26 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.8.7",
"@types/node": "^20.11.3",
"@types/uuid": "^9.0.5",
"@types/web": "^0.0.117",
"less": "^4.2.0",
"ts-standard": "^12.0.2",
"typedoc": "^0.25.3",
"typedoc-material-theme": "^1.0.0",
"typedoc-material-theme": "^1.0.2",
"typedoc-plugin-missing-exports": "^2.1.0",
"typescript": "^5.2.2",
"vite": "^4.4.12",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-dynamic-import": "^1.5.0",
"vite-plugin-node-polyfills": "^0.15.0"
},
"dependencies": {
"@ptkdev/logger": "^1.8.0",
"eruda": "^3.0.1",
"filer": "^1.4.1",
"prism-code-editor": "^2.3.0",
"js-ini": "^1.6.0",
"material-symbols": "^0.14.3",
"prism-code-editor": "^2.3.0",
"semver": "^7.5.4",
"uuid": "^9.0.1"
},
"ts-standard": {

View file

@ -1,7 +1,8 @@
// @ts-nocheck
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 sw = new UVServiceWorker(serverURL);

View file

@ -30,7 +30,6 @@ self.xor = {
}
self.__uv$config = {
prefix: '/service/',
bare: 'https://server.flow-works.me' + '/bare/',
encodeUrl: self.xor.encode,
decodeUrl: self.xor.decode,
handler: '/uv/uv.handler.js',

316
src/HTML.ts Normal file
View 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
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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()
})
}

View file

@ -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
View file

@ -0,0 +1 @@
declare module '*?raw-hex'

View file

@ -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
}

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
View 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))
})

View file

@ -1,8 +1,7 @@
import { v4 as uuid } from 'uuid'
import WindowManager from '../instances/WindowManager'
import { FlowWindowConfig } from '../types'
import { sanitize } from '../utils'
import nullIcon from '../assets/icons/application-default-icon.svg'
import ProcessLib from './ProcessLib'
/**
* Makes an element draggable.
@ -78,9 +77,11 @@ class FlowWindow {
isMinimized = false
isMaximized = false
wm: WindowManager
wm: any
id = uuid()
readonly process: ProcessLib
onClose: () => void
config: FlowWindowConfig
@ -90,13 +91,16 @@ class FlowWindow {
* @param wm The current window manager session.
* @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.config = config
this.onClose = onClose
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.focus()
@ -112,13 +116,13 @@ class FlowWindow {
this.element.style.height = `${config.height ?? 200}px`
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) {
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.close()
this.process.kill().catch((e: any) => console.error(e))
}
(this.header.querySelector('#min') as HTMLElement).onclick = () => this.toggleMin()
@ -159,8 +163,6 @@ class FlowWindow {
shadow.appendChild(shadowBody)
this.content = shadowBody
console.log(this.content)
this.element.appendChild(this.header)
this.element.appendChild(this.realContent)
@ -224,7 +226,7 @@ class FlowWindow {
*/
focus (): void {
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.opacity = '0'
this.element.style.transform = 'translateY(10px)'
const event = new CustomEvent('app_closed', { detail: { win: this } })
window.dispatchEvent(event)
this.onClose()
setTimeout(() => {
this.element.remove()
}, 200)

View file

@ -0,0 +1,5 @@
import HTML from '../HTML'
export default class LibraryLib {
HTML = HTML
}

26
src/structures/ProcLib.ts Normal file
View 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

View 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
View 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

View 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

View file

@ -1,25 +1,25 @@
import icon from '../../assets/icons/web-browser.svg'
import { App } from '../../types'
import { Process } from '../../types'
import FlowWindow from '../../structures/FlowWindow'
export default class BrowserApp implements App {
meta = {
const BrowserApp: Process = {
config: {
name: 'Browser',
description: 'A simple browser app.',
pkg: 'flow.browser',
version: '1.0.0',
icon
}
async open (): Promise<FlowWindow> {
const win = window.wm.createWindow({
title: this.meta.name,
icon: this.meta.icon,
width: 400,
height: 300
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: 'Browser',
icon,
width: 500,
height: 700
}, process)
})
const xor = await process.loadLibrary('lib/XOR')
win.content.style.height = '100%'
win.content.style.display = 'flex'
win.content.style.flexDirection = 'column'
@ -46,6 +46,7 @@ export default class BrowserApp implements App {
#content-container {
flex: 1;
background: white;
}
.add {
border: none;
@ -74,7 +75,7 @@ export default class BrowserApp implements App {
iframe: HTMLIFrameElement = document.createElement('iframe')
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.header.innerHTML = `
@ -91,12 +92,12 @@ export default class BrowserApp implements App {
}
(this.header.querySelector('.title') as HTMLElement).innerText = 'Tab'
this.iframe.src = (win.content.querySelector('input')?.value as string)
} else {
if (this === tabManager.activeTab) {
(win.content.querySelector('.toggle') as HTMLElement).innerHTML = 'toggle_on'
}
this.iframe.src = `/service/${xor.encode(win.content.querySelector('input')?.value as string)}`
return
}
if (this === tabManager.activeTab) {
(win.content.querySelector('.toggle') as HTMLElement).innerHTML = 'toggle_on'
}
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 {
this.tabs.forEach((tab) => {
if (tab.active) {
tab.active = false
tab.iframe.style.display = 'none'
tab.header.classList.remove('active')
if (!tab.active) {
return
}
tab.active = false
tab.iframe.style.display = 'none'
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'
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'
} 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
@ -165,55 +167,9 @@ export default class BrowserApp implements App {
win.content.querySelector('.inp')?.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'Enter') {
if (tabManager.activeTab.proxy) {
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
}
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
}
})
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 = () => {
tabManager.addTab(new Tab('https://google.com'))
@ -236,23 +192,19 @@ export default class BrowserApp implements App {
}
win.content.onfullscreenchange = () => {
if (document.fullscreenElement !== null) {
(win.content.querySelector('.fullscreen') as HTMLElement).innerHTML = 'fullscreen_exit'
} else {
(win.content.querySelector('.fullscreen') as HTMLElement).innerHTML = 'fullscreen'
}
(win.content.querySelector('.fullscreen') as HTMLElement).innerHTML = document.fullscreenElement !== null ? 'fullscreen_exit' : 'fullscreen'
}
(win.content.querySelector('.fullscreen') as HTMLElement).onclick = async () => {
if (document.fullscreenElement !== null) {
await document.exitFullscreen().catch(e => console.error)
if (document.fullscreenElement === null) {
await win.content.requestFullscreen().catch((e: any) => console.error)
} else {
await win.content.requestFullscreen().catch(e => console.error)
await document.exitFullscreen().catch(e => console.error)
}
}
tabManager.addTab(new Tab('https://google.com'))
return win
}
}
export default BrowserApp

View file

@ -1,6 +1,4 @@
import icon from '../../assets/icons/text-editor.svg'
import { App } from '../../types'
import { fullEditor } from 'prism-code-editor/setups'
// this will also import markup, clike, javascript, typescript and jsx
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/python'
import FlowWindow from '../../structures/FlowWindow'
import { Process } from '../../types'
interface EditorConfig {
path: string
@ -34,29 +32,41 @@ const fileLanguageMap: {
py: 'python'
}
export default class EditorApp implements App {
meta = {
const Editor: Process = {
config: {
name: 'Editor',
description: 'A simple editor app.',
pkg: 'flow.editor',
version: '1.0.0',
icon
}
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: 'Editor',
icon,
width: 350,
height: 500,
canResize: false
}, process)
})
async open (data?: EditorConfig): Promise<FlowWindow> {
const win = window.wm.createWindow({
title: this.meta.name,
icon: this.meta.icon,
width: 500,
height: 400
})
const fs = await process.loadLibrary('lib/VirtualFS')
if (data != null) {
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.flexDirection = 'column'
win.content.innerHTML = `
if (data == null) {
await process.launch('lib/FileManager')
setTimeout(() => {
win.close()
}, 10)
} else {
const render = async (): Promise<void> => {
win.content.innerHTML = `
<div style="padding: 5px;display: flex;align-items: center;gap: 5px;">
<div id="file-open">File</div>
<div id="edit-open">Edit</div>
@ -114,50 +124,50 @@ export default class EditorApp implements App {
</style>
`
const fileBtn = win.content.querySelector('#file-open')
const editBtn = win.content.querySelector('#edit-open')
const fileBtn = win.content.querySelector('#file-open')
const editBtn = win.content.querySelector('#edit-open')
const toggleDropdown = (id: string): void => {
const el = win.content.querySelector(`#${id}`)
el?.classList.toggle('show')
}
const toggleDropdown = (id: string): void => {
const el = win.content.querySelector(`#${id}`)
el?.classList.toggle('show')
}
fileBtn?.addEventListener('click', (e) => {
e.stopPropagation()
toggleDropdown('file')
})
fileBtn?.addEventListener('click', (e: Event) => {
e.stopPropagation()
toggleDropdown('file')
})
editBtn?.addEventListener('click', (e) => {
e.stopPropagation()
toggleDropdown('edit')
})
editBtn?.addEventListener('click', (e: Event) => {
e.stopPropagation()
toggleDropdown('edit')
})
win.content.addEventListener('click', () => {
const file = (win.content.querySelector('#file') as HTMLElement)
const edit = (win.content.querySelector('#edit') as HTMLElement)
if (file.classList.contains('show')) {
toggleDropdown('file')
}
if (edit.classList.contains('show')) {
toggleDropdown('edit')
}
})
win.content.addEventListener('click', () => {
const file = (win.content.querySelector('#file') as HTMLElement)
const edit = (win.content.querySelector('#edit') as HTMLElement)
if (file.classList.contains('show')) {
toggleDropdown('file')
}
if (edit.classList.contains('show')) {
toggleDropdown('edit')
}
})
const fileExtension = data.path.split('.').pop()?.toLowerCase() as string
const language = fileLanguageMap[fileExtension] ?? 'text'
const fileExtension = data.path.split('.').pop()?.toLowerCase() as string
const language = fileLanguageMap[fileExtension] ?? 'text'
const value = (await window.fs.promises.readFile(data.path)).toString()
const editor = fullEditor(
win.content.querySelector('.editor') as HTMLElement,
{
language,
theme: 'github-dark',
value
}
)
const value = Buffer.from(await fs.readFile(data.path)).toString()
const editor = fullEditor(
win.content.querySelector('.editor') as HTMLElement,
{
language,
theme: 'github-dark',
value
}
)
const style = document.createElement('style')
style.textContent = `
const style = document.createElement('style')
style.textContent = `
.prism-code-editor {
border-radius: 10px 10px 0 0;
caret-color: var(--text);
@ -186,20 +196,24 @@ export default class EditorApp implements App {
font-family: 'Satoshi', sans-serif;
}
`
editor.scrollContainer.appendChild(style);
(win.content.querySelector('#find') as HTMLElement).onclick = () => {
editor.extensions.searchWidget?.open()
editor.scrollContainer.appendChild(style);
(win.content.querySelector('#find') as HTMLElement).onclick = () => {
editor.extensions.searchWidget?.open()
}
(win.content.querySelector('#save') as HTMLElement).onclick = async () => {
await fs.writeFile(data.path, editor.value)
}
}
await render()
document.addEventListener('fs_update', () => {
render().catch(e => console.error(e))
})
}
(win.content.querySelector('#save') as HTMLElement).onclick = async () => {
await window.fs.promises.writeFile(data.path, editor.value)
}
} else {
await window.flow.openApp('flow.files')
setTimeout(() => {
win.close()
}, 10)
return
}
return win
await process.kill()
await process.launch('apps/Files')
}
}
export default Editor

121
src/system/apps/Files.ts Normal file
View 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

View 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
View 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

View 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

View 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
View 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

View 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

View 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
View 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

View 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

View 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

View 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

View 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
View 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

View 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
View 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

View file

@ -1,56 +1,64 @@
import Kernel from './kernel'
import FlowWindow from './structures/FlowWindow'
export type AppOpenFunction = (data: any) => Promise<FlowWindow>
export type PluginRunFunction = (element: HTMLDivElement, config: any) => void | Promise<void>
import LibraryLib from './structures/LibraryLib'
import ProcessLib from './structures/ProcessLib'
export interface AppClosedEvent extends CustomEvent {
detail: {
win: FlowWindow
token: string
}
}
export interface AppOpenedEvent extends CustomEvent {
detail: {
app: App
proc: Process
token: string
win: FlowWindow
}
}
export interface BaseMeta {
name: string
description: string
pkg: string
version: string
}
export interface AppMeta extends BaseMeta {
icon: string
}
export interface PluginMeta extends BaseMeta {
icon?: string
}
export interface RepoAppMeta extends BaseMeta {
icon?: string
export interface Package {
url: string
executable: Executable
}
export interface Apps {
[key: string]: App
export interface Executable {
config: {
name: string
type: 'process' | 'library'
icon?: string
targetVer: string
}
}
export interface Plugins {
[key: string]: Plugin
export type LibraryData = any
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 {
meta: AppMeta
open: AppOpenFunction
export interface Process extends Executable {
config: {
name: string
type: 'process'
icon?: string
targetVer: string
}
run: (process: ProcessLib) => Promise<any>
}
export interface Plugin {
meta: PluginMeta
run: PluginRunFunction
export interface RepoAppMeta {
name: string
icon?: string
targetVer: string
url: string
}
export interface FlowWindowConfig {
@ -65,28 +73,19 @@ export interface FlowWindowConfig {
minWidth?: 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 {
name: string
id: string
apps: RepoAppMeta[]
}
export interface ProcessInfo {
pid: number
name: string
token: string
}
export interface KernelConfig {
SERVER: string
[key: string]: any
}

View file

@ -4,8 +4,7 @@
* @returns The time.
*/
export const getTime = async (): Promise<string> => {
const config = await window.config()
const use24hrs = config['24HOUR_CLOCK']
const use24hrs = false
const now = new Date()
let hours: string | number = now.getHours()

View file

@ -1,5 +1,8 @@
/** @type {import('typedoc').TypeDocOptions} */
module.exports = {
name: 'FlowOS',
plugin: ['typedoc-material-theme', 'typedoc-plugin-missing-exports'],
themeColor: '#1e1e2e'
plugin: ['typedoc-material-theme'],
themeColor: '#1e1e2e',
entryPoints: ['src/kernel.ts'],
entryPointStrategy: 'expand'
}

View file

@ -1,8 +1,33 @@
import { defineConfig } from 'vite'
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({
plugins: [
nodePolyfills()
hexLoader,
nodePolyfills(),
viteCompression({
algorithm: 'gzip',
ext: '.gz',
deleteOriginFile: false,
threshold: 10240,
disable: false,
verbose: true
})
]
})