diff --git a/.github/workflows/biome.yml b/.github/workflows/biome.yml index 131726b..c2486a7 100644 --- a/.github/workflows/biome.yml +++ b/.github/workflows/biome.yml @@ -14,7 +14,7 @@ jobs: steps: - name: "Checkout code" - uses: "actions/checkout@v4" + uses: "actions/checkout@v5" with: token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d906365..8faac8c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,9 +8,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 'lts/*' - name: Setup pnpm diff --git a/.github/workflows/upk-build.yml b/.github/workflows/upk-build.yml new file mode 100644 index 0000000..36e5c74 --- /dev/null +++ b/.github/workflows/upk-build.yml @@ -0,0 +1,47 @@ +name: Build UPK for Anura +on: + push: + paths: + - 'package.json' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: 'lts/*' + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.13' + - name: Install dependencies + run: pnpm i + - name: Install BareMux v1 + run: pnpm i @mercuryworkshop/bare-mux@^1.1.4 + - name: Download upk-tools.zip + run: curl -L -o upk-tools.zip https://cdn.terbiumon.top/upk-tools.zip + - name: Extract upk-tools + run: unzip -o upk-tools.zip + - name: replace BCC Client v2 with v1 + run: mv bx1bcc.ts src/sys/liquor/bcc.ts + - name: Add BareMux Script + run: npx replace-in-file "" " " index.html + - name: Replace BareMux in codebase + run: bash replace.sh + - name: Build TB React + run: pnpm run build-static + - name: "Run UPK Builder" + run: python3 upk.py + - name: Upload UPK + uses: actions/upload-artifact@v4 + with: + name: UPK + path: terbium-upk.app.zip diff --git a/.gitignore b/.gitignore index cc55a45..4fcfaee 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ dist-ssr *.local *.tsbuildinfo .npmrc -pnpm-lock.yaml package-lock.json # Editor directories and files diff --git a/README.md b/README.md index 168a95b..c0380bb 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - [Vite](https://vite.dev) - [React](https://react.dev) - [TailwindCSS](https://tailwindcss.com) -- [FilerJS](https://github.com/filerjs/filer) +- [TFS](https://github.com/terbiumos/tfs) - [Fflate](https://github.com/101arrowz/fflate/) - [BareMux](https://github.com/mercuryworkshop/bare-mux) diff --git a/SECURITY.md b/SECURITY.md index d7eb9b8..7857491 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -15,6 +15,7 @@ If your version of terbium is unsupported, please do not make a GitHub Issue abo | Version | Supported | | ------- | --------- | +| 2.1.0 (stable) | ❌ | | 2.1.1 (stable) | ✅ | ### Supported Lemonade Versions diff --git a/biome.json b/biome.json index 9c1126c..205b38a 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "vcs": { "enabled": false, "clientKind": "git", @@ -14,6 +14,11 @@ "indentWidth": 4, "lineWidth": 320 }, + "css": { + "parser": { + "tailwindDirectives": true + } + }, "linter": { "enabled": true, "rules": { diff --git a/docs/README.md b/docs/README.md index 5fd9e5a..a982855 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,7 +3,6 @@ Welcome to Terbium v2's Documentation. Here is a simple table of contents to help you get where you need to get - [How to Contribute to Terbium v2](./contributions.md) -- [Quick Guide to Filer](./quick-guide-to-filer.md) - [Creating Terminal Commands](./creating-terminal-commands.md) - [Creating Apps](./creating-apps.md) - [Backend Configuration Options](./backend-configuration.md) diff --git a/docs/apis/readme.md b/docs/apis/readme.md index 66f1bba..74d7055 100644 --- a/docs/apis/readme.md +++ b/docs/apis/readme.md @@ -15,6 +15,7 @@ So you're looking to use Terbium APIs. Well, you're in the right place! Terbium - [Platform](#platform) - [Process](#process) - [Screen](#screen) + - [VFS](#vfs) - [System](#system) - [Mediaisland](#mediaisland) - [File](#file) @@ -456,7 +457,7 @@ So you're looking to use Terbium APIs. Well, you're in the right place! Terbium - **FileBrowser** - Description: Simple FileBrowser Dialog - Parameters: - - `props: { title: string, filter: string, onOk: Function }` - FileBrowser dialog properties. + - `props: { title: string, filter: string, onOk: Function, onCancel: Function, local: boolean }` - FileBrowser dialog properties. - Example: ```javascript await tb.dialog.FileBrowser({ @@ -469,7 +470,7 @@ So you're looking to use Terbium APIs. Well, you're in the right place! Terbium - **DirectoryBrowser** - Description: Simple FileBrowser Dialog - Parameters: - - `props: { title: string, filter: string, onOk: Function }` - FileBrowser dialog properties. + - `props: { title: string, filter: string, onOk: Function, onCancel: Function, local: boolean }` - FileBrowser dialog properties. - Example: ```javascript await tb.dialog.DirectoryBrowser({ @@ -482,7 +483,7 @@ So you're looking to use Terbium APIs. Well, you're in the right place! Terbium - **SaveFile** - Description: Simple File Saving Dialog - Parameters: - - `props: { title: string, defualtDir: string, filename: string, onOk: Function }` - SaveFile dialog properties. + - `props: { title: string, defualtDir: string, filename: string, onOk: Function, onCancel: Function, local: boolean }` - SaveFile dialog properties. - Example: ```javascript await tb.dialog.SaveFile({ @@ -583,6 +584,184 @@ So you're looking to use Terbium APIs. Well, you're in the right place! Terbium tb.screen.capture() ``` +### VFS + - **servers** + - Description: A Map of the current users webdav servers + - Returns: `Object` - VFSOperations + - Example: + ```js + for (const instance of tb.vfs.servers) { + const davInfo = instance[1]; + // Use dav instance info here including a already established connection if one is availible + } + ``` + - **currentServer** + - Description: The current WebDav server to use for operations + - Returns: `Object` - VFSOperations + - Example: + ```js + const client = tb.vfs.currentServer.connection.client; + // use webdav methods here or use VFS Operations as a drop in for working between TFS and VFS + ``` + + - **create** + - Description: (async) Returns a new instance of VFS, You will probably not use this function unless your directly modifying terbiums codebase + - Returns: `Promise` + - Example: + ```js + const vfs = await vfs.create(); + ``` + + - **mount** + - Description: Mounts the inputed server from vfs.servers + - Parameters: + - `serverName: string` - the name of the server to mount + - Example: + ```js + await tb.vfs.mount("servername"); + ``` + + - **mountAll** + - Description: Mounts all servers avalible in vfs.servers + - Example: + ```js + await tb.vfs.mountAll() + ``` + + - **addServer** + - Description: Adds a server to the users WebDav server list + - Parameters: + - `Server: ServerInfo[]` - The server information to put in + - Example: + ```js + await tb.vfs.addServer({ + name: "any name you want for the drive name"; + url: "https://somedavendpoint.com/"; + username: "IloveTerbiumDev"; + password: "XSTARSwasHere"; + }) + ``` + + - **removeServer** + - Description: Removes a server from the users WebDav server list + - Parameters: + - `ServerName: string` - The name of the server to remove + - Example: + ```js + await tb.vfs.removeServer("webdav1") + ``` + + - **setServer** + - Description: Sets `currentServer` to the requested server + - Parameters: + - `ServerName: string` - The server name to set the server too **NOTE** Server MUST be mounted to perform this operation. + - Example: + ```js + await tb.vfs.setServer("webdav1"); + // tb.vfs.currentServer is now the instance of VFSOperations that webdav1 uses + ``` + + - **whatFS** + - Description: Returns Either TFS or VFSOperations as the suitable File System for you to use for said drive + - Parameters: + - `Path: string` - The path to check + - Example: + ```js + const fs = await tb.vfs.whatFS("/mnt/dav"); + // FS is VFSOperations + const fs = await tb.vfs.whatFS("/home/XSTARS/"); + // FS is TFS.fs + ``` + + - **VFSOperations** + > **NOTE:** This is **NOT** an API. This is an instance representing File System actions, WebDav client information, etc., and is referenced by several APIs above. + #### Properties + + - **client**: `WebDavClient` + The WebDav Client Interface. + + #### Methods + + - **readdir(path, callback)** + - Reads the contents of a directory at the given path. + - **Parameters:** + - `path: string` — Directory path. + - `callback: (err: any, files?: any[]) => void` — Called with error or array of file names. + + - **readFile(path, callback)** + - Reads the contents of a file as text. + - **Parameters:** + - `path: string` — File path. + - `callback: (err: any, data?: string) => void` — Called with error or file data. + + - **writeFile(path, data, callback)** + - Writes data to a file, replacing its contents. + - **Parameters:** + - `path: string` — File path. + - `data: string | ArrayBuffer` — Data to write. + - `callback: (err: any) => void` — Called with error if any. + + - **delete(path, callback)** + - Deletes a file at the specified path. + - **Parameters:** + - `path: string` — File path. + - `callback: (err: any) => void` — Called with error if any. + + - **rename(oldPath, newPath, callback)** + - Renames or moves a file from `oldPath` to `newPath`. + - **Parameters:** + - `oldPath: string` — Original file path. + - `newPath: string` — New file path. + - `callback: (err: any) => void` — Called with error if any. + + - **createDirectory(path, callback)** + - Creates a new directory at the specified path. + - **Parameters:** + - `path: string` — Directory path. + - `callback: (err: any) => void` — Called with error if any. + + - **exists(path, callback)** + - Checks if a file or directory exists at the given path. + - **Parameters:** + - `path: string` — Path to check. + - `callback: (err: any, exists?: boolean) => void` — Called with error or existence boolean. + + - **stat(path, callback)** + - Retrieves metadata/statistics about a file or directory. + - **Parameters:** + - `path: string` — Path to check. + - `callback: (err: any, stat?: any) => void` — Called with error or stat object. + + - **copy(source, destination, callback)** + - Copies a file from source to destination. + - **Parameters:** + - `source: string` — Source file path. + - `destination: string` — Destination file path. + - `callback: (err: any) => void` — Called with error if any. + + - **unlink(path, callback)** + - Deletes a file at the specified path (alias for `delete`). + - **Parameters:** + - `path: string` — File path. + - `callback: (err: any) => void` — Called with error if any. + + - **move(source, destination, callback)** + - Moves a file from source to destination (alias for `rename`). + - **Parameters:** + - `source: string` — Source file path. + - `destination: string` — Destination file path. + - `callback: (err: any) => void` — Called with error if any. + + - **appendFile(path, data, callback)** + - Appends data to the end of a file. + - **Parameters:** + - `path: string` — File path. + - `data: string | ArrayBuffer` — Data to append. + - `callback: (err: any) => void` — Called with error if any. + + All of these functions also have a Promises variant that has the exact same syntax except it does not have a callback instead you use it asynchronously + + ### System - **version** - Description: Lists the version of Terbium diff --git a/docs/quick-guide-to-filer.md b/docs/quick-guide-to-filer.md deleted file mode 100644 index 9e54c7f..0000000 --- a/docs/quick-guide-to-filer.md +++ /dev/null @@ -1,83 +0,0 @@ -# Quick Guide To Filer - -> NOTE: Please note that this portion of the Terbium Documentation will be removed starting with version 2.2 of Terbium. We will be migrating to TFS (Our own custom Origin-Private-File-System replacement for Filer). When its completed and released its documentation will be put in place of this documentation. To ensure compatability when the change does happen use `window.tb.fs` instead of `Filer.fs` directly - -This simple guide will give you some basic Filer usages and how we at Terbium believe you should write your Filer code; asynchronously.
-> For more a depth guide refer to the [Filer GitHub](https://github.com/filerjs/filer) - -If your want to use Filer in your app put this in the head -```html - -``` -For starters when you are reading a file or directory you should use the **promises** version of that action, below is a visual example: - -```js -// Bad -await Filer.fs.readFile("/hi/hi.txt", "utf8", (err, data) => { -    if(err) throw err -    console.log(data) -}) - -// Good -let hi = await Filer.fs.promises.readFile("/hi/hi.txt", "utf8") -console.log(hi) -``` - -## Contents -- [fs.mkdir](#mkdir) -- [fs.writeFile](#writeFile) -- [fs.readFile](#readFile) -- [fs.readdir](#readdir) -- [fs.rmdir](#rmdir) -- [fs.unlink](#unlink) - -### fs.mkdir(path, [options], callback) -Makes a directory from the `path` argument -```js -await Filer.fs.promises.mkdir("/hi"); -``` -### fs.writeFile(path, data, [options], callback) -Writes to a file can be any, can be a `string` or `buffer` -> NOTE: If you trying to write something like a image you should turn the data of the image into an array buffer -```js -// UTF8 text file -await Filer.fs.promises.writeFile("/hi/hi.txt", "Hiiiii"); - -// Example of uploading raw image binary buffer -let imgData = e.target.files[0].arrayBuffer(); -await Filer.fs.promises.writeFile("spungerbog.png", Filer.Buffer.from(imgData)); -``` -### fs.readFile(path, [options], callback) -Reads a file, options can be it's encoding `"utf8"` or an object literal `{ encoding: "utf8" }`
-> NOTE: if encoding is not passed in options it will output the raw binary buffer -```js -let hi = JSON.parse(await Filer.fs.promises.readFile("/hi/hi.txt", "utf8")); -// hi is now the contents of hi.txt as a UTF8 String -``` -### fs.readdir(path, [options], callback) -Example directory structure: -```js -/* -* /hi -* | hi.txt -*/ -``` -```js -let dir = await Filer.fs.promises.readdir("/hi"); -// dir is now an array of it's contents ["hi.txt"] -``` -### fs.rmdir(path, callback) -Removes a directory if it is empty. -```js -// remove an empty dir -await Filer.fs.promises.rmdir("/hi/empty-dir"); -``` -> NOTE: If you would like to forcefully and recursively remove a dir look the below code -```js -await (new Filer.fs.Shell()).promises.rm("/hi", {recursive: true}) -``` -### fs.unlink(path, callback) -Remove a file provided from path argument -```js -await Filer.fs.promises.unlink("/hi/hi.txt") -``` \ No newline at end of file diff --git a/index.html b/index.html index 0b98e36..f302ffd 100644 --- a/index.html +++ b/index.html @@ -11,6 +11,28 @@ + +
@@ -18,8 +40,6 @@ - - - + diff --git a/public/apps/media viewer.tapp/index.js b/public/apps/media viewer.tapp/index.js index 0575a66..9cbd776 100644 --- a/public/apps/media viewer.tapp/index.js +++ b/public/apps/media viewer.tapp/index.js @@ -1,7 +1,3 @@ -import * as webdav from "/fs/apps/system/files.tapp/webdav.js"; - -window.webdav = webdav; - window.addEventListener("load", async () => { parent.postMessage(JSON.stringify({ type: "ready" }), "*"); }); @@ -51,11 +47,7 @@ async function openFile(url, ext, dav) { const davUrl = url.split("/dav/")[0] + "/dav/"; const dav = davInstances.find(d => d.url.toLowerCase().includes(davUrl)); if (!dav) throw new Error("No matching dav instance found"); - const client = window.webdav.createClient(dav.url, { - username: dav.username, - password: dav.password, - authType: window.webdav.AuthType.Password, - }); + const client = window.parent.tb.vfs.servers.get(dav.name); let filePath; if (url.startsWith("http")) { const match = url.match(/^https?:\/\/[^\/]+\/dav\/([^\/]+\/)?(.+)$/); @@ -208,11 +200,7 @@ async function openFile(url, ext, dav) { const davUrl = url.split("/dav/")[0] + "/dav/"; const dav = davInstances.find(d => d.url.toLowerCase().includes(davUrl)); if (!dav) throw new Error("No matching dav instance found"); - const client = window.webdav.createClient(dav.url, { - username: dav.username, - password: dav.password, - authType: window.webdav.AuthType.Password, - }); + const client = window.parent.tb.vfs.servers.get(dav.name); let filePath; if (url.startsWith("http")) { const match = url.match(/^https?:\/\/[^\/]+\/dav\/([^\/]+\/)?(.+)$/); @@ -266,11 +254,7 @@ async function openFile(url, ext, dav) { const davUrl = url.split("/dav/")[0] + "/dav/"; const dav = davInstances.find(d => d.url.toLowerCase().includes(davUrl)); if (!dav) throw new Error("No matching dav instance found"); - const client = window.webdav.createClient(dav.url, { - username: dav.username, - password: dav.password, - authType: window.webdav.AuthType.Password, - }); + const client = window.parent.tb.vfs.servers.get(dav.name); let filePath; if (url.startsWith("http")) { const match = url.match(/^https?:\/\/[^\/]+\/dav\/([^\/]+\/)?(.+)$/); @@ -412,7 +396,7 @@ window.addEventListener("message", async e => { const ext = data.path.split(".").pop(); let json = JSON.parse(await window.parent.tb.fs.promises.readFile("/apps/system/files.tapp/extensions.json", "utf8")); if (data.path.includes("http")) { - openFile(data.path, ext, true); + openFile(data.path, ext); } else if (json["image"].includes(ext)) { let img = await window.parent.tb.fs.promises.readFile(data.path); let blob = new Blob([img], { type: "image/" + ext }); diff --git a/public/apps/settings.tapp/accounts/index.html b/public/apps/settings.tapp/accounts/index.html index 211f8d6..6ffbd82 100644 --- a/public/apps/settings.tapp/accounts/index.html +++ b/public/apps/settings.tapp/accounts/index.html @@ -1,47 +1,71 @@ - - - Accounts - - - + + + Accounts + + + - -
-

Accounts

-
- -
- - + + +
+

Account Manager

+ +
+
+ + +
+ + + \ No newline at end of file diff --git a/public/apps/settings.tapp/accounts/index.js b/public/apps/settings.tapp/accounts/index.js index 9653fde..3104650 100644 --- a/public/apps/settings.tapp/accounts/index.js +++ b/public/apps/settings.tapp/accounts/index.js @@ -220,58 +220,102 @@ renderAccounts(); const createAccount = async () => { const askNewAccountDetails = async () => { - await tb.dialog.Message({ - title: "Create Username", - onOk: async username => { - const data = {}; - data["id"] = username; - data["username"] = username; - await tb.dialog.Message({ - title: "Create Password", - onOk: async password => { - if (password !== "") { - data["password"] = await tb.crypto(password); - } else { - data["password"] = false; - } - await tb.dialog.Select({ - title: "Do you want to set up a security question?", - options: [ - { - text: "Yes", - value: "yes", - }, - { - text: "No", - value: "no", + const makeAccount = async () => { + await tb.dialog.Message({ + title: "Create Username", + onOk: async username => { + const data = {}; + data["id"] = username; + data["username"] = username; + await tb.dialog.Message({ + title: "Create Password", + onOk: async password => { + if (password !== "") { + data["password"] = await tb.crypto(password); + } else { + data["password"] = false; + } + await tb.dialog.Select({ + title: "Do you want to set up a security question?", + options: [ + { + text: "Yes", + value: "yes", + }, + { + text: "No", + value: "no", + }, + ], + onOk: async securityChoice => { + if (securityChoice === "yes") { + await tb.dialog.Message({ + title: "Set Security Question", + onOk: async question => { + await tb.dialog.Message({ + title: "Set Security Answer", + onOk: async answer => { + data["securityQuestion"] = { + question: question, + answer: await tb.crypto(answer), + }; + askProfilePicture(data); + }, + }); + }, + }); + } else { + askProfilePicture(data); + } }, - ], - onOk: async securityChoice => { - if (securityChoice === "yes") { - await tb.dialog.Message({ - title: "Set Security Question", - onOk: async question => { - await tb.dialog.Message({ - title: "Set Security Answer", - onOk: async answer => { - data["securityQuestion"] = { - question: question, - answer: await tb.crypto(answer), - }; - askProfilePicture(data); - }, - }); - }, - }); - } else { - askProfilePicture(data); - } - }, - }); + }); + }, + }); + }, + }); + }; + const ping = await parent.tb.libcurl.fetch("https://auth.terbiumon.top/ping"); + if (ping.ok) { + await tb.dialog.Select({ + title: "Select Account Type", + options: [ + { + text: "Local Account", + value: "user", }, - }); - }, - }); + { + text: "Terbium Cloud Account", + value: "tacc", + }, + ], + onOk: async accountType => { + if (accountType === "user") { + makeAccount(); + } else { + const run = async () => { + try { + const resp = await window.parent.tb.tauth.signIn(); + const userDataConv = { + id: resp.data.user.id, + username: resp.data.user.name, + email: resp.data.user.email, + pfp: resp.data.user.image, + password: await tb.crypto(resp.data.user.password), + perm: "user", + }; + await tb.system.users.add(userDataConv); + renderAccounts(); + } catch (e) { + run(); + } + }; + run(); + } + }, + }); + } else { + makeAccount(); + } }; const askProfilePicture = async data => { diff --git a/public/apps/settings.tapp/index.css b/public/apps/settings.tapp/index.css index 8edd901..e1ffad1 100644 --- a/public/apps/settings.tapp/index.css +++ b/public/apps/settings.tapp/index.css @@ -298,3 +298,48 @@ h6 { gap: 6px; align-items: center; } + +.blur-slider input[type="range"] { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 10px; + background: linear-gradient(90deg, #60a5fa 18%, rgba(255, 255, 255, 0.12) 18%); + border-radius: 999px; + outline: none; +} +.blur-slider input[type="range"]::-webkit-slider-runnable-track { + height: 10px; + border-radius: 999px; + background: transparent; +} +.blur-slider input[type="range"]::-moz-range-track { + height: 10px; + border-radius: 999px; + background: transparent; +} +.blur-slider input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + margin-top: -5px; + width: 20px; + height: 20px; + border-radius: 50%; + background: #ffffff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45); + border: 3px solid rgba(255, 255, 255, 0.12); + cursor: pointer; +} +.blur-slider input[type="range"]::-moz-range-thumb { + width: 20px; + height: 20px; + border-radius: 50%; + background: #ffffff; + border: 3px solid rgba(255, 255, 255, 0.12); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45); + cursor: pointer; +} +.blur-hint { + font-size: 0.78rem; + color: rgba(255, 255, 255, 0.55); +} diff --git a/public/apps/settings.tapp/index.html b/public/apps/settings.tapp/index.html index 0e72749..b3f59e2 100644 --- a/public/apps/settings.tapp/index.html +++ b/public/apps/settings.tapp/index.html @@ -188,6 +188,55 @@

Show FPS Counter

💡 Tip: If you notice Terbium lagging with this feature enabled, try disabling it to improve performance on older or potato devices.

+
+

Window Accent Color

+
+
+ + + +
+ +
+
+

Force Windows to be Maximized

+
+
+
+
No
+ + + +
+
+
No
+
Yes
+
+
+
+

Make Maximized windows in "Full Screen" mode

+
+
+
+
No
+ + + +
+
+
No
+
Yes
+
+
+
+

Window Blur Intensity

+
+
+ Window Blur Intensity + 18% +
+ +
@@ -242,7 +291,10 @@

Account

@
- +
+ + +
diff --git a/public/apps/settings.tapp/index.js b/public/apps/settings.tapp/index.js index 2d81801..4611d7e 100644 --- a/public/apps/settings.tapp/index.js +++ b/public/apps/settings.tapp/index.js @@ -82,6 +82,8 @@ window.parent.tb.fs.readFile(`/home/${sessionStorage.getItem("currAcc")}/setting document.querySelector(`[action-for="transports"]`).querySelector(".select-title .text").innerText = data["transport"]; document.querySelector(`[action-for="show-seconds"]`).querySelector(".select-title .text").innerText = showSeconds ? "Yes" : "No"; document.querySelector(`[action-for="24h-12h"]`).querySelector(".select-title .text").innerText = twentyFourHour === "24h" ? "Yes" : "No"; + document.querySelector(`[action-for="wmx"]`).querySelector(".select-title .text").innerText = data["window"]["alwaysMaximized"] ? "Yes" : "No"; + document.querySelector(`[action-for="wfs"]`).querySelector(".select-title .text").innerText = data["window"]["alwaysFullscreen"] ? "Yes" : "No"; }); window.parent.tb.fs.readFile("/system/etc/terbium/settings.json", "utf8", (err, data) => { @@ -137,7 +139,7 @@ const customWallpaper = () => { delete_button.src = "/fs/apps/system/settings.tapp/delete.svg"; delete_button.classList.add("delete-wallpaper"); delete_button.addEventListener("click", async e => { - let data = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); + let data = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.parent.tb.user.username()}/settings.json`, "utf8")); if (data["wallpaper"] === path) { tb_wallpaper.set("/assets/wallpapers/1.png"); } @@ -147,7 +149,7 @@ const customWallpaper = () => { wimg.addEventListener("click", async e => { tb_wallpaper.set(path); }); - await window.parent.tb.fs.promises.writeFile(path, Filer.Buffer.from(buffer)); + await window.parent.tb.fs.promises.writeFile(path, window.parent.tb.buffer.from(buffer), "arraybuffer"); tb_wallpaper.set(path); img_container.append(wimg); img_container.append(delete_button); @@ -160,10 +162,11 @@ const customWallpaper = () => { case "fs": tb.dialog.FileBrowser({ title: "Select a wallpaper from the file system", + local: true, onOk: async filePath => { - const imgdata = await window.parent.tb.fs.promises.readFile(filePath, "base64"); + const imgdata = await window.parent.tb.fs.promises.readFile(filePath, "arraybuffer"); + await window.parent.tb.fs.promises.writeFile("/system/etc/terbium/wallpapers/" + filePath.split("/").pop(), imgdata, "arraybuffer"); tb.desktop.wallpaper.set("/system/etc/terbium/wallpapers/" + filePath.split("/").pop()); - await window.parent.tb.fs.promises.writeFile("/system/etc/terbium/wallpapers/" + filePath.split("/").pop(), imgdata); document.querySelector(".wallpapers").innerHTML = ` @@ -386,9 +389,9 @@ async function getWispSrvs() { getWispSrvs(); async function updateTransport(transport) { - const st = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); + const st = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.parent.tb.user.username()}/settings.json`, "utf8")); st["transport"] = transport; - await window.parent.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(st), "utf8"); + await window.parent.tb.fs.promises.writeFile(`/home/${await window.parent.tb.user.username()}/settings.json`, JSON.stringify(st), "utf8"); } const accentPreview = document.querySelector(".accent-preview"); @@ -396,14 +399,14 @@ const accentMousedown = async () => { const defaultAccent = "#32ae62"; accentPreview.classList.remove("group", "cursor-pointer"); accentPreview.style.setProperty("--accent", defaultAccent); - let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); + let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.parent.tb.user.username()}/settings.json`, "utf8")); settings["accent"] = defaultAccent; - window.parent.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(settings)); + window.parent.tb.fs.promises.writeFile(`/home/${await window.parent.tb.user.username()}/settings.json`, JSON.stringify(settings)); accentPreview.removeEventListener("mousedown", accentMousedown); }; const getAccent = async () => { - const settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); + const settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.parent.tb.user.username()}/settings.json`, "utf8")); var accentColor = settings["accent"]; const defaultAccent = "#32ae62"; if (accentColor !== defaultAccent) { @@ -431,17 +434,18 @@ custom_accent.addEventListener("click", e => { const g = rgb[1]; const b = rgb[2]; color = "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); - let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); + let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.parent.tb.user.username()}/settings.json`, "utf8")); settings["accent"] = color; - window.parent.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(settings)); + window.parent.tb.fs.promises.writeFile(`/home/${await window.parent.tb.user.username()}/settings.json`, JSON.stringify(settings)); } else { - let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); + let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.parent.tb.user.username()}/settings.json`, "utf8")); settings["accent"] = color; - window.parent.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(settings)); + window.parent.tb.fs.promises.writeFile(`/home/${await window.parent.tb.user.username()}/settings.json`, JSON.stringify(settings)); } accentPreview.style.setProperty("--accent", color); accentPreview.classList.add("group", "cursor-pointer"); accentPreview.addEventListener("mousedown", accentMousedown); + window.parent.window.dispatchEvent(new Event("upd-accent")); }); }); @@ -489,7 +493,7 @@ window.parent.tb.fs.readFile(`/home/${sessionStorage.getItem("currAcc")}/user.js pfpEl.src = data["pfp"]; }); -pfpEl.addEventListener("click", e => { +pfpEl.addEventListener("click", async e => { const uploader = document.createElement("input"); uploader.type = "file"; uploader.accept = "img/*"; @@ -502,6 +506,30 @@ pfpEl.addEventListener("click", e => { title: "Resize your Profile picture", img: reader.result, onOk: async img => { + if (await window.parent.tb.tauth.isTACC()) { + tb.dialog.Select({ + title: "Do you want to upload this profile picture to your Terbium Account?", + options: [ + { + text: "Yes", + value: "yes", + }, + { + text: "No", + value: "no", + }, + ], + onOk: async choice => { + if (choice === "yes") { + await window.parent.tb.tauth.updateInfo({ + pfp: img, + }); + } else { + return; + } + }, + }); + } const uSettings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, "utf8")); uSettings["pfp"] = img; pfpEl.src = img; @@ -590,7 +618,7 @@ permEl.addEventListener("click", async () => { sudo: true, title: "Authenticate to change your permissions", defaultUsername: sessionStorage.getItem("currAcc"), - onOk: async (username, password) => { + onOk: async (_username, password) => { const pass = await tb.crypto(password); if (pass === data["password"]) { await tb.dialog.Select({ @@ -628,6 +656,87 @@ permEl.addEventListener("click", async () => { } }); +const actype = document.querySelector(".actype"); +actype.addEventListener("click", async () => { + await tb.dialog.Select({ + title: "Select Option", + options: [ + { + text: "Link Terbium Cloud™ Account", + value: "cloud", + }, + { + text: "Convert to Local Account", + value: "local", + }, + ], + onOk: async choice => { + switch (choice) { + case "cloud": + const res = await window.parent.tb.tauth.signIn(); + actype.innerHTML = "Terbium Cloud\u2122 Account"; + window.parent.tb.fs.readFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, "utf8", async (err, data) => { + if (err) return console.log(err); + data = JSON.parse(data); + data["username"] = res.data.user.name; + await window.parent.tb.fs.promises.writeFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, JSON.stringify(data)); + let desktopDat = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/desktop/.desktop.json`, "utf8")); + desktopDat = desktopDat.map(entry => { + entry.item = entry.item.replace(`/home/${sessionStorage.getItem("currAcc")}/`, `/home/${res.data.user.name}/`); + return entry; + }); + await window.parent.tb.fs.promises.rename(`/home/${sessionStorage.getItem("currAcc")}`, `/home/${res.data.user.name}`); + await window.parent.tb.fs.promises.writeFile(`/home/${res.data.user.name}/desktop/.desktop.json`, JSON.stringify(desktopDat)); + await window.parent.tb.fs.promises.rename(`/apps/user/${sessionStorage.getItem("currAcc")}`, `/apps/user/${res.data.user.name}`); + window.parent.tb.fs.readFile("/system/etc/terbium/settings.json", "utf8", async (err, data) => { + if (err) return console.log(err); + data = JSON.parse(data); + data["defaultUser"] = res.data.user.name; + await window.parent.tb.fs.promises.writeFile("/system/etc/terbium/settings.json", JSON.stringify(data)); + }); + const fcfg = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${res.data.user.name}/files/config.json`, "utf8")); + fcfg.drives["File System"] = `/home/${res.data.user.name}/`; + await window.parent.tb.fs.promises.writeFile(`/apps/user/${res.data.user.name}/files/config.json`, JSON.stringify(fcfg)); + const qcfg = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${res.data.user.name}/files/quick-center.json`, "utf8")); + for (const key in qcfg.paths) { + if (Object.prototype.hasOwnProperty.call(qcfg.paths, key)) { + qcfg.paths[key] = qcfg.paths[key].replace(sessionStorage.getItem("currAcc"), res.data.user.name); + } + } + await window.parent.tb.fs.promises.writeFile(`/apps/user/${res.data.user.name}/files/quick-center.json`, JSON.stringify(qcfg)); + sessionStorage.setItem("currAcc", res.data.user.name); + }); + window.parent.tb.system.users.update({ + username: res.data.user.name, + pfp: res.data.user.image, + }); + break; + case "local": + await window.parent.tb.tauth.signOut(); + actype.innerHTML = "Local Account"; + break; + } + }, + }); +}); + +window.parent.tb.fs.readFile(`/system/etc/terbium/taccs.json`, "utf8", (err, data) => { + if (err) actype.innerHTML = "Local Account"; + const entries = JSON.parse(data); + const act = sessionStorage.getItem("currAcc"); + try { + let isCloud = false; + if (Array.isArray(entries)) { + isCloud = entries.some(e => e && e.username === act); + } else if (entries && typeof entries === "object") { + isCloud = Object.values(entries).some(e => e && e.username === act); + } + actype.innerHTML = isCloud ? "Terbium Cloud\u2122 Account" : "Local Account"; + } catch (e) { + actype.innerHTML = "Local Account"; + } +}); + window.parent.tb.fs.readFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, "utf8", (err, data) => { if (err) return console.log(err); data = JSON.parse(data); @@ -701,7 +810,7 @@ accountsButton.addEventListener("mousedown", e => { const batteryPercentage = document.querySelector(".battery-percentage"); (async () => { - let showBatteryPercentage = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8"))["battery-percent"]; + let showBatteryPercentage = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.parent.tb.user.username()}/settings.json`, "utf8"))["battery-percent"]; const realCheckbox = batteryPercentage.querySelector("input[type='checkbox']"); if (showBatteryPercentage) { realCheckbox.checked = true; @@ -715,7 +824,7 @@ const batteryPercentage = document.querySelector(".battery-percentage"); })(); batteryPercentage.addEventListener("mousedown", async e => { - let data = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); + let data = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.parent.tb.user.username()}/settings.json`, "utf8")); const realCheckbox = batteryPercentage.querySelector("input[type='checkbox']"); realCheckbox.checked = !realCheckbox.checked; const checkIcon = batteryPercentage.querySelector(".checkIcon"); @@ -727,11 +836,11 @@ batteryPercentage.addEventListener("mousedown", async e => { tb.battery.hidePercentage(); } data["battery-percent"] = realCheckbox.checked; - window.parent.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(data)); + window.parent.tb.fs.promises.writeFile(`/home/${await window.parent.tb.user.username()}/settings.json`, JSON.stringify(data)); }); const getBat = async () => { - const battery = await window.tb.battery.canUse(); + const battery = await window.parent.tb.battery.canUse(); if (!battery) { document.querySelector(".battery").remove(); } @@ -751,7 +860,7 @@ showCords.addEventListener("mousedown", async e => { }); async function exportSettings() { - let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); + let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.parent.tb.user.username()}/settings.json`, "utf8")); let data = JSON.stringify(settings); let blob = new Blob([data], { type: "application/json" }); let url = URL.createObjectURL(blob); @@ -761,6 +870,68 @@ async function exportSettings() { a.click(); } +const range = document.getElementById("blurRange"); +const pct = document.getElementById("blurPercent"); +async function render(initial) { + const settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8")); + const v = initial ? settings.window.blurlevel : Number(range.value); + if (initial) range.value = v; + const mapped = Math.round((v / 50) * 100); + pct.textContent = mapped + "%"; + const fill = (v / 50) * 100; + range.style.background = `linear-gradient(90deg,#60a5fa ${fill}%, rgba(255,255,255,0.12) ${fill}%)`; + range.dataset.mappedPercent = mapped; + if (initial) return; + settings.window.blurlevel = v; + window.parent.tb.fs.promises.writeFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, JSON.stringify(settings, null, 4), "utf8"); + window.parent.dispatchEvent(new Event("upd-accent")); +} + +render(true); +range.addEventListener("input", () => render(false)); + +const winAccent = document.querySelector(".winaccent-preview"); +const winAccentPrev = async () => { + const defaultAccent = "#ffffff"; + accentPreview.classList.remove("group", "cursor-pointer"); + accentPreview.style.setProperty("--accent", defaultAccent); + let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.parent.tb.user.username()}/settings.json`, "utf8")); + settings.window.winAccent = defaultAccent; + window.parent.tb.fs.promises.writeFile(`/home/${await window.parent.tb.user.username()}/settings.json`, JSON.stringify(settings)); + winAccent.removeEventListener("mousedown", winAccentPrev); + window.parent.dispatchEvent(new Event("upd-accent")); +}; +winAccentPrev(); + +const custom_waccent = document.querySelector(".custom-waccent"); +custom_waccent.addEventListener("click", e => { + const color_picker = document.createElement("input"); + color_picker.type = "color"; + color_picker.click(); + color_picker.addEventListener("change", async e => { + let color = color_picker.value; + if (color.charAt(0) !== "#") { + const rgb = color.match(/\d+/g); + const r = rgb[0]; + const g = rgb[1]; + const b = rgb[2]; + color = "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); + let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.parent.tb.user.username()}/settings.json`, "utf8")); + settings["window"]["winAccent"] = color; + window.parent.tb.fs.promises.writeFile(`/home/${await window.parent.tb.user.username()}/settings.json`, JSON.stringify(settings)); + window.parent.dispatchEvent(new Event("upd-accent")); + } else { + let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.parent.tb.user.username()}/settings.json`, "utf8")); + settings["window"]["winAccent"] = color; + window.parent.tb.fs.promises.writeFile(`/home/${await window.parent.tb.user.username()}/settings.json`, JSON.stringify(settings)); + window.parent.dispatchEvent(new Event("upd-accent")); + } + winAccent.style.setProperty("--accent", color); + winAccent.classList.add("group", "cursor-pointer"); + winAccent.addEventListener("mousedown", winAccentPrev); + }); +}); + async function convertTBSIF() { const input = document.createElement("input"); input.type = "file"; @@ -770,8 +941,8 @@ async function convertTBSIF() { let reader = new FileReader(); reader.onload = async () => { let tbs_config = reader.result; - let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); - let syssettings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); + let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.parent.tb.user.username()}/settings.json`, "utf8")); + let syssettings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.parent.tb.user.username()}/settings.json`, "utf8")); if (tbs_config.theme && tbs_config.theme !== "default") { syssettings.theme = tbs_config.theme; } @@ -784,7 +955,7 @@ async function convertTBSIF() { if (tbs_config.shadow === "yes") { settings["system-blur"] = true; } - await window.parent.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(settings, null, 2), "utf8"); + await window.parent.tb.fs.promises.writeFile(`/home/${await window.parent.tb.user.username()}/settings.json`, JSON.stringify(settings, null, 2), "utf8"); await window.parent.tb.fs.promises.writeFile("/system/etc/terbium/settings.json", JSON.stringify(syssettings, null, 2), "utf8"); parent.window.location.reload(); }; @@ -819,7 +990,7 @@ const setupWindowOptimizations = async () => { const realCheckbox = windowOptimizationsCheckbox.querySelector("input[type='checkbox']"); const checkIcon = windowOptimizationsCheckbox.querySelector(".checkIcon"); - const setState = async (enabled) => { + const setState = async enabled => { realCheckbox.checked = enabled; if (enabled) { checkIcon.classList.remove("opacity-0", "scale-85"); @@ -851,7 +1022,7 @@ const setupFPSCounter = async () => { const realCheckbox = fpsCounterCheckbox.querySelector("input[type='checkbox']"); const checkIcon = fpsCounterCheckbox.querySelector(".checkIcon"); - const setState = async (enabled) => { + const setState = async enabled => { realCheckbox.checked = enabled; if (enabled) { checkIcon.classList.remove("opacity-0", "scale-85"); @@ -863,7 +1034,7 @@ const setupFPSCounter = async () => { settings.showFPS = enabled; await window.parent.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(settings, null, 2), "utf8"); - window.parent.dispatchEvent(new CustomEvent('settings-changed', { detail: { showFPS: enabled } })); + window.parent.dispatchEvent(new CustomEvent("settings-changed", { detail: { showFPS: enabled } })); }; try { @@ -883,7 +1054,7 @@ setupFPSCounter(); const realCheckbox = animationCheckbox.querySelector("input[type='checkbox']"); realCheckbox.checked = !realCheckbox.checked; const checkIcon = animationCheckbox.querySelector(".checkIcon"); -if(realCheckbox.checked) { +if (realCheckbox.checked) { checkIcon.classList.remove("opacity-0", "scale-85"); } else { checkIcon.classList.add("opacity-0", "scale-85"); diff --git a/public/apps/settings.tapp/select.js b/public/apps/settings.tapp/select.js index 77012cf..da38afa 100644 --- a/public/apps/settings.tapp/select.js +++ b/public/apps/settings.tapp/select.js @@ -91,6 +91,22 @@ selects.forEach(select => { window.tb.fs.writeFile("/system/etc/terbium/settings.json", JSON.stringify(settings)); window.parent.dispatchEvent(new Event("updWeather")); }); + } else if (select.getAttribute("action-for") === "wmx") { + window.tb.fs.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8", (err, data) => { + if (err) return console.log(err); + let settings = JSON.parse(data); + settings["window"]["alwaysMaximized"] = option.getAttribute("value").toLowerCase() === "yes" ? true : false; + window.tb.fs.writeFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, JSON.stringify(settings)); + window.parent.dispatchEvent(new Event("upd-accent")); + }); + } else if (select.getAttribute("action-for") === "wfs") { + window.tb.fs.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8", (err, data) => { + if (err) return console.log(err); + let settings = JSON.parse(data); + settings["window"]["alwaysFullscreen"] = option.getAttribute("value").toLowerCase() === "yes" ? true : false; + window.tb.fs.writeFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, JSON.stringify(settings)); + window.parent.dispatchEvent(new Event("upd-accent")); + }); } } }); diff --git a/public/apps/terminal.tapp/index.js b/public/apps/terminal.tapp/index.js index 641bebb..82887c4 100644 --- a/public/apps/terminal.tapp/index.js +++ b/public/apps/terminal.tapp/index.js @@ -1,7 +1,6 @@ import parser from "yargs-parser"; import http from "iso-http"; import git from "git"; -import * as webdav from "/fs/apps/system/files.tapp/webdav.js"; /** * @typedef {import("yargs-parser").Arguments} argv @@ -21,7 +20,6 @@ const tb = window.tb || window.parent.tb || {}; window.http = http; window.gitfetch = git; -window.webdav = webdav; /** * Converts a hex color to an RGB string @@ -211,8 +209,8 @@ async function handleCommand(name, args) { } try { const script = await scriptRes.text(); - const fn = new Function("args", "displayOutput", "createNewCommandInput", "displayError", "term", "path", "terbium", script); - fn(args, displayOutput, createNewCommandInput, displayError, term, path, window.parent.tb); + const fn = new Function("args", "displayOutput", "createNewCommandInput", "displayError", "term", "path", "terbium", "buffer", script); + fn(args, displayOutput, createNewCommandInput, displayError, term, path, window.parent.tb, window.parent.tb.buffer); } catch (error) { displayError(`Failed to execute command '${name}': ${error.message}`); createNewCommandInput(); @@ -220,6 +218,8 @@ async function handleCommand(name, args) { } } +window.handleCommand = handleCommand; + window.addEventListener("updPath", e => { path = e.detail; }); diff --git a/public/apps/terminal.tapp/scripts/cat.js b/public/apps/terminal.tapp/scripts/cat.js index fe76270..daf3ef0 100644 --- a/public/apps/terminal.tapp/scripts/cat.js +++ b/public/apps/terminal.tapp/scripts/cat.js @@ -9,16 +9,7 @@ async function cat(args) { try { const match = path.match(/\/mnt\/([^\/]+)\//); const davName = match ? match[1].toLowerCase() : ""; - const davInstances = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/davs.json`, "utf8")); - const dav = davInstances.find(d => d.name.toLowerCase() === davName); - const client = window.webdav.createClient(dav.url, { - username: dav.username, - password: dav.password, - authType: window.webdav.AuthType.Password, - }); - const np = path.replace(`/mnt/${davName.toLowerCase()}/`, ""); - const content = await client.getFileContents(`${np}/${args._raw}`); - const text = new TextDecoder().decode(content); + const text = await tb.vfs.servers.get(davName).connection.promises.readFile(`${path}/${args._raw}`, "utf8"); displayOutput(text); createNewCommandInput(); } catch (e) { diff --git a/public/apps/terminal.tapp/scripts/ls.js b/public/apps/terminal.tapp/scripts/ls.js index 31dc4f1..908db78 100644 --- a/public/apps/terminal.tapp/scripts/ls.js +++ b/public/apps/terminal.tapp/scripts/ls.js @@ -14,24 +14,12 @@ async function ls(args) { ]; const header = "| " + columns.map(col => centerText(col.name, col.width)).join(" | ") + " |"; const separator = "|" + columns.map(col => "-".repeat(col.width + 2)).join("|") + "|"; - displayOutput(centerText(`TerbiumOS Network Storage Manager v1.0.1`, header.length)); + displayOutput(centerText(`TerbiumOS Network Storage Manager v1.2.0`, header.length)); displayOutput(header); displayOutput(separator); - const davInstances = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/davs.json`, "utf8")); - for (const dav of davInstances) { - let mounted; - try { - const client = window.webdav.createClient(dav.url, { - username: dav.username, - password: dav.password, - authType: window.webdav.AuthType.Password, - }); - await client.getDirectoryContents("/"); - mounted = true; - } catch { - mounted = false; - } - const row = [centerText(dav.name, columns[0].width), centerText(dav.url, columns[1].width), centerText(mounted ? "Yes" : "No", columns[2].width), centerText(`/mnt/${dav.name.toLowerCase()}`, columns[3].width)]; + for (const instance of window.parent.tb.vfs.servers) { + const dav = instance[1]; + const row = [centerText(dav.name, columns[0].width), centerText(dav.url, columns[1].width), centerText(dav.connected ? "Yes" : "No", columns[2].width), centerText(`/mnt/${dav.name.toLowerCase()}`, columns[3].width)]; displayOutput("| " + row.join(" | ") + " |"); } createNewCommandInput(); @@ -39,15 +27,7 @@ async function ls(args) { try { const match = args._raw.match(/\/mnt\/([^\/]+)\//) || path.match(/\/mnt\/([^\/]+)\//); const davName = match ? match[1].toLowerCase() : ""; - const davInstances = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/davs.json`, "utf8")); - const dav = davInstances.find(d => d.name.toLowerCase() === davName); - const client = window.webdav.createClient(dav.url, { - username: dav.username, - password: dav.password, - authType: window.webdav.AuthType.Password, - }); - const np = args._raw.replace(`/mnt/${davName.toLowerCase()}/`, "") || path.replace(`/mnt/${davName.toLowerCase()}/`, ""); - const contents = await client.getDirectoryContents(np); + const contents = await tb.vfs.servers.get(davName).connection.promises.readdir(`${path}/${args._raw}`); for (const entry of contents) { if (entry.type === "directory") { displayOutput(`${entry.basename}/`); @@ -63,42 +43,24 @@ async function ls(args) { createNewCommandInput(); } else if (args._raw) { try { - tb.sh.ls(path + args._raw, (err, entries) => { - if (err) { - displayError(`ls: ${err.message}`); - createNewCommandInput(); - } else { - entries.forEach(entry => { - displayOutput(entry.name); - }); - createNewCommandInput(); - } + const entries = await tb.sh.promises.ls(path + args._raw); + entries.forEach(entry => { + displayOutput(entry.name); }); + createNewCommandInput(); } catch { - tb.sh.ls(args._raw, (err, entries) => { - if (err) { - displayError(`ls: ${err.message}`); - createNewCommandInput(); - } else { - entries.forEach(entry => { - displayOutput(entry.name); - }); - createNewCommandInput(); - } + const entries = await tb.sh.promises.ls(args._raw); + entries.forEach(entry => { + displayOutput(entry.name); }); + createNewCommandInput(); } } else { - tb.sh.ls(path, (err, entries) => { - if (err) { - displayError(`ls: ${err.message}`); - createNewCommandInput(); - } else { - entries.forEach(entry => { - displayOutput(entry.name); - }); - createNewCommandInput(); - } + const entries = await tb.sh.promises.ls(path); + entries.forEach(entry => { + displayOutput(entry.name); }); + createNewCommandInput(); } } ls(args); diff --git a/public/apps/terminal.tapp/scripts/mkdir.js b/public/apps/terminal.tapp/scripts/mkdir.js index 6abed26..3ac27b8 100644 --- a/public/apps/terminal.tapp/scripts/mkdir.js +++ b/public/apps/terminal.tapp/scripts/mkdir.js @@ -9,15 +9,8 @@ async function mkdir(args) { try { const match = path.match(/\/mnt\/([^\/]+)\//); const davName = match ? match[1].toLowerCase() : ""; - const davInstances = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/davs.json`, "utf8")); - const dav = davInstances.find(d => d.name.toLowerCase() === davName); - const client = window.webdav.createClient(dav.url, { - username: dav.username, - password: dav.password, - authType: window.webdav.AuthType.Password, - }); const np = path.replace(`/mnt/${davName.toLowerCase()}/`, ""); - await client.createDirectory(`${np}/${args._raw}`); + await tb.vfs.servers.get(davName).connection.promises.mkdir(`${np}/${args._raw}`); createNewCommandInput(); } catch (e) { displayError(`TNSM mkdir: ${e.message}`); diff --git a/public/apps/terminal.tapp/scripts/rm.js b/public/apps/terminal.tapp/scripts/rm.js index e630b33..3bd083a 100644 --- a/public/apps/terminal.tapp/scripts/rm.js +++ b/public/apps/terminal.tapp/scripts/rm.js @@ -64,15 +64,8 @@ async function rm(args) { try { const match = path.match(/\/mnt\/([^\/]+)\//); const davName = match ? match[1].toLowerCase() : ""; - const davInstances = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/davs.json`, "utf8")); - const dav = davInstances.find(d => d.name.toLowerCase() === davName); - const client = window.webdav.createClient(dav.url, { - username: dav.username, - password: dav.password, - authType: window.webdav.AuthType.Password, - }); const np = path.replace(`/mnt/${davName.toLowerCase()}/`, ""); - await client.deleteFile(`${np}/${args._raw}`); + await tb.vfs.servers.get(davName).connection.promises.unlink(`${np}/${args._raw}`); createNewCommandInput(); } catch (e) { displayError(`TNSM rmdir: ${e.message}`); diff --git a/public/apps/terminal.tapp/scripts/touch.js b/public/apps/terminal.tapp/scripts/touch.js index 1657584..93650af 100644 --- a/public/apps/terminal.tapp/scripts/touch.js +++ b/public/apps/terminal.tapp/scripts/touch.js @@ -9,15 +9,8 @@ async function touch(args) { try { const match = path.match(/\/mnt\/([^\/]+)\//); const davName = match ? match[1].toLowerCase() : ""; - const davInstances = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/davs.json`, "utf8")); - const dav = davInstances.find(d => d.name.toLowerCase() === davName); - const client = window.webdav.createClient(dav.url, { - username: dav.username, - password: dav.password, - authType: window.webdav.AuthType.Password, - }); const np = path.replace(`/mnt/${davName.toLowerCase()}/`, ""); - await client.putFileContents(`${np}/${args._raw}`, "", { overwrite: true }); + await tb.vfs.servers.get(davName).connection.promises.writeFile(`${np}/${args._raw}`, "", "utf8"); createNewCommandInput(); } catch (e) { displayError(`TNSM touch: ${e.message}`); diff --git a/public/apps/terminal.tapp/scripts/unzip.js b/public/apps/terminal.tapp/scripts/unzip.js index 492299c..7bfcddb 100644 --- a/public/apps/terminal.tapp/scripts/unzip.js +++ b/public/apps/terminal.tapp/scripts/unzip.js @@ -15,7 +15,7 @@ async function uzip(path, target) { try { console.log(`touch ${currentPath.slice(0, -1)}`); displayOutput(`touch ${currentPath.slice(0, -1)}`); - await window.parent.tb.fs.promises.writeFile(currentPath.slice(0, -1), Filer.Buffer.from(content)); + await window.parent.tb.fs.promises.writeFile(currentPath.slice(0, -1), window.parent.tb.buffer.from(content), "arraybuffer"); } catch { displayOutput(`Cant make ${currentPath.slice(0, -1)}`); console.log(`Cant make ${currentPath.slice(0, -1)}`); diff --git a/public/apps/terminal.tapp/terminal_com.js b/public/apps/terminal.tapp/terminal_com.js index 06dbd7c..6f8f42a 100644 --- a/public/apps/terminal.tapp/terminal_com.js +++ b/public/apps/terminal.tapp/terminal_com.js @@ -10,6 +10,6 @@ tb_island.addControl({ id: "terminal-help", click: async () => { term.write("help"); - await handleCommand("help"); + await window.handleCommand("help"); }, }); diff --git a/public/apps/text editor.tapp/text.com.js b/public/apps/text editor.tapp/text.com.js index a3127ff..e43de5e 100644 --- a/public/apps/text editor.tapp/text.com.js +++ b/public/apps/text editor.tapp/text.com.js @@ -24,7 +24,7 @@ tb_island.addControl({ filename: "untitled.txt", onOk: async file => { document.body.setAttribute("path", file); - textarea.value = await tb.fs.promises.readFile(file, "utf8"); + textarea.value = await tb.vfs.whatFS(file).promises.readFile(file, "utf8"); }, }); }, @@ -40,7 +40,7 @@ tb_island.addControl({ title: "Save Text File", filename: "untitled.txt", onOk: async txt => { - tb.fs.writeFile(`${txt}`, textarea.value, err => { + tb.vfs.whatFS(txt).writeFile(`${txt}`, textarea.value, err => { if (err) return alert(err); }); }, diff --git a/public/cursor_changer.js b/public/cursor_changer.js index 6e5abf8..a459e7a 100644 --- a/public/cursor_changer.js +++ b/public/cursor_changer.js @@ -1,41 +1,54 @@ const style = document.createElement("style"); console.log("Cursor Engine Injected"); +const anuraSettings = window.parent.parent.parent.anura.ui.theme.settings; style.textContent = ` - :root { - --cursor-normal: url("/cursors/dark/normal.svg") 6 0, default !important; - --cursor-pointer: url("/cursors/dark/pointer.svg") 6 0, pointer !important; - --cursor-text: url("/cursors/dark/text.svg") 10 0, text !important; - --cursor-crosshair: url("/cursors/dark/crosshair.svg") 0 0, crosshair !important; - --cursor-wait: url("/cursors/dark/wait.svg") 0 0, wait !important; - --cursor-nw-resize: url("/cursors/dark/resize-l.svg") 0 0, nw-resize !important; - --cursor-ne-resize: url("/cursors/dark/resize-r.svg") 0 0, ne-resize !important; - --cursor-sw-resize: url("/cursors/dark/resize-r.svg") 0 0, sw-resize !important; - --cursor-se-resize: url("/cursors/dark/resize-l.svg") 0 0, se-resize !important; - --cursor-n-resize: url("/cursors/dark/resize-v.svg") 0 0, n-resize !important; - --cursor-s-resize: url("/cursors/dark/resize-v.svg") 0 0, s-resize !important; - --cursor-e-resize: url("/cursors/dark/resize-h.svg") 0 0, e-resize !important; - --cursor-w-resize: url("/cursors/dark/resize-h.svg") 0 0, w-resize !important; - } - * { - cursor: var(--cursor-normal); - } - a, a:-webkit-any-link { - cursor: var(--cursor-pointer) !important; - } - input[type="text"], textarea { - cursor: var(--cursor-text) !important; - } - .crosshair { - cursor: var(--cursor-crosshair) !important; - } - .loading { - cursor: var(--cursor-wait) !important; - } - input[disabled], button[disabled] { - cursor: var(--cursor-normal) !important; - } - [contenteditable="true"] { - cursor: var(--cursor-text) !important; - } + :root { + --cursor-normal: url("/cursors/dark/normal.svg") 6 0, default !important; + --cursor-pointer: url("/cursors/dark/pointer.svg") 6 0, pointer !important; + --cursor-text: url("/cursors/dark/text.svg") 10 0, text !important; + --cursor-crosshair: url("/cursors/dark/crosshair.svg") 0 0, crosshair !important; + --cursor-wait: url("/cursors/dark/wait.svg") 0 0, wait !important; + --cursor-nw-resize: url("/cursors/dark/resize-l.svg") 0 0, nw-resize !important; + --cursor-ne-resize: url("/cursors/dark/resize-r.svg") 0 0, ne-resize !important; + --cursor-sw-resize: url("/cursors/dark/resize-r.svg") 0 0, sw-resize !important; + --cursor-se-resize: url("/cursors/dark/resize-l.svg") 0 0, se-resize !important; + --cursor-n-resize: url("/cursors/dark/resize-v.svg") 0 0, n-resize !important; + --cursor-s-resize: url("/cursors/dark/resize-v.svg") 0 0, s-resize !important; + --cursor-e-resize: url("/cursors/dark/resize-h.svg") 0 0, e-resize !important; + --cursor-w-resize: url("/cursors/dark/resize-h.svg") 0 0, w-resize !important; + --theme-fg: ${anuraSettings["foreground"] || "#FFFFFF"}; + --theme-secondary-fg: ${anuraSettings["secondaryForeground"] || "#C1C1C1"}; + --theme-border: ${anuraSettings["border"] || "#444444"}; + --material-border: ${anuraSettings["border"] || "#444444"}; + --theme-dark-border: ${anuraSettings["darkBorder"] || "#000000"}; + --theme-bg: ${anuraSettings["darkBackground"] || "#202124"}; + --material-bg: ${anuraSettings["darkBackground"] || "#202124"}; + --theme-secondary-bg: ${anuraSettings["secondaryBackground"] || "#383838"}; + --theme-dark-bg: ${anuraSettings["darkBackground"] || "#161616"}; + --theme-accent: ${anuraSettings["accent"] || "#4285F4"}; + --matter-helper-theme: ${anuraSettings["accent"] || "#4285F4"}; + } + * { + cursor: var(--cursor-normal); + } + a, a:-webkit-any-link { + cursor: var(--cursor-pointer) !important; + } + input[type="text"], textarea { + cursor: var(--cursor-text) !important; + } + .crosshair { + cursor: var(--cursor-crosshair) !important; + } + .loading { + cursor: var(--cursor-wait) !important; + } + input[disabled], button[disabled] { + cursor: var(--cursor-normal) !important; + } + [contenteditable="true"] { + cursor: var(--cursor-text) !important; + } `; +console.log("Applied TB Styles"); document.head.appendChild(style); diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..c7ecdaf --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,22 @@ +User-agent: * +Allow: / + +User-agent: Googlebot +Allow: / + +User-agent: Bingbot +Allow: / + +User-agent: Slurp +Allow: / + +User-agent: DuckDuckBot +Allow: / + +User-agent: Baiduspider +Allow: / + +User-agent: YandexBot +Allow: / + +Sitemap: https://terbiumon.top/sitemap.xml \ No newline at end of file diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..d1c3719 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,9 @@ + + + + https://terbiumon.top/ + 2025-11-27 + weekly + 1.0 + + \ No newline at end of file diff --git a/public/theme.css b/public/theme.css new file mode 100644 index 0000000..9239117 --- /dev/null +++ b/public/theme.css @@ -0,0 +1,56 @@ +/* Liquor Comptability */ + +:root { + --material-bg: #202124; + --material-border: #444; + --matter-surface-rgb: 32, 33, 36; + --matter-onsurface-rgb: 255, 255, 255; +} + +.background { + background-color: var(--material-bg); +} + +.matter-switch { + color: var(--theme-fg) !important; +} + +.matter-button-contained:not(.settingsIcon, .symbolButton) { + background-color: var(--theme-accent) !important; + /* color: var(--theme-fg); */ +} + +.matter-button-outlined { + color: var(--theme-accent) !important; +} + +.matter-switch > input + span::before, +.matter-switch > input + span::after { + background-color: var(--theme-secondary-fg) !important; +} + +.matter-switch > input { + background-color: var(--theme-secondary-bg) !important; +} + +.matter-switch > input:checked + span::before, +.matter-switch > input:checked + span::after { + background-color: var(--theme-accent) !important; +} + +.matter-switch > input:checked { + background-color: color-mix(in srgb, var(--theme-accent) 75%, black 25%) !important; +} + +.matter-button-outlined { + color: var(--theme-accent) !important; + border-color: var(--theme-border) !important; +} + +.matter-button-outlined::before { + background-color: color-mix(in srgb, var(--theme-accent) 75%, var(--theme-bg) 25%) !important; +} + +.matter-button-text { + color: var(--theme-accent) !important; +} diff --git a/server.ts b/server.ts index cbe2a20..c65a77f 100644 --- a/server.ts +++ b/server.ts @@ -1,27 +1,34 @@ -import { createServer } from "node:http"; -import { baremuxPath } from "@mercuryworkshop/bare-mux/node"; -// @ts-expect-error types -import { epoxyPath } from "@mercuryworkshop/epoxy-transport"; -import { libcurlPath } from "@mercuryworkshop/libcurl-transport"; -import { server as wisp } from "@mercuryworkshop/wisp-js/server"; -import cookieParser from "cookie-parser"; -import cors from "cors"; +import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; +import { Hono } from "hono"; +import { serveStatic } from "@hono/node-server/serve-static"; +import { cors } from "hono/cors"; +import { getCookie, setCookie } from "hono/cookie"; import config from "dotenv"; -import express, { type Request, type Response } from "express"; -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; -import type Socket from "ws"; -import type Head from "ws"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { version } from "./package.json"; +// @ts-expect-error no types +import { server as wisp } from "@mercuryworkshop/wisp-js/server"; -// TODO: Switch to Hono export function TServer() { config.config(); const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); console.log("Starting Terbium..."); - const app = express(); - app.use(cookieParser()); + const app = new Hono(); + + const port = Number.parseInt(process.env.PORT || "8080", 10); + + app.use( + "*", + cors({ + origin: `http://localhost:${port}`, + allowMethods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"], + credentials: true, + }), + ); const masqrCheck = process.env.MASQR && process.env.MASQR.toLowerCase() === "true"; if (masqrCheck) { @@ -30,108 +37,94 @@ export function TServer() { console.log("Masqr is Disabled"); } - async function MasqFail(req, res) { - if (!req.headers.host) { - return; + async function MasqFail(c: any) { + const host = c.req.header("host"); + if (!host) { + return c.html(fs.readFileSync("fail.html", "utf8")); } - const unsafeSuffix = req.headers.host + ".html"; - const safeSuffix = path.normalize(unsafeSuffix).replace(/^(\.\.(\/|\\|$))+/, ""); - const safeJoin = path.join(process.cwd() + "/Masqrd", safeSuffix); + const safeHost = host.split(":")[0].replace(/[^a-zA-Z0-9-_\.]/g, ""); + const safeFilename = path.basename(`${safeHost}.html`); + const safeJoin = path.join(process.cwd(), "Masqrd", safeFilename); try { await fs.promises.access(safeJoin); const failureFileLocal = await fs.promises.readFile(safeJoin, "utf8"); - res.setHeader("Content-Type", "text/html"); - res.send(failureFileLocal); - return; - } catch (e) { - res.setHeader("Content-Type", "text/html"); - res.send(fs.readFileSync("fail.html", "utf8")); - return; + return c.html(failureFileLocal); + } catch { + return c.html(fs.readFileSync("fail.html", "utf8")); } } if (masqrCheck) { - app.use(async (req, res, next) => { - // @ts-expect-error stfu - if (req.headers.host && process.env.WHITELISTED_DOMAINS.includes(req.headers.host)) { - next(); + const whitelisted = (process.env.WHITELISTED_DOMAINS || "") + .split(",") + .map(s => s.trim()) + .filter(Boolean); + + app.use("*", async (c, next) => { + const host = c.req.header("host") ?? ""; + if (host && whitelisted.includes(host)) { + await next(); return; } - if (req.url.includes("/bare")) { - next(); + if (c.req.url.includes("/bare")) { + await next(); return; } - const authheader = req.headers.authorization; - if (req.cookies["authcheck"]) { - next(); + if (getCookie(c, "authcheck")) { + await next(); return; } - if (req.cookies["refreshcheck"] != "true") { - res.cookie("refreshcheck", "true", { maxAge: 10000 }); - MasqFail(req, res); - return; + if (getCookie(c, "refreshcheck") !== "true") { + setCookie(c, "refreshcheck", "true", { maxAge: 10000 }); + return await MasqFail(c); } + const authheader = c.req.header("authorization"); if (!authheader) { - res.setHeader("WWW-Authenticate", "Basic"); - res.status(401); - MasqFail(req, res); - return; + c.header("WWW-Authenticate", "Basic"); + c.status(401); + return await MasqFail(c); } - - const auth = Buffer.from(authheader.split(" ")[1], "base64").toString().split(":"); - const user = auth[0]; - const pass = auth[1]; - const licenseCheck = (await (await fetch(process.env.LICENSE_SERVER_URL + pass + "&host=" + req.headers.host)).json())["status"]; - console.log(`\x1b[0m${process.env.LICENSE_SERVER_URL}${pass}&host=${req.headers.host} ` + `returned: ${licenseCheck}`); - if (licenseCheck == "License valid") { - res.cookie("authcheck", "true", { + const token = authheader.split(" ")[1] ?? ""; + let user = ""; + let pass = ""; + try { + const decoded = Buffer.from(token, "base64").toString(); + [user, pass] = decoded.split(":"); + } catch { + return await MasqFail(c); + } + const licenseResp = await fetch(`${process.env.LICENSE_SERVER_URL}${encodeURIComponent(pass)}&host=${encodeURIComponent(host)}`); + const licenseCheck = (await licenseResp.json())?.status; + console.log(`\x1b[0m${process.env.LICENSE_SERVER_URL}${pass}&host=${host} returned: ${licenseCheck}`); + if (licenseCheck === "License valid") { + setCookie(c, "authcheck", "true", { expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), }); - res.send(""); - return; + return c.redirect("/"); } - MasqFail(req, res); - return; + return await MasqFail(c); }); } app.use( - express.static("dist", { - setHeaders: (res, path) => { - if (path.endsWith(".cjs")) { - res.setHeader("Content-Type", "text/javascript"); - } - }, + "*", + serveStatic({ + root: path.join(__dirname, "dist"), }), ); - app.use("/libcurl/", express.static(libcurlPath)); - app.use("/baremux/", express.static(baremuxPath)); - app.use("/epoxy/", express.static(epoxyPath)); wisp.options.dns_method = "resolve"; wisp.options.dns_servers = ["1.1.1.3", "1.0.0.3"]; wisp.options.dns_result_order = "ipv4first"; - const server = createServer(); - - server.on("request", (req: Request, res: Response) => { - app(req, res); - }); + const server = createServer(nodeHandler); - server.on("upgrade", (req: Request, socket: Socket, head: Head) => { - if (req.url.endsWith("/wisp/")) { - wisp.routeRequest(req, socket, head); + server.on("upgrade", (req: IncomingMessage, socket: any, head: Buffer) => { + if (req.url?.endsWith("/wisp/")) { + wisp.routeRequest(req as any, socket as any, head as any); + } else { + socket.destroy(); } }); - const port = Number.parseInt(process.env.PORT || "8080"); - const corsOptions = { - origin: `http://localhost:${port}`, - methods: "GET,HEAD,PUT,PATCH,POST,DELETE", - credentials: true, - }; - - app.use(cors(corsOptions)); - const manifest = fs.readFileSync(path.join(import.meta.dirname, "package.json"), "utf-8"); - const { version } = JSON.parse(manifest); server.listen(port, () => { console.log(` \x1b[38;2;50;174;98m@@@@@@@@@@@@@@~ B@@@@@@@@#G?. @@ -144,6 +137,47 @@ export function TServer() { \x1b[38;2;50;174;98m^&@@@? B@@@@@@@@&B5~ `); }); + async function nodeHandler(req: IncomingMessage, res: ServerResponse) { + const proto = (req.socket as any).encrypted ? "https" : "http"; + const host = req.headers.host || "localhost"; + const url = new URL(req.url || "/", `${proto}://${host}`); + const request = new Request(url.toString(), { + method: req.method, + headers: req.headers as any, + body: req.method === "GET" || req.method === "HEAD" ? undefined : (req as any), + }); + + try { + const response = await app.fetch(request); + res.statusCode = response.status; + response.headers.forEach((val, key) => { + if (key.toLowerCase() === "set-cookie") { + const prev = res.getHeader("set-cookie"); + if (prev) { + const arr = Array.isArray(prev) ? prev.concat(val) : [String(prev), val]; + res.setHeader("set-cookie", arr); + } else { + res.setHeader("set-cookie", val); + } + } else { + res.setHeader(key, val); + } + }); + if (response.body) { + const reader = response.body.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (value) res.write(Buffer.from(value)); + } + } + res.end(); + } catch (err) { + console.error(err); + res.statusCode = 500; + res.end("Internal Server Error"); + } + } process.on("SIGINT", () => { console.log("\x1b[0m"); diff --git a/src/Boot.tsx b/src/Boot.tsx index c6a7235..49a2126 100644 --- a/src/Boot.tsx +++ b/src/Boot.tsx @@ -41,18 +41,38 @@ export default function Boot() { useEffect(() => { const getEntries = async () => { + let entries = []; if (!(await fileExists("/bootentries.json"))) { - await window.tb.fs.promises.writeFile( - "/bootentries.json", - JSON.stringify([ - { name: "TB React", action: boot.toString() }, - { name: "TB React (Cloaked)", action: cloak.toString() }, - { name: "TB System Recovery", action: recovery.toString() }, - ]), - ); + const ent = [ + { name: "TB React", action: boot.toString() }, + { name: "TB React (Cloaked)", action: cloak.toString() }, + { name: "TB System Recovery", action: recovery.toString() }, + ]; + await window.tb.fs.promises.writeFile("/bootentries.json", JSON.stringify(ent)); + console.log("Added default bootentries"); + entries = ent; + } else { + entries = JSON.parse(await window.tb.fs.promises.readFile("/bootentries.json", "utf8")); } - const entries = JSON.parse(await window.tb.fs.promises.readFile("/bootentries.json", "utf8")); + const FilerDirExists = async (path: string): Promise => { + return new Promise(resolve => { + Filer.fs.stat(path, (err: any, stats: any) => { + if (err) { + if (err.code === "ENOENT") { + resolve(false); + } else { + console.error(err); + resolve(false); + } + } else { + const exists = stats.type === "DIRECTORY"; + resolve(exists); + } + }); + }); + }; + // @ts-expect-error const recreatedEntries = entries.map(entry => ({ ...entry, @@ -60,7 +80,13 @@ export default function Boot() { })); if (localStorage.getItem("setup") === "true" && (!(await dirExists("/system/etc/terbium/")) || !(await dirExists("/apps/system/")))) { const bootent = recreatedEntries.filter((entry: any) => entry.name !== "TB React" && entry.name !== "TB React (Cloaked)"); - setentries(bootent); + const fsxr = await FilerDirExists("/system/etc/terbium"); + if (fsxr) { + sessionStorage.setItem("migrateFs", "true"); + setentries(recreatedEntries); + } else { + setentries(bootent); + } } else { setentries(recreatedEntries); } diff --git a/src/Login.tsx b/src/Login.tsx index c72c65a..08a2ce4 100644 --- a/src/Login.tsx +++ b/src/Login.tsx @@ -31,11 +31,11 @@ export default function Login() { const dirEntries = await Promise.all( entries.map(async entry => { const stat = await window.tb.fs.promises.stat(`/home/${entry}`); - if (stat.isDirectory()) { - try { - await window.tb.fs.promises.access(`/home/${entry}/user.json`); + if (stat && stat.isDirectory()) { + const exists = await window.tb.fs.promises.exists(`/home/${entry}/user.json`); + if (exists) { return entry; - } catch (error) { + } else { return null; } } @@ -116,22 +116,24 @@ export default function Login() { useEffect(() => { const keyCheck = async (e: KeyboardEvent) => { - if (e.key === "Escape") { - setIsLoggingIn(false); - } - if ((!isLoggingIn && e.key !== "Escape") || e.key.match(/^[a-zA-Z0-9]$/)) { - const user = JSON.parse(await window.tb.fs.promises.readFile(`/home/${selectedUser}/user.json`, "utf8")); - if (user.password !== false) { + switch (e.key) { + case "Enter": + if (isLoggingIn && !changingpw) { + login(); + } + break; + case "Escape": + if (isLoggingIn) { + setIsLoggingIn(false); + } else { + } + break; + default: setIsLoggingIn(true); - setTimeout(() => { - if (passwordRef.current && !changingpw) { - passwordRef.current.focus(); - if (e.key.match(/^[a-zA-Z0-9]$/) && passwordRef.current.value.length === 0) { - passwordRef.current.value = e.key; - } - } - }, 200); - } + if (passwordRef.current && !changingpw) { + passwordRef.current.focus(); + } + break; } }; window.addEventListener("keydown", keyCheck); @@ -148,6 +150,8 @@ export default function Login() { style={{ backgroundImage: `url("${wallpaper?.includes("/system/etc/") ? `/fs/${wallpaper}` : wallpaper || ""}")`, backgroundSize: "cover", + backgroundRepeat: "no-repeat", + backgroundPosition: "center", }} >
{ + sessionStorage.setItem("boot", "true"); + window.location.reload(); + }; + const cloak = () => { + const newWindow = window.open("about:blank", "_blank"); + const newDocument = newWindow!.document.open(); + sessionStorage.setItem("boot", "true"); + newDocument.write(` + + + + + + + + + + `); + newDocument.close(); + window.location.href = "https://google.com"; + console.log("Cloak Opened!"); + }; + const recovery = () => { + sessionStorage.setItem("recovery", "true"); + window.location.reload(); + }; + await window.tb.fs.promises.writeFile( + "/bootentries.json", + JSON.stringify([ + { name: "TB React", action: boot.toString() }, + { name: "TB React (Cloaked)", action: cloak.toString() }, + { name: "TB System Recovery", action: recovery.toString() }, + ]), + ); } await download("https://cdn.terbiumon.top/recovery/latest.zip", "/uploaded.zip"); setShowCursor(false); @@ -134,12 +169,47 @@ export default function Recovery() { const content = await file.arrayBuffer(); setProgress(10); if (localStorage.getItem("setup")) { - await window.tb.sh.promises.rm("/system/", { recursive: true }); - await window.tb.sh.promises.rm("/apps/", { recursive: true }); - await window.tb.sh.promises.rm("/home/", { recursive: true }); + await window.tb.sh.format(); + const boot = () => { + sessionStorage.setItem("boot", "true"); + window.location.reload(); + }; + const cloak = () => { + const newWindow = window.open("about:blank", "_blank"); + const newDocument = newWindow!.document.open(); + sessionStorage.setItem("boot", "true"); + newDocument.write(` + + + + + + + + + + `); + newDocument.close(); + window.location.href = "https://google.com"; + console.log("Cloak Opened!"); + }; + const recovery = () => { + sessionStorage.setItem("recovery", "true"); + window.location.reload(); + }; + await window.tb.fs.promises.writeFile( + "/bootentries.json", + JSON.stringify([ + { name: "TB React", action: boot.toString() }, + { name: "TB React (Cloaked)", action: cloak.toString() }, + { name: "TB System Recovery", action: recovery.toString() }, + ]), + ); } setProgress(25); - await window.tb.fs.promises.writeFile("//uploaded.zip", Filer.Buffer.from(content)); + await window.tb.fs.promises.writeFile("//uploaded.zip", window.tb.buffer.from(content), "arraybuffer"); setProgress(35); setShowCursor(false); main.current!.classList.remove("flex"); @@ -230,7 +300,7 @@ export default function Recovery() { for (let i = 0; i < pathParts.length; i++) { currentPath += pathParts[i] + "/"; if (i === pathParts.length - 1 && !relativePath.endsWith("/")) { - await window.tb.fs.promises.writeFile(currentPath.slice(0, -1), Filer.Buffer.from(content)); + await window.tb.fs.promises.writeFile(currentPath.slice(0, -1), window.tb.buffer.from(content), "arraybuffer"); } else if (!(await dirExists(currentPath))) { await window.tb.fs.promises.mkdir(currentPath); } @@ -243,32 +313,126 @@ export default function Recovery() { } async function download(url: string, location: string) { - // @ts-expect-error + if (!window.loadLock) { + window.loadLock = true; + await libcurl.load_wasm("https://cdn.jsdelivr.net/npm/libcurl.js@latest/libcurl.wasm"); + } + // @ts-expect-error types libcurl.set_websocket(`${window.location.protocol.replace("http", "ws")}//${window.location.hostname}:${window.location.port}/wisp/`); const response = await libcurl.fetch(url); if (!response.ok) { throw new Error(`Failed to download the file. Status: ${response.status}`); } const content = await response.arrayBuffer(); - await window.tb.fs.promises.writeFile(location, Filer.Buffer.from(content)); + await window.tb.fs.promises.writeFile(location, window.tb.buffer.from(content), "arraybuffer"); console.log(`File saved successfully at: ${location}`); } + const migrateFs = async () => { + setShowCursor(false); + main.current!.classList.remove("flex"); + main.current!.classList.add("hidden"); + progresscheck.current!.classList.remove("hidden"); + progresscheck.current!.classList.add("flex"); + async function copyRecursive(src: string, dest: string) { + const entries = await Filer.fs.promises.readdir(src); + for (const entry of entries) { + const srcPath = src.endsWith("/") ? src + entry : src + "/" + entry; + const destPath = dest.endsWith("/") ? dest + entry : dest + "/" + entry; + const stat = await Filer.fs.promises.stat(srcPath); + if (stat.isDirectory()) { + if (!(await dirExists(destPath))) { + await window.tb.fs.promises.mkdir(destPath); + } + await copyRecursive(srcPath, destPath); + } else { + const fileBuffer = await Filer.fs.promises.readFile(srcPath); + await window.tb.fs.promises.writeFile(destPath, fileBuffer); + } + statusref.current!.innerText = `Copying: ${srcPath}`; + } + } + await copyRecursive("/", "/"); + setProgress(85); + statusref.current!.innerText = "Recreating Desktop Shortcuts..."; + for (const user of await window.tb.fs.promises.readdir("/home/")) { + const items = JSON.parse(await window.tb.fs.promises.readFile(`/home/${user}/desktop/.desktop.json`, "utf8")); + for (const item of items) { + const target = await Filer.fs.promises.readlink(item.item); + await window.tb.fs.promises.symlink(target, item.item); + statusref.current!.innerText = `Creating shortcut: ${item.name}.lnk...`; + } + } + setProgress(93); + statusref.current!.innerText = "Formatting Filer..."; + const fsh = new Filer.fs.Shell(); + for (const loc of await Filer.fs.promises.readdir("//")) { + await fsh.promises.rm(`/${loc}`, { recursive: true }); + } + setProgress(100); + statusref.current!.innerText = "Migration complete!"; + sessionStorage.clear(); + sessionStorage.setItem("boot", "true"); + localStorage.setItem("setup", "true"); + sessionStorage.removeItem("migrateFs"); + window.location.reload(); + }; + + // @ts-expect-error types + window.migrateFs = migrateFs; + useEffect(() => { const handleKeyDown = async (e: KeyboardEvent) => { if (e.key === "ArrowUp") { - setSelected(prevSelected => (prevSelected === 0 ? (updCache ? 4 : 3) : prevSelected - 1)); + setSelected(prevSelected => (prevSelected === 0 ? (updCache ? 5 : 4) : prevSelected - 1)); } else if (e.key === "ArrowDown") { - setSelected(prevSelected => (prevSelected === (updCache ? 4 : 3) ? 0 : prevSelected + 1)); + setSelected(prevSelected => (prevSelected === (updCache ? 5 : 4) ? 0 : prevSelected + 1)); } else if (e.key === "Enter") { if (selected === 0) { localStorage.clear(); sessionStorage.clear(); sessionStorage.setItem("boot", "true"); if (localStorage.getItem("setup")) { - await window.tb.sh.promises.rm("/system/", { recursive: true }); - await window.tb.sh.promises.rm("/apps/", { recursive: true }); - await window.tb.sh.promises.rm("/home/", { recursive: true }); + await window.tb.sh.format(); + const boot = () => { + sessionStorage.setItem("boot", "true"); + window.location.reload(); + }; + + const cloak = () => { + const newWindow = window.open("about:blank", "_blank"); + const newDocument = newWindow!.document.open(); + sessionStorage.setItem("boot", "true"); + newDocument.write(` + + + + + + + + + + `); + newDocument.close(); + window.location.href = "https://google.com"; + console.log("Cloak Opened!"); + }; + + const recovery = () => { + sessionStorage.setItem("recovery", "true"); + window.location.reload(); + }; + await window.tb.fs.promises.writeFile( + "/bootentries.json", + JSON.stringify([ + { name: "TB React", action: boot.toString() }, + { name: "TB React (Cloaked)", action: cloak.toString() }, + { name: "TB System Recovery", action: recovery.toString() }, + ]), + ); } window.location.reload(); } else if (selected === 1) { @@ -280,8 +444,10 @@ export default function Recovery() { setMsg("BE AWARE if your static hosting this download will NOT work. Proceed?"); setAction("prodins()"); } else if (selected === 2) { + migrateFs(); + } else if (selected === 3) { zipins(); - } else if (selected === 3 && updCache) { + } else if (selected === 4 && updCache) { setShowCursor(false); msgbox.current!.classList.remove("flex"); msgbox.current!.classList.add("hidden"); @@ -291,7 +457,7 @@ export default function Recovery() { await window.tb.fs.promises.writeFile("/system/etc/terbium/hash.cache", hash); await window.tb.sh.promises.rm("/system/tmp/terb-upd/", { recursive: true }); window.location.reload(); - } else if (selected === (updCache ? 4 : 3)) { + } else if (selected === (updCache ? 5 : 4)) { sessionStorage.clear(); window.location.reload(); } @@ -390,9 +556,46 @@ export default function Recovery() { sessionStorage.clear(); sessionStorage.setItem("boot", "true"); if (localStorage.getItem("setup")) { - await window.tb.sh.promises.rm("/system/", { recursive: true }); - await window.tb.sh.promises.rm("/apps/", { recursive: true }); - await window.tb.sh.promises.rm("/home/", { recursive: true }); + await window.tb.sh.format(); + const boot = () => { + sessionStorage.setItem("boot", "true"); + window.location.reload(); + }; + + const cloak = () => { + const newWindow = window.open("about:blank", "_blank"); + const newDocument = newWindow!.document.open(); + sessionStorage.setItem("boot", "true"); + newDocument.write(` + + + + + + + + + + `); + newDocument.close(); + window.location.href = "https://google.com"; + console.log("Cloak Opened!"); + }; + + const recovery = () => { + sessionStorage.setItem("recovery", "true"); + window.location.reload(); + }; + await window.tb.fs.promises.writeFile( + "/bootentries.json", + JSON.stringify([ + { name: "TB React", action: boot.toString() }, + { name: "TB React (Cloaked)", action: cloak.toString() }, + { name: "TB System Recovery", action: recovery.toString() }, + ]), + ); } window.location.reload(); }} @@ -429,6 +632,21 @@ export default function Recovery() { ${showCursor ? "hover:bg-[#ffffff18] hover:border-[#ffffff20]" : null} ` } + onClick={() => { + migrateFs(); + }} + > + Migrate from Filer to OPFS + + { zipins(); }} @@ -441,7 +659,7 @@ export default function Recovery() { "p-2 px-2.5 text-sm font-extrabold lg:text-lg md:text-base border-[1px] rounded-md" + " " + ` - ${selected === 3 && showCursor !== true ? "bg-[#ffffff18] border-[#ffffff20]" : "border-transparent"} + ${selected === 4 && showCursor !== true ? "bg-[#ffffff18] border-[#ffffff20]" : "border-transparent"} ${showCursor ? "hover:bg-[#ffffff18] hover:border-[#ffffff20]" : null} ` } @@ -465,7 +683,7 @@ export default function Recovery() { "p-2 px-2.5 text-sm font-extrabold lg:text-lg md:text-base border-[1px] rounded-md" + " " + ` - ${selected === (updCache ? 4 : 3) && showCursor !== true ? "bg-[#ffffff18] border-[#ffffff20]" : "border-transparent"} + ${selected === (updCache ? 5 : 4) && showCursor !== true ? "bg-[#ffffff18] border-[#ffffff20]" : "border-transparent"} ${showCursor ? "hover:bg-[#ffffff18] hover:border-[#ffffff20]" : null} ` } diff --git a/src/Setup.tsx b/src/Setup.tsx index c79a3d9..f8e81ce 100644 --- a/src/Setup.tsx +++ b/src/Setup.tsx @@ -1,6 +1,6 @@ import Cropper from "cropperjs"; import Compressor from "compressorjs"; -import { useState, useRef } from "react"; +import { useState, useRef, useEffect } from "react"; import "./sys/gui/styles/login.css"; import "./sys/gui/styles/cropper.css"; import "./sys/gui/styles/oobe.css"; @@ -8,6 +8,9 @@ import "./sys/gui/styles/dropdown.css"; import pwd from "./sys/apis/Crypto"; import { init } from "./init"; import { fileExists, User } from "./sys/types"; +import { InformationCircleIcon } from "@heroicons/react/24/outline"; +import { libcurl } from "libcurl.js"; +import { auth, getinfo, setinfo } from "./sys/apis/utils/tauth"; const pw = new pwd(); export default function Setup() { @@ -15,17 +18,164 @@ export default function Setup() { const [currentStep, setCurrentStep] = useState(1); const currentViewRef = useRef(null); var nextButtonClick = () => void 0; - - const Next = () => { + const Next = (step?: number) => { setBeforeSetup(currentStep); - setCurrentStep(prevStep => Math.min(prevStep + 1, 5)); + if (step) { + setCurrentStep(step); + } else { + setCurrentStep(prevStep => Math.min(prevStep + 1, 5)); + } }; const Back = () => { setBeforeSetup(currentStep); - setCurrentStep(prevStep => Math.max(prevStep - 1, 1)); + if (currentStep === 2.1 || currentStep === 2.2) { + sessionStorage.removeItem("tacc"); + setCurrentStep(2); + } else if (currentStep === 2.3 || currentStep === 2.4 || currentStep === 2.5) { + setCurrentStep(2.2); + } else if (currentStep === 3.1) { + setCurrentStep(2.5); + } else if (currentStep === 3) { + if (sessionStorage.getItem("tacc")) { + setCurrentStep(2.5); + } else { + setCurrentStep(2.1); + } + } else { + setCurrentStep(prevStep => Math.max(prevStep - 1, 1)); + } + }; + if (!window.libcurlLock) { + window.libcurlLock = true; + libcurl.load_wasm("https://cdn.jsdelivr.net/npm/libcurl.js@latest/libcurl.wasm"); + } + // @ts-expect-error no types + libcurl.set_websocket(`${location.protocol.replace("http", "ws")}//${location.hostname}:${location.port}/wisp/`); + const authClient = auth; + const randomColors = ["orange", "red", "green", "blue", "purple", "pink", "yellow"]; + const makePFP = () => { + const uploader = document.createElement("input"); + uploader.type = "file"; + uploader.accept = "img/*"; + uploader.onchange = () => { + const files = uploader!.files; + const file = files![0]; + const reader = new FileReader(); + reader.onload = () => { + const img = document.createElement("img"); + img.classList.add("opacity-0", "pointer-events-none"); + img.src = reader.result as string; + img.onload = () => { + const cropper_container = document.createElement("div"); + const cropper_container_styles = ["w-screen", "h-screen", "fixed", "top-0", "left-0", "right-0", "bottom-0", "z-999999", "bg-[#000000a6]", "flex", "flex-col", "justify-center", "items-center", "gap-[10px]"]; + cropper_container_styles.forEach(style => cropper_container.classList.add(style)); + const cropper_img_container = document.createElement("div"); + cropper_img_container.className = "cropper-img-container"; + let cropper_img_container_sizes = ["bg-[#ffffff0a]", "lg:w-[500px]", "lg:h-[500px]", "md:w-[400px]", "md:h-[400px]", "sm:w-[300px]", "sm:h-[300px]", "flex", "justify-center", "items-center", "rounded-[8px]", "overflow-hidden"]; + cropper_img_container_sizes.forEach(size => cropper_img_container.classList.add(size)); + const cropper_img = document.createElement("img"); + cropper_img.src = img.src; + cropper_img.classList.add("cropper-img"); + cropper_img_container.classList.add("w-[500px]"); + cropper_img_container.classList.add("h-[500px]"); + cropper_img.style.objectFit = "cover"; + cropper_img.style.objectPosition = "center"; + cropper_img_container.appendChild(cropper_img); + cropper_container.appendChild(cropper_img_container); + document.body.appendChild(cropper_container); + const cropper = new Cropper(cropper_img, { aspectRatio: 1, viewMode: 1, cropBoxResizable: false, movable: true, rotatable: true, scalable: true, responsive: true }); + const buttons = document.createElement("div"); + buttons.className = "flex w-[500px] justify-between items-center"; + cropper_container.appendChild(buttons); + const save = document.createElement("button"); + save.className = "save broken_button cursor-pointer"; + save.innerText = "Save"; + const save_styles = [ + "bg-[#1d1d1d]", + "text-[#ffffff38]", + "border-[#ffffff22]", + "hover:bg-[#414141]", + "hover:text-[#ffffff8d]", + "focus:bg-[#ffffff1f]", + "focus:text-[#ffffff8d]", + "focus:border-[#73a9ffd6]", + "focus:ring-[#73a9ff74]", + "focus:outline-hidden", + "focus:ring-2", + "ring-[transparent]", + "ring-0", + "border-[1px]", + "font-[600]", + "px-[20px]", + "py-[8px]", + "h-[18px]", + "rounded-[6px]", + "transition", + "duration-150", + ]; + save_styles.forEach(style => save.classList.add(style)); + save.onclick = () => { + const canvas = cropper.getCroppedCanvas(); + const pfp = document.querySelector(".pfp"); + canvas.toBlob(blob => { + new Compressor(blob as Blob, { + quality: 0.5, + success(result) { + const reader = new FileReader(); + reader.readAsDataURL(result); + reader.onload = () => { + (pfp! as HTMLImageElement).style.background = `url(${reader.result})`; + (pfp! as HTMLImageElement).style.backgroundSize = "cover"; + (pfp! as HTMLImageElement).style.backgroundPosition = "center"; + (pfp! as HTMLImageElement).style.backgroundRepeat = "no-repeat"; + (pfp! as HTMLImageElement).setAttribute("data-src", reader.result as string); + document.body.removeChild(cropper_container); + }; + }, + }); + }); + }; + const cancel = document.createElement("button"); + cancel.className = "cancel broken_button cursor-pointer"; + cancel.innerText = "Cancel"; + const cancel_styles = [ + "bg-[#1d1d1d]", + "text-[#ffffff38]", + "border-[#ffffff22]", + "hover:bg-[#414141]", + "hover:text-[#ffffff8d]", + "focus:bg-[#ffffff1f]", + "focus:text-[#ffffff8d]", + "focus:border-[#73a9ffd6]", + "focus:ring-[#73a9ff74]", + "focus:outline-hidden", + "focus:ring-2", + "ring-[transparent]", + "ring-0", + "border-[1px]", + "font-[600]", + "px-[20px]", + "py-[8px]", + "h-[18px]", + "rounded-[6px]", + "transition", + "duration-150", + ]; + cancel_styles.forEach(style => cancel.classList.add(style)); + cancel.onclick = () => { + document.body.removeChild(cropper_container); + }; + buttons.appendChild(cancel); + buttons.appendChild(save); + }; + }; + reader.readAsDataURL(file); + }; + uploader.click(); }; const saveData = async () => { - await init(); + const int = await init(); + console.log(`Init State: ${int}`); let data: User = JSON.parse(sessionStorage.getItem("new-user") as string); sessionStorage.setItem("new-user", JSON.stringify(data)); const usr = data["username"]; @@ -36,6 +186,12 @@ export default function Setup() { } else { pass = false; } + const email = data["email"]; + if (email) { + await window.tb.fs.promises.writeFile("/system/etc/terbium/taccs.json", JSON.stringify([data], null, 2), "utf8"); + } else { + await window.tb.fs.promises.writeFile("/system/etc/terbium/taccs.json", JSON.stringify([], null, 2), "utf8"); + } const userInf: User = { id: usr, username: usr, @@ -49,6 +205,14 @@ export default function Setup() { answer: pw.harden(data.securityQuestion.answer), }; } + if (sessionStorage.getItem("tacc-settings") === "null") { + const tosave = { + settings: userInf, + apps: [], + davs: [], + }; + await setinfo(email as string, data["password"] as string, "tbs", tosave); + } await window.tb.fs.promises.writeFile(`/home/${usr}/user.json`, JSON.stringify(userInf), "utf8"); await window.tb.fs.promises.writeFile("/system/etc/terbium/sudousers.json", JSON.stringify([usr]), "utf8"); await window.tb.fs.promises.mkdir(`/home/${usr}/documents/`); @@ -116,6 +280,397 @@ export default function Setup() { ); }; const Step2 = () => { + setTimeout(() => { + currentViewRef.current?.classList.remove("-translate-x-6"); + currentViewRef.current?.classList.remove("opacity-0"); + }, 150); + return ( +
{ + currentViewRef.current = el; + }} + className="duration-150 -translate-x-6 opacity-0 flex flex-col justify-center items-center" + > + Choose what kind of account you want to use +
+
+ + +
+
+
+ ); + }; + const Step2CA = () => { + const usernameRef = useRef(null); + const passwordRef = useRef(null); + const [connected, setConnected] = useState(false); + const [error, setError] = useState(""); + useEffect(() => { + const test = async () => { + try { + const response = await libcurl.fetch(`https://auth.terbiumon.top/ping`, { method: "GET" }); + if (!response.ok) { + setConnected(false); + Back(); + } + setConnected(true); + } catch { + setConnected(false); + Back(); + } + }; + test(); + }, [connected]); + setTimeout(() => { + currentViewRef.current?.classList.remove("-translate-x-6"); + currentViewRef.current?.classList.remove("opacity-0"); + }, 150); + nextButtonClick = () => { + if (!connected) return; + const password = passwordRef.current?.value || ""; + authClient.signIn.email({ + email: usernameRef.current?.value || "", + password: password, + rememberMe: true, + fetchOptions: { + onSuccess: async data => { + const p = await getinfo(null, null, "tbs"); + if (p.settings === null || p.settings === undefined) { + sessionStorage.setItem("tacc-settings", "null"); + } else { + sessionStorage.setItem("tacc-settings", JSON.stringify(p.settings)); + } + sessionStorage.setItem( + "new-user", + JSON.stringify({ + username: data.data.user.name, + password: password, + perm: "admin", + pfp: data.data.user.image, + email: data.data.user.email, + }), + ); + Next(2.5); + }, + onError: error => { + setError(error.error.message || "An unknown error occurred during registration."); + }, + }, + }); + }; + return ( +
{ + currentViewRef.current = el; + }} + className="duration-150 -translate-x-6 opacity-0 flex flex-col justify-center items-center" + > + {connected ? ( +
+ Sign in with Terbium Cloud™ +
+ {error && ( +
+ {error} +
+ )} +
+ { + if (e.key === "Enter" && passwordRef.current) { + passwordRef.current.focus(); + } + }} + /> + { + if (e.key === "Enter" && passwordRef.current) { + nextButtonClick(); + } + }} + /> +
+ + +
+
+
+
+ ) : ( +
+ Terbium Cloud™ +

Connecting to Authentication servers please wait...

+
+ )} +
+ ); + }; + const Step2CR = () => { + const usernameRef = useRef(null); + const passwordRef = useRef(null); + const pfpRef = useRef(null); + const emailRef = useRef(null); + const [error, setError] = useState(""); + setTimeout(() => { + currentViewRef.current?.classList.remove("-translate-x-6"); + currentViewRef.current?.classList.remove("opacity-0"); + }, 150); + nextButtonClick = () => { + authClient.signUp.email({ + email: emailRef.current?.value || "", + password: passwordRef.current?.value || "", + name: usernameRef.current?.value || "", + fetchOptions: { + onSuccess: async data => { + console.log("Successfully registered:", data); + sessionStorage.setItem( + "new-user", + JSON.stringify({ + username: usernameRef.current?.value, + password: passwordRef.current?.value, + perm: "admin", + pfp: pfpRef.current?.getAttribute("data-src") || `/assets/img/default - ${randomColors[Math.floor(Math.random() * randomColors.length)]}.png`, + }), + ); + Next(2.5); + }, + onError: error => { + if (error.error.message.toLocaleLowerCase() === "missing or null origin") { + localStorage.removeItem("libcurl_cookies"); + nextButtonClick(); + } else { + setError(error.error.message || "An unknown error occurred during registration."); + } + }, + }, + image: pfpRef.current?.getAttribute("data-src") || `/assets/img/default - ${randomColors[Math.floor(Math.random() * randomColors.length)]}.png`, + }); + }; + return ( +
{ + currentViewRef.current = el; + }} + className="duration-150 -translate-x-6 opacity-0 flex flex-col justify-center items-center" + > + Create a Terbium Cloud™ account +
+ {error && ( +
+ {error} +
+ )} +
+
{ + makePFP(); + }} + > +
+

Upload

+
+ { + if (e.key === "Enter" && passwordRef.current) { + passwordRef.current.focus(); + } + }} + /> + { + if (e.key === "Enter" && emailRef.current) { + emailRef.current.focus(); + } + }} + /> + { + if (e.key === "Enter") { + nextButtonClick(); + } + }} + /> + +
+
+
+ ); + }; + // @ts-expect-error future + const Step2FG = () => { + const usernameRef = useRef(null); + const sendBTNRef = useRef(null); + const [emailSent, setEmailSent] = useState(false); + const [error, setError] = useState(""); + setTimeout(() => { + currentViewRef.current?.classList.remove("-translate-x-6"); + currentViewRef.current?.classList.remove("opacity-0"); + }, 150); + nextButtonClick = () => { + if (!emailSent) { + sendBTNRef.current!.innerText = "Email Sent"; + authClient.requestPasswordReset({ + email: usernameRef.current?.value || "", + fetchOptions: { + onSuccess: () => { + setEmailSent(true); + }, + onError: error => { + setError(error.error.message || "An unknown error occurred while attempting to send the password reset email."); + }, + }, + }); + } + Next(2.2); + }; + return ( +
{ + currentViewRef.current = el; + }} + className="duration-150 -translate-x-6 opacity-0 flex flex-col justify-center items-center" + > + Reset Terbium Cloud™ password +
+
+ {error && ( +
+ {error} +
+ )} + { + if (e.key === "Enter") { + nextButtonClick(); + } + }} + /> +
+ + +
+
+
+
+ ); + }; + const Step2CF = () => { + const [hasSettings, setHasSettings] = useState(false); + const userdata = JSON.parse(sessionStorage.getItem("new-user") as string) || {}; + setTimeout(() => { + currentViewRef.current?.classList.remove("-translate-x-6"); + currentViewRef.current?.classList.remove("opacity-0"); + }, 150); + useEffect(() => { + if (sessionStorage.getItem("tacc-settings") !== "null") { + setHasSettings(true); + } + }, [hasSettings]); + nextButtonClick = () => { + if (!hasSettings) { + Next(3); + sessionStorage.setItem("tacc", "true"); + } else { + Next(3.1); + } + }; + return ( +
{ + currentViewRef.current = el; + }} + className="duration-150 -translate-x-6 opacity-0 flex flex-col justify-center items-center gap-1.5" + > + Confirm Identity + +

Welcome, {userdata.username}!

+ {hasSettings ? ( +
+
+ Terbium Settings were found for this account. +
+
+ ) : ( +
+ Terbium Settings were not found for this account. +
+ )} +

Click next to use this account

+
+ ); + }; + const Step2L = () => { setTimeout(() => { currentViewRef.current?.classList.remove("-translate-x-6"); currentViewRef.current?.classList.remove("opacity-0"); @@ -129,7 +684,6 @@ export default function Setup() { nextButtonClick = () => { const pfp = pfpRef.current?.getAttribute("data-src"); - const randomColors = ["orange", "red", "green", "blue", "purple", "pink", "yellow"]; const finalPfp = pfp || `/assets/img/default - ${randomColors[Math.floor(Math.random() * randomColors.length)]}.png`; let passdata: any = JSON.parse(sessionStorage.getItem("new-user") as string) || {}; passdata["pfp"] = finalPfp; @@ -150,7 +704,7 @@ export default function Setup() { currentViewRef.current?.classList.add("-translate-x-6"); currentViewRef.current?.classList.add("opacity-0"); setTimeout(() => { - Next(); + Next(3); }, 150); }; return ( @@ -158,133 +712,16 @@ export default function Setup() { ref={el => { currentViewRef.current = el; }} - className="duration-150 -translate-x-6 opacity-0 flex flex-col justify-center items-center" + className="duration-150 -translate-x-6 opacity-0 flex-col justify-center items-center" > -
+
Setup your account. -
+
{ - const uploader = document.createElement("input"); - uploader.type = "file"; - uploader.accept = "img/*"; - uploader.onchange = () => { - const files = uploader!.files; - const file = files![0]; - const reader = new FileReader(); - reader.onload = () => { - const img = document.createElement("img"); - img.classList.add("opacity-0", "pointer-events-none"); - img.src = reader.result as string; - img.onload = () => { - const cropper_container = document.createElement("div"); - const cropper_container_styles = ["w-screen", "h-screen", "fixed", "top-0", "left-0", "right-0", "bottom-0", "z-999999", "bg-[#000000a6]", "flex", "flex-col", "justify-center", "items-center", "gap-[10px]"]; - cropper_container_styles.forEach(style => cropper_container.classList.add(style)); - const cropper_img_container = document.createElement("div"); - cropper_img_container.className = "cropper-img-container"; - let cropper_img_container_sizes = ["bg-[#ffffff0a]", "lg:w-[500px]", "lg:h-[500px]", "md:w-[400px]", "md:h-[400px]", "sm:w-[300px]", "sm:h-[300px]", "flex", "justify-center", "items-center", "rounded-[8px]", "overflow-hidden"]; - cropper_img_container_sizes.forEach(size => cropper_img_container.classList.add(size)); - const cropper_img = document.createElement("img"); - cropper_img.src = img.src; - cropper_img.classList.add("cropper-img"); - cropper_img_container.classList.add("w-[500px]"); - cropper_img_container.classList.add("h-[500px]"); - cropper_img.style.objectFit = "cover"; - cropper_img.style.objectPosition = "center"; - cropper_img_container.appendChild(cropper_img); - cropper_container.appendChild(cropper_img_container); - document.body.appendChild(cropper_container); - const cropper = new Cropper(cropper_img, { aspectRatio: 1, viewMode: 1, cropBoxResizable: false, movable: true, rotatable: true, scalable: true, responsive: true }); - const buttons = document.createElement("div"); - buttons.className = "flex w-[500px] justify-between items-center"; - cropper_container.appendChild(buttons); - const save = document.createElement("button"); - save.className = "save broken_button cursor-pointer"; - save.innerText = "Save"; - const save_styles = [ - "bg-[#1d1d1d]", - "text-[#ffffff38]", - "border-[#ffffff22]", - "hover:bg-[#414141]", - "hover:text-[#ffffff8d]", - "focus:bg-[#ffffff1f]", - "focus:text-[#ffffff8d]", - "focus:border-[#73a9ffd6]", - "focus:ring-[#73a9ff74]", - "focus:outline-hidden", - "focus:ring-2", - "ring-[transparent]", - "ring-0", - "border-[1px]", - "font-[600]", - "px-[20px]", - "py-[8px]", - "h-[18px]", - "rounded-[6px]", - "transition", - "duration-150", - ]; - save_styles.forEach(style => save.classList.add(style)); - save.onclick = () => { - const canvas = cropper.getCroppedCanvas(); - const pfp = document.querySelector(".pfp"); - canvas.toBlob(blob => { - new Compressor(blob as Blob, { - quality: 0.5, - success(result) { - const reader = new FileReader(); - reader.readAsDataURL(result); - reader.onload = () => { - (pfp! as HTMLImageElement).style.background = `url(${reader.result})`; - (pfp! as HTMLImageElement).style.backgroundSize = "cover"; - (pfp! as HTMLImageElement).style.backgroundPosition = "center"; - (pfp! as HTMLImageElement).style.backgroundRepeat = "no-repeat"; - (pfp! as HTMLImageElement).setAttribute("data-src", reader.result as string); - document.body.removeChild(cropper_container); - }; - }, - }); - }); - }; - const cancel = document.createElement("button"); - cancel.className = "cancel broken_button cursor-pointer"; - cancel.innerText = "Cancel"; - const cancel_styles = [ - "bg-[#1d1d1d]", - "text-[#ffffff38]", - "border-[#ffffff22]", - "hover:bg-[#414141]", - "hover:text-[#ffffff8d]", - "focus:bg-[#ffffff1f]", - "focus:text-[#ffffff8d]", - "focus:border-[#73a9ffd6]", - "focus:ring-[#73a9ff74]", - "focus:outline-hidden", - "focus:ring-2", - "ring-[transparent]", - "ring-0", - "border-[1px]", - "font-[600]", - "px-[20px]", - "py-[8px]", - "h-[18px]", - "rounded-[6px]", - "transition", - "duration-150", - ]; - cancel_styles.forEach(style => cancel.classList.add(style)); - cancel.onclick = () => { - document.body.removeChild(cropper_container); - }; - buttons.appendChild(cancel); - buttons.appendChild(save); - }; - }; - reader.readAsDataURL(file); - }; - uploader.click(); + makePFP(); }} >
@@ -317,24 +754,24 @@ export default function Setup() { }} />
- {showSec ? ( -
-
Security Question (optional)
- - -
- ) : null}
+ {showSec ? ( +
+
Security Question (optional)
+ + +
+ ) : null}
@@ -396,6 +833,110 @@ export default function Setup() {
); }; + const Step3SR = () => { + useEffect(() => { + const t = setTimeout(() => { + currentViewRef.current?.classList.remove("-translate-x-6", "opacity-0"); + }, 150); + return () => clearTimeout(t); + }, []); + nextButtonClick = () => { + Next(5); + }; + const opts = JSON.parse(sessionStorage.getItem("tacc-settings") as string) || []; + type OptionItem = { id: string; label: string; raw: any }; + const getOptions = (cat: string): OptionItem[] => { + const val = (opts[0] as any)[cat]; + if (cat === "settings" && val && typeof val === "object") { + const copy = { ...val }; + return Object.entries(copy).flatMap(([k, v]) => (v && typeof v === "object" && !Array.isArray(v) ? Object.entries(v).map(([kk, vv]) => ({ id: `settings.${k}.${kk}`, label: `${k}.${kk}`, raw: vv })) : [{ id: `settings.${k}`, label: k, raw: v }])); + } + if (Array.isArray(val)) { + return val.length === 0 + ? [] + : val.map((item, i) => { + let label = typeof item === "object" && item ? item.name || item.url || JSON.stringify(item).slice(0, 30) : String(item); + return { id: `${cat}[${i}]`, label, raw: item }; + }); + } + if (val && typeof val === "object") { + return Object.keys(val).map(k => ({ id: `${cat}.${k}`, label: k, raw: val[k] })); + } + return [{ id: cat, label: `${cat}: ${String(val)}`, raw: val }]; + }; + const categories = Object.keys(opts[0]); + const [currMap, setCurrMap] = useState>(() => + categories.reduce( + (acc, cat) => { + acc[cat] = getOptions(cat).map(o => o.id); + return acc; + }, + {} as Record, + ), + ); + const toggleOption = (cat: string, optionId: string) => + setCurrMap(prev => { + const s = new Set(prev[cat] || []); + s.has(optionId) ? s.delete(optionId) : s.add(optionId); + return { ...prev, [cat]: Array.from(s) }; + }); + const setAll = (cat: string, enabled: boolean) => { + const ids = getOptions(cat).map(o => o.id); + setCurrMap(prev => ({ ...prev, [cat]: enabled ? ids : [] })); + }; + const Card: React.FC<{ cat: string }> = ({ cat }) => { + const options = getOptions(cat); + const sel = currMap[cat] || []; + const allSelected = options.length > 0 && options.every(o => sel.includes(o.id)); + const noneSelected = sel.length === 0; + return ( +
+
+
{cat}
+
+ + +
+
+ {options.length === 0 ? ( +
There are no options at this time.
+ ) : ( +
+ {options.map(opt => ( + + ))} +
+ {allSelected ? "All selected" : noneSelected ? "None selected" : `${sel.length} selected`} +
+
+ )} +
+ ); + }; + return ( +
{ + currentViewRef.current = el; + }} + className="duration-150 -translate-x-6 opacity-0 flex flex-col justify-center items-center" + > + Restore Terbium Settings +
+ {categories.map(cat => ( + + ))} +
+
+ ); + }; const Step4 = () => { setTimeout(() => { currentViewRef.current?.classList.remove("-translate-x-6"); @@ -500,11 +1041,18 @@ export default function Setup() { ); }; const Step5 = () => { + const ranRef = useRef(false); setTimeout(() => { currentViewRef.current?.classList.remove("-translate-x-6"); currentViewRef.current?.classList.remove("opacity-0"); }, 150); - saveData(); + useEffect(() => { + if (ranRef.current) return; + ranRef.current = true; + (async () => { + await saveData(); + })(); + }, []); return (

{ @@ -552,22 +1100,24 @@ export default function Setup() { {currentStep < 5 && ( )} - + {currentStep > 2 && currentStep < 5 && ( + + )} ) : null} @@ -581,7 +1131,29 @@ export default function Setup() { TB

- {currentStep === 1 ? : currentStep === 2 ? : currentStep === 3 ? : currentStep === 4 ? : currentStep === 5 ? : null} + {currentStep === 1 ? ( + + ) : currentStep === 2 ? ( + + ) : currentStep === 2.1 ? ( + + ) : currentStep === 2.2 ? ( + + ) : currentStep === 2.3 ? ( + + ) : currentStep === 2.4 ? ( + + ) : currentStep === 2.5 ? ( + + ) : currentStep === 3 ? ( + + ) : currentStep === 3.1 ? ( + + ) : currentStep === 4 ? ( + + ) : currentStep === 5 ? ( + + ) : null}
diff --git a/src/Updater.tsx b/src/Updater.tsx index 808d123..6e50b1e 100644 --- a/src/Updater.tsx +++ b/src/Updater.tsx @@ -1,12 +1,11 @@ import { useEffect, useState, useRef } from "react"; -import { dirExists, fileExists } from "./sys/types"; +import { dirExists, fileExists, UserSettings } from "./sys/types"; import { hash } from "./hash.json"; import paths from "./installer.json"; export default function Updater() { const [progress, setProgress] = useState(0); const statusref = useRef(null); - const isDev = import.meta.env.DEV; async function copyDir(inp: string, dest: string, rn?: boolean) { if (rn === true) { @@ -18,7 +17,7 @@ export default function Updater() { const totalFiles = files.length; for (const [index, file] of files.entries()) { const stats = await window.tb.fs.promises.stat(`${inp}/${file}`); - if (stats.isDirectory()) { + if (stats && stats.isDirectory()) { await window.tb.fs.promises.mkdir(`${dest}/${file}`); await copyDir(`${inp}/${file}`, `${dest}/${file}`, true); } else { @@ -268,16 +267,90 @@ export default function Updater() { await window.tb.fs.promises.writeFile(`/apps/user/${user}/browser/favorites.json`, JSON.stringify([])); await window.tb.fs.promises.writeFile(`/apps/user/${user}/browser/userscripts.json`, JSON.stringify([])); } + // v2.2 Update + for (const user of await window.tb.fs.promises.readdir("/home/")) { + const usrSettings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${user}/settings.json`, "utf8")); + if (!usrSettings.window) { + usrSettings.window = { + winAccent: "#ffffff", + blurlevel: 18, + alwaysMaximized: false, + alwaysFullscreen: false, + }; + usrSettings.showFPS = false; + await window.tb.fs.promises.writeFile(`/home/${user}/settings.json`, JSON.stringify(usrSettings, null, 4)); + } + } setProgress(80); statusref.current!.innerText = "Cleaning up..."; setProgress(95); await window.tb.sh.promises.rm(`/system/tmp/terb-upd/`, { recursive: true }); window.onbeforeunload = null; + sessionStorage.setItem("justUpdated", "true"); setProgress(100); statusref.current!.innerText = "Restarting..."; window.location.reload(); }; - main(); + const migrateFs = async () => { + async function copyRecursive(src: string, dest: string) { + const entries = await Filer.fs.promises.readdir(src); + for (const entry of entries) { + const srcPath = src.endsWith("/") ? src + entry : src + "/" + entry; + const destPath = dest.endsWith("/") ? dest + entry : dest + "/" + entry; + const stat = await Filer.fs.promises.stat(srcPath); + if (stat.isDirectory()) { + if (!(await dirExists(destPath))) { + await window.tb.fs.promises.mkdir(destPath); + } + await copyRecursive(srcPath, destPath); + } else { + const fileBuffer = await Filer.fs.promises.readFile(srcPath); + await window.tb.fs.promises.writeFile(destPath, fileBuffer); + } + statusref.current!.innerText = `Copying: ${srcPath}`; + } + } + await copyRecursive("/", "/"); + setProgress(85); + statusref.current!.innerText = "Recreating Desktop Shortcuts..."; + for (const user of await window.tb.fs.promises.readdir("/home/")) { + const items = JSON.parse(await window.tb.fs.promises.readFile(`/home/${user}/desktop/.desktop.json`, "utf8")); + for (const item of items) { + const target = await Filer.fs.promises.readlink(item.item); + await window.tb.fs.promises.symlink(target, item.item); + statusref.current!.innerText = `Creating shortcut: ${item.name}.lnk...`; + } + } + setProgress(93); + statusref.current!.innerText = "Formatting Filer..."; + const fsh = new Filer.fs.Shell(); + for (const loc of await Filer.fs.promises.readdir("//")) { + await fsh.promises.rm(`/${loc}`, { recursive: true }); + } + setProgress(99); + statusref.current!.innerText = "Migration complete!"; + sessionStorage.removeItem("migrateFs"); + statusref.current!.innerText = "Updating System Files..."; + main(); + }; + const run = async () => { + if (!sessionStorage.getItem("migrateFs")) { + const existsOPFS = await dirExists("/system/etc/terbium/"); + const existsFiler = await Filer.fs.promises + .stat("/system/etc/terbium/") + .then(() => true) + .catch(() => false); + if (!existsOPFS && existsFiler) { + sessionStorage.setItem("migrateFs", "true"); + } + } + if (sessionStorage.getItem("migrateFs")) { + await migrateFs(); + } else { + await main(); + } + }; + run(); }, []); return ( diff --git a/src/init/fs.init.ts b/src/init/fs.init.ts index ae7e9d4..54723fc 100644 --- a/src/init/fs.init.ts +++ b/src/init/fs.init.ts @@ -1,16 +1,20 @@ import paths from "../installer.json"; export async function copyfs() { - paths.forEach(async item => { - if (item.toString().includes("browser.tapp")) return; - if (item.toString().endsWith("/")) { - await window.tb.fs.promises.mkdir(`/apps/system/${item.toString()}`); + for (const item of paths) { + const p = item.toString(); + if (p.includes("browser.tapp")) continue; + if (p.endsWith("/")) { + await window.tb.fs.promises.mkdir(`/apps/system/${p}`); } else { - await fetch(`/apps/${item.toString()}`).then(async res => { - const data = await res.text(); - await window.tb.fs.promises.writeFile(`/apps/system/${item.toString()}`, data); - }); + const res = await fetch(`/apps/${p}`); + if (!res.ok) { + console.error(`Failed to fetch /apps/${p}: ${res.status} ${res.statusText}`); + continue; + } + const data = await res.text(); + await window.tb.fs.promises.writeFile(`/apps/system/${p}`, data); } - }); + } return "Success"; } diff --git a/src/init/index.ts b/src/init/index.ts index f26030b..0b6b8ef 100644 --- a/src/init/index.ts +++ b/src/init/index.ts @@ -1,4 +1,4 @@ -import { dirExists } from "../sys/types"; +import { dirExists, TAuthSSData, UserSettings } from "../sys/types"; import apps from "../apps.json"; import { copyfs } from "./fs.init"; import { hash } from "../hash.json"; @@ -19,6 +19,7 @@ export async function init() { if (!(await dirExists("/apps"))) { await window.tb.fs.promises.mkdir("/apps"); await window.tb.fs.promises.mkdir("/apps/system"); + await copyfs(); await window.tb.fs.promises.mkdir("/apps/user"); await window.tb.fs.promises.writeFile("/apps/web_apps.json", JSON.stringify({ apps: [] })); } else { @@ -119,14 +120,17 @@ export async function init() { let recentApps: any[] = []; await window.tb.fs.promises.writeFile("/system/var/terbium/recent.json", JSON.stringify(recentApps)); } + + const tcaccSettings: TAuthSSData = sessionStorage.getItem("tacc-settings") ? JSON.parse(sessionStorage.getItem("tacc-settings")!) : null; var items: any[] = []; if (!(await dirExists(`/home/${user}`))) { await window.tb.fs.promises.mkdir(`/home/${user}`); - let userSettings = { + let userSettings: UserSettings = { wallpaper: "/assets/wallpapers/1.png", wallpaperMode: "cover", animations: true, + // @ts-ignore proxy: sessionStorage.getItem("selectedProxy") || "Scramjet", transport: "Default (Epoxy)", wispServer: `${location.protocol.replace("http", "ws")}//${location.hostname}:${location.port}/wisp/`, @@ -137,7 +141,21 @@ export async function init() { internet: false, showSeconds: false, }, + showFPS: false, + windowOptimizations: false, + window: { + winAccent: "#ffffff", + blurlevel: 18, + alwaysMaximized: false, + alwaysFullscreen: false, + }, }; + if (tcaccSettings && Array.isArray(tcaccSettings) && tcaccSettings[0] && tcaccSettings[0].settings) { + userSettings = { + ...userSettings, + ...tcaccSettings[0].settings, + }; + } await window.tb.fs.promises.writeFile(`/home/${user}/settings.json`, JSON.stringify(userSettings)); await window.tb.fs.promises.mkdir(`/home/${user}/desktop`); let r2 = []; @@ -194,29 +212,57 @@ export async function init() { }); await window.tb.fs.promises.symlink(`/apps/system/${name}.tapp/index.json`, `/home/${user}/desktop/${name}.lnk`); } - await copyfs(); await window.tb.fs.promises.writeFile(`/home/${user}/desktop/.desktop.json`, JSON.stringify(items)); - await window.tb.fs.promises.writeFile( - `/apps/user/${user}/files/config.json`, - JSON.stringify({ - "quick-center": true, - "sidebar-width": 180, - drives: { - "File System": `/home/${user}/`, - }, - storage: { - "File System": "storage-device", - localStorage: "storage-device", - }, - "open-collapsibles": { + if (tcaccSettings && Array.isArray(tcaccSettings) && tcaccSettings[0]) { + await window.tb.fs.promises.writeFile( + `/apps/user/${user}/files/config.json`, + JSON.stringify({ "quick-center": true, - drives: true, - }, - "show-hidden-files": false, - }), - "utf8", - ); - await window.tb.fs.promises.writeFile(`/apps/user/${user}/files/davs.json`, JSON.stringify([])); + "sidebar-width": 180, + drives: { + "File System": `/home/${user}/`, + ...tcaccSettings[0].davs.reduce((acc: any, d: any) => { + const driveName = d.name || d.driveName; + acc[driveName] = `/mnt/${driveName}/`; + return acc; + }, {}), + }, + storage: { + "File System": "storage-device", + localStorage: "storage-device", + }, + "open-collapsibles": { + "quick-center": true, + drives: true, + }, + "show-hidden-files": false, + }), + "utf8", + ); + await window.tb.fs.promises.writeFile(`/apps/user/${user}/files/davs.json`, JSON.stringify(tcaccSettings[0].davs, null, 2)); + } else { + await window.tb.fs.promises.writeFile( + `/apps/user/${user}/files/config.json`, + JSON.stringify({ + "quick-center": true, + "sidebar-width": 180, + drives: { + "File System": `/home/${user}/`, + }, + storage: { + "File System": "storage-device", + localStorage: "storage-device", + }, + "open-collapsibles": { + "quick-center": true, + drives: true, + }, + "show-hidden-files": false, + }), + "utf8", + ); + await window.tb.fs.promises.writeFile(`/apps/user/${user}/files/davs.json`, JSON.stringify([])); + } await window.tb.fs.promises.mkdir(`/apps/user/${user}/browser`); await window.tb.fs.promises.writeFile(`/apps/user/${user}/browser/favorites.json`, JSON.stringify([])); await window.tb.fs.promises.writeFile(`/apps/user/${user}/browser/userscripts.json`, JSON.stringify([])); @@ -224,15 +270,15 @@ export async function init() { const response = await fetch("/apps/files.tapp/icons.json"); const dat = await response.json(); const iconNames = Object.keys(dat["name-to-path"]); - const icons = Object.values(dat["name-to-path"]); var iconArrays: { [key: string]: string } = {}; await window.tb.fs.promises.mkdir(`/system/etc/terbium/file-icons`); - iconNames.forEach(async name => { - iconArrays[name] = `/system/etc/terbium/file-icons/${name}.svg`; // name, path - const icon = icons[iconNames.indexOf(name)]; - await window.tb.fs.promises.writeFile(`/system/etc/terbium/file-icons/${name}.svg`, icon); - }); + for (const name of iconNames) { + const path = `/system/etc/terbium/file-icons/${name}.svg`; + iconArrays[name] = path; + const icon = dat["name-to-path"][name]; + await window.tb.fs.promises.writeFile(path, icon as any); + } await window.tb.fs.promises.writeFile( `/system/etc/terbium/file-icons.json`, JSON.stringify({ diff --git a/src/main.tsx b/src/main.tsx index b65dcac..678eac6 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -15,18 +15,18 @@ import Updater from "./Updater.tsx"; const Root = () => { const [currPag, setPag] = useState(); - // @ts-expect-error expected, api is limited to fs until boot - if (typeof window.tb === "undefined") window.tb = {}; - if (typeof window.tb.fs === "undefined" && typeof Filer !== "undefined" && Filer.fs) { - console.log("[FS] File System Ready"); - window.tb.fs = Filer.fs; - window.tb.sh = new Filer.fs.Shell(); - } const params = new URLSearchParams(window.location.search); useEffect(() => { const tempTransport = async () => { const connection = new BareMuxConnection("/baremux/worker.js"); - await connection.setTransport("/epoxy/index.mjs", [{ wisp: "wss://wisp.terbiumon.top/wisp/" }]); + await connection.setTransport("/epoxy/index.mjs", [{ wisp: `${location.protocol.replace("http", "ws")}//${location.hostname}:${location.port}/wisp/` }]); + const tbOn = async () => { + while (!window.tb.system?.version) { + await new Promise(res => setTimeout(res, 50)); + } + window.dispatchEvent(new Event("tfsready")); + }; + tbOn(); const { ScramjetController } = $scramjetLoadController(); window.scramjetTb = { prefix: "/service/", @@ -77,7 +77,7 @@ const Root = () => { sha = hash; } if (localStorage.getItem("setup")) { - if (localStorage.getItem("setup") && (sha !== hash || sessionStorage.getItem("skipUpd"))) { + if (localStorage.getItem("setup") && (sha !== hash || sessionStorage.getItem("migrateFs"))) { setPag(); } else { if (sessionStorage.getItem("logged-in") && sessionStorage.getItem("logged-in") === "true") { diff --git a/src/sys/Api.ts b/src/sys/Api.ts index db7dec4..0dab8a8 100644 --- a/src/sys/Api.ts +++ b/src/sys/Api.ts @@ -1,7 +1,7 @@ import { BareMuxConnection } from "@mercuryworkshop/bare-mux"; import type { ScramjetController } from "@mercuryworkshop/scramjet"; import * as fflate from "fflate"; -import { libcurl } from "libcurl.js/bundled"; +import { libcurl } from "libcurl.js"; import apps from "../apps.json"; import { hash } from "../hash.json"; import pwd from "./apis/Crypto"; @@ -25,6 +25,8 @@ import { initializeWebContainer } from "./Node/runtimes/Webcontainers/nodeProc"; import parse from "./Parser"; import { useWindowStore } from "./Store"; import { type COM, type cmprops, type dialogProps, fileExists, type launcherProps, type MediaProps, type NotificationProps, type SysSettings, type User, type UserSettings, type WindowConfig } from "./types"; +import { vFS } from "./vFS"; +import { auth, getinfo, setinfo } from "./apis/utils/tauth"; const system = new System(); const pw = new pwd(); @@ -43,6 +45,7 @@ export default async function Api() { window.tb = { registry: registry, sh: window.tb.sh, + buffer: window.tb.buffer, battery: { async showPercentage() { const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); @@ -508,7 +511,7 @@ export default async function Api() { throw new Error(`Failed to download the file. Status: ${response.status}`); } const content = await response.arrayBuffer(); - await window.tb.fs.promises.writeFile(location, Filer.Buffer.from(content)); + await window.tb.fs.promises.writeFile(location, window.tb.buffer.from(content), "arraybuffer"); console.log(`File saved successfully at: ${location}`); } catch (error) { console.error(error); @@ -516,26 +519,22 @@ export default async function Api() { }, exportfs: async () => { const zip: { [key: string]: Uint8Array } = {}; - // @ts-expect-error No Types as TFS is not production ready yet - if (window.tb.fs.mode === "OPFS") { - throw new Error("OPFS export is not implemented yet, Check back later"); - } else { - async function addzip(inp: string, aPath = "") { - const files = await window.tb.fs.promises.readdir(inp); - for (const file of files) { - const fullPath = `${inp}/${file}`; - const stats = await window.tb.fs.promises.stat(fullPath); - const zipPath = `${aPath}${file}`; - if (stats.isDirectory()) { - await addzip(fullPath, `${zipPath}/`); - } else { - const fileData = await window.tb.fs.promises.readFile(fullPath); - zip[zipPath] = new Uint8Array(fileData); - } + // This is a very inefficient way of zipping the fs but it will be replaced soon + async function addzip(inp: string, aPath = "") { + const files = await window.tb.fs.promises.readdir(inp); + for (const file of files) { + const fullPath = `${inp}/${file}`; + const stats = await window.tb.fs.promises.stat(fullPath); + const zipPath = `${aPath}${file}`; + if (stats && stats.isDirectory()) { + await addzip(fullPath, `${zipPath}/`); + } else { + const fileData = await window.tb.fs.promises.readFile(fullPath); + zip[zipPath] = new Uint8Array(fileData); } } - await addzip("//"); } + await addzip("//"); const link = document.createElement("a"); const zipBlob = new Blob([window.tb.fflate.zipSync(zip)], { type: "application/zip" }); link.href = URL.createObjectURL(zipBlob); @@ -543,7 +542,18 @@ export default async function Api() { link.click(); }, users: { - async list() {}, + async list() { + const usersDir = await window.tb.fs.promises.readdir("/home/"); + const users: string[] = []; + for (const user of usersDir) { + const userJsonPath = `/home/${user}/user.json`; + if (await fileExists(userJsonPath)) { + const userData: User = JSON.parse(await window.tb.fs.promises.readFile(userJsonPath, "utf8")); + users.push(userData.username); + } + } + return users; + }, async add(user: User) { const { username, password, pfp, perm, securityQuestion } = user; const userDir = `/home/${username}`; @@ -562,7 +572,7 @@ export default async function Api() { }; } await window.tb.fs.promises.writeFile(`${userDir}/user.json`, JSON.stringify(userJson)); - const userSettings = { + const userSettings: UserSettings = { wallpaper: "/assets/wallpapers/1.png", wallpaperMode: "cover", animations: true, @@ -576,6 +586,14 @@ export default async function Api() { internet: false, showSeconds: false, }, + showFPS: false, + windowOptimizations: false, + window: { + winAccent: "#ffffff", + blurlevel: 18, + alwaysMaximized: false, + alwaysFullscreen: false, + }, }; await window.tb.fs.promises.writeFile(`${userDir}/settings.json`, JSON.stringify(userSettings)); const defaultDirs = ["desktop", "documents", "downloads", "music", "pictures", "videos"]; @@ -637,7 +655,7 @@ export default async function Api() { } else { leftPos = 1; } - if (topPos * 66 > window.innerHeight - 130) { + if (topPos * 66 > parent.innerHeight - 130) { leftPos = 1.15; if (r2.length === 0) { topPos = 0; @@ -660,13 +678,31 @@ export default async function Api() { await window.tb.fs.promises.symlink(`/apps/system/${name}.tapp/index.json`, `/home/${username}/desktop/${name}.lnk`); } await window.tb.fs.promises.writeFile(`/home/${username}/desktop/.desktop.json`, JSON.stringify(items)); + await window.tb.fs.promises.writeFile( + `/apps/user/${username}/app store/repos.json`, + JSON.stringify([ + { + name: "TB App Repo", + url: "https://raw.githubusercontent.com/TerbiumOS/tb-repo/refs/heads/main/manifest.json", + }, + { + name: "XSTARS XTRAS", + url: "https://raw.githubusercontent.com/Notplayingallday383/app-repo/refs/heads/main/manifest.json", + }, + { + name: "Anura App Repo", + url: "https://raw.githubusercontent.com/MercuryWorkshop/anura-repo/refs/heads/master/manifest.json", + icon: "https://anura.pro/icon.png", + }, + ]), + ); return true; }, async remove(id: string) { const userDir = `/home/${id}`; try { const uDir = await window.tb.fs.promises.stat(userDir); - if (uDir.type === "DIRECTORY") { + if (uDir && uDir.type === "DIRECTORY") { await window.tb.sh.promises.rm(userDir, { recursive: true }); } } catch (err: any) { @@ -674,7 +710,7 @@ export default async function Api() { } try { const appDir = await window.tb.fs.promises.stat(`/apps/user/${id}`); - if (appDir.type === "DIRECTORY") { + if (appDir && appDir.type === "DIRECTORY") { await window.tb.sh.promises.rm(`/apps/user/${id}`, { recursive: true }); } } catch (err: any) { @@ -750,6 +786,185 @@ export default async function Api() { libcurl: libcurl, fflate: fflate, fs: window.tb.fs, + vfs: await vFS.create(), + tauth: { + client: auth, + signIn: () => { + return new Promise((resolve, reject) => { + window.tb.dialog.WebAuth({ + title: "Terbium Cloud Sign In", + message: "Please sign in to your Terbium Cloud Account to continue.", + onOk: async (username: string, password: string) => { + await window.tb.tauth.client.signIn.email({ + email: username, + password: password, + fetchOptions: { + onSuccess: async response => { + const exists = await window.tb.fs.promises.exists("/system/etc/terbium/taccs.json"); + if (!exists) { + await window.tb.fs.promises.writeFile("/system/etc/terbium/taccs.json", JSON.stringify([], null, 2), "utf8"); + } + const conf = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/taccs.json", "utf8")); + conf.push({ + username: response.data.user.name, + perm: "admin", + pfp: response.data.user.image, + email: response.data.user.email, + id: response.data.user.id, + }); + await window.tb.fs.promises.writeFile("/system/etc/terbium/taccs.json", JSON.stringify(conf, null, 2), "utf8"); + console.log("[TAUTH] Saved Account Info to FS"); + const info = response; + info.data.user.password = password; + resolve(info); + }, + onError: error => { + reject(error); + }, + }, + }); + }, + onCancel: () => { + reject(new Error("User cancelled the sign-in process")); + }, + }); + }); + }, + signOut: async () => { + let conf = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/taccs.json", "utf8")); + if (!Array.isArray(conf)) { + if (conf && typeof conf === "object") { + conf = Object.values(conf); + } else { + conf = []; + } + } + const currUser = sessionStorage.getItem("currAcc"); + const idx = conf.findIndex((acc: any) => acc && acc.username === currUser); + if (idx !== -1) { + conf.splice(idx, 1); + await window.tb.fs.promises.writeFile("/system/etc/terbium/taccs.json", JSON.stringify(conf, null, 2), "utf8"); + console.log("[TAUTH] Removed Account Info from FS"); + } + }, + isTACC(username?: string) { + return new Promise(async resolve => { + if (!username) { + username = sessionStorage.getItem("currAcc") || "Guest"; + } + const conf = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/taccs.json", "utf8")); + const exists = conf.some((acc: any) => acc && acc.username === username); + resolve(exists); + }); + }, + updateInfo: async (user: Partial) => { + const target = (user as any).id || user.username || sessionStorage.getItem("currAcc"); + if (!target) throw new Error("No target account specified"); + let conf = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/taccs.json", "utf8")); + const exists = await window.tb.fs.promises.exists("/system/etc/terbium/taccs.json"); + if (!exists) { + await window.tb.fs.promises.writeFile("/system/etc/terbium/taccs.json", JSON.stringify([], null, 2), "utf8"); + } + if (!Array.isArray(conf)) { + if (conf && typeof conf === "object") conf = Object.values(conf); + else conf = []; + } + const idx = conf.findIndex((acc: any) => acc && (acc.username === target || acc.id === target)); + if (idx === -1) throw new Error(`Account '${target}' not found`); + const existing = conf[idx] || {}; + const updated = { ...existing, ...user }; + if (!updated.id && existing.id) updated.id = existing.id; + conf[idx] = updated; + await window.tb.fs.promises.writeFile("/system/etc/terbium/taccs.json", JSON.stringify(conf, null, 2), "utf8"); + if (existing.username && updated.username && existing.username !== updated.username && sessionStorage.getItem("currAcc") === existing.username) { + sessionStorage.setItem("currAcc", updated.username); + } + const run = async () => { + const updobj = { + name: updated.username, + image: updated.pfp, + ...(updated.email ? { email: updated.email } : {}), + ...(updated.password ? { password: updated.password } : {}), + }; + if (updated.email) { + console.log("[TAUTH] Updating email is not currently supported"); + delete updobj.email; + } + await window.tb.tauth.client.updateUser(updobj); + console.log("[TAUTH] Updated TACC info successfully"); + }; + try { + await run(); + } catch (error) { + // @ts-expect-error + if (error.error.message.toLowerCase() === "unauthorized") { + window.tb.dialog.WebAuth({ + title: "Verify Identity to Update Account", + message: "Please sign in to your Terbium Cloud Account to verify it's you.", + onOk: async (username: string, password: string) => { + await window.tb.tauth.client.signIn.email({ + email: username, + password: password, + fetchOptions: { + onSuccess: async () => { + await run(); + }, + onError: error => { + throw new Error(error.error.message); + }, + }, + }); + }, + onCancel: () => { + return new Error("User cancelled the sign-in process"); + }, + }); + } + } + }, + sync: { + retreive: async () => { + const info = await window.tb.tauth.getInfo(); + if (!info) throw new Error("No TACC info found"); + window.tb.tauth.sync.isSyncing = true; + const data = await getinfo(info.email, info.password, "tbs"); + console.log("[TAUTH] Retrieved synced data from cloud"); + console.log(data.settings[0]); + await window.tb.fs.promises.writeFile(`/home/${info.username}/settings.json`, JSON.stringify(data.settings[0].settings, null, 2), "utf8"); + await window.tb.fs.promises.writeFile(`/apps/user/${info.username}/files/davs.json`, JSON.stringify(data.settings[0].davs, null, 2), "utf8"); + window.dispatchEvent(new Event("updWallpaper")); + window.dispatchEvent(new CustomEvent("proxy-change")); + window.dispatchEvent(new Event("upd-accent")); + window.tb.tauth.sync.isSyncing = false; + }, + upload: async () => { + const info = await window.tb.tauth.getInfo(); + if (!info) throw new Error("No TACC info found"); + window.tb.tauth.sync.isSyncing = true; + const settings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${info.username}/settings.json`, "utf8")); + const davs = JSON.parse(await window.tb.fs.promises.readFile(`/apps/user/${info.username}/files/davs.json`, "utf8")); + const toupload = [ + { + settings: settings, + apps: [], + davs: davs, + }, + ]; + setinfo(info.email, info.password, "tbs", toupload); + console.log("[TAUTH] Uploaded synced data to cloud"); + window.tb.tauth.sync.isSyncing = false; + }, + isSyncing: false, + }, + getInfo: async (username?: string) => { + const conf = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/taccs.json", "utf8")); + if (!conf.find((acc: any) => acc && acc.username === username)) { + username = sessionStorage.getItem("currAcc") || "Guest"; + } + const account = conf.find((acc: any) => acc && acc.username === username) || null; + return account; + }, + }, node: { webContainer: {}, servers: new Map(), @@ -810,6 +1025,7 @@ export default async function Api() { icon: win.icon, pid: win.pid, src: win.src, + size: win.size || { width: 800, height: 600 }, }; }); return list; @@ -834,7 +1050,6 @@ export default async function Api() { // @ts-expect-error const stream = await navigator.mediaDevices.getDisplayMedia({ preferCurrentTab: true }); const capture = new ImageCapture(stream.getVideoTracks()[0]); - // @ts-expect-error const frame = await capture.grabFrame(); stream.getVideoTracks()[0].stop(); const canvas: HTMLCanvasElement = document.createElement("canvas"); @@ -986,11 +1201,10 @@ export default async function Api() { }, }; - //@ts-expect-error stfu if (window.loadLock) // this function seems to be called twice, anura doesn't like initing twice, so well, this is the weird fix I chose instead of tackling the root problem - Rafflesia return; - (window as any).loadLock = true; + window.loadLock = true; const anura = await Anura.new({ milestone: 5, @@ -1087,8 +1301,10 @@ export default async function Api() { window.ExternalApp = ExternalApp; window.ExternalLib = ExternalLib; window.electron = new Lemonade(); + window.tb.libcurl.load_wasm("https://cdn.jsdelivr.net/npm/libcurl.js@latest/libcurl.wasm"); const getupds = async () => { if (hash !== (await window.tb.fs.promises.readFile("/system/etc/terbium/hash.cache", "utf8"))) { + await window.tb.fs.promises.writeFile("/system/etc/terbium/hash.cache", "invalid"); window.tb.notification.Toast({ application: "System", iconSrc: "/fs/apps/system/about.tapp/icon.svg", @@ -1144,6 +1360,38 @@ export default async function Api() { document.addEventListener("keyup", up); wsld(); await window.tb.proxy.updateSWs(); + const getchangelog = async () => { + const reCache: Record = await (await window.tb.libcurl.fetch("https://cdn.terbiumon.top/changelogs/versions.json")).json(); + const vInf = reCache[system.version("string") as string]; + if (hash === vInf.hash) { + window.tb.window.create({ + title: "Changelog", + src: vInf.changeFile, + icon: "/fs/apps/system/about.tapp/icon.svg", + size: { + width: 600, + height: 400, + }, + proxy: true, + }); + } + }; + if (sessionStorage.getItem("justUpdated") === "true") { + getchangelog(); + sessionStorage.removeItem("justUpdated"); + } + if (await window.tb.tauth.isTACC()) { + window.tb.fs.watch(`/home/${await window.tb.user.username()}/settings.json`, { recursive: true }, (e: string, _f: string) => { + if (e === "change" && window.tb.tauth.sync.isSyncing !== true) { + window.tb.tauth.sync.upload(); + } + }); + window.tb.fs.watch(`/apps/user/${await window.tb.user.username()}/files/davs.json`, { recursive: true }, (e: string, _f: string) => { + if (e === "change" && window.tb.tauth.sync.isSyncing !== true) { + window.tb.tauth.sync.upload(); + } + }); + } window.tb.node.webContainer = await initializeWebContainer(); document.addEventListener("libcurl_load", wsld); } diff --git a/src/sys/Filer.d.ts b/src/sys/Filer.d.ts index 912c04d..8702bf6 100644 --- a/src/sys/Filer.d.ts +++ b/src/sys/Filer.d.ts @@ -28,7 +28,7 @@ type FilerFS = { ) => Promise; ls: (dir: string) => Promise; mkdirp: (dir: string) => Promise; - rm: (path: string) => Promise; + rm: (path: string, options?: { recursive?: boolean; force?: boolean }) => Promise; tempDir: () => Promise; touch: (filePath: string) => Promise; }; diff --git a/src/sys/Store.ts b/src/sys/Store.ts index 097f594..02b9aad 100644 --- a/src/sys/Store.ts +++ b/src/sys/Store.ts @@ -94,6 +94,8 @@ const useWindowStore = create()(set => ({ title: appName, icon: config.icon, src: config.src, + size: config.size, + proxy: config.proxy, weight: 1, }; } @@ -180,16 +182,16 @@ const useWindowStore = create()(set => ({ set((state: WindowState) => { const window = state.windows.find(w => w.wid === wid); if (!window) return state; - + const indexes = state.windows.map(w => w.zIndex ?? 0); const maxIndex = Math.max(...indexes); - + // Optimization: Check if window is already at highest z-index // This can be disabled if window optimization setting is off if (window.focused && window.zIndex === maxIndex) { return state; // No update needed } - + set({ currentPID: window.pid }); window.zIndex = maxIndex + 1; diff --git a/src/sys/apis/Dialogs.tsx b/src/sys/apis/Dialogs.tsx index 3c3ab5b..3242483 100644 --- a/src/sys/apis/Dialogs.tsx +++ b/src/sys/apis/Dialogs.tsx @@ -47,6 +47,13 @@ export default function DialogContainer() { ); } +const formatSize = (size: number) => { + if (size < 1024) return `${size} B`; + if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`; + if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(2)} MB`; + return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`; +}; + export function Alert({ title, message, onOk }: dialogProps) { const container = useRef(null); const dialog = useRef(null); @@ -341,50 +348,110 @@ export function Permissions({ title, message, onOk, onCancel }: dialogProps) { ); } -export function FileBrowser({ title, filter, onOk, onCancel }: dialogProps) { +export function FileBrowser({ title, filter, local, onOk, onCancel }: dialogProps) { if (!title) throw new Error("title is required"); - const [selectedEntry, setselectedEntry] = useState(null); - const [currentDirectory, setCurrentDirectory] = useState("/"); + const [selectedEntry, setSelectedEntry] = useState(null); + const [currentDirectory, setCurrentDirectory] = useState("storage devices"); const [fileEntries, setFileEntries] = useState([]); const [loading, setLoading] = useState(true); const [showBackButton, setShowBackButton] = useState(false); + const [storageInfo, setStorageInfo] = useState<{ usage: number; quota: number } | null>(null); const anura = window.parent.anura; - const openDirectory = async (directory: string) => { - setLoading(true); - try { - const entries = await anura.fs.promises.readdir(directory); - const entriesInfo = await Promise.all( - entries.map(async entry => { - const fileInfo = await anura.fs.promises.stat(`${directory}/${entry}`); - const isDirectory = fileInfo.isDirectory(); - if (!filter || isDirectory || (filter !== "*.*" && entry.endsWith(filter))) { - return { entry, isDirectory }; + useEffect(() => { + const openDirectory = async (directory: string) => { + setLoading(true); + try { + if (directory.startsWith("/mnt/")) { + const serverName = directory.split("/")[2]; + const server = window.tb.vfs.servers.get(serverName); + if (server && server.connected && server.connection?.client) { + const client = server.connection.client; + const path = directory.replace(`/mnt/${serverName}`, "") || "/"; + const entries = await client.getDirectoryContents(path); + const entriesInfo = entries.map((entry: any) => ({ + entry: entry.basename, + isDirectory: entry.type === "directory", + type: "external", + connected: true, + })); + setFileEntries( + entriesInfo.filter((info: any) => { + if (!filter || info.isDirectory || (filter !== "*.*" && info.entry.endsWith(filter))) { + return true; + } + return false; + }), + ); + setShowBackButton(true); + setLoading(false); + return; } - return null; - }), - ); - setFileEntries(entriesInfo.filter(Boolean)); - setShowBackButton(directory !== "//"); - } catch (error) { - console.error(error); - } finally { + } + const entries = await anura.fs.promises.readdir(directory); + const entriesInfo = await Promise.all( + entries.map(async entry => { + const fileInfo = await anura.fs.promises.stat(`${directory}/${entry}`); + const isDirectory = fileInfo.isDirectory(); + const type = "internal"; + if (!filter || isDirectory || (filter !== "*.*" && entry.endsWith(filter))) { + return { entry, isDirectory, type }; + } + return null; + }), + ); + setFileEntries(entriesInfo.filter(Boolean)); + setShowBackButton(true); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + if (currentDirectory === "storage devices") { + navigator.storage.estimate().then(({ usage, quota }) => { + setStorageInfo({ usage: usage as number, quota: quota as number }); + }); setLoading(false); + const entries = local + ? [ + { + entry: "File System", + type: "internal", + }, + ] + : [ + { + entry: "File System", + type: "internal", + }, + ...Array.from(window.tb.vfs.servers.values()).map(server => ({ + entry: server.name, + type: "external", + connected: server.connected, + })), + ]; + setFileEntries(entries); + setShowBackButton(false); + } else { + openDirectory(currentDirectory); } - }; - useEffect(() => { - openDirectory(currentDirectory); - }, [currentDirectory]); - const entClick = (entry: string, isDirectory: boolean) => { - if (isDirectory) { + }, [currentDirectory, filter, anura]); + + const entClick = (entry: string, isDirectory: boolean, type: string) => { + if (currentDirectory === "storage devices") { + if (type === "internal") { + setCurrentDirectory("//"); + } else { + window.tb.vfs.setServer(entry); + setCurrentDirectory(`/mnt/${entry}`); + } + } else if (isDirectory) { setCurrentDirectory(`${currentDirectory}/${entry}`); } else { - if (currentDirectory.startsWith("///")) { - setCurrentDirectory(currentDirectory.slice(3)); - } - setselectedEntry(`${currentDirectory}/${entry}`); + setSelectedEntry(`${currentDirectory}/${entry}`); const files = document.querySelectorAll(".file-item"); files.forEach(file => { - if (file.getAttribute("[data-entry]") !== `${entry}`) { + if (file.getAttribute("data-entry") !== `${entry}`) { file.classList.remove("bg-[#ffffff18]"); } }); @@ -409,6 +476,17 @@ export function FileBrowser({ title, filter, onOk, onCancel }: dialogProps) { }, 300); }); }; + const setPath = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + const value = (e.target as HTMLInputElement).value; + if (value === "storage devices") { + setCurrentDirectory("storage devices"); + } else { + setCurrentDirectory(value); + } + } + }; + return (
@@ -418,25 +496,50 @@ export function FileBrowser({ title, filter, onOk, onCancel }: dialogProps) { ) : (
{fileEntries.length === 0 ? (
No files found
) : ( - fileEntries.map(({ entry, isDirectory }) => ( + fileEntries.map(({ entry, isDirectory, type, connected }) => (
) => { - entClick(entry, isDirectory); + className="file-item flex flex-col gap-1 select-none p-1.5 first:rounded-t-lg last:rounded-b-lg duration-150" + onMouseDown={e => { + entClick(entry, isDirectory, type); + const files = document.querySelectorAll(".file-item"); + files.forEach(file => { + file.classList.remove("bg-[#ffffff18]"); + }); e.currentTarget.classList.add("bg-[#ffffff18]"); }} >
- {isDirectory ? ( + {currentDirectory === "storage devices" ? ( + type === "external" ? ( + + + + + + ) : ( + + + + + ) + ) : isDirectory ? ( @@ -451,13 +554,22 @@ export function FileBrowser({ title, filter, onOk, onCancel }: dialogProps) { )}
{entry}
+ {currentDirectory === "storage devices" && entry === "File System" && storageInfo ? ( +
+ {(() => { + return `${formatSize(storageInfo.usage)} of ${formatSize(storageInfo.quota)}`; + })()} +
+ ) : type === "external" && currentDirectory === "storage devices" ? ( +
WebDav Device
+ ) : null}
)) )}
)} -
+
@@ -465,15 +577,20 @@ export function FileBrowser({ title, filter, onOk, onCancel }: dialogProps) { )} + setCurrentDirectory(e.target.value)} /> + {showBackButton && ( + + )} setCurrentDirectory(e.target.value)} /> + {showBackButton && ( + + )} setSelectedEntry(e.target.value)} + onChange={e => onPathChange(e as unknown as React.KeyboardEvent)} />