Compare commits
87 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa9876cb21 | ||
|
|
76d60330ea | ||
|
|
6bc0694776 | ||
|
|
a80ba8d598 | ||
|
|
c1bcfe6023 | ||
|
|
c43f16c5a3 | ||
|
|
65c7db6793 | ||
|
|
c9cff82d2d | ||
|
|
2779bac5da | ||
|
|
b44034c114 | ||
|
|
8ee0f166ae | ||
|
|
cca0087220 | ||
|
|
93559fc358 | ||
|
|
f06db1db91 | ||
|
|
abbb05f02d | ||
|
|
a1df530743 | ||
|
|
031f11c456 | ||
|
|
f675f7d8cc | ||
|
|
6ccfab84ac | ||
|
|
ae6866118b | ||
|
|
46f56682f1 | ||
|
|
5c8eafce99 | ||
|
|
aa5fb9e290 | ||
|
|
470a1fa1e0 | ||
|
|
3a64f5801a | ||
|
|
bc3d82fd56 | ||
|
|
d561cce786 | ||
|
|
3e92a445ec | ||
|
|
d84c1c4232 | ||
|
|
608a2733f9 | ||
|
|
c4018528a8 | ||
|
|
ad38f19f29 | ||
|
|
011142dc1a | ||
|
|
159787bb72 | ||
|
|
a16057f606 | ||
|
|
ff9630c082 | ||
|
|
12cd47a4b6 | ||
|
|
876793c520 | ||
|
|
2e208c91e1 | ||
|
|
cd525b6a93 | ||
|
|
934d6e80a0 | ||
|
|
f207101c6a | ||
|
|
10c0dbe48a | ||
|
|
2352c53881 | ||
|
|
568e2f3100 | ||
|
|
1aad68f3f4 | ||
|
|
c0eacb7034 | ||
|
|
971bf1ca9a | ||
|
|
7f7e565102 | ||
|
|
cc3583bfdc | ||
|
|
0c2ffbbb9f | ||
|
|
688d8db713 | ||
|
|
e630ae664c | ||
|
|
54a92c5d79 | ||
|
|
c08dec6743 | ||
|
|
4e4615ecb1 | ||
|
|
11540b945a | ||
|
|
d26ef5e482 | ||
|
|
518e821014 | ||
|
|
d610c8a471 | ||
|
|
aff12d0ee1 | ||
|
|
a377319bd1 | ||
|
|
d9ab009afd | ||
|
|
1482869019 | ||
|
|
1a62c95d4a | ||
|
|
4bd8f9b058 | ||
|
|
a08b8d938f | ||
|
|
9b3c748a17 | ||
|
|
cf8f1b844a | ||
|
|
48294da1ea | ||
|
|
0cf920970f | ||
|
|
99f2577ee0 | ||
|
|
04ccdf1442 | ||
|
|
316df265c3 | ||
|
|
b637fc5954 | ||
|
|
df060b0a91 | ||
|
|
eb161da875 | ||
|
|
e0c3253490 | ||
|
|
77c0d57770 | ||
|
|
bf23629905 | ||
|
|
423dd46da0 | ||
|
|
acdba11691 | ||
|
|
2dd5b5f887 | ||
|
|
f106776bee | ||
|
|
8d3b629185 | ||
|
|
ae73cf821b | ||
|
|
4ae5fc3019 |
46 changed files with 45598 additions and 42247 deletions
49
.github/workflows/deploy.yml
vendored
Normal file
49
.github/workflows/deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
uses: bahmutov/npm-install@v1
|
||||||
|
|
||||||
|
- name: Build project
|
||||||
|
run: npx vite build --base="/FlowOS/"
|
||||||
|
|
||||||
|
- name: Upload production-ready build files
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: production-files
|
||||||
|
path: ./dist
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: production-files
|
||||||
|
path: ./dist
|
||||||
|
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
publish_dir: ./dist
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"npmPublish": false
|
"npmPublish": false
|
||||||
}],
|
}],
|
||||||
["@semantic-release/git", {
|
["@semantic-release/git", {
|
||||||
"assets": ["package.json", "CHANGELOG.md"],
|
"assets": ["package.json", "package-lock.json"],
|
||||||
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||||
}],
|
}],
|
||||||
"@semantic-release/github"
|
"@semantic-release/github"
|
||||||
|
|
|
||||||
11
README.md
11
README.md
|
|
@ -50,5 +50,16 @@ FlowOS is made with the following software:
|
||||||
* [Vite](https://vitejs.dev)
|
* [Vite](https://vitejs.dev)
|
||||||
* [Ultraviolet](https://github.com/titaniumnetwork-dev/ultraviolet)
|
* [Ultraviolet](https://github.com/titaniumnetwork-dev/ultraviolet)
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
* [ThinLiquid](https://github.com/ThinLiquid)
|
||||||
|
* [proudparrot2](https://github.com/proudparrot2)
|
||||||
|
* [FlameCaster](https://github.com/FlameCaster)
|
||||||
|
* [RisingGlitch](https://github.com/RisingGlitch)
|
||||||
|
* [httphypixelnet](https://github.com/httphypixelnet)
|
||||||
|
* [idk-pixel](https://github.com/idk-pixel)
|
||||||
|
### Special Thanks
|
||||||
|
* [datkat21](https://github.com/datkat21)
|
||||||
|
* [alexfeed1990](https://github.com/alexfeed1990)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
FlowOS is licensed under the MIT license. See the `LICENSE` file for more information.
|
FlowOS is licensed under the MIT license. See the `LICENSE` file for more information.
|
||||||
|
|
|
||||||
14
index.html
14
index.html
|
|
@ -5,8 +5,20 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>FlowOS</title>
|
<title>FlowOS</title>
|
||||||
<link rel="shortcut icon" href="./src/assets/flow.png" type="image/png">
|
<link rel="shortcut icon" href="./src/assets/flow.png" type="image/png">
|
||||||
|
|
||||||
|
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-9675905177363247" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<!-- Google tag (gtag.js) -->
|
||||||
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-X4XVQRWEVM"></script>
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
|
gtag('config', 'G-X4XVQRWEVM');
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script src="./src/kernel.ts" type="module"></script>
|
<script src="./src/bootloader.ts" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
175
package-lock.json
generated
175
package-lock.json
generated
|
|
@ -1,14 +1,16 @@
|
||||||
{
|
{
|
||||||
"name": "flowos",
|
"name": "flowos",
|
||||||
"version": "1.0.0-indev.0",
|
"version": "2.4.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "flowos",
|
"name": "flowos",
|
||||||
"version": "1.0.0-indev.0",
|
"version": "2.4.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ansi-to-html": "^0.7.2",
|
||||||
|
"chalk": "^5.3.0",
|
||||||
"eruda": "^3.0.1",
|
"eruda": "^3.0.1",
|
||||||
"js-ini": "^1.6.0",
|
"js-ini": "^1.6.0",
|
||||||
"material-symbols": "^0.14.3",
|
"material-symbols": "^0.14.3",
|
||||||
|
|
@ -29,7 +31,7 @@
|
||||||
"typedoc-material-theme": "^1.0.2",
|
"typedoc-material-theme": "^1.0.2",
|
||||||
"typedoc-plugin-missing-exports": "^2.1.0",
|
"typedoc-plugin-missing-exports": "^2.1.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^4.4.12",
|
"vite": "^4.5.2",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-dynamic-import": "^1.5.0",
|
"vite-plugin-dynamic-import": "^1.5.0",
|
||||||
"vite-plugin-node-polyfills": "^0.15.0"
|
"vite-plugin-node-polyfills": "^0.15.0"
|
||||||
|
|
@ -303,6 +305,23 @@
|
||||||
"node": ">=v18"
|
"node": ">=v18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@commitlint/load/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@commitlint/load/node_modules/cosmiconfig": {
|
"node_modules/@commitlint/load/node_modules/cosmiconfig": {
|
||||||
"version": "8.3.6",
|
"version": "8.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
|
||||||
|
|
@ -400,6 +419,23 @@
|
||||||
"node": ">=v18"
|
"node": ">=v18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@commitlint/types/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||||
|
|
@ -1877,6 +1913,20 @@
|
||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-to-html": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==",
|
||||||
|
"dependencies": {
|
||||||
|
"entities": "^2.2.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ansi-to-html": "bin/ansi-to-html"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansicolors": {
|
"node_modules/ansicolors": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz",
|
||||||
|
|
@ -2376,16 +2426,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
|
||||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-styles": "^4.1.0",
|
|
||||||
"supports-color": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
|
@ -3239,6 +3284,14 @@
|
||||||
"integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==",
|
"integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/env-ci": {
|
"node_modules/env-ci": {
|
||||||
"version": "11.0.0",
|
"version": "11.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/env-ci/-/env-ci-11.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/env-ci/-/env-ci-11.0.0.tgz",
|
||||||
|
|
@ -3906,6 +3959,22 @@
|
||||||
"url": "https://opencollective.com/eslint"
|
"url": "https://opencollective.com/eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint/node_modules/eslint-scope": {
|
"node_modules/eslint/node_modules/eslint-scope": {
|
||||||
"version": "7.2.2",
|
"version": "7.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
||||||
|
|
@ -5066,6 +5135,22 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/inquirer/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/inquirer/node_modules/escape-string-regexp": {
|
"node_modules/inquirer/node_modules/escape-string-regexp": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||||
|
|
@ -5937,6 +6022,22 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/log-symbols/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/log-symbols/node_modules/is-unicode-supported": {
|
"node_modules/log-symbols/node_modules/is-unicode-supported": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
||||||
|
|
@ -6055,18 +6156,6 @@
|
||||||
"marked": ">=1 <12"
|
"marked": ">=1 <12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/marked-terminal/node_modules/chalk": {
|
|
||||||
"version": "5.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
|
|
||||||
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/material-symbols": {
|
"node_modules/material-symbols": {
|
||||||
"version": "0.14.5",
|
"version": "0.14.5",
|
||||||
"resolved": "https://registry.npmjs.org/material-symbols/-/material-symbols-0.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/material-symbols/-/material-symbols-0.14.5.tgz",
|
||||||
|
|
@ -9397,6 +9486,22 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ora/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ora/node_modules/is-unicode-supported": {
|
"node_modules/ora/node_modules/is-unicode-supported": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
||||||
|
|
@ -11868,9 +11973,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "4.5.1",
|
"version": "4.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz",
|
||||||
"integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==",
|
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.18.10",
|
"esbuild": "^0.18.10",
|
||||||
|
|
@ -11936,6 +12041,22 @@
|
||||||
"vite": ">=2.0.0"
|
"vite": ">=2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite-plugin-compression/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite-plugin-dynamic-import": {
|
"node_modules/vite-plugin-dynamic-import": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.5.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "flowos",
|
"name": "flowos",
|
||||||
"version": "1.0.0",
|
"version": "2.4.0",
|
||||||
"description": "The most aesthetic webOS.",
|
"description": "The most aesthetic webOS.",
|
||||||
"main": "src/kernel.ts",
|
"main": "src/bootloader.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"docs": "typedoc src/**",
|
"docs": "typedoc src/**",
|
||||||
"test": "ts-standard",
|
"test": "ts-standard",
|
||||||
|
|
@ -28,12 +28,14 @@
|
||||||
"typedoc-material-theme": "^1.0.2",
|
"typedoc-material-theme": "^1.0.2",
|
||||||
"typedoc-plugin-missing-exports": "^2.1.0",
|
"typedoc-plugin-missing-exports": "^2.1.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^4.4.12",
|
"vite": "^4.5.2",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-dynamic-import": "^1.5.0",
|
"vite-plugin-dynamic-import": "^1.5.0",
|
||||||
"vite-plugin-node-polyfills": "^0.15.0"
|
"vite-plugin-node-polyfills": "^0.15.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ansi-to-html": "^0.7.2",
|
||||||
|
"chalk": "^5.3.0",
|
||||||
"eruda": "^3.0.1",
|
"eruda": "^3.0.1",
|
||||||
"js-ini": "^1.6.0",
|
"js-ini": "^1.6.0",
|
||||||
"material-symbols": "^0.14.3",
|
"material-symbols": "^0.14.3",
|
||||||
|
|
|
||||||
56
public/privacy.html
Normal file
56
public/privacy.html
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<h1>Privacy Policy for Flow Works</h1>
|
||||||
|
|
||||||
|
<p>At FlowOS, accessible from https://flow-works.me, one of our main priorities is the privacy of our visitors. This Privacy Policy document contains types of information that is collected and recorded by FlowOS and how we use it.</p>
|
||||||
|
|
||||||
|
<p>If you have additional questions or require more information about our Privacy Policy, do not hesitate to contact us.</p>
|
||||||
|
|
||||||
|
<h2>Log Files</h2>
|
||||||
|
|
||||||
|
<p>FlowOS follows a standard procedure of using log files. These files log visitors when they visit websites. All hosting companies do this and a part of hosting services' analytics. The information collected by log files include internet protocol (IP) addresses, browser type, Internet Service Provider (ISP), date and time stamp, referring/exit pages, and possibly the number of clicks. These are not linked to any information that is personally identifiable. The purpose of the information is for analyzing trends, administering the site, tracking users' movement on the website, and gathering demographic information.</p>
|
||||||
|
|
||||||
|
<h2>Cookies and Web Beacons</h2>
|
||||||
|
|
||||||
|
<p>Like any other website, FlowOS uses "cookies". These cookies are used to store information including visitors' preferences, and the pages on the website that the visitor accessed or visited. The information is used to optimize the users' experience by customizing our web page content based on visitors' browser type and/or other information.</p>
|
||||||
|
|
||||||
|
<h2>Google DoubleClick DART Cookie</h2>
|
||||||
|
|
||||||
|
<p>Google is one of a third-party vendor on our site. It also uses cookies, known as DART cookies, to serve ads to our site visitors based upon their visit to www.website.com and other sites on the internet. However, visitors may choose to decline the use of DART cookies by visiting the Google ad and content network Privacy Policy at the following URL – <a href="https://policies.google.com/technologies/ads">https://policies.google.com/technologies/ads</a></p>
|
||||||
|
|
||||||
|
<h2>Our Advertising Partners</h2>
|
||||||
|
|
||||||
|
<p>Some of advertisers on our site may use cookies and web beacons. Our advertising partners are listed below. Each of our advertising partners has their own Privacy Policy for their policies on user data. For easier access, we hyperlinked to their Privacy Policies below.</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>Google</p>
|
||||||
|
<p><a href="https://policies.google.com/technologies/ads">https://policies.google.com/technologies/ads</a></p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Privacy Policies</h2>
|
||||||
|
|
||||||
|
<P>You may consult this list to find the Privacy Policy for each of the advertising partners of FlowOS.</p>
|
||||||
|
|
||||||
|
<p>Third-party ad servers or ad networks uses technologies like cookies, JavaScript, or Web Beacons that are used in their respective advertisements and links that appear on FlowOS, which are sent directly to users' browser. They automatically receive your IP address when this occurs. These technologies are used to measure the effectiveness of their advertising campaigns and/or to personalize the advertising content that you see on websites that you visit.</p>
|
||||||
|
|
||||||
|
<p>Note that FlowOS has no access to or control over these cookies that are used by third-party advertisers.</p>
|
||||||
|
|
||||||
|
<h2>Third Party Privacy Policies</h2>
|
||||||
|
|
||||||
|
<p>FlowOS's Privacy Policy does not apply to other advertisers or websites. Thus, we are advising you to consult the respective Privacy Policies of these third-party ad servers for more detailed information. It may include their practices and instructions about how to opt-out of certain options. </p>
|
||||||
|
|
||||||
|
<p>You can choose to disable cookies through your individual browser options. To know more detailed information about cookie management with specific web browsers, it can be found at the browsers' respective websites.</p>
|
||||||
|
|
||||||
|
<h2>Children's Information</h2>
|
||||||
|
|
||||||
|
<p>Another part of our priority is adding protection for children while using the internet. We encourage parents and guardians to observe, participate in, and/or monitor and guide their online activity.</p>
|
||||||
|
|
||||||
|
<p>FlowOS does not knowingly collect any Personal Identifiable Information from children under the age of 13. If you think that your child provided this kind of information on our website, we strongly encourage you to contact us immediately and we will do our best efforts to promptly remove such information from our records.</p>
|
||||||
|
|
||||||
|
<h2>Online Privacy Policy Only</h2>
|
||||||
|
|
||||||
|
<p>This Privacy Policy applies only to our online activities and is valid for visitors to our website with regards to the information that they shared and/or collect in FlowOS. This policy is not applicable to any information collected offline or via channels other than this website.</p>
|
||||||
|
|
||||||
|
<h2>Consent</h2>
|
||||||
|
|
||||||
|
<p>By using our website, you hereby consent to our Privacy Policy and agree to its Terms and Conditions.</p>
|
||||||
|
|
@ -1,9 +1,21 @@
|
||||||
// @ts-nocheck
|
/*global UVServiceWorker,__uv$config*/
|
||||||
|
/*
|
||||||
|
* Stock service worker script.
|
||||||
|
* Users can provide their own sw.js if they need to extend the functionality of the service worker.
|
||||||
|
* Ideally, this will be registered under the scope in uv.config.js so it will not need to be modified.
|
||||||
|
* However, if a user changes the location of uv.bundle.js/uv.config.js or sw.js is not relative to them, they will need to modify this script locally.
|
||||||
|
*/
|
||||||
|
|
||||||
importScripts('/uv/uv.sw.js');
|
|
||||||
|
importScripts(`uv.config.js`);
|
||||||
|
|
||||||
const params = new URL(self.location.href).searchParams
|
const params = new URL(self.location.href).searchParams
|
||||||
const serverURL = atob(params.get('url'));
|
const serverURL = atob(params.get('url'));
|
||||||
const sw = new UVServiceWorker(serverURL);
|
self.__uv$config.bare = `${serverURL}/bare/`
|
||||||
|
|
||||||
self.addEventListener('fetch', event => event.respondWith(sw.fetch(event)));
|
importScripts('uv.bundle.js');
|
||||||
|
importScripts(__uv$config.sw || 'uv.sw.js');
|
||||||
|
|
||||||
|
const sw = new UVServiceWorker();
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => event.respondWith(sw.fetch(event)));
|
||||||
|
|
|
||||||
39014
public/uv.bundle.js
Normal file
39014
public/uv.bundle.js
Normal file
File diff suppressed because one or more lines are too long
7
public/uv.bundle.js.map
Normal file
7
public/uv.bundle.js.map
Normal file
File diff suppressed because one or more lines are too long
2939
public/uv.client.js
Normal file
2939
public/uv.client.js
Normal file
File diff suppressed because it is too large
Load diff
7
public/uv.client.js.map
Normal file
7
public/uv.client.js.map
Normal file
File diff suppressed because one or more lines are too long
40
public/uv.config.js
Normal file
40
public/uv.config.js
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*global Ultraviolet*/
|
||||||
|
|
||||||
|
const xor = {
|
||||||
|
encode: (str) => encodeURIComponent(
|
||||||
|
str
|
||||||
|
.toString()
|
||||||
|
.split('')
|
||||||
|
.map((char, ind) => {
|
||||||
|
const indCheck = ind % 2 === 0 ? false : true
|
||||||
|
|
||||||
|
return indCheck ? String.fromCharCode(char.charCodeAt(0) ^ 2) : char
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
),
|
||||||
|
decode: (str) => {
|
||||||
|
const [input, ...search] = str.split('?')
|
||||||
|
|
||||||
|
return (
|
||||||
|
decodeURIComponent(input)
|
||||||
|
.split('')
|
||||||
|
.map((char, ind) => {
|
||||||
|
const indCheck = ind % 2 === 0 ? false : true
|
||||||
|
|
||||||
|
return indCheck ? String.fromCharCode(char.charCodeAt(0) ^ 2) : char
|
||||||
|
})
|
||||||
|
.join('') + ((search.length > 0) ? `?${search.join('?')}` : '')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.__uv$config = {
|
||||||
|
prefix: '/service/',
|
||||||
|
encodeUrl: xor.encode,
|
||||||
|
decodeUrl: xor.decode,
|
||||||
|
handler: '/uv.handler.js',
|
||||||
|
client: '/uv.client.js',
|
||||||
|
bundle: '/uv.bundle.js',
|
||||||
|
config: '/uv.config.js',
|
||||||
|
sw: '/uv.sw.js',
|
||||||
|
};
|
||||||
1218
public/uv.handler.js
Normal file
1218
public/uv.handler.js
Normal file
File diff suppressed because it is too large
Load diff
7
public/uv.handler.js.map
Normal file
7
public/uv.handler.js.map
Normal file
File diff suppressed because one or more lines are too long
368
public/uv.sw.js
Normal file
368
public/uv.sw.js
Normal file
|
|
@ -0,0 +1,368 @@
|
||||||
|
(() => {
|
||||||
|
// src/uv.sw.js
|
||||||
|
var Ultraviolet = self.Ultraviolet;
|
||||||
|
var cspHeaders = [
|
||||||
|
"cross-origin-embedder-policy",
|
||||||
|
"cross-origin-opener-policy",
|
||||||
|
"cross-origin-resource-policy",
|
||||||
|
"content-security-policy",
|
||||||
|
"content-security-policy-report-only",
|
||||||
|
"expect-ct",
|
||||||
|
"feature-policy",
|
||||||
|
"origin-isolation",
|
||||||
|
"strict-transport-security",
|
||||||
|
"upgrade-insecure-requests",
|
||||||
|
"x-content-type-options",
|
||||||
|
"x-download-options",
|
||||||
|
"x-frame-options",
|
||||||
|
"x-permitted-cross-domain-policies",
|
||||||
|
"x-powered-by",
|
||||||
|
"x-xss-protection"
|
||||||
|
];
|
||||||
|
var emptyMethods = ["GET", "HEAD"];
|
||||||
|
var UVServiceWorker = class extends Ultraviolet.EventEmitter {
|
||||||
|
constructor(config = __uv$config) {
|
||||||
|
super();
|
||||||
|
if (!config.bare)
|
||||||
|
config.bare = "/bare/";
|
||||||
|
if (!config.prefix)
|
||||||
|
config.prefix = "/service/";
|
||||||
|
this.config = config;
|
||||||
|
const addresses = (Array.isArray(config.bare) ? config.bare : [config.bare]).map((str) => new URL(str, location).toString());
|
||||||
|
this.address = addresses[~~(Math.random() * addresses.length)];
|
||||||
|
this.bareClient = new Ultraviolet.BareClient(this.address);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Event & {request: Request}} param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async fetch({ request }) {
|
||||||
|
let fetchedURL;
|
||||||
|
try {
|
||||||
|
if (!request.url.startsWith(location.origin + this.config.prefix))
|
||||||
|
return await fetch(request);
|
||||||
|
const ultraviolet = new Ultraviolet(this.config, this.address);
|
||||||
|
if (typeof this.config.construct === "function") {
|
||||||
|
this.config.construct(ultraviolet, "service");
|
||||||
|
}
|
||||||
|
const db = await ultraviolet.cookie.db();
|
||||||
|
ultraviolet.meta.origin = location.origin;
|
||||||
|
ultraviolet.meta.base = ultraviolet.meta.url = new URL(
|
||||||
|
ultraviolet.sourceUrl(request.url)
|
||||||
|
);
|
||||||
|
const requestCtx = new RequestContext(
|
||||||
|
request,
|
||||||
|
this,
|
||||||
|
ultraviolet,
|
||||||
|
!emptyMethods.includes(request.method.toUpperCase()) ? await request.blob() : null
|
||||||
|
);
|
||||||
|
if (ultraviolet.meta.url.protocol === "blob:") {
|
||||||
|
requestCtx.blob = true;
|
||||||
|
requestCtx.base = requestCtx.url = new URL(
|
||||||
|
requestCtx.url.pathname
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (request.referrer && request.referrer.startsWith(location.origin)) {
|
||||||
|
const referer = new URL(
|
||||||
|
ultraviolet.sourceUrl(request.referrer)
|
||||||
|
);
|
||||||
|
if (requestCtx.headers.origin || ultraviolet.meta.url.origin !== referer.origin && request.mode === "cors") {
|
||||||
|
requestCtx.headers.origin = referer.origin;
|
||||||
|
}
|
||||||
|
requestCtx.headers.referer = referer.href;
|
||||||
|
}
|
||||||
|
const cookies = await ultraviolet.cookie.getCookies(db) || [];
|
||||||
|
const cookieStr = ultraviolet.cookie.serialize(
|
||||||
|
cookies,
|
||||||
|
ultraviolet.meta,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
requestCtx.headers["user-agent"] = navigator.userAgent;
|
||||||
|
if (cookieStr)
|
||||||
|
requestCtx.headers.cookie = cookieStr;
|
||||||
|
const reqEvent = new HookEvent(requestCtx, null, null);
|
||||||
|
this.emit("request", reqEvent);
|
||||||
|
if (reqEvent.intercepted)
|
||||||
|
return reqEvent.returnValue;
|
||||||
|
fetchedURL = requestCtx.blob ? "blob:" + location.origin + requestCtx.url.pathname : requestCtx.url;
|
||||||
|
const response = await this.bareClient.fetch(fetchedURL, {
|
||||||
|
headers: requestCtx.headers,
|
||||||
|
method: requestCtx.method,
|
||||||
|
body: requestCtx.body,
|
||||||
|
credentials: requestCtx.credentials,
|
||||||
|
mode: location.origin !== requestCtx.address.origin ? "cors" : requestCtx.mode,
|
||||||
|
cache: requestCtx.cache,
|
||||||
|
redirect: requestCtx.redirect
|
||||||
|
});
|
||||||
|
const responseCtx = new ResponseContext(requestCtx, response);
|
||||||
|
const resEvent = new HookEvent(responseCtx, null, null);
|
||||||
|
this.emit("beforemod", resEvent);
|
||||||
|
if (resEvent.intercepted)
|
||||||
|
return resEvent.returnValue;
|
||||||
|
for (const name of cspHeaders) {
|
||||||
|
if (responseCtx.headers[name])
|
||||||
|
delete responseCtx.headers[name];
|
||||||
|
}
|
||||||
|
if (responseCtx.headers.location) {
|
||||||
|
responseCtx.headers.location = ultraviolet.rewriteUrl(
|
||||||
|
responseCtx.headers.location
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (request.destination === "document") {
|
||||||
|
const header = responseCtx.headers["content-disposition"];
|
||||||
|
if (!/\s*?((inline|attachment);\s*?)filename=/i.test(header)) {
|
||||||
|
const type = /^\s*?attachment/i.test(header) ? "attachment" : "inline";
|
||||||
|
const [filename] = new URL(response.finalURL).pathname.split("/").slice(-1);
|
||||||
|
responseCtx.headers["content-disposition"] = `${type}; filename=${JSON.stringify(filename)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (responseCtx.headers["set-cookie"]) {
|
||||||
|
Promise.resolve(
|
||||||
|
ultraviolet.cookie.setCookies(
|
||||||
|
responseCtx.headers["set-cookie"],
|
||||||
|
db,
|
||||||
|
ultraviolet.meta
|
||||||
|
)
|
||||||
|
).then(() => {
|
||||||
|
self.clients.matchAll().then(function(clients) {
|
||||||
|
clients.forEach(function(client) {
|
||||||
|
client.postMessage({
|
||||||
|
msg: "updateCookies",
|
||||||
|
url: ultraviolet.meta.url.href
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
delete responseCtx.headers["set-cookie"];
|
||||||
|
}
|
||||||
|
if (responseCtx.body) {
|
||||||
|
switch (request.destination) {
|
||||||
|
case "script":
|
||||||
|
case "worker":
|
||||||
|
{
|
||||||
|
const scripts = [
|
||||||
|
ultraviolet.bundleScript,
|
||||||
|
ultraviolet.clientScript,
|
||||||
|
ultraviolet.configScript,
|
||||||
|
ultraviolet.handlerScript
|
||||||
|
].map((script) => JSON.stringify(script)).join(",");
|
||||||
|
responseCtx.body = `if (!self.__uv && self.importScripts) { ${ultraviolet.createJsInject(
|
||||||
|
this.address,
|
||||||
|
this.bareClient.manifest,
|
||||||
|
ultraviolet.cookie.serialize(
|
||||||
|
cookies,
|
||||||
|
ultraviolet.meta,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
request.referrer
|
||||||
|
)} importScripts(${scripts}); }
|
||||||
|
`;
|
||||||
|
responseCtx.body += ultraviolet.js.rewrite(
|
||||||
|
await response.text()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "style":
|
||||||
|
responseCtx.body = ultraviolet.rewriteCSS(
|
||||||
|
await response.text()
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "iframe":
|
||||||
|
case "document":
|
||||||
|
if (isHtml(
|
||||||
|
ultraviolet.meta.url,
|
||||||
|
responseCtx.headers["content-type"] || ""
|
||||||
|
)) {
|
||||||
|
responseCtx.body = ultraviolet.rewriteHtml(
|
||||||
|
await response.text(),
|
||||||
|
{
|
||||||
|
document: true,
|
||||||
|
injectHead: ultraviolet.createHtmlInject(
|
||||||
|
ultraviolet.handlerScript,
|
||||||
|
ultraviolet.bundleScript,
|
||||||
|
ultraviolet.clientScript,
|
||||||
|
ultraviolet.configScript,
|
||||||
|
this.address,
|
||||||
|
this.bareClient.manifest,
|
||||||
|
ultraviolet.cookie.serialize(
|
||||||
|
cookies,
|
||||||
|
ultraviolet.meta,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
request.referrer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (requestCtx.headers.accept === "text/event-stream") {
|
||||||
|
responseCtx.headers["content-type"] = "text/event-stream";
|
||||||
|
}
|
||||||
|
if (crossOriginIsolated) {
|
||||||
|
responseCtx.headers["Cross-Origin-Embedder-Policy"] = "require-corp";
|
||||||
|
}
|
||||||
|
this.emit("response", resEvent);
|
||||||
|
if (resEvent.intercepted)
|
||||||
|
return resEvent.returnValue;
|
||||||
|
return new Response(responseCtx.body, {
|
||||||
|
headers: responseCtx.headers,
|
||||||
|
status: responseCtx.status,
|
||||||
|
statusText: responseCtx.statusText
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (!["document", "iframe"].includes(request.destination))
|
||||||
|
return new Response(void 0, { status: 500 });
|
||||||
|
console.error(err);
|
||||||
|
return renderError(err, fetchedURL, this.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static Ultraviolet = Ultraviolet;
|
||||||
|
};
|
||||||
|
self.UVServiceWorker = UVServiceWorker;
|
||||||
|
var ResponseContext = class {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {RequestContext} request
|
||||||
|
* @param {import("@tomphttp/bare-client").BareResponseFetch} response
|
||||||
|
*/
|
||||||
|
constructor(request, response) {
|
||||||
|
this.request = request;
|
||||||
|
this.raw = response;
|
||||||
|
this.ultraviolet = request.ultraviolet;
|
||||||
|
this.headers = {};
|
||||||
|
for (const key in response.rawHeaders)
|
||||||
|
this.headers[key.toLowerCase()] = response.rawHeaders[key];
|
||||||
|
this.status = response.status;
|
||||||
|
this.statusText = response.statusText;
|
||||||
|
this.body = response.body;
|
||||||
|
}
|
||||||
|
get url() {
|
||||||
|
return this.request.url;
|
||||||
|
}
|
||||||
|
get base() {
|
||||||
|
return this.request.base;
|
||||||
|
}
|
||||||
|
set base(val) {
|
||||||
|
this.request.base = val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var RequestContext = class {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Request} request
|
||||||
|
* @param {UVServiceWorker} worker
|
||||||
|
* @param {Ultraviolet} ultraviolet
|
||||||
|
* @param {BodyInit} body
|
||||||
|
*/
|
||||||
|
constructor(request, worker, ultraviolet, body = null) {
|
||||||
|
this.ultraviolet = ultraviolet;
|
||||||
|
this.request = request;
|
||||||
|
this.headers = Object.fromEntries(request.headers.entries());
|
||||||
|
this.method = request.method;
|
||||||
|
this.address = worker.address;
|
||||||
|
this.body = body || null;
|
||||||
|
this.cache = request.cache;
|
||||||
|
this.redirect = request.redirect;
|
||||||
|
this.credentials = "omit";
|
||||||
|
this.mode = request.mode === "cors" ? request.mode : "same-origin";
|
||||||
|
this.blob = false;
|
||||||
|
}
|
||||||
|
get url() {
|
||||||
|
return this.ultraviolet.meta.url;
|
||||||
|
}
|
||||||
|
set url(val) {
|
||||||
|
this.ultraviolet.meta.url = val;
|
||||||
|
}
|
||||||
|
get base() {
|
||||||
|
return this.ultraviolet.meta.base;
|
||||||
|
}
|
||||||
|
set base(val) {
|
||||||
|
this.ultraviolet.meta.base = val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function isHtml(url, contentType = "") {
|
||||||
|
return (Ultraviolet.mime.contentType(contentType || url.pathname) || "text/html").split(";")[0] === "text/html";
|
||||||
|
}
|
||||||
|
var HookEvent = class {
|
||||||
|
#intercepted;
|
||||||
|
#returnValue;
|
||||||
|
constructor(data = {}, target = null, that = null) {
|
||||||
|
this.#intercepted = false;
|
||||||
|
this.#returnValue = null;
|
||||||
|
this.data = data;
|
||||||
|
this.target = target;
|
||||||
|
this.that = that;
|
||||||
|
}
|
||||||
|
get intercepted() {
|
||||||
|
return this.#intercepted;
|
||||||
|
}
|
||||||
|
get returnValue() {
|
||||||
|
return this.#returnValue;
|
||||||
|
}
|
||||||
|
respondWith(input) {
|
||||||
|
this.#returnValue = input;
|
||||||
|
this.#intercepted = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function hostnameErrorTemplate(fetchedURL, bareServer) {
|
||||||
|
const parsedFetchedURL = new URL(fetchedURL);
|
||||||
|
const script = `remoteHostname.textContent = ${JSON.stringify(
|
||||||
|
parsedFetchedURL.hostname
|
||||||
|
)};bareServer.href = ${JSON.stringify(bareServer)};uvHostname.textContent = ${JSON.stringify(location.hostname)};reload.addEventListener("click", () => location.reload());uvVersion.textContent = ${JSON.stringify(
|
||||||
|
"2.0.0"
|
||||||
|
)};`;
|
||||||
|
return `<!DOCTYPE html><html><head><meta charset='utf-8' /><title>Error</title></head><body><h1>This site can\u2019t be reached</h1><hr /><p><b id="remoteHostname"></b>\u2019s server IP address could not be found.</p><p>Try:</p><ul><li>Verifying you entered the correct address</li><li>Clearing the site data</li><li>Contacting <b id="uvHostname"></b>'s administrator</li><li>Verifying the <a id='bareServer' title='Bare server'>${config.bare}</a> isn't censored</li></ul><button id="reload">Reload</button><hr /><p><i>Ultraviolet v<span id="uvVersion"></span></i></p><script src="${"data:application/javascript," + encodeURIComponent(script)}"><\/script></body></html>`;
|
||||||
|
}
|
||||||
|
function errorTemplate(title, code, id, message, trace, fetchedURL, bareServer) {
|
||||||
|
if (message === "The specified host could not be resolved.")
|
||||||
|
return hostnameErrorTemplate(fetchedURL, bareServer);
|
||||||
|
const script = `errorTitle.textContent = ${JSON.stringify(title)};errorCode.textContent = ${JSON.stringify(code)};` + (id ? `errorId.textContent = ${JSON.stringify(id)};` : "") + `errorMessage.textContent = ${JSON.stringify(message)};errorTrace.value = ${JSON.stringify(trace)};fetchedURL.textContent = ${JSON.stringify(fetchedURL)};bareServer.href = ${JSON.stringify(bareServer)};for (const node of document.querySelectorAll("#uvHostname")) node.textContent = ${JSON.stringify(
|
||||||
|
location.hostname
|
||||||
|
)};reload.addEventListener("click", () => location.reload());uvVersion.textContent = ${JSON.stringify(
|
||||||
|
"2.0.0"
|
||||||
|
)};`;
|
||||||
|
return `<!DOCTYPE html><html><head><meta charset='utf-8' /><title>Error</title></head><body><h1 id='errorTitle'></h1><hr /><p>Failed to load <b id="fetchedURL"></b></p><p id="errorMessage"></p><table><tbody><tr><td>Code:</td><td id="errorCode"></td></tr>` + (id ? '<tr><td>ID:</td><td id="errorId"></td></tr>' : "") + `</tbody></table><textarea id="errorTrace" cols="40" rows="10" readonly></textarea><p>Try:</p><ul><li>Checking your internet connection</li><li>Verifying you entered the correct address</li><li>Clearing the site data</li><li>Contacting <b id="uvHostname"></b>'s administrator</li><li>Verify the <a id='bareServer' title='Bare server'>${config.bare}</a> isn't censored</li></ul><p>If you're the administrator of <b id="uvHostname"></b>, try:</p><ul><li>Restarting your Bare server</li><li>Updating Ultraviolet</li><li>Troubleshooting the error on the <a href="https://github.com/titaniumnetwork-dev/Ultraviolet" target="_blank">GitHub repository</a></li></ul><button id="reload">Reload</button><hr /><p><i>Ultraviolet v<span id="uvVersion"></span></i></p><script src="${"data:application/javascript," + encodeURIComponent(script)}"><\/script></body></html>`;
|
||||||
|
}
|
||||||
|
function isBareError(err) {
|
||||||
|
return err instanceof Error && typeof err.body === "object";
|
||||||
|
}
|
||||||
|
function renderError(err, fetchedURL, bareServer) {
|
||||||
|
let status;
|
||||||
|
let title;
|
||||||
|
let code;
|
||||||
|
let id = "";
|
||||||
|
let message;
|
||||||
|
if (isBareError(err)) {
|
||||||
|
status = err.status;
|
||||||
|
title = "Error communicating with the Bare server";
|
||||||
|
message = err.body.message;
|
||||||
|
code = err.body.code;
|
||||||
|
id = err.body.id;
|
||||||
|
} else {
|
||||||
|
status = 500;
|
||||||
|
title = "Error processing your request";
|
||||||
|
message = "Internal Server Error";
|
||||||
|
code = err instanceof Error ? err.name : "UNKNOWN";
|
||||||
|
}
|
||||||
|
return new Response(
|
||||||
|
errorTemplate(
|
||||||
|
title,
|
||||||
|
code,
|
||||||
|
id,
|
||||||
|
message,
|
||||||
|
String(err),
|
||||||
|
fetchedURL,
|
||||||
|
bareServer
|
||||||
|
),
|
||||||
|
{
|
||||||
|
status,
|
||||||
|
headers: {
|
||||||
|
"content-type": "text/html"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
//# sourceMappingURL=uv.sw.js.map
|
||||||
7
public/uv.sw.js.map
Normal file
7
public/uv.sw.js.map
Normal file
File diff suppressed because one or more lines are too long
39306
public/uv/uv.bundle.js
39306
public/uv/uv.bundle.js
File diff suppressed because one or more lines are too long
|
|
@ -1,38 +0,0 @@
|
||||||
self.xor = {
|
|
||||||
randomMax: 100,
|
|
||||||
randomMin: -100,
|
|
||||||
|
|
||||||
encode: (str) => {
|
|
||||||
if (!str) return str
|
|
||||||
return encodeURIComponent(
|
|
||||||
str
|
|
||||||
.toString()
|
|
||||||
.split('')
|
|
||||||
.map((char, ind) =>
|
|
||||||
ind % 2 ? String.fromCharCode(char.charCodeAt() ^ 2) : char
|
|
||||||
)
|
|
||||||
.join('')
|
|
||||||
)
|
|
||||||
},
|
|
||||||
decode: (str) => {
|
|
||||||
if (!str) return str
|
|
||||||
const [input, ...search] = str.split('?')
|
|
||||||
|
|
||||||
return (
|
|
||||||
decodeURIComponent(input)
|
|
||||||
.split('')
|
|
||||||
.map((char, ind) =>
|
|
||||||
ind % 2 ? String.fromCharCode(char.charCodeAt(0) ^ 2) : char
|
|
||||||
)
|
|
||||||
.join('') + (search.length ? '?' + search.join('?') : '')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.__uv$config = {
|
|
||||||
prefix: '/service/',
|
|
||||||
encodeUrl: self.xor.encode,
|
|
||||||
decodeUrl: self.xor.decode,
|
|
||||||
handler: '/uv/uv.handler.js',
|
|
||||||
bundle: '/uv/uv.bundle.js',
|
|
||||||
config: '/uv/uv.config.js',
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,789 +0,0 @@
|
||||||
importScripts('/uv/uv.bundle.js');
|
|
||||||
importScripts('/uv/uv.config.js');
|
|
||||||
|
|
||||||
class UVServiceWorker extends EventEmitter {
|
|
||||||
constructor(serverURL, config = __uv$config) {
|
|
||||||
super();
|
|
||||||
config.bare = serverURL + '/bare/';
|
|
||||||
this.addresses = typeof config.bare === 'string' ? [ new URL(config.bare, location) ] : config.bare.map(str => new URL(str, location));
|
|
||||||
this.headers = {
|
|
||||||
csp: [
|
|
||||||
'cross-origin-embedder-policy',
|
|
||||||
'cross-origin-opener-policy',
|
|
||||||
'cross-origin-resource-policy',
|
|
||||||
'content-security-policy',
|
|
||||||
'content-security-policy-report-only',
|
|
||||||
'expect-ct',
|
|
||||||
'feature-policy',
|
|
||||||
'origin-isolation',
|
|
||||||
'strict-transport-security',
|
|
||||||
'upgrade-insecure-requests',
|
|
||||||
'x-content-type-options',
|
|
||||||
'x-download-options',
|
|
||||||
'x-frame-options',
|
|
||||||
'x-permitted-cross-domain-policies',
|
|
||||||
'x-powered-by',
|
|
||||||
'x-xss-protection',
|
|
||||||
],
|
|
||||||
forward: [
|
|
||||||
'accept-encoding',
|
|
||||||
'connection',
|
|
||||||
'content-length',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
this.method = {
|
|
||||||
empty: [
|
|
||||||
'GET',
|
|
||||||
'HEAD'
|
|
||||||
]
|
|
||||||
};
|
|
||||||
this.statusCode = {
|
|
||||||
empty: [
|
|
||||||
204,
|
|
||||||
304,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
this.config = config;
|
|
||||||
this.browser = Ultraviolet.Bowser.getParser(self.navigator.userAgent).getBrowserName();
|
|
||||||
|
|
||||||
if (this.browser === 'Firefox') {
|
|
||||||
this.headers.forward.push('user-agent');
|
|
||||||
this.headers.forward.push('content-type');
|
|
||||||
};
|
|
||||||
};
|
|
||||||
async fetch({ request }) {
|
|
||||||
if (!request.url.startsWith(location.origin + (this.config.prefix || '/service/'))) {
|
|
||||||
return fetch(request);
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
|
|
||||||
const ultraviolet = new Ultraviolet(this.config);
|
|
||||||
|
|
||||||
if (typeof this.config.construct === 'function') {
|
|
||||||
this.config.construct(ultraviolet, 'service');
|
|
||||||
};
|
|
||||||
|
|
||||||
const db = await ultraviolet.cookie.db();
|
|
||||||
|
|
||||||
ultraviolet.meta.origin = location.origin;
|
|
||||||
ultraviolet.meta.base = ultraviolet.meta.url = new URL(ultraviolet.sourceUrl(request.url));
|
|
||||||
|
|
||||||
const requestCtx = new RequestContext(
|
|
||||||
request,
|
|
||||||
this,
|
|
||||||
ultraviolet,
|
|
||||||
!this.method.empty.includes(request.method.toUpperCase()) ? await request.blob() : null
|
|
||||||
);
|
|
||||||
|
|
||||||
if (ultraviolet.meta.url.protocol === 'blob:') {
|
|
||||||
requestCtx.blob = true;
|
|
||||||
requestCtx.base = requestCtx.url = new URL(requestCtx.url.pathname);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (request.referrer && request.referrer.startsWith(location.origin)) {
|
|
||||||
const referer = new URL(ultraviolet.sourceUrl(request.referrer));
|
|
||||||
|
|
||||||
if (requestCtx.headers.origin || ultraviolet.meta.url.origin !== referer.origin && request.mode === 'cors') {
|
|
||||||
requestCtx.headers.origin = referer.origin;
|
|
||||||
};
|
|
||||||
|
|
||||||
requestCtx.headers.referer = referer.href;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cookies = await ultraviolet.cookie.getCookies(db) || [];
|
|
||||||
const cookieStr = ultraviolet.cookie.serialize(cookies, ultraviolet.meta, false);
|
|
||||||
|
|
||||||
if (this.browser === 'Firefox' && !(request.destination === 'iframe' || request.destination === 'document')) {
|
|
||||||
requestCtx.forward.shift();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (cookieStr) requestCtx.headers.cookie = cookieStr;
|
|
||||||
requestCtx.headers.Host = requestCtx.url.host;
|
|
||||||
|
|
||||||
|
|
||||||
const reqEvent = new HookEvent(requestCtx, null, null);
|
|
||||||
this.emit('request', reqEvent);
|
|
||||||
|
|
||||||
if (reqEvent.intercepted) return reqEvent.returnValue;
|
|
||||||
|
|
||||||
const response = await fetch(requestCtx.send);
|
|
||||||
|
|
||||||
if (response.status === 500) {
|
|
||||||
return Promise.reject('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const responseCtx = new ResponseContext(requestCtx, response, this);
|
|
||||||
const resEvent = new HookEvent(responseCtx, null, null);
|
|
||||||
|
|
||||||
this.emit('beforemod', resEvent);
|
|
||||||
if (resEvent.intercepted) return resEvent.returnValue;
|
|
||||||
|
|
||||||
for (const name of this.headers.csp) {
|
|
||||||
if (responseCtx.headers[name]) delete responseCtx.headers[name];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (responseCtx.headers.location) {
|
|
||||||
responseCtx.headers.location = ultraviolet.rewriteUrl(responseCtx.headers.location);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (responseCtx.headers['set-cookie']) {
|
|
||||||
Promise.resolve(ultraviolet.cookie.setCookies(responseCtx.headers['set-cookie'], db, ultraviolet.meta)).then(() => {
|
|
||||||
self.clients.matchAll().then(function (clients){
|
|
||||||
clients.forEach(function(client){
|
|
||||||
client.postMessage({
|
|
||||||
msg: 'updateCookies',
|
|
||||||
url: ultraviolet.meta.url.href,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
delete responseCtx.headers['set-cookie'];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (responseCtx.body) {
|
|
||||||
switch(request.destination) {
|
|
||||||
case 'script':
|
|
||||||
case 'worker':
|
|
||||||
responseCtx.body = `if (!self.__uv && self.importScripts) importScripts('${__uv$config.bundle}', '${__uv$config.config}', '${__uv$config.handler}');\n`;
|
|
||||||
responseCtx.body += ultraviolet.js.rewrite(
|
|
||||||
await response.text()
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'style':
|
|
||||||
responseCtx.body = ultraviolet.rewriteCSS(
|
|
||||||
await response.text()
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'iframe':
|
|
||||||
case 'document':
|
|
||||||
if (isHtml(ultraviolet.meta.url, (responseCtx.headers['content-type'] || ''))) {
|
|
||||||
responseCtx.body = ultraviolet.rewriteHtml(
|
|
||||||
await response.text(),
|
|
||||||
{
|
|
||||||
document: true ,
|
|
||||||
injectHead: ultraviolet.createHtmlInject(
|
|
||||||
this.config.handler,
|
|
||||||
this.config.bundle,
|
|
||||||
this.config.config,
|
|
||||||
ultraviolet.cookie.serialize(cookies, ultraviolet.meta, true),
|
|
||||||
request.referrer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
if (requestCtx.headers.accept === 'text/event-stream') {
|
|
||||||
responseCtx.headers['content-type'] = 'text/event-stream';
|
|
||||||
};
|
|
||||||
|
|
||||||
this.emit('response', resEvent);
|
|
||||||
if (resEvent.intercepted) return resEvent.returnValue;
|
|
||||||
|
|
||||||
return new Response(responseCtx.body, {
|
|
||||||
headers: responseCtx.headers,
|
|
||||||
status: responseCtx.status,
|
|
||||||
statusText: responseCtx.statusText,
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch(err) {
|
|
||||||
return new Response(err.toString(), {
|
|
||||||
status: 500,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
getBarerResponse(response) {
|
|
||||||
const headers = {};
|
|
||||||
const raw = JSON.parse(response.headers.get('x-bare-headers'));
|
|
||||||
|
|
||||||
for (const key in raw) {
|
|
||||||
headers[key.toLowerCase()] = raw[key];
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
headers,
|
|
||||||
status: +response.headers.get('x-bare-status'),
|
|
||||||
statusText: response.headers.get('x-bare-status-text'),
|
|
||||||
body: !this.statusCode.empty.includes(+response.headers.get('x-bare-status')) ? response.body : null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
get address() {
|
|
||||||
return this.addresses[Math.floor(Math.random() * this.addresses.length)];
|
|
||||||
};
|
|
||||||
static Ultraviolet = Ultraviolet;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.UVServiceWorker = UVServiceWorker;
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseContext {
|
|
||||||
constructor(request, response, worker) {
|
|
||||||
const { headers, status, statusText, body } = !request.blob ? worker.getBarerResponse(response) : {
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
headers: Object.fromEntries([...response.headers.entries()]),
|
|
||||||
body: response.body,
|
|
||||||
};
|
|
||||||
this.request = request;
|
|
||||||
this.raw = response;
|
|
||||||
this.ultraviolet = request.ultraviolet;
|
|
||||||
this.headers = headers;
|
|
||||||
this.status = status;
|
|
||||||
this.statusText = statusText;
|
|
||||||
this.body = body;
|
|
||||||
};
|
|
||||||
get url() {
|
|
||||||
return this.request.url;
|
|
||||||
}
|
|
||||||
get base() {
|
|
||||||
return this.request.base;
|
|
||||||
};
|
|
||||||
set base(val) {
|
|
||||||
this.request.base = val;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
class RequestContext {
|
|
||||||
constructor(request, worker, ultraviolet, body = null) {
|
|
||||||
this.ultraviolet = ultraviolet;
|
|
||||||
this.request = request;
|
|
||||||
this.headers = Object.fromEntries([...request.headers.entries()]);
|
|
||||||
this.method = request.method;
|
|
||||||
this.forward = [...worker.headers.forward];
|
|
||||||
this.address = worker.address;
|
|
||||||
this.body = body || null;
|
|
||||||
this.redirect = request.redirect;
|
|
||||||
this.credentials = 'omit';
|
|
||||||
this.mode = request.mode === 'cors' ? request.mode : 'same-origin';
|
|
||||||
this.blob = false;
|
|
||||||
};
|
|
||||||
get send() {
|
|
||||||
return new Request((!this.blob ? this.address.href + 'v1/' : 'blob:' + location.origin + this.url.pathname), {
|
|
||||||
method: this.method,
|
|
||||||
headers: {
|
|
||||||
'x-bare-protocol': this.url.protocol,
|
|
||||||
'x-bare-host': this.url.hostname,
|
|
||||||
'x-bare-path': this.url.pathname + this.url.search,
|
|
||||||
'x-bare-port': this.url.port || (this.url.protocol === 'https:' ? '443' : '80'),
|
|
||||||
'x-bare-headers': JSON.stringify(this.headers),
|
|
||||||
'x-bare-forward-headers': JSON.stringify(this.forward),
|
|
||||||
},
|
|
||||||
redirect: this.redirect,
|
|
||||||
credentials: this.credentials,
|
|
||||||
mode: location.origin !== this.address.origin ? 'cors' : this.mode,
|
|
||||||
body: this.body
|
|
||||||
});
|
|
||||||
};
|
|
||||||
get url() {
|
|
||||||
return this.ultraviolet.meta.url;
|
|
||||||
};
|
|
||||||
set url(val) {
|
|
||||||
this.ultraviolet.meta.url = val;
|
|
||||||
};
|
|
||||||
get base() {
|
|
||||||
return this.ultraviolet.meta.base;
|
|
||||||
};
|
|
||||||
set base(val) {
|
|
||||||
this.ultraviolet.meta.base = val;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function isHtml(url, contentType = '') {
|
|
||||||
return (Ultraviolet.mime.contentType((contentType || url.pathname)) || 'text/html').split(';')[0] === 'text/html';
|
|
||||||
};
|
|
||||||
|
|
||||||
class HookEvent {
|
|
||||||
#intercepted;
|
|
||||||
#returnValue;
|
|
||||||
constructor(data = {}, target = null, that = null) {
|
|
||||||
this.#intercepted = false;
|
|
||||||
this.#returnValue = null;
|
|
||||||
this.data = data;
|
|
||||||
this.target = target;
|
|
||||||
this.that = that;
|
|
||||||
};
|
|
||||||
get intercepted() {
|
|
||||||
return this.#intercepted;
|
|
||||||
};
|
|
||||||
get returnValue() {
|
|
||||||
return this.#returnValue;
|
|
||||||
};
|
|
||||||
respondWith(input) {
|
|
||||||
this.#returnValue = input;
|
|
||||||
this.#intercepted = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
var R = typeof Reflect === 'object' ? Reflect : null
|
|
||||||
var ReflectApply = R && typeof R.apply === 'function'
|
|
||||||
? R.apply
|
|
||||||
: function ReflectApply(target, receiver, args) {
|
|
||||||
return Function.prototype.apply.call(target, receiver, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
var ReflectOwnKeys
|
|
||||||
if (R && typeof R.ownKeys === 'function') {
|
|
||||||
ReflectOwnKeys = R.ownKeys
|
|
||||||
} else if (Object.getOwnPropertySymbols) {
|
|
||||||
ReflectOwnKeys = function ReflectOwnKeys(target) {
|
|
||||||
return Object.getOwnPropertyNames(target)
|
|
||||||
.concat(Object.getOwnPropertySymbols(target));
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
ReflectOwnKeys = function ReflectOwnKeys(target) {
|
|
||||||
return Object.getOwnPropertyNames(target);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProcessEmitWarning(warning) {
|
|
||||||
if (console && console.warn) console.warn(warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) {
|
|
||||||
return value !== value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function EventEmitter() {
|
|
||||||
EventEmitter.init.call(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backwards-compat with node 0.10.x
|
|
||||||
EventEmitter.EventEmitter = EventEmitter;
|
|
||||||
|
|
||||||
EventEmitter.prototype._events = undefined;
|
|
||||||
EventEmitter.prototype._eventsCount = 0;
|
|
||||||
EventEmitter.prototype._maxListeners = undefined;
|
|
||||||
|
|
||||||
// By default EventEmitters will print a warning if more than 10 listeners are
|
|
||||||
// added to it. This is a useful default which helps finding memory leaks.
|
|
||||||
var defaultMaxListeners = 10;
|
|
||||||
|
|
||||||
function checkListener(listener) {
|
|
||||||
if (typeof listener !== 'function') {
|
|
||||||
throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
|
|
||||||
enumerable: true,
|
|
||||||
get: function() {
|
|
||||||
return defaultMaxListeners;
|
|
||||||
},
|
|
||||||
set: function(arg) {
|
|
||||||
if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {
|
|
||||||
throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.');
|
|
||||||
}
|
|
||||||
defaultMaxListeners = arg;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
EventEmitter.init = function() {
|
|
||||||
|
|
||||||
if (this._events === undefined ||
|
|
||||||
this._events === Object.getPrototypeOf(this)._events) {
|
|
||||||
this._events = Object.create(null);
|
|
||||||
this._eventsCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._maxListeners = this._maxListeners || undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Obviously not all Emitters should be limited to 10. This function allows
|
|
||||||
// that to be increased. Set to zero for unlimited.
|
|
||||||
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
|
|
||||||
if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
|
|
||||||
throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
|
|
||||||
}
|
|
||||||
this._maxListeners = n;
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
function _getMaxListeners(that) {
|
|
||||||
if (that._maxListeners === undefined)
|
|
||||||
return EventEmitter.defaultMaxListeners;
|
|
||||||
return that._maxListeners;
|
|
||||||
}
|
|
||||||
|
|
||||||
EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
|
|
||||||
return _getMaxListeners(this);
|
|
||||||
};
|
|
||||||
|
|
||||||
EventEmitter.prototype.emit = function emit(type) {
|
|
||||||
var args = [];
|
|
||||||
for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);
|
|
||||||
var doError = (type === 'error');
|
|
||||||
|
|
||||||
var events = this._events;
|
|
||||||
if (events !== undefined)
|
|
||||||
doError = (doError && events.error === undefined);
|
|
||||||
else if (!doError)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// If there is no 'error' event listener then throw.
|
|
||||||
if (doError) {
|
|
||||||
var er;
|
|
||||||
if (args.length > 0)
|
|
||||||
er = args[0];
|
|
||||||
if (er instanceof Error) {
|
|
||||||
// Note: The comments on the `throw` lines are intentional, they show
|
|
||||||
// up in Node's output if this results in an unhandled exception.
|
|
||||||
throw er; // Unhandled 'error' event
|
|
||||||
}
|
|
||||||
// At least give some kind of context to the user
|
|
||||||
var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : ''));
|
|
||||||
err.context = er;
|
|
||||||
throw err; // Unhandled 'error' event
|
|
||||||
}
|
|
||||||
|
|
||||||
var handler = events[type];
|
|
||||||
|
|
||||||
if (handler === undefined)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (typeof handler === 'function') {
|
|
||||||
ReflectApply(handler, this, args);
|
|
||||||
} else {
|
|
||||||
var len = handler.length;
|
|
||||||
var listeners = arrayClone(handler, len);
|
|
||||||
for (var i = 0; i < len; ++i)
|
|
||||||
ReflectApply(listeners[i], this, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
function _addListener(target, type, listener, prepend) {
|
|
||||||
var m;
|
|
||||||
var events;
|
|
||||||
var existing;
|
|
||||||
|
|
||||||
checkListener(listener);
|
|
||||||
|
|
||||||
events = target._events;
|
|
||||||
if (events === undefined) {
|
|
||||||
events = target._events = Object.create(null);
|
|
||||||
target._eventsCount = 0;
|
|
||||||
} else {
|
|
||||||
// To avoid recursion in the case that type === "newListener"! Before
|
|
||||||
// adding it to the listeners, first emit "newListener".
|
|
||||||
if (events.newListener !== undefined) {
|
|
||||||
target.emit('newListener', type,
|
|
||||||
listener.listener ? listener.listener : listener);
|
|
||||||
|
|
||||||
// Re-assign `events` because a newListener handler could have caused the
|
|
||||||
// this._events to be assigned to a new object
|
|
||||||
events = target._events;
|
|
||||||
}
|
|
||||||
existing = events[type];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existing === undefined) {
|
|
||||||
// Optimize the case of one listener. Don't need the extra array object.
|
|
||||||
existing = events[type] = listener;
|
|
||||||
++target._eventsCount;
|
|
||||||
} else {
|
|
||||||
if (typeof existing === 'function') {
|
|
||||||
// Adding the second element, need to change to array.
|
|
||||||
existing = events[type] =
|
|
||||||
prepend ? [listener, existing] : [existing, listener];
|
|
||||||
// If we've already got an array, just append.
|
|
||||||
} else if (prepend) {
|
|
||||||
existing.unshift(listener);
|
|
||||||
} else {
|
|
||||||
existing.push(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for listener leak
|
|
||||||
m = _getMaxListeners(target);
|
|
||||||
if (m > 0 && existing.length > m && !existing.warned) {
|
|
||||||
existing.warned = true;
|
|
||||||
// No error code for this since it is a Warning
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
var w = new Error('Possible EventEmitter memory leak detected. ' +
|
|
||||||
existing.length + ' ' + String(type) + ' listeners ' +
|
|
||||||
'added. Use emitter.setMaxListeners() to ' +
|
|
||||||
'increase limit');
|
|
||||||
w.name = 'MaxListenersExceededWarning';
|
|
||||||
w.emitter = target;
|
|
||||||
w.type = type;
|
|
||||||
w.count = existing.length;
|
|
||||||
ProcessEmitWarning(w);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
EventEmitter.prototype.addListener = function addListener(type, listener) {
|
|
||||||
return _addListener(this, type, listener, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
|
|
||||||
|
|
||||||
EventEmitter.prototype.prependListener =
|
|
||||||
function prependListener(type, listener) {
|
|
||||||
return _addListener(this, type, listener, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
function onceWrapper() {
|
|
||||||
if (!this.fired) {
|
|
||||||
this.target.removeListener(this.type, this.wrapFn);
|
|
||||||
this.fired = true;
|
|
||||||
if (arguments.length === 0)
|
|
||||||
return this.listener.call(this.target);
|
|
||||||
return this.listener.apply(this.target, arguments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _onceWrap(target, type, listener) {
|
|
||||||
var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };
|
|
||||||
var wrapped = onceWrapper.bind(state);
|
|
||||||
wrapped.listener = listener;
|
|
||||||
state.wrapFn = wrapped;
|
|
||||||
return wrapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
EventEmitter.prototype.once = function once(type, listener) {
|
|
||||||
checkListener(listener);
|
|
||||||
this.on(type, _onceWrap(this, type, listener));
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
EventEmitter.prototype.prependOnceListener =
|
|
||||||
function prependOnceListener(type, listener) {
|
|
||||||
checkListener(listener);
|
|
||||||
this.prependListener(type, _onceWrap(this, type, listener));
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Emits a 'removeListener' event if and only if the listener was removed.
|
|
||||||
EventEmitter.prototype.removeListener =
|
|
||||||
function removeListener(type, listener) {
|
|
||||||
var list, events, position, i, originalListener;
|
|
||||||
|
|
||||||
checkListener(listener);
|
|
||||||
|
|
||||||
events = this._events;
|
|
||||||
if (events === undefined)
|
|
||||||
return this;
|
|
||||||
|
|
||||||
list = events[type];
|
|
||||||
if (list === undefined)
|
|
||||||
return this;
|
|
||||||
|
|
||||||
if (list === listener || list.listener === listener) {
|
|
||||||
if (--this._eventsCount === 0)
|
|
||||||
this._events = Object.create(null);
|
|
||||||
else {
|
|
||||||
delete events[type];
|
|
||||||
if (events.removeListener)
|
|
||||||
this.emit('removeListener', type, list.listener || listener);
|
|
||||||
}
|
|
||||||
} else if (typeof list !== 'function') {
|
|
||||||
position = -1;
|
|
||||||
|
|
||||||
for (i = list.length - 1; i >= 0; i--) {
|
|
||||||
if (list[i] === listener || list[i].listener === listener) {
|
|
||||||
originalListener = list[i].listener;
|
|
||||||
position = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position < 0)
|
|
||||||
return this;
|
|
||||||
|
|
||||||
if (position === 0)
|
|
||||||
list.shift();
|
|
||||||
else {
|
|
||||||
spliceOne(list, position);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (list.length === 1)
|
|
||||||
events[type] = list[0];
|
|
||||||
|
|
||||||
if (events.removeListener !== undefined)
|
|
||||||
this.emit('removeListener', type, originalListener || listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
|
|
||||||
|
|
||||||
EventEmitter.prototype.removeAllListeners =
|
|
||||||
function removeAllListeners(type) {
|
|
||||||
var listeners, events, i;
|
|
||||||
|
|
||||||
events = this._events;
|
|
||||||
if (events === undefined)
|
|
||||||
return this;
|
|
||||||
|
|
||||||
// not listening for removeListener, no need to emit
|
|
||||||
if (events.removeListener === undefined) {
|
|
||||||
if (arguments.length === 0) {
|
|
||||||
this._events = Object.create(null);
|
|
||||||
this._eventsCount = 0;
|
|
||||||
} else if (events[type] !== undefined) {
|
|
||||||
if (--this._eventsCount === 0)
|
|
||||||
this._events = Object.create(null);
|
|
||||||
else
|
|
||||||
delete events[type];
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// emit removeListener for all listeners on all events
|
|
||||||
if (arguments.length === 0) {
|
|
||||||
var keys = Object.keys(events);
|
|
||||||
var key;
|
|
||||||
for (i = 0; i < keys.length; ++i) {
|
|
||||||
key = keys[i];
|
|
||||||
if (key === 'removeListener') continue;
|
|
||||||
this.removeAllListeners(key);
|
|
||||||
}
|
|
||||||
this.removeAllListeners('removeListener');
|
|
||||||
this._events = Object.create(null);
|
|
||||||
this._eventsCount = 0;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
listeners = events[type];
|
|
||||||
|
|
||||||
if (typeof listeners === 'function') {
|
|
||||||
this.removeListener(type, listeners);
|
|
||||||
} else if (listeners !== undefined) {
|
|
||||||
// LIFO order
|
|
||||||
for (i = listeners.length - 1; i >= 0; i--) {
|
|
||||||
this.removeListener(type, listeners[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
function _listeners(target, type, unwrap) {
|
|
||||||
var events = target._events;
|
|
||||||
|
|
||||||
if (events === undefined)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
var evlistener = events[type];
|
|
||||||
if (evlistener === undefined)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
if (typeof evlistener === 'function')
|
|
||||||
return unwrap ? [evlistener.listener || evlistener] : [evlistener];
|
|
||||||
|
|
||||||
return unwrap ?
|
|
||||||
unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
EventEmitter.prototype.listeners = function listeners(type) {
|
|
||||||
return _listeners(this, type, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
EventEmitter.prototype.rawListeners = function rawListeners(type) {
|
|
||||||
return _listeners(this, type, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
EventEmitter.listenerCount = function(emitter, type) {
|
|
||||||
if (typeof emitter.listenerCount === 'function') {
|
|
||||||
return emitter.listenerCount(type);
|
|
||||||
} else {
|
|
||||||
return listenerCount.call(emitter, type);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
EventEmitter.prototype.listenerCount = listenerCount;
|
|
||||||
function listenerCount(type) {
|
|
||||||
var events = this._events;
|
|
||||||
|
|
||||||
if (events !== undefined) {
|
|
||||||
var evlistener = events[type];
|
|
||||||
|
|
||||||
if (typeof evlistener === 'function') {
|
|
||||||
return 1;
|
|
||||||
} else if (evlistener !== undefined) {
|
|
||||||
return evlistener.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
EventEmitter.prototype.eventNames = function eventNames() {
|
|
||||||
return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];
|
|
||||||
};
|
|
||||||
|
|
||||||
function arrayClone(arr, n) {
|
|
||||||
var copy = new Array(n);
|
|
||||||
for (var i = 0; i < n; ++i)
|
|
||||||
copy[i] = arr[i];
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
|
|
||||||
function spliceOne(list, index) {
|
|
||||||
for (; index + 1 < list.length; index++)
|
|
||||||
list[index] = list[index + 1];
|
|
||||||
list.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
function unwrapListeners(arr) {
|
|
||||||
var ret = new Array(arr.length);
|
|
||||||
for (var i = 0; i < ret.length; ++i) {
|
|
||||||
ret[i] = arr[i].listener || arr[i];
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function once(emitter, name) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
function errorListener(err) {
|
|
||||||
emitter.removeListener(name, resolver);
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolver() {
|
|
||||||
if (typeof emitter.removeListener === 'function') {
|
|
||||||
emitter.removeListener('error', errorListener);
|
|
||||||
}
|
|
||||||
resolve([].slice.call(arguments));
|
|
||||||
};
|
|
||||||
|
|
||||||
eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });
|
|
||||||
if (name !== 'error') {
|
|
||||||
addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addErrorHandlerIfEventEmitter(emitter, handler, flags) {
|
|
||||||
if (typeof emitter.on === 'function') {
|
|
||||||
eventTargetAgnosticAddListener(emitter, 'error', handler, flags);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
|
|
||||||
if (typeof emitter.on === 'function') {
|
|
||||||
if (flags.once) {
|
|
||||||
emitter.once(name, listener);
|
|
||||||
} else {
|
|
||||||
emitter.on(name, listener);
|
|
||||||
}
|
|
||||||
} else if (typeof emitter.addEventListener === 'function') {
|
|
||||||
// EventTarget does not have `error` event semantics like Node
|
|
||||||
// EventEmitters, we do not listen for `error` events here.
|
|
||||||
emitter.addEventListener(name, function wrapListener(arg) {
|
|
||||||
// IE does not have builtin `{ once: true }` support so we
|
|
||||||
// have to do it manually.
|
|
||||||
if (flags.once) {
|
|
||||||
emitter.removeEventListener(name, wrapListener);
|
|
||||||
}
|
|
||||||
listener(arg);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
30
src/HTML.ts
30
src/HTML.ts
|
|
@ -196,6 +196,24 @@ export default class HTML {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepend an element. Typically used as a `.prepend(new HTML(...))` call.
|
||||||
|
* @param elem The element to prepend.
|
||||||
|
* @returns HTML
|
||||||
|
*/
|
||||||
|
prepend (elem: string | HTMLElement | HTML): HTML {
|
||||||
|
if (elem instanceof HTMLElement) {
|
||||||
|
this.elm.prepend(elem)
|
||||||
|
} else if (elem instanceof HTML) {
|
||||||
|
this.elm.prepend(elem.elm)
|
||||||
|
} else if (typeof elem === 'string') {
|
||||||
|
const newElem = document.createElement(elem)
|
||||||
|
this.elm.prepend(newElem)
|
||||||
|
return new HTML(newElem.tagName)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Append multiple elements. Typically used as a `.appendMany(new HTML(...), new HTML(...)` call.
|
* Append multiple elements. Typically used as a `.appendMany(new HTML(...), new HTML(...)` call.
|
||||||
* @param elements The elements to append.
|
* @param elements The elements to append.
|
||||||
|
|
@ -208,6 +226,18 @@ export default class HTML {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepend multiple elements. Typically used as a `.prependMany(new HTML(...), new HTML(...)` call.
|
||||||
|
* @param elements The elements to prepend.
|
||||||
|
* @returns HTML
|
||||||
|
*/
|
||||||
|
prependMany (...elements: any[]): HTML {
|
||||||
|
for (const elem of elements) {
|
||||||
|
this.prepend(elem)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the innerHTML of the element.
|
* Clear the innerHTML of the element.
|
||||||
* @returns HTML
|
* @returns HTML
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
@import url(https://api.fontshare.com/v2/css?f[]=satoshi@1,2&display=swap);
|
@import url(https://api.fontshare.com/v2/css?f[]=satoshi@1,2&display=swap);
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--primary: #89b4fa;
|
||||||
--text: #cdd6f4;
|
--text: #cdd6f4;
|
||||||
--surface-2: #585b70;
|
--surface-2: #585b70;
|
||||||
--surface-1: #45475a;
|
--surface-1: #45475a;
|
||||||
|
|
@ -48,9 +49,6 @@ html {
|
||||||
|
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
.bx {
|
|
||||||
font-size: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
toolbar {
|
toolbar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -59,12 +57,32 @@ toolbar {
|
||||||
background: var(--mantle);
|
background: var(--mantle);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
border-radius: 20px 20px 0 0;
|
||||||
|
border: 1px solid var(--surface-0);
|
||||||
|
box-shadow: 1px 3px 10px rgba(0, 0, 0, 0.2), 0 0px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
user-select: none;
|
||||||
|
position: relvative;
|
||||||
|
z-index: 1001;
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
&.outlined {
|
&.outlined {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: var(--base);
|
background: var(--base);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--surface-0);
|
||||||
|
box-shadow: 1px 3px 10px rgba(0, 0, 0, 0.1), 0 0px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-toolbar-id="start"] {
|
||||||
|
cursor: pointer;
|
||||||
|
& * {
|
||||||
|
transition: color .1s ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-toolbar-id="controls"] {
|
&[data-toolbar-id="controls"] {
|
||||||
|
|
@ -76,6 +94,7 @@ toolbar {
|
||||||
|
|
||||||
&[data-toolbar-id="plugins"] {
|
&[data-toolbar-id="plugins"] {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
|
cursor: pointer;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,7 +134,8 @@ window-area {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.2), 0 0px 10px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.2), 0 0px 10px rgba(0, 0, 0, 0.2);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid;
|
||||||
|
border-color: color-mix(in srgb, var(--text) 20%, transparent);
|
||||||
background: var(--crust);
|
background: var(--crust);
|
||||||
transition: 0.2s opacity, 0.2s transform;
|
transition: 0.2s opacity, 0.2s transform;
|
||||||
|
|
||||||
|
|
@ -147,103 +167,9 @@ window-area {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
launcher {
|
.gradient-blur {
|
||||||
position: absolute;
|
mask: linear-gradient(to bottom, transparent, black 5%), linear-gradient(to top, transparent, black 35%);
|
||||||
display: flex;
|
mask-size: 100% 50%;
|
||||||
flex-direction: column;
|
mask-repeat: no-repeat;
|
||||||
justify-content: center;
|
mask-position: top, bottom;
|
||||||
align-items: center;
|
|
||||||
top: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
z-index: 99999999999999999999999;
|
|
||||||
width: calc(100vw + 20px);
|
|
||||||
height: calc(100vh + 20px);
|
|
||||||
gap: 20px;
|
|
||||||
transition: 0.2s opacity, 0.2s backdrop-filter;
|
|
||||||
margin: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
apps {
|
|
||||||
max-height: 70vh;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 40px;
|
|
||||||
justify-content: center;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 40px;
|
|
||||||
|
|
||||||
app {
|
|
||||||
flex: 1 0 21%;
|
|
||||||
flex-grow: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
min-width: 125px;
|
|
||||||
max-width: 125px;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: var(--app-radius);
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
width: fit-content;
|
|
||||||
height: 100%;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
background: var(--mantle);
|
|
||||||
border: 2px solid var(--crust);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
width: 300px;
|
|
||||||
max-width: 100vw;
|
|
||||||
text-align: center;
|
|
||||||
transition: border 0.2s;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
border: 2px solid var(--text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
preloader {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 9999999;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
background: var(--crust);
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: opacity 1s;
|
|
||||||
|
|
||||||
.status, .done {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.done div {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flex {
|
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
133
src/bootloader.ts
Normal file
133
src/bootloader.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
/**
|
||||||
|
* FlowOS Bootloader
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Kernel, { spaces } from './kernel'
|
||||||
|
import HTML from './HTML'
|
||||||
|
|
||||||
|
import logo from './assets/flow.png'
|
||||||
|
|
||||||
|
const body = new HTML(document.body)
|
||||||
|
|
||||||
|
body.html('<style>* { box-sizing: border-box }</style>')
|
||||||
|
body.style({
|
||||||
|
margin: '0',
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
overflow: 'hidden'
|
||||||
|
})
|
||||||
|
|
||||||
|
const boot = new HTML('div').styleJs({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
background: '#11111b',
|
||||||
|
padding: '100px',
|
||||||
|
'font-family': 'monospace',
|
||||||
|
userSelect: 'none',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}).appendTo(body)
|
||||||
|
|
||||||
|
boot.appendMany(
|
||||||
|
new HTML('div')
|
||||||
|
.styleJs({
|
||||||
|
display: 'flex',
|
||||||
|
height: '40px',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '10px'
|
||||||
|
})
|
||||||
|
.appendMany(
|
||||||
|
new HTML('img').attr({
|
||||||
|
src: logo,
|
||||||
|
height: '40px'
|
||||||
|
}),
|
||||||
|
new HTML('h1').text('FlowOS').styleJs({
|
||||||
|
color: 'white'
|
||||||
|
})
|
||||||
|
),
|
||||||
|
new HTML('img').attr({
|
||||||
|
src: logo
|
||||||
|
}).styleJs({
|
||||||
|
position: 'absolute',
|
||||||
|
right: '-8vw',
|
||||||
|
top: '-7vw',
|
||||||
|
opacity: '0.03',
|
||||||
|
height: '50vw',
|
||||||
|
'pointer-events': 'none',
|
||||||
|
zIndex: '0'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const terminal = new HTML('div').style({
|
||||||
|
color: '#89b4fa',
|
||||||
|
padding: '10px 3px',
|
||||||
|
'word-break': 'break-all',
|
||||||
|
'white-space': 'pre-wrap',
|
||||||
|
flex: '1',
|
||||||
|
'user-select': 'text',
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: '2'
|
||||||
|
}).appendTo(boot)
|
||||||
|
|
||||||
|
const progress = new HTML('div').style({
|
||||||
|
width: '0',
|
||||||
|
background: '#89b4fa',
|
||||||
|
transition: 'width 0.5s cubic-bezier(1,0,0,1)',
|
||||||
|
height: '5px'
|
||||||
|
})
|
||||||
|
new HTML('div').style({
|
||||||
|
height: '5px',
|
||||||
|
width: '100%',
|
||||||
|
background: '#181825'
|
||||||
|
}).appendTo(boot)
|
||||||
|
.append(progress)
|
||||||
|
|
||||||
|
const write = (content: string): void => {
|
||||||
|
terminal.text(terminal.getText() + content)
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeln = (content = ''): void => {
|
||||||
|
write(`${content}\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalConsoleLog = console.log
|
||||||
|
const originalConsoleError = console.error
|
||||||
|
const originalConsoleWarn = console.warn
|
||||||
|
const originalConsoleGroup = console.group
|
||||||
|
window.console.log = (...args: any) => {
|
||||||
|
originalConsoleLog(...args)
|
||||||
|
writeln(args)
|
||||||
|
}
|
||||||
|
window.console.warn = (...args: any) => {
|
||||||
|
originalConsoleWarn(...args)
|
||||||
|
writeln(args)
|
||||||
|
}
|
||||||
|
window.console.error = (...args: any) => {
|
||||||
|
originalConsoleError(...args)
|
||||||
|
writeln(args)
|
||||||
|
}
|
||||||
|
window.console.group = (...args: any) => {
|
||||||
|
originalConsoleGroup(...args)
|
||||||
|
writeln(spaces + String(args))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const args = new URLSearchParams(window.location.search)
|
||||||
|
const kernel = new Kernel()
|
||||||
|
writeln('/-----------------------------------------------\\')
|
||||||
|
writeln('| FlowOS is now discontinued. Starting in 10s...|')
|
||||||
|
writeln('\\-----------------------------------------------/')
|
||||||
|
setTimeout(() => {
|
||||||
|
kernel.boot(boot, progress, args).catch(e => console.error(e))
|
||||||
|
}, 10000)
|
||||||
|
} catch (e) {
|
||||||
|
writeln()
|
||||||
|
writeln('An error occured while booting FlowOS.')
|
||||||
|
writeln('Please report this error to Flow Works.')
|
||||||
|
writeln()
|
||||||
|
console.error(e.stack)
|
||||||
|
writeln()
|
||||||
|
terminal.html(terminal.getHTML() + '<a href="#" onclick="indexedDB.deleteDatabase(`virtualfs`);window.location.reload()">Would you like to reset the VirtualFS?</a>')
|
||||||
|
}
|
||||||
161
src/kernel.ts
161
src/kernel.ts
|
|
@ -1,27 +1,42 @@
|
||||||
import './assets/style.less'
|
import pkg from '../package.json'
|
||||||
import { version } from '../package.json'
|
import VirtualFS from './system/VirtualFS'
|
||||||
import { v4 as uuid } from 'uuid'
|
import HTML from './HTML'
|
||||||
|
import { Executable, KernelConfig, Package, Permission, Process, ProcessInfo, FileSystem } from './types'
|
||||||
import ProcessLib from './structures/ProcessLib'
|
import ProcessLib from './structures/ProcessLib'
|
||||||
import ProcLib from './structures/ProcLib'
|
|
||||||
import { Executable, Process, Package, ProcessInfo, KernelConfig, Permission } from './types'
|
|
||||||
import semver from 'semver'
|
import semver from 'semver'
|
||||||
|
import ProcLib from './structures/ProcLib'
|
||||||
|
import { v4 as uuid } from 'uuid'
|
||||||
|
import eruda from 'eruda'
|
||||||
|
import { parse } from 'js-ini'
|
||||||
|
|
||||||
declare global {
|
export const spaces = ' '
|
||||||
interface Window {
|
|
||||||
kernel: Kernel
|
const print = {
|
||||||
}
|
ok: (action: string, text: string) => console.log(`[ OK ] ${action} ${text}`),
|
||||||
|
failed: (action: string, text: string, error: any) => console.error(`[FAILED] ${action} ${text}`),
|
||||||
|
none: (action: string, text: string) => console.group(`${action} ${text}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search)
|
const handle = async (type: 'target' | 'service' | 'mount', name: string, Instance: any): Promise<boolean | any> => {
|
||||||
|
try {
|
||||||
async function enableDebug (): Promise<void> {
|
if (type !== 'target') print.none(type === 'mount' ? 'Mounting' : 'Starting', name)
|
||||||
const { default: eruda } = await import('eruda')
|
const instance = typeof Instance === 'object' ? Instance : new Instance()
|
||||||
eruda.init()
|
const data = await instance.init()
|
||||||
return await Promise.resolve()
|
print.ok(
|
||||||
|
type === 'service'
|
||||||
|
? 'Started'
|
||||||
|
: type === 'mount'
|
||||||
|
? 'Mounted'
|
||||||
|
: 'Reached target',
|
||||||
|
name
|
||||||
|
)
|
||||||
|
console.groupEnd()
|
||||||
|
return typeof Instance === 'object' ? data : instance
|
||||||
|
} catch (e) {
|
||||||
|
print.failed('Failed', `to start ${name}`, e)
|
||||||
|
console.error(`${spaces}${e.stack.split('\n').join(`\n${spaces}`) as string}`)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.get('debug') != null) {
|
|
||||||
enableDebug().catch(e => console.error(e))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Kernel {
|
export default class Kernel {
|
||||||
|
|
@ -32,31 +47,101 @@ export default class Kernel {
|
||||||
[key: string]: Package
|
[key: string]: Package
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
fs: any
|
fs: FileSystem | false
|
||||||
|
|
||||||
config: KernelConfig
|
config: KernelConfig | false
|
||||||
lastPid: number = 0
|
lastPid: number = 0
|
||||||
|
|
||||||
constructor (version: string) {
|
constructor () {
|
||||||
this.codename = 'Mochi'
|
this.codename = 'Pocky'
|
||||||
this.version = version
|
this.version = pkg.version
|
||||||
}
|
}
|
||||||
|
|
||||||
setFS (fs: any, process: ProcessLib): void {
|
private async setTheme (themeName: string): Promise<void> {
|
||||||
if (process.permission === Permission.SYSTEM) {
|
if (this.fs === false) throw new Error('Filesystem hasn\'t been initiated.')
|
||||||
this.fs = fs
|
const file = await this.fs.readFile(`/etc/themes/${themeName}.theme`)
|
||||||
|
const { extras, colors } = JSON.parse(Buffer.from(file).toString())
|
||||||
|
for (const color in colors) {
|
||||||
|
document.documentElement.style.setProperty(`--${color}`, colors[color])
|
||||||
}
|
}
|
||||||
|
document.documentElement.style.setProperty('--primary', extras[parse(Buffer.from(await this.fs.readFile('/etc/flow')).toString()).THEME_PRIMARY as string])
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfig (data: any, process: ProcessLib): void {
|
async boot (boot: HTML, progress: HTML, args: URLSearchParams): Promise<void> {
|
||||||
if (process.permission === Permission.SYSTEM) {
|
progress.style({ width: '0%' })
|
||||||
this.config = data
|
const bootArgs = args.toString().replace(/=($|&)/g, '=true ')
|
||||||
document.dispatchEvent(new CustomEvent('config_update', {
|
console.log(`FlowOS - v${pkg.version}, Flow Works (c) ${new Date().getFullYear()}`)
|
||||||
detail: {
|
console.log()
|
||||||
config: this.config
|
console.log(`User Agent : ${navigator.userAgent}`)
|
||||||
|
console.log(`Boot Args : ${bootArgs === '' ? 'None' : bootArgs}`)
|
||||||
|
console.log()
|
||||||
|
console.log('...')
|
||||||
|
console.log()
|
||||||
|
if (args.has('debug')) eruda.init()
|
||||||
|
this.fs = await handle('target', 'Virtual File Systems', VirtualFS)
|
||||||
|
if (this.fs === false) return
|
||||||
|
else progress.style({ width: '20%' })
|
||||||
|
this.config = await handle('target', 'FlowOS Configuration', {
|
||||||
|
init: async () => {
|
||||||
|
if (this.fs === false) return
|
||||||
|
return parse(Buffer.from(await this.fs.readFile('/etc/flow')).toString()) as any
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
|
if (this.config === false) return
|
||||||
|
else progress.style({ width: '40%' })
|
||||||
|
await this.setTheme(this.config.THEME)
|
||||||
|
document.addEventListener('theme_update', () => {
|
||||||
|
if (this.config === false) return
|
||||||
|
this.setTheme(this.config.THEME).catch(e => console.error(e))
|
||||||
|
})
|
||||||
|
const tmp = await handle('mount', 'Temporary Directory (/tmp)', {
|
||||||
|
init: async () => {
|
||||||
|
if (this.fs === false) return false
|
||||||
|
if (await this.fs.exists('/tmp')) {
|
||||||
|
await this.fs.rmdir('/tmp')
|
||||||
}
|
}
|
||||||
|
return await this.fs.mkdir('/tmp')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (tmp === false) return
|
||||||
|
else progress.style({ width: '60%' })
|
||||||
|
const sw = await handle('service', 'Service Worker', {
|
||||||
|
init: async () => {
|
||||||
|
if (this.config === false) return false
|
||||||
|
const registrations = await navigator.serviceWorker.getRegistrations()
|
||||||
|
for (const registration of registrations) {
|
||||||
|
await registration.unregister()
|
||||||
|
}
|
||||||
|
await navigator.serviceWorker.register(`/uv-sw.js?url=${encodeURIComponent(btoa(this.config.SERVER))}&e=${uuid()}`, {
|
||||||
|
scope: '/service/'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (sw === false) return
|
||||||
|
else progress.style({ width: '80%' })
|
||||||
|
await handle('service', 'Desktop Environment', {
|
||||||
|
init: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
import('./assets/style.less')
|
||||||
|
.then(() => {
|
||||||
|
boot.style({ display: 'none' })
|
||||||
|
import('material-symbols')
|
||||||
|
.then(async () => {
|
||||||
|
if (this.fs === false) return
|
||||||
|
console.log()
|
||||||
|
console.log('Welcome to FlowOS!')
|
||||||
|
console.log()
|
||||||
|
progress.style({ width: '100%' })
|
||||||
|
setTimeout(() => {
|
||||||
|
this.startExecutable('Desktop', Permission.SYSTEM).catch(e => console.error(e))
|
||||||
|
}, 750)
|
||||||
|
})
|
||||||
|
.catch(e => { throw e })
|
||||||
|
})
|
||||||
|
.catch(e => { throw e })
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async startExecutable (url: string, permission = Permission.USER, data = {}): Promise<{ procLib: ProcessLib, executable: Process } | any> {
|
async startExecutable (url: string, permission = Permission.USER, data = {}): Promise<{ procLib: ProcessLib, executable: Process } | any> {
|
||||||
|
|
@ -67,7 +152,7 @@ export default class Kernel {
|
||||||
const importedExecutable = (await module()) as any
|
const importedExecutable = (await module()) as any
|
||||||
executable = importedExecutable.default
|
executable = importedExecutable.default
|
||||||
} catch {
|
} catch {
|
||||||
if (this.fs === undefined) throw new Error('Filesystem hasn\'t been initiated.')
|
if (this.fs === false) 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 dataURL = `data:text/javascript;base64,${Buffer.from(await this.fs.readFile(`/opt/${url}.js`)).toString('base64')}`
|
||||||
const importedExecutable = await import(dataURL)
|
const importedExecutable = await import(dataURL)
|
||||||
executable = importedExecutable.default
|
executable = importedExecutable.default
|
||||||
|
|
@ -111,7 +196,7 @@ export default class Kernel {
|
||||||
const importedExecutable = (await module()) as any
|
const importedExecutable = (await module()) as any
|
||||||
executable = importedExecutable.default
|
executable = importedExecutable.default
|
||||||
} catch {
|
} catch {
|
||||||
if (this.fs === undefined) throw new Error('Filesystem hasn\'t been initiated.')
|
if (this.fs === false) 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 dataURL = `data:text/javascript;base64,${Buffer.from(await this.fs.readFile(`/opt/${url}.js`)).toString('base64')}`
|
||||||
const importedExecutable = await import(dataURL)
|
const importedExecutable = await import(dataURL)
|
||||||
executable = importedExecutable.default
|
executable = importedExecutable.default
|
||||||
|
|
@ -126,9 +211,3 @@ export default class Kernel {
|
||||||
return executable
|
return executable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
import('material-symbols')
|
|
||||||
const kernel = new Kernel(version)
|
|
||||||
kernel.startExecutable('BootLoader', Permission.SYSTEM).catch(e => console.error(e))
|
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import semver from 'semver'
|
import semver from 'semver'
|
||||||
import Kernel from '../kernel'
|
import Kernel from '../kernel'
|
||||||
import { Process, Executable, Package, Library, Permission, LoadedLibrary, LibraryPath } from '../types'
|
import { Process, Executable, Package, Library, Permission, LoadedLibrary, LibraryPath, FileSystem } from '../types'
|
||||||
import FlowWindow from './FlowWindow'
|
import FlowWindow from './FlowWindow'
|
||||||
import LibraryLib from './LibraryLib'
|
import LibraryLib from './LibraryLib'
|
||||||
import ProcLib from './ProcLib'
|
import ProcLib from './ProcLib'
|
||||||
|
|
@ -9,6 +9,7 @@ export default class ProcessLib {
|
||||||
readonly pid: number
|
readonly pid: number
|
||||||
readonly token: string
|
readonly token: string
|
||||||
process: Process
|
process: Process
|
||||||
|
fs: FileSystem
|
||||||
private readonly _kernel: Kernel
|
private readonly _kernel: Kernel
|
||||||
readonly kernel: {
|
readonly kernel: {
|
||||||
getExecutable: (url: string) => Promise<Executable>
|
getExecutable: (url: string) => Promise<Executable>
|
||||||
|
|
@ -17,8 +18,7 @@ export default class ProcessLib {
|
||||||
[key: string]: Package
|
[key: string]: Package
|
||||||
}
|
}
|
||||||
config: any
|
config: any
|
||||||
setConfig: (data: any) => void
|
setConfig: (config: any) => any
|
||||||
setFS: (fs: any) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly permission: Permission
|
readonly permission: Permission
|
||||||
|
|
@ -32,6 +32,8 @@ export default class ProcessLib {
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor (url: string, pid: number, token: string, permission = Permission.USER, data = {}, process: Process, kernel: Kernel) {
|
constructor (url: string, pid: number, token: string, permission = Permission.USER, data = {}, process: Process, kernel: Kernel) {
|
||||||
|
if (kernel.fs === false) return
|
||||||
|
this.fs = kernel.fs
|
||||||
this.permission = permission
|
this.permission = permission
|
||||||
this.pid = pid
|
this.pid = pid
|
||||||
this.token = token
|
this.token = token
|
||||||
|
|
@ -41,8 +43,9 @@ export default class ProcessLib {
|
||||||
processList: kernel.processList,
|
processList: kernel.processList,
|
||||||
packageList: kernel.packageList,
|
packageList: kernel.packageList,
|
||||||
config: kernel.config,
|
config: kernel.config,
|
||||||
setConfig: (data: any) => kernel.setConfig(data, this),
|
setConfig: (config) => {
|
||||||
setFS: (fs: any) => kernel.setFS(fs, this)
|
if (this.permission >= Permission.ELEVATED) kernel.config = config
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.process = process
|
this.process = process
|
||||||
this.data = data
|
this.data = data
|
||||||
|
|
@ -65,8 +68,7 @@ export default class ProcessLib {
|
||||||
const importedExecutable = (await module()) as any
|
const importedExecutable = (await module()) as any
|
||||||
executable = importedExecutable.default
|
executable = importedExecutable.default
|
||||||
} catch {
|
} catch {
|
||||||
if (this._kernel.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 dataURL = `data:text/javascript;base64,${Buffer.from(await this._kernel.fs.readFile(`/opt/${url}.js`)).toString('base64')}`
|
|
||||||
const importedExecutable = await import(dataURL)
|
const importedExecutable = await import(dataURL)
|
||||||
executable = importedExecutable.default
|
executable = importedExecutable.default
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,200 +0,0 @@
|
||||||
import HTML from '../HTML'
|
|
||||||
import { AppClosedEvent, AppOpenedEvent, Directory, FileSystemObject, 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() as FileSystemObject
|
|
||||||
if (fileSystem === undefined) {
|
|
||||||
await write(defaultFS)
|
|
||||||
} else {
|
|
||||||
const appsDirectory = ((fileSystem.root.children.home as Directory).children.Applications as Directory).children
|
|
||||||
const defaultAppsDirectory = ((defaultFS.root.children.home as Directory).children.Applications as Directory).children
|
|
||||||
for (const file in defaultAppsDirectory) {
|
|
||||||
if (appsDirectory[file] === undefined && defaultAppsDirectory[file] !== undefined) {
|
|
||||||
console.log(file)
|
|
||||||
appsDirectory[file] = defaultAppsDirectory[file]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await write(fileSystem)
|
|
||||||
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((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)
|
|
||||||
}).catch((e: any) => console.error(e))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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"]')?.elm as HTMLElement)
|
|
||||||
})
|
|
||||||
|
|
||||||
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
|
|
||||||
118
src/system/Desktop.ts
Normal file
118
src/system/Desktop.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import HTML from '../HTML'
|
||||||
|
import { Process } from '../types'
|
||||||
|
import nullIcon from '../assets/icons/application-default-icon.svg'
|
||||||
|
|
||||||
|
const BootLoader: Process = {
|
||||||
|
config: {
|
||||||
|
name: 'Desktop',
|
||||||
|
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 } = process
|
||||||
|
const wm = await process.loadLibrary('lib/WindowManager')
|
||||||
|
const launcher = await process.loadLibrary('lib/Launcher')
|
||||||
|
const { Input } = await process.loadLibrary('lib/Components')
|
||||||
|
|
||||||
|
const input = Input.new().attr({
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'Search'
|
||||||
|
}).style({
|
||||||
|
width: '100%',
|
||||||
|
'border-radius': '10px',
|
||||||
|
padding: '5px',
|
||||||
|
'margin-bottom': '10px',
|
||||||
|
position: 'sticky',
|
||||||
|
top: '0'
|
||||||
|
}).on('keyup', () => {
|
||||||
|
apps.elm.innerHTML = ''
|
||||||
|
renderApps().catch(e => console.error(e))
|
||||||
|
}).appendTo(launcher.element)
|
||||||
|
|
||||||
|
const apps = new HTML('div').style({
|
||||||
|
overflow: 'scroll',
|
||||||
|
height: 'max-content',
|
||||||
|
position: 'relative'
|
||||||
|
})
|
||||||
|
|
||||||
|
new HTML('div').style({
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'scroll',
|
||||||
|
'padding-bottom': '30px'
|
||||||
|
}).append(apps).appendTo(launcher.element).class('gradient-blur')
|
||||||
|
|
||||||
|
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((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('div').style({
|
||||||
|
display: 'flex',
|
||||||
|
'align-items': 'center',
|
||||||
|
padding: '5px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
gap: '10px',
|
||||||
|
'border-bottom': '1px solid var(--surface-0)'
|
||||||
|
}).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`,
|
||||||
|
height: '40px'
|
||||||
|
}).appendTo(appElement)
|
||||||
|
new HTML('div').text(executable.config.name).appendTo(appElement)
|
||||||
|
}).catch((e: any) => console.error(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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.qs('div[data-toolbar-id="start"]')?.on('click', () => {
|
||||||
|
launcher.toggle()
|
||||||
|
})
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
launcher.element.prepend(new HTML('div')
|
||||||
|
.html(`
|
||||||
|
<ins class="adsbygoogle"
|
||||||
|
style="display:block"
|
||||||
|
data-ad-format="fluid"
|
||||||
|
data-ad-layout-key="-if+5+1+2-3"
|
||||||
|
data-ad-client="ca-pub-9675905177363247"
|
||||||
|
data-ad-slot="2569234651"></ins>
|
||||||
|
<script>
|
||||||
|
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||||
|
</script>
|
||||||
|
`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BootLoader
|
||||||
|
|
@ -32,7 +32,7 @@ const UserAccessControl: Process = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wm.createModal('User Account Control', message, process)
|
wm.createModal('allow', 'User Account Control', message, process)
|
||||||
.then(async ({ value, win }: {
|
.then(async ({ value, win }: {
|
||||||
value: boolean
|
value: boolean
|
||||||
win: FlowWindow
|
win: FlowWindow
|
||||||
|
|
|
||||||
674
src/system/VirtualFS.ts
Normal file
674
src/system/VirtualFS.ts
Normal file
|
|
@ -0,0 +1,674 @@
|
||||||
|
import { parse, stringify } from 'js-ini'
|
||||||
|
import { Directory, Errors, File, Permission, Stats } from '../types'
|
||||||
|
import path from 'path'
|
||||||
|
const p = path
|
||||||
|
|
||||||
|
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')
|
||||||
|
},
|
||||||
|
'ThemeMaker.app': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('apps/ThemeMaker')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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')
|
||||||
|
},
|
||||||
|
'Browser.lnk': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('/home/Applications/Browser.app')
|
||||||
|
},
|
||||||
|
'Files.lnk': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from('/home/Applications/Files.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: {
|
||||||
|
themes: {
|
||||||
|
type: 'directory',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.SYSTEM,
|
||||||
|
children: {
|
||||||
|
'Latte.theme': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from(JSON.stringify({
|
||||||
|
name: 'Catppuccin Latté',
|
||||||
|
extras: {
|
||||||
|
rosewater: '#dc8a78',
|
||||||
|
flamingo: '#dd7878',
|
||||||
|
pink: '#ea76cb',
|
||||||
|
mauve: '#8839ef',
|
||||||
|
red: '#d20f39',
|
||||||
|
maroon: '#e64553',
|
||||||
|
peach: '#fe640b',
|
||||||
|
yellow: '#df8e1d',
|
||||||
|
green: '#40a02b',
|
||||||
|
teal: '#179299',
|
||||||
|
sky: '#04a5e5',
|
||||||
|
sapphire: '#209fb5',
|
||||||
|
blue: '#1e66f5',
|
||||||
|
lavender: '#7287fd'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
text: '#4c4f69',
|
||||||
|
'surface-2': '#acb0be',
|
||||||
|
'surface-1': '#bcc0cc',
|
||||||
|
'surface-0': '#ccd0da',
|
||||||
|
base: '#eff1f5',
|
||||||
|
mantle: '#e6e9ef',
|
||||||
|
crust: '#dce0e8'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
'Frappe.theme': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from(JSON.stringify({
|
||||||
|
name: 'Catppuccin Frappé',
|
||||||
|
extras: {
|
||||||
|
rosewater: '#f2d5cf',
|
||||||
|
flamingo: '#eebebe',
|
||||||
|
pink: '#f4b8e4',
|
||||||
|
mauve: '#ca9ee6',
|
||||||
|
red: '#e78284',
|
||||||
|
maroon: '#ea999c',
|
||||||
|
peach: '#ef9f76',
|
||||||
|
yellow: '#e5c890',
|
||||||
|
green: '#a6d189',
|
||||||
|
teal: '#81c8be',
|
||||||
|
sky: '#99d1db',
|
||||||
|
sapphire: '#85c1dc',
|
||||||
|
blue: '#8caaee',
|
||||||
|
lavender: '#babbf1'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
text: '#c6d0f5',
|
||||||
|
'surface-2': '#626880',
|
||||||
|
'surface-1': '#51576d',
|
||||||
|
'surface-0': '#414559',
|
||||||
|
base: '#303446',
|
||||||
|
mantle: '#292c3c',
|
||||||
|
crust: '#232634'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
'Macchiato.theme': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from(JSON.stringify({
|
||||||
|
name: 'Catppuccin Macchiato',
|
||||||
|
extras: {
|
||||||
|
rosewater: '#f4dbd6',
|
||||||
|
flamingo: '#f0c6c6',
|
||||||
|
pink: '#f5bde6',
|
||||||
|
mauve: '#c6a0f6',
|
||||||
|
red: '#ed8796',
|
||||||
|
maroon: '#ee99a0',
|
||||||
|
peach: '#f5a97f',
|
||||||
|
yellow: '#eed49f',
|
||||||
|
green: '#a6da95',
|
||||||
|
teal: '#8bd5ca',
|
||||||
|
sky: '#91d7e3',
|
||||||
|
sapphire: '#7dc4e4',
|
||||||
|
blue: '#8aadf4',
|
||||||
|
lavender: '#b7bdf8'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
text: '#cad3f5',
|
||||||
|
'surface-2': '#5b6078',
|
||||||
|
'surface-1': '#494d64',
|
||||||
|
'surface-0': '#363a4f',
|
||||||
|
base: '#24273a',
|
||||||
|
mantle: '#1e2030',
|
||||||
|
crust: '#181926'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
'Mocha.theme': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from(JSON.stringify({
|
||||||
|
name: 'Catpuccin Mocha',
|
||||||
|
extras: {
|
||||||
|
rosewater: '#f5e0dc',
|
||||||
|
flamingo: '#f2cdcd',
|
||||||
|
pink: '#f5c2e7',
|
||||||
|
mauve: '#cba6f7',
|
||||||
|
red: '#f38ba8',
|
||||||
|
maroon: '#eba0ac',
|
||||||
|
peach: '#fab387',
|
||||||
|
yellow: '#f9e2af',
|
||||||
|
green: '#a6e3a1',
|
||||||
|
teal: '#94e2d5',
|
||||||
|
sky: '#89dceb',
|
||||||
|
sapphire: '#74c7ec',
|
||||||
|
blue: '#89b4fa',
|
||||||
|
lavender: '#b4befe'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
text: '#cdd6f4',
|
||||||
|
'surface-2': '#585b70',
|
||||||
|
'surface-1': '#45475a',
|
||||||
|
'surface-0': '#313244',
|
||||||
|
base: '#1e1e2e',
|
||||||
|
mantle: '#181825',
|
||||||
|
crust: '#11111b'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
'RosePine.theme': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from(JSON.stringify({
|
||||||
|
name: 'Rosé Pine',
|
||||||
|
extras: {
|
||||||
|
love: '#eb6f92',
|
||||||
|
gold: '#f6c177',
|
||||||
|
rose: '#ebbcba',
|
||||||
|
pine: '#31748f',
|
||||||
|
foam: '#9ccfd8',
|
||||||
|
iris: '#c4a7e7'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
text: '#e0def4',
|
||||||
|
'surface-2': '#524f67',
|
||||||
|
'surface-1': '#403d52',
|
||||||
|
'surface-0': '#21202e',
|
||||||
|
base: '#26233a',
|
||||||
|
mantle: '#1f1d2e',
|
||||||
|
crust: '#191724'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
'RosePineMoon.theme': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from(JSON.stringify({
|
||||||
|
name: 'Rosé Pine Moon',
|
||||||
|
extras: {
|
||||||
|
love: '#eb6f92',
|
||||||
|
gold: '#f6c177',
|
||||||
|
rose: '#ebbcba',
|
||||||
|
pine: '#31748f',
|
||||||
|
foam: '#9ccfd8',
|
||||||
|
iris: '#c4a7e7'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
text: '#e0def4',
|
||||||
|
'surface-2': '#2a283e',
|
||||||
|
'surface-1': '#44415a',
|
||||||
|
'surface-0': '#56526e',
|
||||||
|
base: '#393552',
|
||||||
|
mantle: '#2a273f',
|
||||||
|
crust: '#232136'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
'RosePineDawn.theme': {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission: Permission.USER,
|
||||||
|
content: Buffer.from(JSON.stringify({
|
||||||
|
name: 'Rosé Pine Dawn',
|
||||||
|
extras: {
|
||||||
|
love: '#b4637a',
|
||||||
|
gold: '#ea9d34',
|
||||||
|
rose: '#d7827e',
|
||||||
|
pine: '#286983',
|
||||||
|
foam: '#56949f',
|
||||||
|
iris: '#907aa9'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
text: '#575279',
|
||||||
|
'surface-2': '#cecacd',
|
||||||
|
'surface-1': '#dfdad9',
|
||||||
|
'surface-0': '#f4ede8',
|
||||||
|
base: '#f2e9e1',
|
||||||
|
mantle: '#fffaf3',
|
||||||
|
crust: '#faf4ed'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
flow: {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: false,
|
||||||
|
permission: Permission.ELEVATED,
|
||||||
|
content: Buffer.from([
|
||||||
|
'SERVER=https://server.flow-works.me',
|
||||||
|
'24_HOUR=false',
|
||||||
|
'THEME=Mocha',
|
||||||
|
'THEME_PRIMARY=blue'
|
||||||
|
].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: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VirtualFS {
|
||||||
|
private fileSystem: { root: Directory }
|
||||||
|
private db: IDBDatabase | null = null
|
||||||
|
|
||||||
|
private async addMissingFiles (): Promise<void> {
|
||||||
|
const addDirectoryRecursive = async (directory: Directory, directoryPath: string): Promise<void> => {
|
||||||
|
for (const [key, value] of Object.entries(directory.children)) {
|
||||||
|
const path = p.join(directoryPath, key)
|
||||||
|
if (value.type === 'file' && path.startsWith('/etc/themes/')) {
|
||||||
|
await this.writeFile(path, Buffer.from(value.content).toString())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (value.type === 'directory') {
|
||||||
|
if (!await this.exists(path)) {
|
||||||
|
await this.mkdir(path)
|
||||||
|
}
|
||||||
|
await addDirectoryRecursive(value, path)
|
||||||
|
} else if (value.type === 'file' && !await this.exists(path)) {
|
||||||
|
await this.writeFile(path, Buffer.from(value.content).toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await addDirectoryRecursive(defaultFS.root, '/')
|
||||||
|
|
||||||
|
await this.readFile('/etc/flow').then(async (data: Uint8Array) => {
|
||||||
|
const dataString = Buffer.from(data).toString()
|
||||||
|
const config = parse(dataString)
|
||||||
|
|
||||||
|
if (config.SERVER == null) {
|
||||||
|
config.SERVER = 'https://server.flow-works.me'
|
||||||
|
await this.writeFile('/etc/flow', stringify(config))
|
||||||
|
}
|
||||||
|
if (config['24_HOUR'] == null) {
|
||||||
|
config['24_HOUR'] = 'FALSE'
|
||||||
|
await this.writeFile('/etc/flow', stringify(config))
|
||||||
|
}
|
||||||
|
if (config.THEME == null) {
|
||||||
|
config.THEME = 'Mocha'
|
||||||
|
await this.writeFile('/etc/flow', stringify(config))
|
||||||
|
}
|
||||||
|
if (config.THEME_PRIMARY == null) {
|
||||||
|
config.THEME_PRIMARY = 'blue'
|
||||||
|
await this.writeFile('/etc/flow', stringify(config))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async init (dbName = 'virtualfs'): Promise<VirtualFS> {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(dbName)
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const target = event.target as IDBRequest
|
||||||
|
const db = target.result
|
||||||
|
db.createObjectStore('fs')
|
||||||
|
}
|
||||||
|
request.onerror = () => {
|
||||||
|
reject(new Error('Failed to open database'))
|
||||||
|
}
|
||||||
|
request.onsuccess = async (event) => {
|
||||||
|
const target = event.target as IDBRequest
|
||||||
|
this.db = target.result
|
||||||
|
await navigator.storage.persist()
|
||||||
|
this.fileSystem = await this.read()
|
||||||
|
if (this.fileSystem == null) await this.write(defaultFS)
|
||||||
|
else await this.addMissingFiles()
|
||||||
|
resolve(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly read = async (): Promise<{ root: Directory }> => {
|
||||||
|
if (this.db == null) throw new Error('Database is null')
|
||||||
|
const transaction = this.db.transaction(['fs'], 'readonly')
|
||||||
|
const store = transaction.objectStore('fs')
|
||||||
|
const getRequest = store.get('fs')
|
||||||
|
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
getRequest.onsuccess = (event) => {
|
||||||
|
const target = event.target as IDBRequest
|
||||||
|
resolve(target.result)
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequest.onerror = (event) => {
|
||||||
|
reject(getRequest.error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly write = async (fileSystemObject: { root: Directory }): Promise<void> => {
|
||||||
|
this.fileSystem = fileSystemObject
|
||||||
|
await this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly save = async (): Promise<void> => {
|
||||||
|
if (this.db == null) throw new Error('Database is null')
|
||||||
|
const transaction = this.db.transaction(['fs'], 'readwrite')
|
||||||
|
const store = transaction.objectStore('fs')
|
||||||
|
const putRequest = store.put(this.fileSystem, 'fs')
|
||||||
|
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
putRequest.onsuccess = () => {
|
||||||
|
document.dispatchEvent(new CustomEvent('fs_update', {}))
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
putRequest.onerror = () => {
|
||||||
|
reject(putRequest.error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly navigatePath = async (path: string): Promise<{ current: Directory | File, parts: string[] }> => {
|
||||||
|
const parts = path.split('/').filter(x => x !== '')
|
||||||
|
let current = this.fileSystem.root
|
||||||
|
for (const part of parts) {
|
||||||
|
current = current.children[part] as Directory
|
||||||
|
}
|
||||||
|
return { current, parts }
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly 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 = this.fileSystem.root
|
||||||
|
for (const part of parts) {
|
||||||
|
current = current.children[part] as Directory
|
||||||
|
}
|
||||||
|
return { current, parts, filename }
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly handlePermissions = async (path: string): Promise<void> => {
|
||||||
|
const { current } = await this.navigatePath(path)
|
||||||
|
if (current.permission === Permission.SYSTEM) throw new Error(Errors.EPERM)
|
||||||
|
}
|
||||||
|
|
||||||
|
unlink = async (path: string): Promise<void> => {
|
||||||
|
const { current, filename } = await this.navigatePathParent(path)
|
||||||
|
|
||||||
|
if (!current.children[filename].deleteable) throw new Error(Errors.EPERM)
|
||||||
|
await this.handlePermissions(path)
|
||||||
|
|
||||||
|
Reflect.deleteProperty(current.children, filename)
|
||||||
|
|
||||||
|
console.debug(`unlink ${path}`)
|
||||||
|
await this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
readFile = async (path: string): Promise<Buffer> => {
|
||||||
|
const { current } = await this.navigatePath(path)
|
||||||
|
|
||||||
|
await this.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 this.navigatePathParent(path)
|
||||||
|
|
||||||
|
let permission
|
||||||
|
|
||||||
|
if (typeof current.children[filename] === 'undefined') {
|
||||||
|
permission = Permission.USER
|
||||||
|
} else {
|
||||||
|
await this.handlePermissions(path)
|
||||||
|
permission = current.children[filename].permission
|
||||||
|
}
|
||||||
|
|
||||||
|
current.children[filename] = {
|
||||||
|
type: 'file',
|
||||||
|
deleteable: true,
|
||||||
|
permission,
|
||||||
|
content: Buffer.from(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(`write ${path}`)
|
||||||
|
await this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir = async (path: string): Promise<void> => {
|
||||||
|
const { current, filename } = await this.navigatePathParent(path)
|
||||||
|
|
||||||
|
let permission
|
||||||
|
|
||||||
|
if (typeof current.children[filename] === 'undefined') {
|
||||||
|
permission = Permission.USER
|
||||||
|
} else {
|
||||||
|
await this.handlePermissions(path)
|
||||||
|
permission = current.children[filename].permission
|
||||||
|
}
|
||||||
|
|
||||||
|
current.children[filename] = {
|
||||||
|
type: 'directory',
|
||||||
|
deleteable: true,
|
||||||
|
permission: path === '/tmp' ? Permission.USER : permission,
|
||||||
|
children: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(`mkdir ${path}`)
|
||||||
|
await this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
rmdir = async (path: string): Promise<void> => {
|
||||||
|
const { current, filename } = await this.navigatePathParent(path)
|
||||||
|
|
||||||
|
if (!current.deleteable && path !== '/tmp') throw new Error(Errors.EPERM)
|
||||||
|
if (path !== '/tmp') await this.handlePermissions(path)
|
||||||
|
|
||||||
|
if (current.children[filename].type !== 'directory') throw new Error(Errors.ENOTDIR)
|
||||||
|
|
||||||
|
Reflect.deleteProperty(current.children, filename)
|
||||||
|
|
||||||
|
console.debug(`rmdir ${path}`)
|
||||||
|
await this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
readdir = async (path: string): Promise<string[]> => {
|
||||||
|
const { current } = await this.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<Stats> => {
|
||||||
|
const { current } = await this.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 this.navigatePathParent(oldPath)
|
||||||
|
const { current: newCurrent, filename: newFilename } = await this.navigatePathParent(newPath)
|
||||||
|
|
||||||
|
if (!oldCurrent.deleteable) throw new Error(Errors.EPERM)
|
||||||
|
if (!newCurrent.deleteable) throw new Error(Errors.EPERM)
|
||||||
|
|
||||||
|
await this.handlePermissions(oldPath)
|
||||||
|
await this.handlePermissions(newPath)
|
||||||
|
|
||||||
|
newCurrent.children[newFilename] = oldCurrent.children[oldFilename]
|
||||||
|
Reflect.deleteProperty(oldCurrent.children, oldFilename)
|
||||||
|
|
||||||
|
console.debug(`rename ${oldPath} -> ${newPath}`)
|
||||||
|
await this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
exists = async (path: string): Promise<boolean> => {
|
||||||
|
console.debug(`exists ${path}`)
|
||||||
|
try {
|
||||||
|
const { current } = await this.navigatePath(path)
|
||||||
|
return current !== undefined
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VirtualFS
|
||||||
|
|
@ -12,26 +12,6 @@ interface EditorConfig {
|
||||||
path: string
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileLanguageMap: {
|
|
||||||
[key: string]: string
|
|
||||||
} = {
|
|
||||||
c: 'clike',
|
|
||||||
cpp: 'clike',
|
|
||||||
java: 'clike',
|
|
||||||
cs: 'clike',
|
|
||||||
ts: 'typescript',
|
|
||||||
js: 'javascript',
|
|
||||||
mjs: 'javascript',
|
|
||||||
cjs: 'javascript',
|
|
||||||
jsx: 'jsx',
|
|
||||||
tsx: 'tsx',
|
|
||||||
html: 'html',
|
|
||||||
md: 'markdown',
|
|
||||||
css: 'css',
|
|
||||||
xml: 'xml',
|
|
||||||
py: 'python'
|
|
||||||
}
|
|
||||||
|
|
||||||
const Editor: Process = {
|
const Editor: Process = {
|
||||||
config: {
|
config: {
|
||||||
name: 'Editor',
|
name: 'Editor',
|
||||||
|
|
@ -40,6 +20,8 @@ const Editor: Process = {
|
||||||
targetVer: '1.0.0-indev.0'
|
targetVer: '1.0.0-indev.0'
|
||||||
},
|
},
|
||||||
run: async (process) => {
|
run: async (process) => {
|
||||||
|
const MIMETypes = await process.loadLibrary('lib/MIMETypes')
|
||||||
|
|
||||||
if (Object.keys(process.data).length > 0) {
|
if (Object.keys(process.data).length > 0) {
|
||||||
const win = await process.loadLibrary('lib/WindowManager').then((wm: any) => {
|
const win = await process.loadLibrary('lib/WindowManager').then((wm: any) => {
|
||||||
return wm.createWindow({
|
return wm.createWindow({
|
||||||
|
|
@ -51,7 +33,7 @@ const Editor: Process = {
|
||||||
}, process)
|
}, process)
|
||||||
})
|
})
|
||||||
|
|
||||||
const fs = await process.loadLibrary('lib/VirtualFS')
|
const fs = process.fs
|
||||||
|
|
||||||
const data = process.data as EditorConfig
|
const data = process.data as EditorConfig
|
||||||
|
|
||||||
|
|
@ -153,8 +135,44 @@ const Editor: Process = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const fileExtension = data.path.split('.').pop()?.toLowerCase() as string
|
const fileExtension = (data.path.split('.').pop() as string).toLowerCase()
|
||||||
const language = fileLanguageMap[fileExtension] ?? 'text'
|
console.log('owo ' + fileExtension, MIMETypes)
|
||||||
|
const mime = fileExtension in MIMETypes ? MIMETypes[fileExtension].type : 'text/plain'
|
||||||
|
let language = 'text'
|
||||||
|
|
||||||
|
switch (mime) {
|
||||||
|
case 'text/markdown':
|
||||||
|
language = 'markdown'
|
||||||
|
break
|
||||||
|
case 'text/css':
|
||||||
|
language = 'css'
|
||||||
|
break
|
||||||
|
case 'text/html':
|
||||||
|
language = 'html'
|
||||||
|
break
|
||||||
|
case 'text/javascript':
|
||||||
|
language = 'javascript'
|
||||||
|
break
|
||||||
|
case 'text/jsx':
|
||||||
|
language = 'jsx'
|
||||||
|
break
|
||||||
|
case 'application/x-flow-theme':
|
||||||
|
case 'application/json':
|
||||||
|
language = 'clike'
|
||||||
|
break
|
||||||
|
case 'text/typescript':
|
||||||
|
language = 'typescript'
|
||||||
|
break
|
||||||
|
case 'text/tsx':
|
||||||
|
language = 'tsx'
|
||||||
|
break
|
||||||
|
case 'application/python':
|
||||||
|
language = 'python'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
language = 'text'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
const value = Buffer.from(await fs.readFile(data.path)).toString()
|
const value = Buffer.from(await fs.readFile(data.path)).toString()
|
||||||
const editor = fullEditor(
|
const editor = fullEditor(
|
||||||
|
|
@ -169,6 +187,7 @@ const Editor: Process = {
|
||||||
const style = document.createElement('style')
|
const style = document.createElement('style')
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
.prism-code-editor {
|
.prism-code-editor {
|
||||||
|
color: var(--text);
|
||||||
border-radius: 10px 10px 0 0;
|
border-radius: 10px 10px 0 0;
|
||||||
caret-color: var(--text);
|
caret-color: var(--text);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
@ -208,6 +227,9 @@ const Editor: Process = {
|
||||||
document.addEventListener('fs_update', () => {
|
document.addEventListener('fs_update', () => {
|
||||||
render().catch(e => console.error(e))
|
render().catch(e => console.error(e))
|
||||||
})
|
})
|
||||||
|
document.addEventListener('theme_update', () => {
|
||||||
|
render().catch(e => console.error(e))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ const Files: Process = {
|
||||||
}, process)
|
}, process)
|
||||||
})
|
})
|
||||||
|
|
||||||
const fs = await process.loadLibrary('lib/VirtualFS')
|
const fs = process.fs
|
||||||
const MIMETypes = await process.loadLibrary('lib/MIMETypes')
|
const MIMETypes = await process.loadLibrary('lib/MIMETypes')
|
||||||
|
|
||||||
win.content.style.display = 'flex'
|
win.content.style.display = 'flex'
|
||||||
|
|
@ -74,6 +74,7 @@ const Files: Process = {
|
||||||
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.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 () => {
|
(element.querySelector('.rename') as HTMLElement).onclick = async () => {
|
||||||
const value = prompt('Rename')
|
const value = prompt('Rename')
|
||||||
|
console.log(value)
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
await fs.rename(dir + seperator + file, dir + seperator + value)
|
await fs.rename(dir + seperator + file, dir + seperator + value)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const ImageViewer: Process = {
|
||||||
}, process)
|
}, process)
|
||||||
})
|
})
|
||||||
|
|
||||||
const fs = await process.loadLibrary('lib/VirtualFS')
|
const fs = process.fs
|
||||||
const MIMETypes: Record<string, { type: string }> = await process.loadLibrary('lib/MIMETypes')
|
const MIMETypes: Record<string, { type: string }> = await process.loadLibrary('lib/MIMETypes')
|
||||||
const HTML = await process.loadLibrary('lib/HTML')
|
const HTML = await process.loadLibrary('lib/HTML')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const Settings: Process = {
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
icon,
|
icon,
|
||||||
targetVer: '1.0.0-indev.0'
|
targetVer: '2.0.0'
|
||||||
},
|
},
|
||||||
run: async process => {
|
run: async process => {
|
||||||
const win = await process
|
const win = await process
|
||||||
|
|
@ -24,17 +24,33 @@ const Settings: Process = {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const fs = await process.loadLibrary('lib/VirtualFS')
|
const { fs } = process
|
||||||
const HTML = await process.loadLibrary('lib/HTML')
|
const HTML = await process.loadLibrary('lib/HTML')
|
||||||
|
|
||||||
const { Input, Button } = await process.loadLibrary('lib/Components')
|
const { Input, Button, Dropdown } = await process.loadLibrary('lib/Components')
|
||||||
|
|
||||||
const render = async (config: any): Promise<void> => {
|
const render = async (config: any): Promise<void> => {
|
||||||
win.content.innerHTML = ''
|
win.content.innerHTML = ''
|
||||||
for (const item in config) {
|
for (const item in config) {
|
||||||
const input = Input.new().attr({
|
let input = Input.new()
|
||||||
|
|
||||||
|
if (item === 'THEME_PRIMARY') {
|
||||||
|
const { extras } = JSON.parse(Buffer.from(await fs.readFile(`/etc/themes/${config.THEME as string}.theme`)).toString())
|
||||||
|
input = Dropdown.new(Object.keys(extras))
|
||||||
|
} else if (item === 'THEME') {
|
||||||
|
input = Dropdown.new((await fs.readdir('/etc/themes')).map((theme: string) => theme.replace('.theme', '')))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item === 'THEME_PRIMARY' || item === 'THEME') {
|
||||||
|
(input.elm as HTMLSelectElement).value = config[item]
|
||||||
|
} else {
|
||||||
|
input.attr({
|
||||||
value: config[item]
|
value: config[item]
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(input.getValue())
|
||||||
|
|
||||||
new HTML('div')
|
new HTML('div')
|
||||||
.appendMany(
|
.appendMany(
|
||||||
new HTML('label')
|
new HTML('label')
|
||||||
|
|
@ -50,10 +66,10 @@ const Settings: Process = {
|
||||||
})
|
})
|
||||||
.appendMany(
|
.appendMany(
|
||||||
input,
|
input,
|
||||||
Button.new().text('Save').on('click', async () => {
|
Button.new().text('Save').on('click', () => {
|
||||||
config[item] = input.getValue()
|
config[item] = input.getValue()
|
||||||
process.kernel.setConfig(config)
|
fs.writeFile('/etc/flow', stringify(config))
|
||||||
await fs.writeFile('/etc/flow', stringify(config))
|
.then(() => {
|
||||||
document.dispatchEvent(
|
document.dispatchEvent(
|
||||||
new CustomEvent('config_update', {
|
new CustomEvent('config_update', {
|
||||||
detail: {
|
detail: {
|
||||||
|
|
@ -61,6 +77,11 @@ const Settings: Process = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
if (item === 'THEME' || item === 'THEME_PRIMARY') {
|
||||||
|
document.dispatchEvent(new CustomEvent('theme_update', {}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => console.error(e))
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import { Process, RepoData } from '../../types'
|
import { Process, RepoData } from '../../types'
|
||||||
import icon from '../../assets/icons/softwarecenter.svg'
|
import icon from '../../assets/icons/softwarecenter.svg'
|
||||||
|
|
||||||
import { sanitize } from '../../utils'
|
|
||||||
import nullIcon from '../../assets/icons/application-default-icon.svg'
|
import nullIcon from '../../assets/icons/application-default-icon.svg'
|
||||||
|
|
||||||
const Store: Process = {
|
const Store: Process = {
|
||||||
|
|
@ -21,69 +19,177 @@ const Store: Process = {
|
||||||
}, process)
|
}, process)
|
||||||
})
|
})
|
||||||
|
|
||||||
const fs = await process.loadLibrary('lib/VirtualFS')
|
const { fs } = process
|
||||||
|
const HTML = await process.loadLibrary('lib/HTML')
|
||||||
win.content.style.background = 'var(--base)'
|
const { Button, Icon } = await process.loadLibrary('lib/Components')
|
||||||
|
|
||||||
fetch(`${process.kernel.config.SERVER as string}/apps/list/`)
|
fetch(`${process.kernel.config.SERVER as string}/apps/list/`)
|
||||||
.then(async (res) => await res.json())
|
.then(async (res) => await res.json())
|
||||||
.then(handle)
|
.then(handle)
|
||||||
.catch(e => console.error(e))
|
.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 {
|
async function updateList (): Promise<void> {
|
||||||
win.content.innerHTML = `
|
const res = fetch(`${process.kernel.config.SERVER as string}/apps/list/`)
|
||||||
<div class="repos" style="display: flex;flex-direction: column;gap: 10px;"></div>
|
const repos = await (await res).json()
|
||||||
`
|
const div = new HTML(win.content).qs('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>
|
|
||||||
`
|
|
||||||
|
|
||||||
|
repos.forEach(async (repo: string, index: number) => {
|
||||||
|
const repoDiv = div?.qsa('div')?.[index]
|
||||||
|
repoDiv?.html('')
|
||||||
|
fetch(`${process.kernel.config.SERVER as string}/cors/?url=${repo}`)
|
||||||
|
.then(async res => await res.json())
|
||||||
|
.then((repo: RepoData) => {
|
||||||
repo.apps.forEach((app) => {
|
repo.apps.forEach((app) => {
|
||||||
(win.content.querySelector(`div[data-repo-id="${sanitize(repo.id)}"] > .apps`) as HTMLElement).innerHTML += `
|
fs.exists(`/home/Applications/${app.url.split('/').at(-1)?.replace('.js', '.app') as string}`)
|
||||||
<div data-pkg="${sanitize(app.name)}" style="display: flex;gap: 20px;">
|
.then((exists: boolean) => {
|
||||||
<img src="${sanitize(app.icon ?? nullIcon)}" height="59.5px" style="border-radius: var(--app-radius);">
|
const button = Button.new().style({
|
||||||
<div>
|
display: 'flex',
|
||||||
<h3 style="margin: 0;margin-bottom: 10px;">${sanitize(app.name)}</h3>
|
gap: '5px',
|
||||||
<div style="display: flex;gap:5px;align-items: center;">
|
'align-items': 'center'
|
||||||
<code style="font-family: monospace;">${sanitize(app.targetVer)}</code>
|
}).text('Uninstall')
|
||||||
<span class="material-symbols-rounded">download</span>
|
.prepend(Icon.new('delete'))
|
||||||
</div>
|
.on('click', () => uninstall(app.url))
|
||||||
</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) {
|
if (exists) {
|
||||||
(win.content.querySelector(`div[data-pkg="${sanitize(app.name)}"] div > .material-symbols-rounded`) as HTMLElement).innerHTML = 'delete';
|
fetch(`${process.kernel.config.SERVER as string}/cors?url=${app.url}`)
|
||||||
|
.then(async (res) => await res.text())
|
||||||
(win.content.querySelector(`div[data-pkg="${sanitize(app.name)}"] div > .material-symbols-rounded`) as HTMLElement).onclick = async () => {
|
.then(async (data) => {
|
||||||
await fs.unlink(`/home/Applications/${app.url.split('/').at(-1)?.replace('.js', '.app') as string}`)
|
const local = Buffer.from(await fs.readFile(`/opt/apps/${app.url.split('/').at(-1) as string}`)).toString()
|
||||||
await fs.unlink(`/opt/apps/${app.url.split('/').at(-1) as string}`)
|
if (local !== data) {
|
||||||
|
button.text('Update')
|
||||||
|
.prepend(Icon.new('update'))
|
||||||
|
.on('click', () => install(app.url))
|
||||||
}
|
}
|
||||||
|
}).catch(e => console.error(e))
|
||||||
} else {
|
} else {
|
||||||
(win.content.querySelector(`div[data-pkg="${sanitize(app.name)}"] div > .material-symbols-rounded`) as HTMLElement).onclick = () => {
|
button.text('Install')
|
||||||
install(app.url)
|
.prepend(Icon.new('download'))
|
||||||
|
.on('click', () => install(app.url))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}).catch((e: any) => console.error(e))
|
new HTML('div')
|
||||||
}).catch((e: any) => console.error(e))
|
.style({
|
||||||
|
display: 'flex',
|
||||||
|
'flex-direction': 'row',
|
||||||
|
gap: '10px',
|
||||||
|
padding: '10px',
|
||||||
|
background: 'var(--base)',
|
||||||
|
'border-radius': '10px'
|
||||||
})
|
})
|
||||||
|
.appendMany(
|
||||||
|
new HTML('img').attr({
|
||||||
|
src: app.icon ?? nullIcon
|
||||||
|
}).style({
|
||||||
|
'aspect-ratio': '1 / 1',
|
||||||
|
width: '60px',
|
||||||
|
height: '60px'
|
||||||
|
}),
|
||||||
|
new HTML('div').appendMany(
|
||||||
|
new HTML('h3').style({
|
||||||
|
margin: '0'
|
||||||
|
}).text(app.name),
|
||||||
|
button
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// @ts-expect-error
|
||||||
|
.appendTo(repoDiv)
|
||||||
|
}).catch(e => console.error(e))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(e => console.error(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle (repos: string[]): void {
|
||||||
|
win.content.innerHTML = ''
|
||||||
|
const div = new HTML('div').appendTo(win.content)
|
||||||
|
repos.forEach((repo) => {
|
||||||
|
fetch(`${process.kernel.config.SERVER as string}/cors/?url=${repo}`)
|
||||||
|
.then(async res => await res.json())
|
||||||
|
.then((repo: RepoData) => {
|
||||||
|
const icon = Icon.new('arrow_drop_up')
|
||||||
|
new HTML('h2').text(repo.name).style({
|
||||||
|
margin: '0',
|
||||||
|
padding: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '5px',
|
||||||
|
'align-items': 'center'
|
||||||
|
})
|
||||||
|
.prepend(icon)
|
||||||
|
.appendTo(div)
|
||||||
|
.on('click', () => {
|
||||||
|
repoDiv.style({
|
||||||
|
height: repoDiv.elm.style.height === '0px' ? 'max-content' : '0'
|
||||||
|
})
|
||||||
|
icon.text(`arrow_drop_${repoDiv.elm.style.height === '0px' ? 'up' : 'down'}`)
|
||||||
|
})
|
||||||
|
const repoDiv = new HTML('div').appendTo(div).style({
|
||||||
|
height: '0',
|
||||||
|
display: 'flex',
|
||||||
|
'flex-direction': 'column',
|
||||||
|
gap: '10px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
padding: '0 10px'
|
||||||
|
})
|
||||||
|
repo.apps.forEach((app) => {
|
||||||
|
fs.exists(`/home/Applications/${app.url.split('/').at(-1)?.replace('.js', '.app') as string}`)
|
||||||
|
.then((exists: boolean) => {
|
||||||
|
const button = Button.new().style({
|
||||||
|
display: 'flex',
|
||||||
|
gap: '5px',
|
||||||
|
'align-items': 'center'
|
||||||
|
}).text('Uninstall')
|
||||||
|
.prepend(Icon.new('delete'))
|
||||||
|
.on('click', () => uninstall(app.url))
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
fetch(`${process.kernel.config.SERVER as string}/cors?url=${app.url}`)
|
||||||
|
.then(async (res) => await res.text())
|
||||||
|
.then(async (data) => {
|
||||||
|
const local = Buffer.from(await fs.readFile(`/opt/apps/${app.url.split('/').at(-1) as string}`)).toString()
|
||||||
|
if (local !== data) {
|
||||||
|
button.text('Update')
|
||||||
|
.prepend(Icon.new('update'))
|
||||||
|
.un('click', () => uninstall(app.url))
|
||||||
|
.on('click', () => install(app.url))
|
||||||
|
}
|
||||||
|
}).catch(e => console.error(e))
|
||||||
|
} else {
|
||||||
|
button.text('Install')
|
||||||
|
.prepend(Icon.new('download'))
|
||||||
|
.un('click', () => uninstall(app.url))
|
||||||
|
.on('click', () => install(app.url))
|
||||||
|
}
|
||||||
|
|
||||||
|
new HTML('div')
|
||||||
|
.style({
|
||||||
|
display: 'flex',
|
||||||
|
'flex-direction': 'row',
|
||||||
|
gap: '10px',
|
||||||
|
padding: '10px',
|
||||||
|
background: 'var(--base)',
|
||||||
|
'border-radius': '10px'
|
||||||
|
})
|
||||||
|
.appendMany(
|
||||||
|
new HTML('img').attr({
|
||||||
|
src: app.icon ?? nullIcon
|
||||||
|
}).style({
|
||||||
|
'aspect-ratio': '1 / 1',
|
||||||
|
width: '60px',
|
||||||
|
height: '60px'
|
||||||
|
}),
|
||||||
|
new HTML('div').appendMany(
|
||||||
|
new HTML('h3').style({
|
||||||
|
margin: '0'
|
||||||
|
}).text(app.name),
|
||||||
|
button
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.appendTo(repoDiv)
|
||||||
|
}).catch(e => console.error(e))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(e => console.error(e))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,8 +198,18 @@ const Store: Process = {
|
||||||
.then(async (data) => {
|
.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(`/home/Applications/${url.split('/').at(-1)?.replace('.js', '.app') as string}`, `apps/${url.split('/').at(-1)?.split('.')[0] as string}`)
|
||||||
await fs.writeFile(`/opt/apps/${url.split('/').at(-1) as string}`, data)
|
await fs.writeFile(`/opt/apps/${url.split('/').at(-1) as string}`, data)
|
||||||
|
await updateList()
|
||||||
}).catch(e => console.error(e))
|
}).catch(e => console.error(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uninstall (url: string): void {
|
||||||
|
fs.unlink(`/home/Applications/${url.split('/').at(-1)?.replace('.js', '.app') as string}`)
|
||||||
|
.then(async () => {
|
||||||
|
await fs.unlink(`/opt/apps/${url.split('/').at(-1) as string}`)
|
||||||
|
await updateList()
|
||||||
|
})
|
||||||
|
.catch(e => console.error(e))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
77
src/system/apps/ThemeMaker.ts
Normal file
77
src/system/apps/ThemeMaker.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { Process } from '../../types'
|
||||||
|
import icon from '../../assets/icons/theme-config.svg'
|
||||||
|
|
||||||
|
const ThemeConfig: Process = {
|
||||||
|
config: {
|
||||||
|
name: 'Theme Maker',
|
||||||
|
type: 'process',
|
||||||
|
icon,
|
||||||
|
targetVer: '2.0.0'
|
||||||
|
},
|
||||||
|
run: async (process) => {
|
||||||
|
const wm = await process.loadLibrary('lib/WindowManager')
|
||||||
|
const HTML = await process.loadLibrary('lib/HTML')
|
||||||
|
const { Input, Button } = await process.loadLibrary('lib/Components')
|
||||||
|
|
||||||
|
const win = wm.createWindow({
|
||||||
|
title: 'Theme Maker',
|
||||||
|
icon,
|
||||||
|
width: 600,
|
||||||
|
height: 200
|
||||||
|
}, process)
|
||||||
|
|
||||||
|
const content = new HTML(win.content)
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
'crust',
|
||||||
|
'mantle',
|
||||||
|
'base',
|
||||||
|
'surface-0',
|
||||||
|
'surface-1',
|
||||||
|
'surface-2',
|
||||||
|
'text'
|
||||||
|
]
|
||||||
|
|
||||||
|
const name = Input.new().attr({
|
||||||
|
value: 'My Theme'
|
||||||
|
})
|
||||||
|
|
||||||
|
content.appendMany(
|
||||||
|
new HTML('div')
|
||||||
|
.appendMany(
|
||||||
|
new HTML('label').text('Name: '),
|
||||||
|
name
|
||||||
|
),
|
||||||
|
...items.map((item) => {
|
||||||
|
return new HTML('div')
|
||||||
|
.appendMany(
|
||||||
|
new HTML('label').text(`${item[0].toUpperCase() + item.slice(1)}: `),
|
||||||
|
Input.new().attr({
|
||||||
|
type: 'color',
|
||||||
|
id: item,
|
||||||
|
value: '#000000'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
Button.new().text('Create').on('click', () => {
|
||||||
|
const theme: {
|
||||||
|
[key: string]: any
|
||||||
|
} = {
|
||||||
|
name: name.getValue(),
|
||||||
|
colors: {}
|
||||||
|
}
|
||||||
|
items.forEach((item) => {
|
||||||
|
theme.colors[item] = content.qs(`#${item}`)?.getValue()
|
||||||
|
})
|
||||||
|
process.fs.writeFile(`/etc/themes/${theme.name.replace(/\s/g, '') as string}.theme`, JSON.stringify(theme))
|
||||||
|
.then(() => {
|
||||||
|
wm.createModal('ok', 'Theme Manager', 'Theme created successfully.', process)
|
||||||
|
.catch(e => console.error(e))
|
||||||
|
})
|
||||||
|
.catch(e => console.error(e))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThemeConfig
|
||||||
|
|
@ -26,17 +26,63 @@ const Components: Library = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Button: {
|
Button: {
|
||||||
new: () => {
|
new: (type: 'normal' | 'primary' = 'normal') => {
|
||||||
|
function shiftColor (col: string, amt: number): string {
|
||||||
|
const num = parseInt(col, 16)
|
||||||
|
const r = (num >> 16) + amt
|
||||||
|
const b = ((num >> 8) & 0x00_FF) + amt
|
||||||
|
const g = (num & 0x00_00_FF) + amt
|
||||||
|
const newColor = g | (b << 8) | (r << 16)
|
||||||
|
return newColor.toString(16)
|
||||||
|
}
|
||||||
|
|
||||||
const { HTML } = library
|
const { HTML } = library
|
||||||
const button = new HTML('button')
|
const button = new HTML('button')
|
||||||
button.style({
|
button.style({
|
||||||
'border-radius': '5px',
|
'border-radius': '5px',
|
||||||
padding: '2.5px',
|
padding: '2.5px 5px',
|
||||||
background: 'transparent',
|
background: 'var(--base)',
|
||||||
border: '1px solid var(--surface-0)'
|
border: '1px solid var(--surface-0)'
|
||||||
})
|
})
|
||||||
|
if (type === 'normal') {
|
||||||
|
button.style({
|
||||||
|
background: 'var(--base)'
|
||||||
|
})
|
||||||
|
} else if (type === 'primary') {
|
||||||
|
button.style({
|
||||||
|
background: 'var(--primary)',
|
||||||
|
color: 'var(--base)',
|
||||||
|
border: `1px solid #${shiftColor(document.documentElement.style.getPropertyValue('--primary').replace('#', ''), -40)}`
|
||||||
|
})
|
||||||
|
}
|
||||||
return button
|
return button
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Icon: {
|
||||||
|
new: (icon: string, size = 'inherit') => {
|
||||||
|
const { HTML } = library
|
||||||
|
return new HTML('i')
|
||||||
|
.class('material-symbols-rounded')
|
||||||
|
.text(icon)
|
||||||
|
.style({
|
||||||
|
'font-size': size
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Dropdown: {
|
||||||
|
new: (options: string[]) => {
|
||||||
|
const { HTML } = library
|
||||||
|
const dropdown = new HTML('select')
|
||||||
|
dropdown.style({
|
||||||
|
'border-radius': '5px',
|
||||||
|
padding: '2.5px',
|
||||||
|
background: 'var(--base)',
|
||||||
|
border: '1px solid var(--surface-1)'
|
||||||
|
}).appendMany(
|
||||||
|
...options.map((option) => new HTML('option').text(option))
|
||||||
|
)
|
||||||
|
return dropdown
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,25 +9,34 @@ const Launcher: Library = {
|
||||||
targetVer: '1.0.0-indev.0'
|
targetVer: '1.0.0-indev.0'
|
||||||
},
|
},
|
||||||
init: (l, k) => {
|
init: (l, k) => {
|
||||||
Launcher.data.element = new l.HTML('launcher').style({
|
Launcher.data.element = new l.HTML('div').style({
|
||||||
opacity: '0',
|
background: `${document.documentElement.style.getPropertyValue('--mantle')}E0`,
|
||||||
'backdrop-filter': 'blur(0px)',
|
width: '350px',
|
||||||
'pointer-events': 'none'
|
height: '500px',
|
||||||
|
margin: '20px',
|
||||||
|
padding: '10px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
'border-radius': '20px',
|
||||||
|
border: '1px solid var(--surface-0)',
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '-500px',
|
||||||
|
left: '0',
|
||||||
|
transition: 'bottom 0.5s cubic-bezier(1,0,0,1)',
|
||||||
|
'z-index': '1000',
|
||||||
|
'box-shadow': '0 0 10px 0 rgba(0,0,0,0.5)',
|
||||||
|
'user-select': 'none',
|
||||||
|
'backdrop-filter': 'blur(10px)'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
toggle: () => {
|
toggle: () => {
|
||||||
if (isLauncherOpen) {
|
if (isLauncherOpen) {
|
||||||
Launcher.data.element.style({
|
Launcher.data.element.style({
|
||||||
opacity: '0',
|
bottom: '-500px'
|
||||||
'backdrop-filter': 'blur(0px)',
|
|
||||||
'pointer-events': 'none'
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Launcher.data.element.style({
|
Launcher.data.element.style({
|
||||||
opacity: '1',
|
bottom: '68px'
|
||||||
'backdrop-filter': 'blur(20px)',
|
|
||||||
'pointer-events': 'all'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,30 @@ const MIMETypes: Library = {
|
||||||
opensWith: ['apps/ImageViewer'],
|
opensWith: ['apps/ImageViewer'],
|
||||||
icon: 'image'
|
icon: 'image'
|
||||||
},
|
},
|
||||||
|
cjs: {
|
||||||
|
type: 'application/javascript',
|
||||||
|
description: 'CommonJS Module',
|
||||||
|
opensWith: ['apps/Editor'],
|
||||||
|
icon: 'code'
|
||||||
|
},
|
||||||
|
htm: {
|
||||||
|
type: 'text/html',
|
||||||
|
description: 'HTML Document',
|
||||||
|
opensWith: ['apps/Editor'],
|
||||||
|
icon: 'code'
|
||||||
|
},
|
||||||
|
html: {
|
||||||
|
type: 'text/html',
|
||||||
|
description: 'HTML Document',
|
||||||
|
opensWith: ['apps/Editor'],
|
||||||
|
icon: 'code'
|
||||||
|
},
|
||||||
|
js: {
|
||||||
|
type: 'text/javascript',
|
||||||
|
description: 'JavaScript File',
|
||||||
|
opensWith: ['apps/Editor'],
|
||||||
|
icon: 'code'
|
||||||
|
},
|
||||||
lnk: {
|
lnk: {
|
||||||
type: 'application/x-ms-shortcut',
|
type: 'application/x-ms-shortcut',
|
||||||
description: 'Windows Shortcut',
|
description: 'Windows Shortcut',
|
||||||
|
|
@ -56,6 +80,12 @@ const MIMETypes: Library = {
|
||||||
opensWith: ['apps/Editor'],
|
opensWith: ['apps/Editor'],
|
||||||
icon: 'markdown'
|
icon: 'markdown'
|
||||||
},
|
},
|
||||||
|
mjs: {
|
||||||
|
type: 'text/javascript',
|
||||||
|
description: 'JavaScript Module',
|
||||||
|
opensWith: ['apps/Editor'],
|
||||||
|
icon: 'code'
|
||||||
|
},
|
||||||
mp4: {
|
mp4: {
|
||||||
type: 'video/mp4',
|
type: 'video/mp4',
|
||||||
description: 'MP4 Video',
|
description: 'MP4 Video',
|
||||||
|
|
@ -74,6 +104,12 @@ const MIMETypes: Library = {
|
||||||
opensWith: ['apps/ImageViewer'],
|
opensWith: ['apps/ImageViewer'],
|
||||||
icon: 'image'
|
icon: 'image'
|
||||||
},
|
},
|
||||||
|
theme: {
|
||||||
|
type: 'application/x-flow-theme',
|
||||||
|
description: 'FlowOS Theme',
|
||||||
|
opensWith: ['apps/Editor'],
|
||||||
|
icon: 'palette'
|
||||||
|
},
|
||||||
txt: {
|
txt: {
|
||||||
type: 'text/plain',
|
type: 'text/plain',
|
||||||
description: 'Text Document',
|
description: 'Text Document',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { Library } from '../../types'
|
import { AppClosedEvent, AppOpenedEvent, Library } from '../../types'
|
||||||
|
import nullIcon from '../../assets/icons/application-default-icon.svg'
|
||||||
|
import { getTime } from '../../utils'
|
||||||
|
|
||||||
const StatusBar: Library = {
|
const StatusBar: Library = {
|
||||||
config: {
|
config: {
|
||||||
|
|
@ -8,6 +10,75 @@ const StatusBar: Library = {
|
||||||
},
|
},
|
||||||
init: (l, k, p) => {
|
init: (l, k, p) => {
|
||||||
StatusBar.data.element = new l.HTML('toolbar')
|
StatusBar.data.element = new l.HTML('toolbar')
|
||||||
|
StatusBar.data.element.html(`
|
||||||
|
<div class="outlined" data-toolbar-id="start"><span class="material-symbols-rounded">space_dashboard</span></div>
|
||||||
|
|
||||||
|
<div data-toolbar-id="apps"></div>
|
||||||
|
<div style="flex:1;"></div>
|
||||||
|
<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.data.element.qs('div[data-toolbar-id="calendar"]')?.text(time)
|
||||||
|
}).catch(e => console.error)
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
if ('getBattery' in navigator) {
|
||||||
|
(navigator as any).getBattery().then((battery: any) => {
|
||||||
|
StatusBar.data.updateBatteryIcon(battery)
|
||||||
|
|
||||||
|
battery.addEventListener('levelchange', () => {
|
||||||
|
StatusBar.data.updateBatteryIcon(battery)
|
||||||
|
})
|
||||||
|
|
||||||
|
battery.addEventListener('chargingchange', () => {
|
||||||
|
StatusBar.data.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(`${(k.config as any).SERVER as string}/bare/`)
|
||||||
|
.then(() => {
|
||||||
|
const endTime = performance.now()
|
||||||
|
const pingTime = endTime - startTime
|
||||||
|
StatusBar.data.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 l.HTML('app').appendMany(
|
||||||
|
new l.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.data.element.qs('div[data-toolbar-id="apps"]')?.elm as HTMLElement)
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('app_closed', (e: AppClosedEvent): void => {
|
||||||
|
StatusBar.data.element.qs('div[data-toolbar-id="apps"]')?.qs(`img[data-id="${e.detail.token}"]`)?.elm.parentElement?.remove()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
updateBatteryIcon (battery: any) {
|
updateBatteryIcon (battery: any) {
|
||||||
|
|
|
||||||
|
|
@ -1,461 +0,0 @@
|
||||||
import Kernel from '../../kernel'
|
|
||||||
import ProcessLib from '../../structures/ProcessLib'
|
|
||||||
import { Directory, Errors, File, Library, Permission, Stats } from '../../types'
|
|
||||||
|
|
||||||
console.debug = (...args: any[]) => {
|
|
||||||
console.log('[VirtualFS]', ...args)
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Stats> => {
|
|
||||||
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
|
|
||||||
|
|
@ -2,6 +2,7 @@ import HTML from '../../HTML'
|
||||||
import FlowWindow from '../../structures/FlowWindow'
|
import FlowWindow from '../../structures/FlowWindow'
|
||||||
import ProcessLib from '../../structures/ProcessLib'
|
import ProcessLib from '../../structures/ProcessLib'
|
||||||
import { FlowWindowConfig, Library } from '../../types'
|
import { FlowWindowConfig, Library } from '../../types'
|
||||||
|
import nullIcon from '../../assets/icons/application-default-icon.svg'
|
||||||
|
|
||||||
const WindowManager: Library = {
|
const WindowManager: Library = {
|
||||||
config: {
|
config: {
|
||||||
|
|
@ -46,12 +47,12 @@ const WindowManager: Library = {
|
||||||
WindowManager.data.windowArea.elm.appendChild(win.element)
|
WindowManager.data.windowArea.elm.appendChild(win.element)
|
||||||
return win
|
return win
|
||||||
},
|
},
|
||||||
createModal: async (title: string, text: string, process: ProcessLib) => {
|
createModal: async (type: 'allow' | 'ok', title: string, text: string, process: ProcessLib) => {
|
||||||
const win = new FlowWindow(process, WindowManager.data, {
|
const win = new FlowWindow(process, WindowManager.data, {
|
||||||
title,
|
title,
|
||||||
icon: '',
|
icon: '',
|
||||||
width: 300,
|
width: 350,
|
||||||
height: 200,
|
height: 150,
|
||||||
canResize: false
|
canResize: false
|
||||||
})
|
})
|
||||||
const appOpenedEvent = {
|
const appOpenedEvent = {
|
||||||
|
|
@ -61,22 +62,59 @@ const WindowManager: Library = {
|
||||||
win
|
win
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.dispatchEvent(new CustomEvent('app_opened', appOpenedEvent))
|
|
||||||
|
|
||||||
const { Button } = await process.loadLibrary('lib/Components')
|
const { Button } = await process.loadLibrary('lib/Components')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: await new Promise((resolve) => {
|
value: await new Promise((resolve) => {
|
||||||
new HTML('h3').text(title).appendTo(win.content)
|
win.content.style.padding = '10px'
|
||||||
new HTML('p').text(text).appendTo(win.content)
|
win.content.style.display = 'flex'
|
||||||
Button.new().text('Allow').appendTo(win.content).on('click', () => {
|
win.content.style.flexDirection = 'column'
|
||||||
|
win.content.style.gap = '10px'
|
||||||
|
|
||||||
|
const container = new HTML('div').style({
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: 'max-content'
|
||||||
|
}).appendTo(win.content)
|
||||||
|
|
||||||
|
new HTML('img').attr({ src: process.process.config.icon ?? nullIcon }).style({
|
||||||
|
height: '100%',
|
||||||
|
'aspect-ratio': '1 / 1',
|
||||||
|
borderRadius: '50%',
|
||||||
|
alignSelf: 'center'
|
||||||
|
}).appendTo(container)
|
||||||
|
|
||||||
|
const space = new HTML('div').style({
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
'flex-direction': 'column',
|
||||||
|
'justify-content': 'center',
|
||||||
|
height: 'max-content'
|
||||||
|
}).appendTo(container)
|
||||||
|
|
||||||
|
new HTML('h3').text(title).style({ margin: '0' }).appendTo(space)
|
||||||
|
new HTML('p').text(text).style({ margin: '0' }).appendTo(space)
|
||||||
|
const div = new HTML('div').style({ display: 'flex', gap: '10px', alignItems: 'right' }).appendTo(space)
|
||||||
|
|
||||||
|
if (type === 'allow') document.dispatchEvent(new CustomEvent('app_opened', appOpenedEvent))
|
||||||
|
|
||||||
|
if (type === 'allow') {
|
||||||
|
Button.new('primary').text('Allow').appendTo(div).on('click', () => {
|
||||||
resolve(true)
|
resolve(true)
|
||||||
win.close()
|
win.close()
|
||||||
})
|
})
|
||||||
Button.new().text('Deny').appendTo(win.content).on('click', () => {
|
Button.new().text('Deny').appendTo(div).on('click', () => {
|
||||||
resolve(false)
|
resolve(false)
|
||||||
win.close()
|
win.close()
|
||||||
})
|
})
|
||||||
|
} else if (type === 'ok') {
|
||||||
|
Button.new('primary').text('OK').appendTo(div).on('click', () => {
|
||||||
|
win.close()
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
WindowManager.data.windows.push(win)
|
WindowManager.data.windows.push(win)
|
||||||
WindowManager.data.windowArea.elm.appendChild(win.element)
|
WindowManager.data.windowArea.elm.appendChild(win.element)
|
||||||
|
|
|
||||||
12
src/types.ts
12
src/types.ts
|
|
@ -3,7 +3,6 @@ import Kernel from './kernel'
|
||||||
import FlowWindow from './structures/FlowWindow'
|
import FlowWindow from './structures/FlowWindow'
|
||||||
import LibraryLib from './structures/LibraryLib'
|
import LibraryLib from './structures/LibraryLib'
|
||||||
import ProcessLib from './structures/ProcessLib'
|
import ProcessLib from './structures/ProcessLib'
|
||||||
import Components from './system/lib/Components'
|
|
||||||
import MIMETypes from './system/lib/MIMETypes'
|
import MIMETypes from './system/lib/MIMETypes'
|
||||||
|
|
||||||
export interface AppClosedEvent extends CustomEvent {
|
export interface AppClosedEvent extends CustomEvent {
|
||||||
|
|
@ -154,7 +153,7 @@ export interface WindowManager {
|
||||||
windows: FlowWindow[]
|
windows: FlowWindow[]
|
||||||
getHighestZIndex: () => number
|
getHighestZIndex: () => number
|
||||||
createWindow: (config: FlowWindowConfig, process: ProcessLib) => FlowWindow
|
createWindow: (config: FlowWindowConfig, process: ProcessLib) => FlowWindow
|
||||||
createModal: (title: string, text: string, process: ProcessLib) => Promise<ModalData>
|
createModal: (type: 'allow' | 'ok', title: string, text: string, process: ProcessLib) => Promise<ModalData>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Launcher {
|
export interface Launcher {
|
||||||
|
|
@ -173,15 +172,20 @@ export interface StatusBar {
|
||||||
updateIcon: (ms: number) => void
|
updateIcon: (ms: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IComponents {
|
||||||
|
[key: string]: {
|
||||||
|
new: (...args: any[]) => InstanceType<typeof HTML>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type LoadedLibrary<T> =
|
export type LoadedLibrary<T> =
|
||||||
T extends 'lib/VirtualFS' ? FileSystem :
|
|
||||||
T extends 'lib/WindowManager' ? WindowManager :
|
T extends 'lib/WindowManager' ? WindowManager :
|
||||||
T extends 'lib/HTML' ? typeof HTML :
|
T extends 'lib/HTML' ? typeof HTML :
|
||||||
T extends 'lib/Launcher' ? Launcher :
|
T extends 'lib/Launcher' ? Launcher :
|
||||||
T extends 'lib/XOR' ? XOR :
|
T extends 'lib/XOR' ? XOR :
|
||||||
T extends 'lib/StatusBar' ? StatusBar :
|
T extends 'lib/StatusBar' ? StatusBar :
|
||||||
T extends 'lib/MIMETypes' ? typeof MIMETypes.data :
|
T extends 'lib/MIMETypes' ? typeof MIMETypes.data :
|
||||||
T extends 'lib/Components' ? typeof Components.data :
|
T extends 'lib/Components' ? IComponents :
|
||||||
any
|
any
|
||||||
|
|
||||||
export type LibraryPath = 'lib/VirtualFS' | 'lib/WindowManager' | string
|
export type LibraryPath = 'lib/VirtualFS' | 'lib/WindowManager' | string
|
||||||
|
|
|
||||||
|
|
@ -16,18 +16,16 @@ export const getTime = async (): Promise<string> => {
|
||||||
if (hours === 0) {
|
if (hours === 0) {
|
||||||
hours = 12
|
hours = 12
|
||||||
} else if (hours > 12) {
|
} else if (hours > 12) {
|
||||||
hours = hours % 12
|
hours %= 12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hours = (hours < 10) ? `0${hours}` : hours
|
hours = (hours < 10) ? `0${hours}` : hours
|
||||||
minutes = (minutes < 10) ? `0${minutes}` : minutes
|
minutes = (minutes < 10) ? `0${minutes}` : minutes
|
||||||
|
|
||||||
const timeString = use24hrs
|
return use24hrs
|
||||||
? `${hours}:${minutes}`
|
? `${hours}:${minutes}`
|
||||||
: `${hours}:${minutes} ${period}`
|
: `${hours}:${minutes} ${period}`
|
||||||
|
|
||||||
return timeString
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,6 @@ module.exports = {
|
||||||
name: 'FlowOS',
|
name: 'FlowOS',
|
||||||
plugin: ['typedoc-material-theme'],
|
plugin: ['typedoc-material-theme'],
|
||||||
themeColor: '#1e1e2e',
|
themeColor: '#1e1e2e',
|
||||||
entryPoints: ['src/kernel.ts'],
|
entryPoints: ['src/bootloader.ts'],
|
||||||
entryPointStrategy: 'expand'
|
entryPointStrategy: 'expand'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,5 +29,8 @@ export default defineConfig({
|
||||||
disable: false,
|
disable: false,
|
||||||
verbose: true
|
verbose: true
|
||||||
})
|
})
|
||||||
]
|
],
|
||||||
|
build: {
|
||||||
|
target: 'ESNEXT'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue