Repository: TerbiumOS/web-v2 Branch: main Commit: f58eb2e98a9d Files: 326 Total size: 2.4 MB Directory structure: gitextract_z8f305yy/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.md │ │ └── config.yml │ ├── dependabot.yml │ └── workflows/ │ ├── biome.yml │ ├── test.yml │ └── upk-build.yml ├── .gitignore ├── .node_version ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── .zed/ │ └── settings.json ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── biome.json ├── bootstrap.ts ├── docs/ │ ├── README.md │ ├── anura-compat.md │ ├── apis/ │ │ └── readme.md │ ├── backend-configuration.md │ ├── contributions.md │ ├── creating-apps.md │ ├── creating-terminal-commands.md │ ├── lemonade-compat.md │ ├── lemonade.md │ ├── static-hosting.md │ └── upk-build.md ├── env.d.ts ├── eslint.config.js ├── fail.html ├── index.html ├── package.json ├── postcss.config.js ├── public/ │ ├── anura-sw.js │ ├── apps/ │ │ ├── about.tapp/ │ │ │ ├── app.css │ │ │ ├── index.html │ │ │ └── index.json │ │ ├── app store.tapp/ │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── index.json │ │ ├── browser.tapp/ │ │ │ ├── index.css │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── newtab.html │ │ │ └── userscripts.html │ │ ├── calculator.tapp/ │ │ │ ├── index.css │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── index.json │ │ ├── feedback.tapp/ │ │ │ └── index.json │ │ ├── files.tapp/ │ │ │ ├── cm.css │ │ │ ├── extensions.json │ │ │ ├── files.com.js │ │ │ ├── icons.json │ │ │ ├── index.css │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── properties/ │ │ │ │ ├── index.html │ │ │ │ └── index.js │ │ │ └── webdav.js │ │ ├── fsapp.app/ │ │ │ ├── GUI.js │ │ │ ├── appview.html │ │ │ ├── components/ │ │ │ │ ├── File.mjs │ │ │ │ ├── Folder.mjs │ │ │ │ ├── Selector.mjs │ │ │ │ ├── SideBar.mjs │ │ │ │ └── TopBar.mjs │ │ │ ├── filemanager.css │ │ │ ├── index.html │ │ │ ├── index.mjs │ │ │ ├── manifest.json │ │ │ └── operations.js │ │ ├── libfilepicker.lib/ │ │ │ ├── GUI.js │ │ │ ├── README.md │ │ │ ├── file.html │ │ │ ├── filemanager.css │ │ │ ├── folder.html │ │ │ ├── handler.js │ │ │ ├── install.js │ │ │ ├── manifest.json │ │ │ └── operations.js │ │ ├── libfileview.lib/ │ │ │ ├── fileHandler.js │ │ │ ├── icons.json │ │ │ ├── install.js │ │ │ └── manifest.json │ │ ├── libpersist.lib/ │ │ │ ├── install.js │ │ │ ├── manifest.json │ │ │ └── src/ │ │ │ └── index.js │ │ ├── media viewer.tapp/ │ │ │ ├── index.css │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ └── media.com.js │ │ ├── nfsadapter/ │ │ │ ├── FileSystemDirectoryHandle.js │ │ │ ├── FileSystemFileHandle.js │ │ │ ├── FileSystemHandle.js │ │ │ ├── adapters/ │ │ │ │ ├── anuraadapter.js │ │ │ │ ├── memory.js │ │ │ │ └── sandbox.js │ │ │ ├── config.js │ │ │ ├── nfsadapter.js │ │ │ └── util.js │ │ ├── settings.tapp/ │ │ │ ├── accounts/ │ │ │ │ ├── index.html │ │ │ │ └── index.js │ │ │ ├── index.css │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── island.js │ │ │ ├── message.js │ │ │ ├── radio.css │ │ │ ├── select.css │ │ │ └── select.js │ │ ├── task manager.tapp/ │ │ │ ├── index.css │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── index.json │ │ ├── terminal.tapp/ │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── logo.txt │ │ │ ├── scripts/ │ │ │ │ ├── cat.js │ │ │ │ ├── cd.js │ │ │ │ ├── clear.js │ │ │ │ ├── curl.js │ │ │ │ ├── echo.js │ │ │ │ ├── exit.js │ │ │ │ ├── git.js │ │ │ │ ├── help.js │ │ │ │ ├── info.json │ │ │ │ ├── info.schema.json │ │ │ │ ├── ls.js │ │ │ │ ├── mkdir.js │ │ │ │ ├── nano.js │ │ │ │ ├── node.js │ │ │ │ ├── ping.js │ │ │ │ ├── pkg.js │ │ │ │ ├── pkill.js │ │ │ │ ├── pwd.js │ │ │ │ ├── rm.js │ │ │ │ ├── rmdir.js │ │ │ │ ├── ssh-keygen.js │ │ │ │ ├── ssh.js │ │ │ │ ├── sysfetch.js │ │ │ │ ├── taskkill.js │ │ │ │ ├── tb.js │ │ │ │ ├── touch.js │ │ │ │ └── unzip.js │ │ │ ├── ssh-util.js │ │ │ ├── terminal.css │ │ │ └── terminal_com.js │ │ └── text editor.tapp/ │ │ ├── index.css │ │ ├── index.html │ │ ├── index.js │ │ ├── index.json │ │ └── text.com.js │ ├── assets/ │ │ ├── fs.ui/ │ │ │ ├── fs.css │ │ │ └── fs.js │ │ ├── libs/ │ │ │ ├── comlink.min.umd.js │ │ │ ├── idb-keyval.js │ │ │ ├── mime.iife.js │ │ │ └── workbox/ │ │ │ ├── workbox-background-sync.dev.js │ │ │ ├── workbox-background-sync.prod.js │ │ │ ├── workbox-broadcast-update.dev.js │ │ │ ├── workbox-broadcast-update.prod.js │ │ │ ├── workbox-cacheable-response.dev.js │ │ │ ├── workbox-cacheable-response.prod.js │ │ │ ├── workbox-core.dev.js │ │ │ ├── workbox-core.prod.js │ │ │ ├── workbox-expiration.dev.js │ │ │ ├── workbox-expiration.prod.js │ │ │ ├── workbox-navigation-preload.dev.js │ │ │ ├── workbox-navigation-preload.prod.js │ │ │ ├── workbox-offline-ga.dev.js │ │ │ ├── workbox-offline-ga.prod.js │ │ │ ├── workbox-precaching.dev.js │ │ │ ├── workbox-precaching.prod.js │ │ │ ├── workbox-range-requests.dev.js │ │ │ ├── workbox-range-requests.prod.js │ │ │ ├── workbox-routing.dev.js │ │ │ ├── workbox-routing.prod.js │ │ │ ├── workbox-strategies.dev.js │ │ │ ├── workbox-strategies.prod.js │ │ │ ├── workbox-streams.dev.js │ │ │ ├── workbox-streams.prod.js │ │ │ ├── workbox-sw.js │ │ │ ├── workbox-window.dev.es5.mjs │ │ │ ├── workbox-window.dev.mjs │ │ │ ├── workbox-window.dev.umd.js │ │ │ ├── workbox-window.prod.es5.mjs │ │ │ ├── workbox-window.prod.mjs │ │ │ └── workbox-window.prod.umd.js │ │ ├── materialsymbols.css │ │ └── matter.css │ ├── cursor_changer.js │ ├── lib/ │ │ └── dreamland/ │ │ ├── all.js │ │ ├── dev.js │ │ ├── minimal.js │ │ └── ssr.js │ ├── manifest.json │ ├── media_interactions.js │ ├── robots.txt │ ├── sitemap.xml │ ├── theme.css │ └── uv/ │ └── uv.config.js ├── server.ts ├── src/ │ ├── App.tsx │ ├── Boot.tsx │ ├── CustomOS.tsx │ ├── Loading.tsx │ ├── Login.tsx │ ├── Recovery.tsx │ ├── Setup.tsx │ ├── Updater.tsx │ ├── index.css │ ├── init/ │ │ ├── fs.init.ts │ │ └── index.ts │ ├── main.tsx │ ├── sys/ │ │ ├── Api.ts │ │ ├── Filer.d.ts │ │ ├── FilerWP.d.ts │ │ ├── Node/ │ │ │ └── runtimes/ │ │ │ ├── Webcontainers/ │ │ │ │ ├── nodeFSIntegration.ts │ │ │ │ ├── nodeProc.ts │ │ │ │ └── util/ │ │ │ │ └── getFileTree.ts │ │ │ ├── shims/ │ │ │ │ ├── apis/ │ │ │ │ │ ├── child_process.ts │ │ │ │ │ └── http.ts │ │ │ │ ├── path-remapper.ts │ │ │ │ └── util/ │ │ │ │ └── Stub.ts │ │ │ └── util/ │ │ │ └── getFileTree.ts │ │ ├── Parser.ts │ │ ├── Store.ts │ │ ├── apis/ │ │ │ ├── Crypto.ts │ │ │ ├── Date.ts │ │ │ ├── Dialogs.tsx │ │ │ ├── Mediaisland.tsx │ │ │ ├── Notifications.tsx │ │ │ ├── Registry.ts │ │ │ ├── SysSearch.ts │ │ │ ├── System.ts │ │ │ ├── Time.ts │ │ │ ├── Xor.ts │ │ │ └── utils/ │ │ │ ├── WindowPerformanceMonitor.ts │ │ │ ├── file.ts │ │ │ ├── startupHandler.ts │ │ │ ├── tauth.ts │ │ │ └── winPreview.ts │ │ ├── gui/ │ │ │ ├── AppIsland.tsx │ │ │ ├── Battery.tsx │ │ │ ├── ContextMenu.tsx │ │ │ ├── Desktop.tsx │ │ │ ├── Dock.tsx │ │ │ ├── FPSCounter.tsx │ │ │ ├── NotificationCenter.tsx │ │ │ ├── Power.tsx │ │ │ ├── Search.tsx │ │ │ ├── Shell.tsx │ │ │ ├── Weather.tsx │ │ │ ├── Wifi.tsx │ │ │ ├── WinSwitcher.tsx │ │ │ ├── WindowArea.tsx │ │ │ └── styles/ │ │ │ ├── boot.css │ │ │ ├── contextmenu.css │ │ │ ├── cropper.css │ │ │ ├── dialog.css │ │ │ ├── dock.css │ │ │ ├── dropdown.css │ │ │ ├── liquor.css │ │ │ ├── loader.css │ │ │ ├── login.css │ │ │ ├── mediaisland.css │ │ │ ├── notification.css │ │ │ ├── oobe.css │ │ │ ├── shell.css │ │ │ ├── wifi.css │ │ │ └── win_switcher.css │ │ ├── lemonade/ │ │ │ ├── app.ts │ │ │ ├── clipboard.ts │ │ │ ├── dialog.ts │ │ │ ├── index.ts │ │ │ ├── ipc.ts │ │ │ ├── net.ts │ │ │ ├── notification.ts │ │ │ ├── screen.ts │ │ │ ├── shell.ts │ │ │ └── window.ts │ │ ├── libcurl.d.ts │ │ ├── liquor/ │ │ │ ├── AliceWM.ts │ │ │ ├── Anura.ts │ │ │ ├── Boot.ts │ │ │ ├── api/ │ │ │ │ ├── ContextMenuAPI.tsx │ │ │ │ ├── Dialog.ts │ │ │ │ ├── FilerFS.ts │ │ │ │ ├── Files.ts │ │ │ │ ├── Filesystem.ts │ │ │ │ ├── LocalFS.ts │ │ │ │ ├── Networking.ts │ │ │ │ ├── Notification.ts │ │ │ │ ├── NotificationService.tsx │ │ │ │ ├── Platform.ts │ │ │ │ ├── Process.ts │ │ │ │ ├── Settings.ts │ │ │ │ ├── Systray.ts │ │ │ │ ├── TFS.ts │ │ │ │ ├── Theme.ts │ │ │ │ ├── UI.ts │ │ │ │ ├── URIHandler.ts │ │ │ │ └── WmApi.tsx │ │ │ ├── bcc.ts │ │ │ ├── coreapps/ │ │ │ │ ├── App.tsx │ │ │ │ └── ExternalApp.tsx │ │ │ ├── libs/ │ │ │ │ ├── ExternalLib.tsx │ │ │ │ └── lib.tsx │ │ │ └── types/ │ │ │ ├── Filer.d.ts │ │ │ └── V86Starter.d.ts │ │ ├── types.ts │ │ └── vFS.ts │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = tab indent_size = 4 end_of_line = lf charset = utf-8 insert_final_newline = true ================================================ FILE: .gitattributes ================================================ public/apps/terminal.tapp/scripts/** linguist-vendored ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: NovaAppsInc patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: snoot2204 tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: # Replace with a single Buy Me a Coffee username thanks_dev: # Replace with a single thanks.dev username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: Bug report with a given code from the OS about: This should be used when the OS gives a Error Code and/or Error Message title: '' labels: bug assignees: NovaAppsInc, Notplayingallday383 --- **Error Code and/or Error Message** Provide the Error Code and/or Error Message provided from the OS; it they required field should be copied to you clipboard if consented to it. **Screenshots** If applicable, add screenshots to help explain your problem. **OS Version and platform info** - Site you used [e.g. someterbiumrepl.repl.co] - Version (The latest version currently is v2.0, If in the About app it doesn't say v2.0 then consider updating your instance.) - Browser [e.g. Chrome, Firefox] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false default: bug-report.md ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "pnpm" directory: "/" schedule: interval: "weekly" day: "saturday" ================================================ FILE: .github/workflows/biome.yml ================================================ name: "Biome Code Quality Assurance" on: push: pull_request: workflow_dispatch: jobs: quality: runs-on: "ubuntu-latest" permissions: contents: "write" steps: - name: "Checkout code" uses: "actions/checkout@v5" with: token: "${{ secrets.GITHUB_TOKEN }}" - name: "Setup Biome" uses: "biomejs/setup-biome@v2" with: version: "latest" - name: "Format with Biome" run: "biome format --write ." - name: "Check for Git changes" id: "verify-changed-files" run: | if [ -n "$(git status --porcelain)" ]; then echo "changed=true" >> $GITHUB_OUTPUT else echo "changed=false" >> $GITHUB_OUTPUT fi - name: "Commit Git changes" if: steps.verify-changed-files.outputs.changed == 'true' && github.event_name == 'push' run: | git -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \ commit -am "chore: auto-fix formatting and linting with Biome" - name: "Push Git changes" if: steps.verify-changed-files.outputs.changed == 'true' && github.event_name == 'push' uses: "ad-m/github-push-action@master" with: github_token: "${{ secrets.GITHUB_TOKEN }}" branch: "${{ github.ref_name }}" - name: "Fail if formatting was needed" if: steps.verify-changed-files.outputs.changed == 'true' run: | echo "Biome formatting changes were needed. Please pull the latest changes." exit 1 ================================================ FILE: .github/workflows/test.yml ================================================ name: Build Check on: push: pull_request: 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: Install dependencies run: pnpm i - name: Build TB React run: pnpm run build-static ================================================ FILE: .github/workflows/upk-build.yml ================================================ name: Build UPK for Anura on: push: branches: - main 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: 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 to latest release uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); const latestRelease = await github.rest.repos.getLatestRelease({ owner: context.repo.owner, repo: context.repo.repo }); await github.rest.repos.uploadReleaseAsset({ owner: context.repo.owner, repo: context.repo.repo, release_id: latestRelease.data.id, name: 'terbium-upk.app.zip', data: fs.readFileSync('terbium-upk.app.zip') }); ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* vite.config.ts.timestamp-*.mjs node_modules dist dist-ssr *.local *.tsbuildinfo .npmrc package-lock.json # Editor directories and files .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? .env # Other src/apps.json src/hash.json src/installer.json ================================================ FILE: .node_version ================================================ 22.19.0 ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": ["biomejs.biome", "prosser.json-schema-2020-validation", "andersonbruceb.json-in-html"] } ================================================ FILE: .vscode/settings.json ================================================ { "editor.formatOnSave": false } ================================================ FILE: .zed/settings.json ================================================ { "format_on_save": "on", "disable_ai": true, "languages": { "HTML": { "formatter": { "language_server": { "name": "biome" } } }, "JavaScript": { "formatter": { "language_server": { "name": "biome" } }, "code_actions_on_format": { "source.fixAll.biome": true, "source.organizeImports.biome": true } }, "TypeScript": { "formatter": { "language_server": { "name": "biome" } }, "code_actions_on_format": { "source.fixAll.biome": true, "source.organizeImports.biome": true } }, "TSX": { "formatter": { "language_server": { "name": "biome" } }, "code_actions_on_format": { "source.fixAll.biome": true, "source.organizeImports.biome": true } } }, "lsp": { "biome": { "settings": { "require_config_file": true } } } } ================================================ FILE: LICENSE.txt ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 2026 TerbiumOS This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: README.md ================================================

Terbium v2

## Some of the technologies used - [Vite](https://vite.dev) - [React](https://react.dev) - [TailwindCSS](https://tailwindcss.com) - [TFS](https://github.com/terbiumos/tfs) - [Fflate](https://github.com/101arrowz/fflate/) - [BareMux](https://github.com/mercuryworkshop/bare-mux) ## Features - All new UI - A Dynamic Shell - Better Window Manager - A desktop - App Store - A brand new terminal - Anura Compatability layer (Liquor) - Electron Compatability layer (Lemonade) - And lots more! ## Setup > NOTE: Terbium **WILL NOT** build on versions of Node older than version 20. To get started it's pretty easy, you need either npm, or pnpm, which can be installed by running: `npm i -g pnpm` and then you just need to the run following command: ```bash pnpm i && pnpm start # Replace pnpm with npm if your not going to use pnpm ``` and visit [http://localhost:3000](http://localhost:3000) you should be good to go! If you are developing/modifying terbium you can just run `pnpm run dev`, **DO NOT** use the development server for Production use. Instead, just run `pnpm start`. For any further backend configuration visit the `.env` file to configure the backend a bit. > Warning
> If you are going to static host Terbium, you will need to change the wisp server, and you would need to follow those steps. Refer to this [document](./docs/static-hosting.md) for more information. ### Documentation If you wish to develop or just learn more about Terbium's components and stuff, feel free to read our [Documentation](/docs/README.md) If you're looking to see what Anura APIs and features are supported in terbium, refer to: [here](/docs/anura-compat.md), if you're looking to see what Electron API's are supported in terbium refer to: [here](/docs/lemonade-compat.md) ### Contributors - [SNOOT](https://github.com/NovaAppsInc) - [XSTARS](https://github.com/Notplayingallday383) - [illusionTBA](https://github.com/illusionTBA) - [Rafflesia](https://github.com/ProgrammerIn-wonderland) - [Riftriot](https://github.com/Riftriot) - [ironswordX](https://github.com/ironswordX) - [Ryan](https://github.com/MovByte) Licensed under the [**AGPL3 License**](https://www.gnu.org/licenses/agpl-3.0.en.html) ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Terbium Versions | Version | Supported | | ------- | --------- | | 2.0.0-beta | ❌ | | 2.0.0-beta2 | ❌ | | 2.0.0-beta3 | ❌ | | 2.1.x | ❌ | | 2.2.x | ✅ | | 2.3.x | ✅ | If your version of terbium is unsupported, please do not make a GitHub Issue about it. Please update to a newer version if your running a unsupported version. ### Supported Liquor Versions | Version | Supported | | ------- | --------- | | 2.1.0 (stable) | ❌ | | 2.1.1 (stable) | ✅ | ### Supported Lemonade Versions | Version | Supported | | ------- | --------- | | 1.0.0 (stable) | ❌ | | 1.1.0 (stable) | ✅ | ## Reporting a Vulnerability In the case that you somehow manage to find a vulnerability in Terbium please contact security@terbiumon.top REMEMBER: Please DO NOT report vulnerabilities in the repository Issues tab. ## What You Should Report If you are wondering what counts as a vulnerability, heres a good list: - XSS in the URL your using - The ability to execute malicious code on the server hosting Terbium - Any kind of leak of data/information in the code ================================================ FILE: biome.json ================================================ { "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, "files": { "includes": [ "**", "!**/node_modules/**", "!**/dist/**", "!**/public/assets/**", "!**/public/apps/*.lib/**", "!**/public/apps/nfsadapter/**", "!**/public/lib/**", "!**/.vscode/**", "!**/.zed/**", "!**/docs/**", "!**/public/apps/*.app/**", "!**/public/apps/files.tapp/webdav.js", "!**/public/apps/terminal.tapp/ssh-util.js" ] }, "formatter": { "enabled": true, "indentStyle": "tab", "indentWidth": 4, "lineWidth": 320 }, "css": { "parser": { "tailwindDirectives": true } }, "linter": { "enabled": true, "rules": { "recommended": true, "style": { "noParameterAssign": "error", "useAsConstAssertion": "error", "useDefaultParameterLast": "error", "useEnumInitializers": "error", "useSelfClosingElements": "error", "useSingleVarDeclarator": "error", "noUnusedTemplateLiteral": "error", "useNumberNamespace": "error", "noInferrableTypes": "error", "noUselessElse": "error" } } }, "javascript": { "formatter": { "quoteStyle": "double", "arrowParentheses": "asNeeded", "lineWidth": 320 } }, "assist": { "enabled": true, "actions": { "source": { "organizeImports": "on" } } } } ================================================ FILE: bootstrap.ts ================================================ import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import consola from "consola"; import { TServer } from "./server"; import { version } from "./package.json"; import open from "open"; import { exec } from "child_process"; import AdmZip from "adm-zip"; consola.info("Bootstrapping TerbiumOS [v" + version + "]"); export default async function Bootstrap() { const args = process.argv; const nodever = fs.readFileSync(".node_version", "utf-8").trim(); if (process.version < nodever) { consola.warn("Your version of Node.JS is not supported. Please update node to use Terbium. (Current version: " + process.version + ", Required version: " + nodever + " or higher)"); } await BuildApps(); await CreateAppsPaths(); if (!fs.existsSync(".env")) await CreateEnv(); await Updater(); consola.success("TerbiumOS bootstrapped successfully"); if (!(args.includes("--apps-only") || args.includes("--dev"))) { TServer(); } } export async function BuildApps() { consola.start("Building apps..."); const __dirname = path.dirname(fileURLToPath(import.meta.url)), baseDir = path.join(__dirname, "./public/apps"), outputDir = path.join(__dirname, "./src"), outputJsonPath = path.join(outputDir, "apps.json"), result: { name: string; config: any }[] = []; function scanDirectory(dir: string) { fs.readdirSync(dir, { withFileTypes: true }).forEach(i => { if (i.isDirectory()) { const indexFilePath = path.join(dir, i.name, "index.json"); if (fs.existsSync(indexFilePath)) { try { const data = JSON.parse(fs.readFileSync(indexFilePath, "utf-8")); if (data.name && data.config) { if (data.name !== "Browser") { data.config.src = data.config.src.replace(`/apps/${data.name.toLowerCase()}.tapp/`, `/fs/apps/system/${data.name.toLowerCase()}.tapp/`); data.config.icon = data.config.icon.replace(`/apps/${data.name.toLowerCase()}.tapp/`, `/fs/apps/system/${data.name.toLowerCase()}.tapp/`); } result.push({ name: data.name, config: data.config }); } } catch (t) { consola.error(`Error parsing ${indexFilePath}:`, t instanceof Error ? t.message : String(t)); } } } else if (i.name.endsWith(".tapp.zip")) { const zipPath = path.join(dir, i.name); try { const zip = new AdmZip(zipPath); const configEntry = zip.getEntry(".tbconfig"); if (configEntry) { const configData = JSON.parse(configEntry.getData().toString("utf-8")); if (configData.title && configData.wmArgs) { result.push({ name: configData.title, config: configData.wmArgs }); } } } catch (t) { consola.error(`Error reading ${zipPath}:`, t instanceof Error ? t.message : String(t)); } } }); } scanDirectory(baseDir), fs.existsSync(path.join(__dirname, "./build")) || fs.mkdirSync(path.join(__dirname, "./build")), fs.existsSync(outputJsonPath) || fs.writeFileSync(outputJsonPath, "[]", "utf-8"), fs.writeFileSync(outputJsonPath, JSON.stringify(result, null, 2), "utf-8"), consola.success(`Aggregated JSON saved to ${outputJsonPath}`); exec("git rev-parse HEAD", (error, stdout, stderr) => { if (error || stderr) { consola.error("Failed to get git commit hash"); fs.writeFileSync(path.join(__dirname, "./src/hash.json"), JSON.stringify({ hash: "2b14b5", repository: "terbiumos/web-v2" }, null, 2), "utf-8"); } else { const hash = stdout.trim(); exec("git remote get-url origin", (remoteError, remoteStdout, remoteStderr) => { const repoUrl = remoteStdout.trim(); const data = { hash, repository: repoUrl.replace("https://github.com/", "") }; if (remoteError || remoteStderr) { consola.error("Failed to get repository URL"); fs.writeFileSync(path.join(__dirname, "./src/hash.json"), JSON.stringify({ hash: null, repository: null }, null, 2), "utf-8"); } else { fs.writeFileSync(path.join(__dirname, "./src/hash.json"), JSON.stringify(data, null, 2), "utf-8"); consola.success(`Git hash and repo saved to ${path.join(__dirname, "./src/hash.json")}`); } }); } }); return true; } export async function CreateAppsPaths() { interface Apps { [appName: string]: (string | { [path: string]: string[] })[]; } consola.start("Creating apps paths..."); const __dirname = path.dirname(fileURLToPath(import.meta.url)), baseDir = path.join(__dirname, "./public/apps"), outputDir = path.join(__dirname, "./src"), outputJsonPath = path.join(outputDir, "installer.json"), output: string[] = []; function collectPaths(dir: string, base: string = dir): void { const files: fs.Dirent[] = fs.readdirSync(dir, { withFileTypes: true }); files.forEach((file: fs.Dirent) => { const fullPath = path.join(dir, file.name); const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, "/"); if (file.isDirectory()) { output.push(relativePath + "/"); collectPaths(fullPath, base); } else { output.push(relativePath); } }); } const accmp: string[] = []; fs.readdirSync(baseDir, { withFileTypes: true }).forEach(app => { if (app.isDirectory() && app.name.toLocaleLowerCase().endsWith(".tapp")) { const appPath = path.join(baseDir, app.name); if (app.name.toLowerCase() === "settings.tapp") { collectPaths(appPath); if (fs.existsSync(path.join(appPath, "accounts"))) { collectPaths(path.join(appPath, "accounts")); accmp.push(...output.splice(output.indexOf("settings.tapp/accounts/"))); } } else { collectPaths(appPath); } } else if (app.name.toLowerCase().endsWith(".tapp.zip")) { accmp.push(app.name); } }); output.push(...accmp); fs.writeFileSync(outputJsonPath, JSON.stringify(output, null, 2), "utf-8"); consola.success(`Installer JSON saved to ${outputJsonPath}`); return true; } export async function CreateEnv() { const port = (await consola.prompt("Enter a port for the server to run on (3000): ", { type: "text", default: "3000", placeholder: "3000", cancel: "default", })) || 3000; const masqr = await consola.prompt("Enable Masqr? (no): ", { type: "text", default: "false", placeholder: "no", cancel: "default", }); if (masqr === "no" || masqr === "false" || masqr === "n") { fs.writeFileSync(".env", `MASQR=${false}\nPORT=${port}`); } else { const licenseServer = (await consola.prompt("Enter the masqr license server URL: ")) || ""; const whitelist = (await consola.prompt("Enter a comma separated array of domains to whitelist (Ex: ['https://balls.com', 'https://tomp.app']): ")) || []; fs.writeFileSync(".env", `MASQR=${true}\nPORT=${port}\nLICENSE_SERVER_URL=${licenseServer}\nWHITELISTED_DOMAINS=${whitelist}\n`); } consola.success("Environment file created"); return true; } export async function Updater() { consola.start("Checking for updates..."); exec("git remote get-url origin", async (remoteError, remoteStdout, remoteStderr) => { if (remoteError || remoteStderr) { consola.error("Failed to get local repository URL"); return; } const repo = `https://raw.githubusercontent.com/${remoteStdout.trim().replace("https://github.com/", "").replace(".git", "")}/refs/heads/main/package.json` || "https://raw.githubusercontent.com/TerbiumOS/web-v2/refs/heads/main/package.json"; try { const response = await fetch(repo); const ver = (await response.json()).version; if (ver > version) { const res = await consola.prompt(`A new version of Terbium is available. Would you like to download it? (New Version: ${ver}, Current: ${version})`, { type: "confirm", }); if (res) { consola.info("Downloading new version..."); exec("git pull", async (remoteError, remoteStdout, remoteStderr) => { if (remoteError || remoteStderr) { consola.error("Failed to update Terbium, Please update manually"); open(`${remoteStdout.trim()}/releases/latest`); return; } consola.success("Terbium updated successfully"); await BuildApps(); await CreateAppsPaths(); }); return; } else return; } else { consola.success("Terbium is up to date"); } } catch (e) { consola.error(`Failed to check for updates, ${e}`); } }); return true; } Bootstrap(); ================================================ FILE: docs/README.md ================================================ # Table of Contents 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) - [Creating Terminal Commands](./creating-terminal-commands.md) - [Creating Apps](./creating-apps.md) - [Backend Configuration Options](./backend-configuration.md) - [Anura Compatability & API Support](./anura-compat.md) - [Electron Compatability & API Support](./lemonade-compat.md) - [Introduction to Lemonade](./lemonade.md) - [Terbium API Documentation](./apis/readme.md) - [Static Hosting](./static-hosting.md) - [UPK Builds](./upk-build.md) If you cannot find what your looking for feel free to ask in the Terbium Discord or in the TN Discord ================================================ FILE: docs/anura-compat.md ================================================ # Liquor Compatability Liquor is our Compatability layer for Anura. We named it liquor because its similar to how wine works. If your wondering what version of anura liquor is based off of, its based of Anura v2.1 "Starboy", as of now Liquor mostly targets almost all of Anura v2.1's APIs ## API Support Below we have a small chart with a list of the current supported apis in Liquor | API | Support | Notes | | :--: | :---: | :---: | | anura.fs | Full | - | | anura.registerExternalApp | Full | - | | anura.registerExternalLib | Full | - | | anura.notifacation | Full | - | | anura.x86 | **NO** | Not Implemented | | anura.filePicker | Full | - | | anura.net | Full | - | | anura.URIHandler | Partial | Working but not perfect | | anura.install | Full | - | | anura.wm | Full | - | | anura.config | Full | - | | anura.files | Full | - | | anura.dialog | Full | - | | anura.localfs | Full | - | | anura.platform | Full | - | | anura.process | Partial | Stubbed to work, Not Fully implemented | | anura.ui | Partial | Stubbed to work, Not fully implemented | | anura.filesystem | Full | - | | anura.libs | Full | - | | anura.version | Full | - | | anura.systray | Partial | - | | anura.python | **NO** | Not Implemented (Deprecated) | **⚠️ NOTE** Anura Plugins are **NOT** supported on liquor due to service worker infrastructure differences Liquor also bundles a few things that will be enabled. They are listed below | App Name | Version | Last Updated | | :--: | :---: | :---: | | fsapp.app | 2.0-tb | 3/21/2025 | | libfilepicker.lib | 2.0-tb | 11/14/2024 | | libfileview.lib | 2.0-tb | 11/14/2024 | | libpersist.lib | 2.0 | 8/17/2024 | | anura.bcc | BX2-tb/BX1 (UPK only) | 11/25/2024 | ================================================ FILE: docs/apis/readme.md ================================================ # API Docs **Last Updated**: v2.3.0 - 03/31/2026 So you're looking to use Terbium APIs. Well, you're in the right place! Terbium has a decent amount of components which I will break down below. The pages will include a description of the functions and code examples. ## Table of Contents - [Battery](#battery) - [Launcher](#launcher) - [Theme](#theme) - [Desktop](#desktop) - [Window](#window) - [Context Menu](#contextmenu) - [User](#user) - [Proxy](#proxy) - [Notification](#notification) - [Dialog](#dialog) - [Node](#node) - [Platform](#platform) - [Process](#process) - [Screen](#screen) - [VFS](#vfs) - [System](#system) - [Terbium Cloud (tauth)](#terbium-cloud-tauth) - [Mediaplayer](#mediaplayer) - [File](#file) - [Additional Libraries](#additional-libraries) ### Battery - **showPercentage** - Description: Shows the battery percentage in the system tray. - Returns: `Promise` - Returns "Success" if successful. - Example: ```javascript await tb.battery.showPercentage(); console.log("Battery percentage is now visible"); ``` - **hidePercentage** - Description: Hides the battery percentage in the system tray. - Returns: `Promise` - Returns "Success" if successful. - Example: ```javascript await tb.battery.hidePercentage(); console.log("Battery percentage is now hidden"); ``` - **canUse** - Description: Checks if the Battery Manager API is available on the current browser. - Returns: `Promise` - `true` if available, `false` otherwise. - Example: ```javascript const canUseBattery = await tb.battery.canUse(); if (canUseBattery) { console.log("Battery API is supported"); } ``` ### Launcher - **addApp** - Description: Adds an app to the app launcher. - Parameters: - `props: { name: string, icon: string, src: string, etc }` - Returns: `Promise` - Example: ```javascript const wasadded = await tb.launcher.addApp({ name: "Example App", icon: "/home/icon.png", }); console.log(wasadded) ``` - **removeApp** - Description: Removes an app from the app launcher. - Parameters: - `name: string` - The app name to remove. - Returns: `Promise` - Example: ```js const removed = await tb.launcher.removeApp("exampleapp"); if (removed) { console.log("App removed successfully"); } else { console.log("App not found"); } ``` ### Theme [⚠ Deprecated] > NOTE: The Theme API is deprecated and remains as a stub for legacy applications - **get** - Description: Gets the current theme settings. - Returns: `Promise` - Theme value. - Example: ```javascript const themeSettings = await tb.theme.get(); console.log("Current Theme Settings:", themeSettings); ``` - **set** - Description: Sets the theme settings. - Parameters: - `data: string` - New theme value. - Returns: `Promise` - `true` if successful. - Example: ```javascript await tb.theme.set("#ffffff"); console.log("Theme set successfully"); ``` ### Desktop - **preferences** - **setTheme** - Description: Sets the theme color. - Parameters: - `color: string` - The new theme color. - Example: ```javascript await tb.desktop.preferences.setTheme("#ff0000"); console.log("Theme color set successfully"); ``` - **theme** - Description: Retrieves the current theme color. - Returns: `Promise` - The current theme color. - Example: ```javascript const currentTheme = await tb.desktop.preferences.theme(); console.log("Current theme color:", currentTheme); ``` - **setAccent** - Description: Sets the accent color. - Parameters: - `color: string` - The new accent color. - Example: ```javascript await tb.desktop.preferences.setAccent("#00ff00"); console.log("Accent color set successfully"); ``` - **getAccent** - Description: Gets the accent color. - Example: ```javascript await tb.desktop.preferences.getAccent(); ``` - **wallpaper** - **set** - Description: Sets the wallpaper path. - Parameters: - `path: string` - The file path of the wallpaper image. - Example: ```javascript await tb.desktop.wallpaper.set("/path/to/wallpaper.jpg"); console.log("Wallpaper set successfully"); ``` - **contain** - Description: Sets the wallpaper mode to "contain". - Example: ```javascript await tb.desktop.wallpaper.contain(); console.log("Wallpaper mode set to contain"); ``` - **stretch** - Description: Sets the wallpaper mode to "stretch". - Example: ```javascript await tb.desktop.wallpaper.stretch(); console.log("Wallpaper mode set to stretch"); ``` - **cover** - Description: Sets the wallpaper mode to "cover". - Example: ```javascript await tb.desktop.wallpaper.cover(); console.log("Wallpaper mode set to cover"); ``` - **fillMode** - Description: Retrieves the current wallpaper mode. - Returns: `Promise` - The current wallpaper mode. - Example: ```javascript const currentMode = await tb.desktop.wallpaper.fillMode(); console.log("Current wallpaper mode:", currentMode); ``` - **dock** - **pin** - Description: Pins a new application to the dock. - Parameters: - `app: any` - The application to pin. - Returns: `Promise` - Returns 'Success' if the app was pinned successfully. - Example: ```javascript await tb.desktop.dock.pin({ title: "MyApp", path: "/path/to/myapp" }); console.log("Application pinned successfully"); ``` - **unpin** - Description: Unpins an application from the dock. - Parameters: - `app: string` - The title of the application to unpin. - Returns: `Promise` - Returns 'Success' if the app was unpinned successfully. - Example: ```javascript await tb.desktop.dock.unpin("MyApp"); console.log("Application unpinned successfully"); ``` ### Window - **create** - Description: Creates a new window using window configuration. - Parameters: - `props: any` - Window configuration object. - Example: ```javascript tb.window.create({ title: "My Window", src: "/fs/apps/system/about.tapp/index.html" }); ``` - **close** - Description: closes the active window. - Example: ```javascript tb.window.close() ``` - **minimize** - Description: minimizes the active window. - Example: ```javascript tb.window.minimize() ``` - **maximize** - Description: maximize the active window. - Example: ```javascript tb.window.maximize() ``` - **reload** - Description: refreshes the iframe (if present) in the active window. - Example: ```javascript tb.window.reload() ``` - **changeSrc** - Description: Changes the src of the iframe (if present) in the active window. - Example: ```js tb.window.changeSrc("/fs/apps/system/about.tapp/index.html") ``` - **getId** - Description: Gets the ID of the currently active window. - Returns: `number` - Window ID. - Example: ```javascript const windowId = tb.window.getId(); console.log("Current Window ID:", windowId); ``` - **content** - **get** - Description: Gets the current HTML Content from inside the window - Returns: `Promise` - The HTML Content inside the window. - Example: ```javascript await tb.window.content.get() ``` - **set** - Description: Sets the current HTML Content from inside the window - Example: ```javascript tb.window.content.set(`
hi (put any HTML Content here)
`) ``` - **titlebar** - **setColor** - Description: Sets the fore-color of all the window's titlebars - Example: ```javascript tb.window.titlebar.setColor('#fff') ``` - **setText** - Description: Sets the current window's title - Example: ```javascript tb.window.titlebar.setText('TB Docs') ``` - **setBackgroundColor** - Description: Sets the background-color of all the window's titlebars - Example: ```javascript tb.window.titlebar.setBackgroundColor('#000') ``` - **island** - **addControl** - Description: Adds a control to the TB App Island - Example: ```javascript tb.window.island.addControl({ text: "", appname: "", id: "", click: () => { // Execute code here for when clicked } }) ``` - **removeControl** - Description: Removes a control from the TB App Island - Parameters: - `control_id: string` - The ID used when adding the control. - Example: ```javascript tb.window.island.removeControl("") ``` ### ContextMenu - **create** - Description: Creates a Context Menu at your desired location - Parameters: - `props: { x: number, y: number, options: Array, titlebar?: boolean, iframe?: boolean }` - Context menu properties. - Example: ```javascript tb.contextmenu.create({ x: 0, y: 0, options: [ { text: "Option 1", click: () => console.log("Option 1 clicked") }, { text: "Option 2", click: () => console.log("Option 2 clicked") }, ] }); ``` - **close** - Description: Closes the currently open context menu. - Example: ```javascript tb.contextmenu.close(); ``` ### User - **username** - Description: Fetches the username of the current user. - Returns: `Promise` - User's username. - Example: ```javascript const username = await tb.user.username(); console.log("username:", username); ``` - **pfp** - Description: Fetches the profile picture of the current user. - Returns: `Promise` - URL/Base64 Encoding of the profile picture. - Example: ```javascript const pfp = await tb.user.pfp(); console.log("PFP:", pfp); ``` ### Proxy - **get** - Description: Gets the current proxy settings. - Returns: `Promise` - Proxy settings. - Example: ```javascript const proxySettings = await tb.proxy.get(); console.log("Using:", proxySettings); ``` - **set** - Description: Selects the proxy. - Parameters: - `proxy: string` - New proxy settings. - Returns: `Promise` - `true` if successful. - Example: ```javascript await tb.proxy.set("Ultraviolet"); console.log("Proxy set successfully"); ``` - **updateSWs** - Description: Updates the Transport and Wisp Server of the proxy. - Example: ```javascript await tb.proxy.updateSWs(); console.log("Service Workers updated successfully"); ``` - **encode** - Description: Encodes a URL in the desired format (Only avalible in XOR Currently) - Parameters: - `url: string` - The url to encode - `encoder: string` - The encoder (Only avalible in XOR currently) - Returns: `Promise` - Example: ```javascript await tb.proxy.encode('https://google.com', 'XOR') ``` - **decode** - Description: Decodes a URL in the desired format (Only avalible in XOR Currently) - Parameters: - `url: string` - The url to decode - `decoder: string` - The decoder (Only avalible in XOR currently) - Returns: `Promise` - Example: ```javascript await tb.proxy.decode('https://google.com', 'XOR') ``` ### Notification - **Message [🧪Experimental]** - Description: The notification that has an input field. - Parameters: - `props: { message: string, application: string, iconSrc: string, onOk?: Function, txt?: string, time?: number }` - Notification properties. - Example: ```javascript tb.notification.Message({ message: "test", application: "System", iconSrc: "/assets/img/logo.png", txt: "fieldtext" }); ``` - **Toast** - Description: A simple notification - Parameters: - `props: { message: string, application: string, iconSrc: string, time?: number }` - Notification properties. - Example: ```javascript tb.notification.Toast({ message: "test", application: "System", iconSrc: "/assets/img/logo.png", time: 10000 }); ``` - **Installing** - Description: An installing/progress style notification. If you pass a task Promise (or async function), the notification stays visible until the task finishes, then it automatically shows a completion toast (or failure toast if it throws). - Parameters: - `props: { message: string, application: string, iconSrc: string, time?: number }` - Installing notification properties. - `task?: Promise | (() => Promise)` - Optional async task to track. - `doneToast?: Partial` - Optional completion toast overrides. - `failToast?: Partial` - Optional failure toast overrides. - Returns: `Promise | void` - Returns the task result when a task is passed. - Example: ```javascript await tb.notification.Installing( { message: "Extracting archive...", application: "Files", iconSrc: "/assets/img/logo.png" }, async () => await unzip("pathtoalargezipfolder.zip", "/home/user/documents"), { message: "Archive extracted successfully" }, { message: "Archive extraction failed" } ); ``` ### Dialog - **Alert** - Description: The Alert dialog - Parameters: - `props: { title: string, message: string }` - Alert properties. - Example: ```javascript tb.dialog.Alert({ title: "Alert", message: "This is an alert message." }); ``` - **Message** - Description: Displays a message dialog with specified properties. - Parameters: - `props: { title: string, defaultValue?: string, onOk?: Function, onCancel?: Function }` - Message dialog properties. - Example: ```javascript await tb.dialog.Message({ title: "Example Message", defaultValue: "Default value", onOk: (value) => console.log("OK clicked with value:", value), onCancel: () => console.log("Cancel clicked") }); ``` - **Select** - Description: Lets you select a value from a dropdown - Parameters: - `props: { title: string, message?: string, options: Array<{text: string, value: any}>, onOk?: Function, onCancel?: Function }` - Select dialog properties. - Example: ```javascript await tb.dialog.Select({ title: "Enter the permission level you wish to set", options: [{ text: "Admin", value: "admin" }, { text: "User", value: "user" }, { text: "Group", value: "group" }, { text: "Public", value: "public" }], onOk: async (perm) => { console.log(perm); } }); ``` - **Auth** - Description: TB Permissions Authentication Dialog - Parameters: - `props: { title: string, defaultUsername?: string, onOk?: Function, onCancel?: Function }` - Auth dialog properties. - `options?: { sudo: boolean }` - Additional options to indicate if this is for sudo authentication. - Example: ```javascript await tb.dialog.Auth({ title: "Example Message", defaultUsername: "Default value", onOk: (user, pass) => console.log("User and unhashed pass", user, pass), onCancel: () => console.log("Cancel clicked") }, { sudo: false }); ``` - **Permissions** - Description: Yes or No Dialog - Parameters: - `props: { title: string, message: string, onOk?: Function, onCancel?: Function }` - Permission dialog properties. - Example: ```javascript await tb.dialog.Permissions({ title: "Example Message", message: "Do you want to continue?", onOk: () => console.log("OK clicked"), onCancel: () => console.log("Cancel clicked") }); ``` - **FileBrowser** - Description: Simple FileBrowser Dialog - Parameters: - `props: { title: string, filter?: string, onOk?: Function, onCancel?: Function, local?: boolean }` - FileBrowser dialog properties. - Example: ```javascript await tb.dialog.FileBrowser({ title: "Select a file", filter: ".txt", onOk: (value) => console.log("File selected:", value), }); ``` - **DirectoryBrowser** - Description: Simple Directory Browser Dialog - Parameters: - `props: { title: string, defualtDir?: string, onOk?: Function, onCancel?: Function, local?: boolean }` - DirectoryBrowser dialog properties. - Example: ```javascript await tb.dialog.DirectoryBrowser({ title: "Select a directory", defualtDir: "/home/", onOk: (value) => console.log("Selected Dir:", value), }); ``` - **SaveFile** - Description: Simple File Saving Dialog - Parameters: - `props: { title: string, defualtDir?: string, filename?: string, onOk?: Function, onCancel?: Function, local?: boolean }` - SaveFile dialog properties. - Example: ```javascript await tb.dialog.SaveFile({ title: "Example Title", defualtDir: "/home/", filename: "tbdocs.md", onOk: (value) => console.log("Saved file to:", value) }); ``` - **Cropper** - Description: Image Cropper - Parameters: - `props: { title: string, img: string, onOk?: Function }` - Cropper dialog properties. **Image should be formatted in Base64** - Returns: `Promise` - Resolves image when the dialog is closed - Example: ```javascript await tb.dialog.Cropper({ title: "Example Title", img: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==", onOk: (img) => console.log("new image", img) }); ``` - **WebAuth** - Description: Simple Authentication Dialog (for use in Web Authentication) - Parameters: - `props: { title: string, message?: string, defaultUsername?: string, onOk?: Function, onCancel?: Function }` - Auth dialog properties. > NOTE: Because by default the password is not hashed, please encrypt the password if you plan to store it using `tb.crypto` - Example: ```javascript await tb.dialog.WebAuth({ title: "Example Message", defaultUsername: "Default value", onOk: (user, pass) => console.log("User and unhashed pass", user, pass), onCancel: () => console.log("Cancel clicked") }); ``` ### Node - **webContainer** - Description: The current webContainer instance for the Node Subsystem. Refer to [WebContainers API](https://webcontainers.io/api) for documentation. - Returns: `WebContainer` instance - **servers** - Description: A Map of ports running on the Node Subsystem - Returns: `Map` - Map of port numbers to server URLs - **isReady** - Description: Returns whether or not the WebContainer is booted. - Returns: `boolean` - `true` if ready, `false` otherwise - **start** - Description: Boots the WebContainer - Example: ```javascript tb.node.start(); console.log("WebContainer started"); ``` - **stop** - Description: Stops the WebContainer - Returns: `boolean` - `true` if stopped successfully - Example: ```javascript try { const stopped = tb.node.stop(); console.log("WebContainer stopped"); } catch (err) { console.error("No WebContainer is running"); } ``` ### Platform - **getPlatform** - Description: Gets the current platform the user is using - Returns: `Promise` - Platform ("mobile" or "desktop") - Example: ```javascript const platform = await tb.platform.getPlatform(); console.log(`You're on: ${platform}`); ``` ### Process - **kill** - Description: Kill a process. The argument may be a PID (number or numeric string) or any object that resolves to a process when compared by PID. - Parameters: - `config: string | number | any` - The PID of the process to terminate (or an object containing a `pid` property). - Example: ```javascript // simple kill by PID tb.process.kill(69420); // you can also look up a process then kill it const procs = tb.process.list(); const first = Object.values(procs)[0]; tb.process.kill(first.pid); ``` - **list** - Description: Return the current process table. - Returns: `Record` - a map of PID to process information (see `ProcessInfo` type in `types.ts` for fields such as name, pid, parent, children, status, memory, cpu, etc.) - Example: ```javascript const processes = tb.process.list(); console.log(processes); ``` - **procs** - Description: Public property exposing the live process record. It is equivalent to calling `tb.process.list()` but can be modified directly when spawning new entries. - Example: ```javascript console.log(tb.process.procs); // same as tb.process.list() ``` - **create** - Description: Creates a new process entry. This is primarily used by the runtime when spawning windows or background tasks, but you can call it manually for testing. - Parameters: - `type: "window" | "runtime"` – the kind of process to create. - `config: any` – configuration object describing the process (window size, title, etc.). - Example: ```javascript tb.process.create("runtime", { name: "my-task" }); ``` - **parse** - **build [🧪Experimental]** - Description: Building Process of Custom TML Formatted Apps - Parameters: - `src: string` - Source string to build - Returns: `void` - Example: ```javascript tb.process.parse.build("..."); ``` ### Screen - **captureScreen** - Description: Creates a screenshot of your screen and saves it - Returns: `Promise` > NOTE: The screen capture API is used with the alt+shift keybind. Be aware of that to prevent any conflictions with your application if you use a similar keybind. - Example: ```javascript await tb.screen.captureScreen(); ``` ### 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 - Returns: `string` - Terbium version. - Example: ```javascript const terbiumVersion = tb.system.version(); console.log("Terbium v:", terbiumVersion); ``` - **instance** - **repo** - Description: Lists the repository information - Returns: `string` - Repository information. - Example: ```javascript const repo = tb.system.instance.repo; console.log("The repo is: " + repo); ``` - **hash** - Description: Lists the git commit hash - Returns: `string` - Git hash. - Example: ```javascript const hash = tb.system.instance.hash; console.log("The git hash is: " + hash); ``` - **openApp** - Description: Opens an installed application - Parameters: - `pkg: string` - Package ID of the app. - Example: ```javascript await tb.system.openApp("browser"); ``` - **download** - Description: Download a file from the internet to the File System - Parameters: - `url: string` - URL of the file to download. - `location: string` - Destination path in the file system. - Returns: `Promise` - Example: ```javascript await tb.system.download('https://example.com/example.txt', '/home/exampledownload.txt'); ``` - **exportfs** - Description: Exports the file system as a zip file - Parameters: - `startPath?: string` - Starting path (default: "/") - `filename?: string` - Output filename (default: "tbfs.backup.zip") - Returns: `Promise` - URL of the created zip file - Example: ```javascript await tb.system.exportfs("/home/", "backup.zip"); ``` - **users** - **list** - Description: Lists all users in the system - Returns: `Promise` - Array of usernames - Example: ```javascript const users = await tb.system.users.list(); console.log(users); ``` - **add** - Description: Adds a user to the system - Parameters: - `user: { username: string, password: string, pfp: string, perm: string, securityQuestion?: { question: string, answer: string } }` - User information - Returns: `Promise` - `true` if successful - Example: ```javascript await tb.system.users.add({ username: 'XSTARS', password: 'terbium1234', pfp: 'data:image/png;base64,...', perm: 'Admin' }); ``` - **remove** - Description: Removes a user from the system - Parameters: - `id: string` - Username to remove - Returns: `Promise` - `true` if successful - Example: ```javascript await tb.system.users.remove('XSTARS'); ``` - **update** - Description: Updates the data on a user - Parameters: - `user: { username: string, password?: string, pfp?: string, perm?: string, securityQuestion?: object }` - User information to update - Returns: `Promise` - Example: ```javascript await tb.system.users.update({ username: 'XSTARS', password: 'iloveterbium', pfp: 'data:image/png;base64,...', perm: 'Public' }); ``` - **renameUser** - Description: Renames a user in the system - Parameters: - `olduser: string` - Current username - `newuser: string` - New username - Returns: `Promise` - Example: ```javascript await tb.system.users.renameUser('oldname', 'newname'); ``` - **startup** - **addProc** - Description: Adds a new process to the startup list. This will register a package or command to run on system or user startup. - Parameters: - `pkgorname: string` - The package name or unique identifier for the startup entry. - `target: "System" | "User"` - Whether the startup entry is registered system-wide or for the current user. - `cmd?: string` - Optional command to execute for the entry (if different from the package default). - Returns: `Promise` - Example: ```javascript await tb.system.startup.addProc('my-service', 'System', 'alert("alert evaled")'); ``` - **removeProc** - Description: Removes a previously registered startup process. - Parameters: - `pkgorname: string` - The package name or identifier of the entry to remove. - `target: "System" | "User"` - The scope from which to remove the entry. - Returns: `Promise` - Example: ```javascript await tb.system.startup.removeProc('my-service', 'System'); ``` - **enable** - Description: Enables a registered startup process so it will run at boot for the specified scope. - Parameters: - `pkgorname: string` - The package name or identifier of the entry to enable. - `target: "System" | "User"` - The scope in which to enable the entry. - Returns: `Promise` - Example: ```javascript await tb.system.startup.enable('my-service', 'User'); ``` - **disable** - Description: Disables a registered startup process so it will not run at boot. - Parameters: - `pkgorname: string` - The package name or identifier of the entry to disable. - `target: "System" | "User"` - The scope in which to disable the entry. - Returns: `Promise` - Example: ```javascript await tb.system.startup.disable('my-service', 'User'); ``` - **list** - Description: Lists all configured startup entries. - Returns: `Promise` - An array of startup entries (each entry contains details such as name, target, command, enabled state). - Example: ```javascript const procs = await tb.system.startup.list(); console.log(procs); ``` - **bootmenu** - **addEntry** - Description: Adds a boot entry into the Terbium Boot Menu - Parameters: - `name: string` - The name to display in the boot menu - `file: string` - The file to boot from (file path) - Returns: `Promise` - Example: ```javascript await tb.system.bootmenu.addEntry('Legacy TB', '/legacy-tb/index.html'); ``` - **removeEntry** - Description: Removes a boot entry from the Terbium Boot Menu - Parameters: - `name: string` - The name of the entry to remove - Returns: `Promise` - Example: ```javascript await tb.system.bootmenu.removeEntry('Legacy TB'); ``` ### Terbium Cloud (tauth) - **client** - Description: The authentication client instance for Terbium Cloud services - Returns: `AuthClient` - Authentication client object - **signIn** - Description: Sign in to Terbium Cloud Account - Returns: `Promise` - Sign-in response with user data - Example: ```javascript try { const result = await tb.tauth.signIn(); console.log("Signed in:", result.data.user); } catch (err) { console.error("Sign-in cancelled or failed:", err); } ``` - **signOut** - Description: Sign out from Terbium Cloud Account - Returns: `Promise` - Example: ```javascript await tb.tauth.signOut(); console.log("Signed out successfully"); ``` - **isTACC** - Description: Checks if the current user (or specified user) is a Terbium Cloud Account - Parameters: - `username?: string` - Username to check (defaults to current user) - Returns: `Promise` - `true` if user has TACC, `false` otherwise - Example: ```javascript const hasTACC = await tb.tauth.isTACC(); if (hasTACC) { console.log("User has a Terbium Cloud Account"); } ``` - **updateInfo** - Description: Updates Terbium Cloud Account information - Parameters: - `user: Partial` - User information to update (can include username, pfp, email, password, etc.) - Returns: `Promise` - Example: ```javascript await tb.tauth.updateInfo({ username: "newusername", pfp: "data:image/png;base64,..." }); ``` - **reauth** - Description: Logs back into Terbium Cloud - Returns: `Promise` - Example: ```javascript await tb.tauth.reauth(); ``` - **getInfo** - Description: Gets Terbium Cloud Account information - Parameters: - `username?: string` - Username to get info for (defaults to current user) - Returns: `Promise` - User account information or null if not found - Example: ```javascript const info = await tb.tauth.getInfo(); if (info) { console.log("Account info:", info); } ``` - **sync** - **retreive** - Description: Retrieves synced data from Terbium Cloud (settings, WebDAV servers, etc.) - Returns: `Promise` - Example: ```javascript await tb.tauth.sync.retreive(); console.log("Settings synced from cloud"); ``` - **upload** - Description: Uploads local settings and data to Terbium Cloud - Returns: `Promise` - Example: ```javascript await tb.tauth.sync.upload(); console.log("Settings uploaded to cloud"); ``` - **isSyncing** - Description: Indicates whether a sync operation is currently in progress - Returns: `boolean` - `true` if syncing, `false` otherwise - Example: ```javascript if (tb.tauth.sync.isSyncing) { console.log("Sync in progress..."); } ``` ### Mediaplayer > NOTE: Make sure that the endtime for the music and video island is formatted in seconds and not milliseconds or minutes, that applies to the time parameter (start time) as well. - **music** - Description: Activates the Music optimized Media Island - Parameters: - `props: { artist: string, track_name: string, album?: string, time?: number, background: string, endtime: number, onSeek?: void, onPausePlay: void, onNext?: void; onBack?: void }` - Music player properties. - Example: ```javascript tb.mediaplayer.music({ track_name: "Starboy", artist: "The Weeknd", endtime: 231, background: "https://is1-ssl.mzstatic.com/image/thumb/Music126/v4/02/17/ce/0217ce34-c2b9-3d3d-1dec-586db3948753/23UMGIM22526.rgb.jpg/1200x1200bf-60.jpg" }); ``` - **video** - Description: Activates the Video optimized Media Island - Parameters: - `props: { creator: string, video_name: string, time?: number, background: string, endtime: number, onSeek?: void, onPausePlay: void, onNext?: void; onBack?: void }` - Video player properties. - Example: ```javascript tb.mediaplayer.video({ video_name: "The school smp one year later...", creator: "Playingallday383", endtime: 1273, background: "https://i.ytimg.com/vi/kiKmSq4gxNU/hqdefault.jpg" }); ``` - **hide** - Description: Hides the media island. - Example: ```javascript tb.mediaplayer.hide(); ``` - **pauseplay** - Description: Pauses or plays the content connected to the media island - Example: ```javascript tb.mediaplayer.pauseplay(); ``` - **isExisting** - Description: Tells you if the media island is already present or not. - Returns: `Promise` - `true` if media island exists, `false` otherwise - Example: ```javascript const exists = await tb.mediaplayer.isExisting(); if (exists) { console.log('A media island is already there'); } ``` ### File - **handler** - **openFile** - Description: Opens a file with the associated app based on file type. - Parameters: - `path: string` - Path of the file. - `type: string` - Type of the file (e.g., "text", "image", "video", "audio", "pdf", "webpage"). - Returns: `Promise` - Example: ```javascript await tb.file.handler.openFile("/home/example.txt", "text"); ``` - **addHandler** - Description: Adds a handler for a specific file extension - Parameters: - `app: string` - App name to handle the file type - `ext: string` - File extension - Returns: `Promise` - Returns `true` if succeeded - Example: ```javascript await tb.file.handler.addHandler("ruffle", "swf"); ``` - **removeHandler** - Description: Removes a handler for a specific file extension - Parameters: - `ext: string` - File extension - Returns: `Promise` - Returns `true` if succeeded - Example: ```javascript await tb.file.handler.removeHandler("swf"); ``` - **icons** - **get** - Description: Gets icon path for a file extension. - Parameters: - `ext: string` - File extension. - Returns: `Promise` - Icon path. - Example: ```javascript const icon = await tb.file.icons.get("png"); console.log(icon); ``` - **set** - Description: Sets icon path for a file extension. - Parameters: - `ext: string` - File extension. - `iconPath: string` - Path to icon. - Returns: `Promise` - Example: ```javascript await tb.file.icons.set("log", "/assets/img/file-log.png"); ``` - **remove** - Description: Removes custom icon mapping for a file extension. - Parameters: - `ext: string` - File extension. - Returns: `Promise` - Example: ```javascript await tb.file.icons.remove("log"); ``` ### Additional Libraries - **[libcurl](https://www.npmjs.com/package/libcurl.js)** - Description: The libcurl networking API, used in Anura.net, TB Apps and tb.system.download - **[fflate](https://www.npmjs.com/package/fflate)** - Description: ZIP compression/decompression tool for Anura File Manager and TB Files App - **fs** - Description: File system API (TFS) for reading/writing files - **crypto** - Description: Password encryption tool - Parameters: - `pass: string` - Password to encrypt - `file?: string` - (optional) File to save the password to - Returns: `Promise` - Encrypted password or "Complete" if saved to file - **vfs** - Description: Virtual File System for WebDAV servers and remote storage - **buffer** - Description: Buffer utility (from Filer) for working with binary data - **registry** - Description: System registry for storing and retrieving system-wide configuration - **sh** - Description: Shell interface for file system operations - **liquor (Anura)** - Description: Anura subsystem stub, provides compatibility with Anura applications - **lemonade (Electron)** - Description: Electron API compatibility layer for desktop-like features Have fun developing for Terbium! ================================================ FILE: docs/backend-configuration.md ================================================ # Backend Configuration Options Terbium's backend is pretty cool and is configurable with the .env file in the root of this directory, alternatively during the setup if your .env file does not exist it will walk you through setting it up. Now with this being said theres a couple of things that could be confusing, so heres a rundown of the configurations: - MASQR: This enables [MASQR](https://github.com/titaniumnetwork-dev/masqr-project) (A Anti Link Leaking System) - License Server Url: This is another MASQR Configuration that is the License Server Url where MASQR communicated and validates keys with. If you dont have masqr enabled you do not need to change it. - Whitelisted Domains: This is another MASQR Configuration that is the domains MASQR will not apply to. Also in the backend, is [WispJS](https://github.com/MercuryWorkshop/wisp-client-js/tree/rewrite) pointing to /wisp/, please note that by default on all terbium instances the DNS Servers 1.1.1.3 and 1.0.0.3. ================================================ FILE: docs/contributions.md ================================================ # How to Contribute to Terbium v2 Table of Contents - [Understanding the File Structure](#understanding-the-file-structure) - [Learning What should and shouldn't be touched](#learning-what-should-and-shouldnt-be-touched) ## Understanding the File Structure Terbium v2 for the most part is written in [React](https://react.dev). If you haven't already make sure you have all the dependencies installed which can be done via `pnpm i` (or the package manager of your choice). Terbium has 3 Folders that you should pay attention to and that are referenced throughout Terbium's Code. - src: The location of the webOS's frontend code & apis Such as the `login`, `desktop`, `gui components` etc - sys: The location of the APIs and GUI components/styles - gui: The location of GUI Components - styles: Stylesheets for all the components - liquor: The location of all liquor related components and APIs - apis: The location of the API's code - public: The static folder for normal html, js, css components such as Default Applications, Libraries, and Anura Applications & Service Workers ## Learning What should and shouldn't be touched For the most part this is self explanatory. Unless you absolutely know what your doing **DO NOT** mess with anything in the `sys` folder or any typescript configuration files as they are system critical and will break things if you modify them without knowing what you're doing. If you do however know what you're doing and wish to expand upon the current functionality feel free to poke around in the `sys`. Modifying apps in the `public` folder is fine and shouldn't break anything since it is not system dependent except for in the OOBE when it copies assets from there to the file system. If you wish to add Terminal Commands refer to [Creating Terminal Commands](./creating-terminal-commands.md) ================================================ FILE: docs/creating-apps.md ================================================ # Creating new applications Table of Contents: - [Introduction](#introduction) - [Using TB Features](#using-tb-features) - [Island Controls](#island-controls) - [WM Controls](#wm-controls) - [Generating Manifests](#generating-manifests) - [Adding your application to the app launcher](#adding-your-application-to-the-app-launcher) - [Creating and Submiting your Application to the app store repo](#creating-new-applications) - [Formatting PWAs](#formatting-pwas) - [Formatting TAPPs](#formatting-tapps) - [Formatting Anura Apps](#formatting-anura-apps) ## Introduction Creating a Terbium Application is really easy to do. First you need to decide wither or not you want your app to be a PWA or a TAPP. Once you have decided you can follow the steps bellow. ## Using TB Features Terbium v2 has Introduced many changes to the WM and General Window Functionality compared to ["Legacy Terbium"](https://github.com/terbiumos/webOS). Some of these new API Implementations for the WM that you want to use are as follows: ```json title: { text: "", }, icon: "", src: "", native: true, size: { width: 900, height: 650, }, single: false, resizable: true, snapable: false, ``` - title: The Title of the Application - if you want to customize the weight follow the title example from above, if not just simply make the `title` field a string - icon: The Applications Icon - src: The Applications Main Page (Commonly index.html or an internet url) - native: Weither or not the application uses a custom UI or a normal ui - size: The Size of the applications window - width: The Width of the Application - height: The Height of the Application - minWidth: The Minimum Width of the Application - minHeight: The Minimum Height of the Application - single: Determiens if the Application allows you to open multiple windows or not - resizable: Determines if the Application allows you to resize it - snapable: Toggle for allowing window snapping - minimizable: Toggle for allowing window to be minimized - maximizable: Toggle for allowing the window to be maximized - closable: Toggle for allowing the window to be closed - controls: An array of which buttons are allowed to be on the window > A few new API's to be aware of: > - parent.window.tb.window.create({`wmargs`}): Allows you to create a Window in JS. Fill in `wmargs` with the Arguments you made above > - parent.window.tb.file.handler.openFile(fileItem.getAttribute("path"), ""): Allows you to open a File in its respective app easily > - For API's in more in debpt visit the [API Docs](./apis/readme.md) ### Island Controls The App Island allows you to have custom items in the left corner where it says the App Title. To set it up you will be needing the app id and you will need to create a JS Script in your application to have the following JS Code: ```js const tb = parent.window.tb const tb_island = tb.window.island; const tb_window = tb.window; const tb_context_menu = tb.context_menu; tb_island.addControl({ text: "", app_id: "com.tb.", id: "", click: () => { // Execute code here for when clicked } }) ``` If you wish to use a context menu instead you can use this code instead ```js const tb = parent.window.tb const tb_island = tb.window.island; const tb_window = tb.window; const tb_context_menu = tb.context_menu; const tb_dialog = tb.dialog; tb_island.addControl({ text: "", app_id: "com.tb.", id: "", click: () => { const ctx = document.createElement("div"); ctx.classList.add("context-menu", "fade-in"); if(parent.document.querySelector(".context-menu")) { parent.document.querySelector(".context-menu").remove(); } ctx.id = "_ctx"; ctx.style.left = `6px`; ctx.style.top = parent.document.querySelector(".app_island").clientHeight + 12 + "px"; let isTrash = document.querySelector(".exp").getAttribute("path") === "/home/trash" ? true : false; const options = [ { text: "", click: () => { // Execute code here for when clicked } } ] options.forEach(option => { if(option === null) return; const btn = document.createElement("button"); btn.classList.add("context-menu-button"); btn.innerText = option.text; btn.onclick = option.click; ctx.appendChild(btn); }) parent.document.body.appendChild(ctx); parent.window.addEventListener("click", (e) => { if (!e.target.classList.contains("app_control")) { if(parent.document.querySelector(".context-menu")) { parent.document.querySelector(".context-menu").classList.add("fade-out"); setTimeout(() => { if(parent.document.querySelector(".context-menu")) parent.document.querySelector(".context-menu").remove(); }, 150); } } }); } }) parent.document.querySelector(`[control-id="files-et"]`).classList.add("hidden"); ``` What you can implement in the click is tottally up to you and you can do things like open a new window, alert something, etc ### WM Controls The WM Controls for the title bar are the only ones around right now. Those can be added by adding the following to the WM args: ```json title: { text: "", weight: , html: `` }, ``` ## Generating Manifests To generate a TAPP Manifest for your TAPP Package you can use the TB Dev SDK 24 App avalible in the XSTARS XTRAS Repo and either use the manifest tool in the app gui or use it via the command line with tbsdk --gen-manifest --{args} ## Adding your Application to the App Launcher > NOTE: If you are going to submit this app to the app store ignore this step and follow the app store specific steps Adding your Application to the app launcher is pretty easy to do. If you want it to be installed on your site itself add the entry to the array in `/src/init/index.ts` or you can do the same thing within your terbium window itself using the api `tb.launcher.addApp({propshere})` or by editing the file: `/system/var/terbium/start.json` ```json { title: "Calculator", icon: "/apps/calculator.tapp/icon.svg", src: "/apps/calculator.tapp/index.html", snapable: false, maximizable: false, size: { width: 338, height: 556 }, }, ``` Once you fill and insert that you should notice your app has appeared in your launcher! ## Creating and Submiting your Application to the app store repo > Make sure you forked the repository: https://github.com/TerbiumOS/app-repo so you can make changes To submit your repo follow the instructions below and make sure to follow our basic guidelines for your app to get accepted: ## Formatting PWAs Pull Requests for apps are always viewed and heres some basic guidelines for your app to get accepted - App is in the repo in the folder `assets/com.tb.{appname}` - Icon should be defined in the `assets/com.tb.{appname}/icon.[ext]` - WM Arguments and Metadata is in the `apps.json` file in this repo - Example ```json { "name": "YouTube", "icon": "https://raw.githubusercontent.com/TerbiumOS/app-repo/main/assets/com.tb.youtube/icon.png", "description": "Share your videos with friends, family, and the world.", "authors": ["Google"], "pkg-name": "youtube", "images": [ "https://raw.githubusercontent.com/TerbiumOS/app-repo/main/assets/com.tb.youtube/images/1.png" ], "wmArgs": { "title": { "text": "YouTube", "weight": 600 }, "icon": "https://raw.githubusercontent.com/TerbiumOS/app-repo/main/assets/com.tb.youtube/icon.png", "src": "https://youtube.com", "size": { "width": 600, "height": 400 }, "single": true, "resizable": true } } ``` ## Formatting TAPP's - The easiest way to creat TAPP's is to download the TB Dev SDK 2025 from the XSTARZ XTRAS repo and use the feilds to generate your TAPP and Manifest. - The app should be put into the assets folder with the naming scheme: {appname}.TAPP.zip or com.tb.{appname}.TAPP.zip - Also you can put into the app repo manifest where you want to download the TAPP from if you want to host it somewhere else for some reason, Image assets can still be stored in a folder in the assets folder as long as it follows the naming scheme of com.tb.{appname} - Example ```json { "name": "About Proxy", "icon": "https://aboutproxy.pages.dev/aboutbrowser/darkfavi.png", "description": "Chrome for your browser", "images": [ "https://raw.githubusercontent.com/TerbiumOS/app-repo/main/assets/com.tb.youtube/images/1.png" ], "authors": ["r58playz"], "pkg-name": "aboutproxy", "pkg-download": "https://tbapps.pages.dev/assets/aboutproxy.TAPP.zip" }, ``` ## Formatting Anura Apps - Terbium app repos also support having Anura Apps however they must be formatted like so: - You can store the app in the assets folder as a regular zip file - Example ```json { "name": "Snae Player", "icon": "https://raw.githubusercontent.com/MercuryWorkshop/anura-repo/master/apps/anura.music/icon.png", "description": "A music client ported to Anura", "authors": ["Mercury Workshop"], "pkg-name": "snaeplayer", "anura-pkg": "https://raw.githubusercontent.com/MercuryWorkshop/anura-repo/master/apps/anura.music/app.zip" } ``` ================================================ FILE: docs/creating-terminal-commands.md ================================================ # Creating Terminal Commands **Last Updated**: TSH-v2.3 - 02/11/2026 Welcome — creating commands for the Terbium Terminal is simple and flexible. This document is refreshed to cover the new APIs, interactive behavior (passthrough), built-in commands, and best practices. ## Where to put your command Place your script in the Terminal app's `scripts` folder. From the root (`//`) open `fs/apps/system/terminal.tapp/scripts/` (or `fs/apps/user//terminal/`) and add your `.js` file. The Terminal executes the script's code directly and expects your file to register a callable function (typical pattern: define and then call a function that accepts `args`). ## Minimal example ```js function hello(args) { displayOutput(`Hi, how are you ${args[0] || 'stranger'}!`); createNewCommandInput(); // show a new prompt } hello(args); ``` Run: `hello Alice` → prints `Hi, how are you Alice!` and then returns a prompt. ## Script API (what your script can call) When your script runs inside an active Terminal session, it receives the following helper functions and objects (passed as parameters): - `args` — Array of raw arguments (strings) from the command line (e.g. for `foo a b` args = [`"a"`,`"b"`]). - `displayOutput(message, ...styles)` — Print message to the terminal. Supports `%c` style placeholders with CSS-style strings (`'color: #ff0000'`). - `displayError(message)` — Print an error message (red, prefixed with `ERR:`). - `createNewCommandInput()` — Show a new prompt (call when your command is complete). Note: this is debounced and will no-op during interactive passthrough mode (see below). - `term` — The raw xterm instance (advanced use only). - `path` — Current working path string. - `terbium` or `tb` — The [Terbium API](./apis/readme.md) - `buffer` — Internal buffer object (rarely needed). - `setTabTitle(title)` — (Available when running inside a session) Change the tab label for this session (e.g. `setTabTitle('Node: JSH')`). - `exitPassthrough()` — (Passed to interactive command scripts) Use this to explicitly end passthrough mode when your script finishes or the spawned interactive child exits. Important: when running scripts from outside a session (no active session), fewer helpers are available (for example `setTabTitle` may *not* be provided). Write scripts defensively and check for the existence of optional helpers if you need to support both contexts. ## Interactive / passthrough mode Some commands (for example `node` and `nano`) are interactive shells that echo input and control terminal state. The Terminal will automatically: - Enter **passthrough** mode when it detects common interactive commands. In passthrough mode: - The engine stops processing typed commands itself. - Local echo is disabled (the interactive program will echo input itself). - `createNewCommandInput()` is intentionally ignored while passthrough is active (to avoid double prompts). - Exit passthrough when the interactive program prints an exit message (or when your script calls `exitPassthrough()` explicitly). If you need to spawn an interactive process from a script, call `exitPassthrough()` after the process exits so the engine will restore the prompt. ## Best practices - Always call `createNewCommandInput()` when your command finishes to show the prompt (unless your command intentionally stays interactive). - Avoid calling `createNewCommandInput()` multiple times in quick succession — the engine debounces prompt creation (short window) to prevent double prompts. - Use `displayError()` for errors so output is styled consistently. - Use `setTabTitle()` to reflect session state (e.g., `setTabTitle('Node: JSH')` while running a REPL). - Check for optional helpers if you expect your script to run both inside and outside a session. ## Example: setting tab title and using passthrough ```js // This example is a pattern — actual interactive processes depend on your environment async function nodeWrapper(args) { setTabTitle && setTabTitle('Node: JSH'); displayOutput('Starting Node.js...'); // If your script starts an interactive child, you can rely on the engine's passthrough, // or call exitPassthrough() explicitly when the child finishes: // (pseudo-code) // await spawnNode(args).finally(() => exitPassthrough && exitPassthrough()); } nodeWrapper(args); ``` ## Troubleshooting - If you see duplicated input while in a REPL, it's usually because both the engine and the program are echoing — ensure passthrough is active for interactive programs (engine handles common shells automatically). - If the prompt appears twice, the engine now debounces `createNewCommandInput()` calls; check long-running scripts that might call it twice and avoid redundant calls. If you'd like, I can add a small script template generator under `scripts/` to scaffold correct patterns (including usage of `setTabTitle()` and `exitPassthrough()`), and a short test harness that runs a few sample scripts to verify behavior. Happy scripting! 🚀 ================================================ FILE: docs/lemonade-compat.md ================================================ # Lemonade Compatability Lemonade is our compatability layer for Electron, Its named Lemonade because of inspiration of how Nintendo Emulators are named. The current specifications of Lemonade are up to date with the current version of electron (36.4.0) ## API Support Below we have a small chart with a list of the current supported apis in Lemonaed | API | Support | Notes | | :--: | :---: | :---: | | BrowserWindow | Full | Works well enough for stable use | | Notification | Full | Works well enough for stable use | | Net | Full | Works well enough for stable use | | Dialog | Full | Works well enough for stable use | More apis will be added in the future just like liquor but Lemonade currently provides drop in support for the most common Electron API's ================================================ FILE: docs/lemonade.md ================================================ # # Introduction to Lemonade Lemonade provides an Electron-compatible API layer for web-based applications, allowing Electron apps to run in a browser environment with minimal modifications. ## Overview Lemonade mimics Electron's API structure and provides browser-based implementations of common Electron modules. This allows developers to write code that works in both Electron and web environments. ## Supported Modules ### Core Modules #### `app` - Application Lifecycle - `app.getName()` / `app.setName()` - Get/set application name - `app.getVersion()` - Get application version - `app.getPath(name)` - Get special directories (home, appData, userData, temp, downloads, documents, desktop) - `app.isReady()` / `app.whenReady()` - Check application ready state - `app.quit()` / `app.exit()` - Exit application - `app.relaunch()` - Reload the application - `app.getLocale()` - Get user locale #### `BrowserWindow` - Window Management - Constructor with options (width, height, title, icon, resizable, etc.) - `loadURL(url)` - Load a URL (with proxy support) - `loadFile(path)` - Load a local file - `show()` / `hide()` - Show/hide window - `minimize()` / `maximize()` - Minimize/maximize window - `close()` / `destroy()` - Close window - `setTitle(title)` / `getTitle()` - Window title - `setSize(width, height)` / `getSize()` - Window size - `setPosition(x, y)` / `getPosition()` - Window position - `center()` - Center window on screen - `setFullScreen(flag)` / `isFullScreen()` - Fullscreen mode - Event listeners: `on()`, `once()`, `removeListener()` - Events: `close`, `closed`, `show`, `hide`, `focus`, `blur`, `maximize`, `minimize` #### `dialog` - Native Dialogs - `showOpenDialog(options)` - File/directory picker - `showSaveDialog(options)` - Save file dialog - `showMessageBox(options)` - Message box with buttons - `showErrorBox(title, content)` - Error alert #### `shell` - Desktop Integration - `openExternal(url)` - Open URL in default browser - `openPath(path)` - Open file/directory - `showItemInFolder(path)` - Show file in folder - `moveItemToTrash(path)` - Move to trash - `beep()` - Play system beep sound #### `clipboard` - Clipboard Access - `readText()` / `writeText(text)` - Text operations - `readHTML()` / `writeHTML(html)` - HTML operations - `readImage()` / `writeImage(image)` - Image operations - `clear()` - Clear clipboard - Supports async clipboard API #### `ipcRenderer` / `ipcMain` - Inter-Process Communication - `send(channel, ...args)` - Send message - `invoke(channel, ...args)` - Request/response pattern - `on(channel, listener)` - Listen for messages - `once(channel, listener)` - Listen once - `removeListener(channel, listener)` - Remove listener #### `net` - Network Requests - `request(url, options)` - Make HTTP request - `fetch(url, options)` - Fetch API wrapper - `isOnline()` - Check online status - Supports timeout and abort signals #### `screen` - Display Information - `getPrimaryDisplay()` - Get primary display info - `getAllDisplays()` - Get all displays - `getDisplayNearestPoint(point)` - Find display at point - Display info includes: bounds, workArea, size, scaleFactor, rotation #### `process` - Process Information & Node.js Execution - `process.platform` - OS platform (always "linux" in WebContainer) - `process.arch` - Architecture (x64) - `process.versions` - Version information (Node.js 18.x) - `process.env` - Environment variables - `process.argv` - Command line arguments - `process.cwd()` - Current working directory - `process.uptime()` - Process uptime - `process.memoryUsage()` - Memory statistics - `process.isNodeAvailable` - Check if WebContainer Node.js is ready - **`process.exec(command, args, options)`** - Execute Node.js command via WebContainer - **`process.spawn(command, args, options)`** - Spawn Node.js process via WebContainer - **`process.runScript(scriptPath, args)`** - Run a Node.js script file - **`process.evalNode(code)`** - Evaluate JavaScript in Node.js context - `process.kill(pid)` - Kill a process by PID **Note:** This module integrates with Terbium's WebContainer (`tb.node`) to provide real Node.js execution instead of simulation. #### `Notification` - System Notifications - Constructor with options (title, subtitle, body, icon) - Event handling with `on()` / `off()` - `show()` / `close()` - Display notification ## Usage Examples ### Basic Window Creation ```typescript import { BrowserWindow } from './sys/lemonade'; const win = new BrowserWindow({ width: 800, height: 600, title: 'My App', resizable: true, }); win.loadURL('https://example.com'); win.on('close', () => { console.log('Window closing'); }); ``` ### Dialog Usage ```typescript import { dialog } from './sys/lemonade'; const result = await dialog.showOpenDialog({ title: 'Select File', properties: ['openFile'], }); console.log('Selected:', result); ``` ### IPC Communication ```typescript import { ipcRenderer } from './sys/lemonade'; // Send message ipcRenderer.send('message-channel', 'Hello'); // Listen for response ipcRenderer.on('response-channel', (event, data) => { console.log('Received:', data); }); // Request/response pattern const result = await ipcRenderer.invoke('get-data', params); ``` ### Clipboard Operations ```typescript import { clipboard } from './sys/lemonade'; // Write text await clipboard.writeText('Hello World'); // Read text const text = await clipboard.readText(); // Write HTML await clipboard.writeHTML('Bold text'); ``` ### Application Paths ```typescript import { app } from './sys/lemonade'; const homePath = app.getPath('home'); const appDataPath = app.getPath('appData'); const userDataPath = app.getPath('userData'); ``` ### Node.js Execution with WebContainer ```typescript import { process } from './sys/lemonade'; // Check if Node.js is available if (process.isNodeAvailable) { // Execute a Node.js command const exitCode = await process.exec('node', ['--version']); // Run a script file from Terbium's filesystem await process.runScript('/home/user/script.js', ['arg1', 'arg2']); // Evaluate Node.js code directly const output = await process.evalNode('console.log(process.version)'); // Spawn a long-running process const proc = await process.spawn('node', ['server.js']); proc.output.pipeTo(new WritableStream({ write(chunk) { console.log(chunk); } })); // Install npm packages await process.exec('npm', ['install', 'express']); } ``` ### Shell Integration ```typescript import { shell } from './sys/lemonade'; // Open URL await shell.openExternal('https://example.com'); // Show file shell.showItemInFolder('/path/to/file.txt'); // Move to trash await shell.moveItemToTrash('/path/to/file.txt'); ``` ## Architecture Lemonade wraps the underlying `window.tb` API (TerbiumOS API) and provides Electron-compatible interfaces. When an Electron method is called, Lemonade translates it to the appropriate `window.tb` call. ### Key Adapters - **Window Management**: Maps to `window.tb.window.*` - **File System**: Maps to `window.tb.fs.*` - **Dialogs**: Maps to `window.tb.dialog.*` - **Notifications**: Maps to `window.tb.notification.*` - **Network**: Maps to `window.tb.libcurl.fetch` - **Proxy**: Handles URL proxying via `window.tb.proxy.encode` - **Node.js**: Uses `window.tb.node.webContainer` for real Node.js execution via WebContainer ## Limitations Since Lemonade runs in a web environment, some Electron features have limitations: Terbium's virtual file system 2. **Native Menus**: Not fully implemented 3. **System Tray**: Not available in web 4. **Native Notifications**: Uses Terbium's notification system 5. **Process Control**: Some methods are simulated (can't exit browser) 6. **Multiple Windows**: Limited support via Terbium's window manager 7. **Synchronous APIs**: Some sync methods are async 8. **Node.js**: Requires WebContainer to be initialized (`tb.node.isReady === true`) 7. **Synchronous APIs**: Some sync methods are async ## Future Enhancements Potential additions: - Menu/MenuItem support - Tray icons (where possible) - PowerMonitor - Protocol handling - Content tracing - Crash reporter - Native image handling - Web contents manipulation - Session management - Cookies API - Download manager ## Compatibility Lemonade aims to maintain API compatibility with Electron 18+. Not all features are available due to browser limitations, but the API surface matches Electron where possible. ## Development To extend Lemonade: 1. Add new module in `src/sys/lemonade/modulename.ts` 2. Export from `src/sys/lemonade/index.ts` 3. Implement Electron-compatible API 4. Map to `window.tb` equivalents 5. Document usage and limitations ================================================ FILE: docs/static-hosting.md ================================================ # Static Hosting Terbium For this tutorial, Cloudflare pages will be used however the instructions will be similar on other static hosts. ### Step 1. Fork this repository and connect your github account to the static host of your choise. > NOTE: On Cloudflare pages, Terbium is automatically configured to use Node 20. If your using a different host check with them that you are using Node 20 or later as Terbium **WILL NOT** build on older versions. ### Step 2. Under the `build` section command put: `npm i; npm run build-static` **LEAVE THE START COMMAND BLANK IF IT EXISTS** Then under the output directory put the folder: `dist` and click Deploy ### Step 3. (Optional) Now that the sites deployed, you have probably noticed that the Default wisp server wont be running since your static hosting. If you wish to change this navigate to `sys/init/index.ts` scroll down to Line 41 and replace the line: `${location.protocol.replace("http", "ws")}//${location.hostname}:${location.port}/wisp/` with the wisp server of your choice as a string. If you dont want to do this you dont have to as you can change it in the OOBE. ================================================ FILE: docs/upk-build.md ================================================ # UPK Building By default, if you fork the Terbium v2 repo on github included will be a workflow that will automatically upload the latest UPK release to either the latest github release (if applicable) or to a new github tag However if you want to build it yourself on your local machine you can you just need the following dependencies: - NodeJS 22 or later - Python 3.12 or later First, download [UPK Tools](https://cdn.terbiumon.top/upk-tools.zip) from the terbium cdn: **⚠️ NOTE** Be sure to check the source you are downloading the UPK Tools from. If your downloading it form a source that is **NOT** hosted under `terbiumon.top` please make sure its not malicious content. TerbiumOS Developement is not responsible for any damage caused by fradulent downloads in non-official repositories Next, extract the zip file in your terbium instance and run either upk-all.ps1 if your on windows or upk-all.sh if your on any other unix system This file will automatically install all the needed tools and nescessities for the UPK Build and compile it to a zip file named `terbium-upk.app.zip` which you can open in any anura instance and install Terbium on it ================================================ FILE: env.d.ts ================================================ declare namespace NodeJS { interface ProcessEnv { port: number; masqr: boolean; licensingURL: string | any; whitelistedDomains: string[]; } } ================================================ FILE: eslint.config.js ================================================ import js from "@eslint/js"; import globals from "globals"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; import tseslint from "typescript-eslint"; export default tseslint.config( { ignores: ["dist", "public"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { "react-hooks": reactHooks, "react-refresh": reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-explicit-any": "off", "no-var": "off", "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], }, }, ); ================================================ FILE: fail.html ================================================ Welcome to nginx!

Welcome to nginx!

If you see this page, the nginx web server is successfully installed and working. Further configuration is required. If you are expecting another page, please check your network or Refresh this page

For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.

Thank you for using nginx.

================================================ FILE: index.html ================================================ Terbium
================================================ FILE: package.json ================================================ { "name": "tb-v2", "private": true, "version": "2.3.0", "type": "module", "scripts": { "start": "npm run build && tsx bootstrap.ts", "start:nobuild": "tsx bootstrap.ts", "build-apps-json": "tsx bootstrap.ts --apps-only", "build-static": "touch .env && tsx bootstrap.ts --apps-only && vite build", "dev": "tsx bootstrap.ts --dev && vite dev", "build": "tsx bootstrap.ts --apps-only && vite build", "lint": "eslint .", "vite": "vite", "preview": "vite preview", "check": "biome check . --write", "fmt": "biome format --write ." }, "dependencies": { "@heroicons/react": "^2.2.0", "@hono/node-server": "^1.19.12", "@mercuryworkshop/bare-mux": "^2.1.8", "@mercuryworkshop/epoxy-transport": "^2.1.28", "@mercuryworkshop/libcurl-transport": "^1.5.2", "@mercuryworkshop/scramjet": "https://cdn.terbiumon.top/scramjet-v2.0-alpha.tgz", "@mercuryworkshop/wisp-js": "^0.4.1", "@paralleldrive/cuid2": "^3.3.0", "@terbiumos/tfs": "1.0.22", "@titaniumnetwork-dev/ultraviolet": "^3.2.10", "@webcontainer/api": "^1.6.1", "better-auth": "^1.5.6", "compressorjs": "^1.2.1", "cropperjs": "1.6.2", "crypto-js": "^4.2.0", "dotenv": "^17.3.1", "fflate": "^0.8.2", "hono": "^4.12.9", "htmlparser2": "^10.1.0", "libcurl.js": "^0.7.4", "modern-screenshot": "^4.6.8", "path-browserify": "^1.0.1", "react": "^19.2.4", "react-dom": "^19.2.4", "zustand": "5.0.12" }, "devDependencies": { "@biomejs/biome": "2.4.10", "@eslint/js": "^10.0.1", "@tailwindcss/postcss": "^4.2.2", "@types/adm-zip": "^0.5.8", "@types/node": "^25.3.3", "@types/path-browserify": "^1.0.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react-swc": "^4.3.0", "adm-zip": "^0.5.17", "autoprefixer": "^10.4.27", "consola": "^3.4.2", "eslint": "^10.1.0", "eslint-plugin-react-hooks": "7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", "open": "^11.0.0", "postcss": "^8.5.8", "tailwindcss": "^4.2.2", "tsx": "^4.21.0", "typescript": "^6.0.2", "typescript-eslint": "^8.58.0", "vite": "^8.0.3", "vite-plugin-static-copy": "^3.4.0" }, "pnpm": { "onlyBuiltDependencies": [ "@prisma/engines", "@swc/core", "@tailwindcss/oxide", "bufferutil", "esbuild", "prisma" ], "ignoredBuiltDependencies": [ "@mercuryworkshop/scramjet" ] }, "engines": { "node": ">=20.19.0" } } ================================================ FILE: postcss.config.js ================================================ export default { plugins: { "@tailwindcss/postcss": {}, autoprefixer: {}, }, }; ================================================ FILE: public/anura-sw.js ================================================ /* global workbox */ /** @type {import('@terbiumos/tfs').TFS} */ // was a workaround for a firefox quirk where crossOriginIsolated // is not reported properly in a service worker, now its just assumed for // compatibility with UV Object.defineProperty(globalThis, "crossOriginIsolated", { value: true, writable: false, }); // Not recommended but a bypass for libs that expect window to exist self.window = self; // Due to anura's filesystem only being available once an anura instance is running, // we need a temporary filesystem to store files that are requested for caching. // As the anura filesystem is a wrapper around Filer, we can use default Filer here. importScripts("/assets/libs/filer.min.js"); importScripts("/tfs/tfs.js"); // Importing mime importScripts("/assets/libs/mime.iife.js"); // Download handler importScripts("/assets/libs/dl-handler.min.js"); // self.fs = new Filer.FileSystem({ // name: "anura-mainContext", // provider: new Filer.FileSystem.providers.IndexedDB(), // }); const filerfs = new Filer.FileSystem({ name: "anura-mainContext", provider: new Filer.FileSystem.providers.IndexedDB(), }); const filersh = new filerfs.Shell(); (async () => { const handle = await navigator.storage.getDirectory(); window.tfs = new window.tfs(handle); self.opfs = window.tfs.fs; self.opfssh = window.tfs.sh; })(); async function currentFs() { // isConnected will return true if the anura instance is running, and otherwise infinitely wait. // it will never return false, but it may hang indefinitely if the anura instance is not running. // here, we race the isConnected promise with a timeout to prevent hanging indefinitely. if (!self.isConnected) { // An anura instance has not been started yet to populate the isConnected promise. // We automatically know that the filesystem is not connected. return { fs: self.opfs || filerfs, sh: self.opfssh || filersh, }; } const CONN_TIMEOUT = 1000; const winner = await Promise.race([ new Promise(resolve => setTimeout(() => { resolve({ fs: self.opfs || filerfs, sh: self.opfssh || filersh, fallback: true, }); }, CONN_TIMEOUT), ), self.isConnected.then(() => ({ fs: self.anurafs, sh: self.anurash, })), ]); if (winner.fallback) { console.debug("Falling back to Filer"); // unset isConnected so that we don't hold up future requests self.isConnected = undefined; } return winner; } self.Buffer = Filer.Buffer; importScripts("/assets/libs/comlink.min.umd.js"); importScripts("/assets/libs/idb-keyval.js"); importScripts("/assets/libs/workbox/workbox-sw.js"); workbox.setConfig({ debug: false, modulePathPrefix: "/assets/libs/workbox/", }); const supportedWebDAVMethods = [ "OPTIONS", "PROPFIND", "PROPPATCH", "MKCOL", "GET", "HEAD", "POST", // sometimes used for special operations "PUT", "DELETE", "COPY", "MOVE", "LOCK", "UNLOCK", ]; async function handleDavRequest({ request, url }) { const fsCallback = (await currentFs()).fs; const fs = fsCallback.promises; const shell = new (await currentFs()).sh(); const method = request.method; const path = decodeURIComponent(url.pathname.replace(/^\/dav/, "") || "/"); const getBuffer = async () => new Uint8Array(await request.arrayBuffer()); const getDestPath = () => decodeURIComponent(new URL(request.headers.get("Destination"), url).pathname.replace(/^\/dav/, "")); try { switch (method) { case "OPTIONS": return new Response(null, { status: 204, headers: { Allow: "OPTIONS, PROPFIND, PROPPATCH, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE, LOCK, UNLOCK", DAV: "1, 2", }, }); case "PROPFIND": { try { const stats = await fs.stat(path); const isDirectory = stats.type === "DIRECTORY"; const href = url.pathname; let responses = ""; const renderEntry = async (entryPath, stat) => { const isDir = stat.type === "DIRECTORY"; const contentLength = isDir ? "" : `${stat.size}`; const contentType = isDir ? "" : `${mime.default.getType(entryPath) || "application/octet-stream"}`; const creationDate = new Date(stat.ctime).toISOString(); const lastModified = new Date(stat.mtime).toUTCString(); const resourcetype = isDir ? "" : ""; return ` ${entryPath} HTTP/1.1 200 OK ${resourcetype} ${contentLength} ${contentType} ${creationDate} ${lastModified} `; }; if (isDirectory) { responses = await renderEntry(href.endsWith("/") ? href : href + "/", stats); const files = await fs.readdir(path); const fileResponses = await Promise.all( files.map(async file => { const fullPath = path.endsWith("/") ? path + file : `${path}/${file}`; const stat = await fs.stat(fullPath); const entryHref = `${href.endsWith("/") ? href : href + "/"}${file}`; return renderEntry(entryHref, stat); }), ); responses += fileResponses.join(""); } else { responses = await renderEntry(href, stats); } const xml = ` ${responses} `.trim(); return new Response(xml, { headers: { "Content-Type": "application/xml" }, status: 207, }); } catch (err) { console.error(path, err); const xml = ` ${url.pathname} HTTP/1.1 404 Not Found `.trim(); return new Response(xml, { headers: { "Content-Type": "application/xml" }, status: 207, // multi-status }); } } case "PROPPATCH": return new Response(null, { status: 207 }); // No-op case "MKCOL": try { await fs.mkdir(path); return new Response(null, { status: 201 }); } catch { return new Response(null, { status: 405 }); } case "GET": case "HEAD": { try { const data = await fs.readFile(path, "arraybuffer"); return new Response(method === "HEAD" ? null : new Blob([data]), { headers: { "Content-Type": mime.default.getType(path) || "application/octet-stream", }, status: 200, }); } catch { return new Response(null, { status: 404 }); } } case "PUT": { const buffer = await getBuffer(); try { console.log(buffer); await fs.writeFile(path, Filer.Buffer.from(buffer)); return new Response(null, { status: 201 }); } catch { return new Response(null, { status: 500 }); } } case "DELETE": try { await shell.promises.rm(path, { recursive: true }); return new Response(null, { status: 204 }); } catch { return new Response(null, { status: 404 }); } case "COPY": { // This is technically invalid -- Copy should handle full folders as well but filer doesn't have a convinient way to do this :/ // take this broken solution in the interim - Rafflesia const dest = getDestPath(); try { await shell.promises.cpr(path, dest); return new Response(null, { status: 201 }); } catch (e) { console.error(e); return new Response(null, { status: 404 }); } } case "MOVE": { const dest = getDestPath(); try { await fs.rename(path, dest); return new Response(null, { status: 201 }); } catch { return new Response(null, { status: 500 }); } } case "LOCK": case "UNLOCK": { return new Response(``, { status: 200, headers: { "Content-Type": "application/xml", "Lock-Token": ``, }, }); } case "POST": return new Response("POST not implemented", { status: 204 }); default: return new Response("Unsupported WebDAV method", { status: 405, }); } } catch (err) { return new Response(`Internal error: ${err.message}`, { status: 500 }); } } for (const method of supportedWebDAVMethods) { workbox.routing.registerRoute( /\/dav/, async event => { return await handleDavRequest(event); }, method, ); } workbox.core.skipWaiting(); workbox.core.clientsClaim(); var cacheenabled = false; const callbacks = {}; const filepickerCallbacks = {}; addEventListener("message", event => { if (event.data.anura_target === "anura.x86.proxy") { const callback = callbacks[event.data.id]; callback(event.data.value); } if (event.data.anura_target === "anura.cache") { cacheenabled = event.data.value; idbKeyval.set("cacheenabled", event.data.value); } if (event.data.anura_target === "anura.filepicker.result") { const callback = filepickerCallbacks[event.data.id]; callback(event.data.value); } if (event.data.anura_target === "anura.comlink.init") { self.swShared = Comlink.wrap(event.data.value); swShared.test.then(console.log); self.isConnected = swShared.test; } if (event.data.anura_target === "anura.nohost.set") { self.anurafs = swShared.anura.fs; self.anurash = swShared.sh; } }); workbox.routing.registerRoute(/\/extension\//, async ({ url }) => { const { fs } = await currentFs(); console.debug("Caught a aboutbrowser extension request"); try { return new Response(await fs.promises.readFile(url.pathname)); } catch (e) { return new Response("File not found bruh", { status: 404 }); } }); workbox.routing.registerRoute( /\/showFilePicker/, async ({ url }) => { const id = crypto.randomUUID(); const clients = (await self.clients.matchAll()).filter(v => new URL(v.url).pathname === "/"); if (clients.length < 1) return new Response("no clients were available to take your request"); const client = clients[0]; const regex = url.searchParams.get("regex") || ".*"; const type = url.searchParams.get("type") || "file"; client.postMessage({ anura_target: "anura.filepicker", regex, id, type, }); const resp = await new Promise(resolve => { filepickerCallbacks[id] = resolve; }); return new Response(JSON.stringify(resp), { status: resp.cancelled ? 444 : 200, }); }, "GET", ); async function serveFile(path, fsOverride, shOverride) { let fs; let sh; if (fsOverride && shOverride) { fs = fsOverride; sh = shOverride; } else { const { fs: fs_, sh: sh_ } = await currentFs(); fs = fsOverride || fs_; sh = shOverride || sh_; } if (!fs) { // HOPEFULLY this will never happen, // as the filesystem should always have a backup return new Response( JSON.stringify({ error: "No filesystem available.", }), { status: 500, headers: { "Content-Type": "application/json", ...corsheaders, }, }, ); } try { const stats = await fs.promises.stat(path); if (stats.type === "DIRECTORY") { // Can't do withFileTypes because it is unserializable const entries = await Promise.all((await fs.promises.readdir(path)).map(async e => await fs.promises.stat(`${path}/${e}`))); function page() { return `

Index of

${entries .map( entry => ` `, ) .join("")}
Name Type Size Last Modified
${ entry.type === "DIRECTORY" ? ` ` : ` ` } ${entry.name} ${entry.type} ${ entry.type === "DIRECTORY" ? "-" : entry.size > 1024 * 1024 * 1024 ? `${(entry.size / (1024 * 1024 * 1024)).toFixed(2)} GB` : entry.size > 1024 * 1024 ? `${(entry.size / (1024 * 1024)).toFixed(2)} MB` : entry.size > 1024 ? `${(entry.size / 1024).toFixed(2)} KB` : `${entry.size} bytes` } ${new Date(entry.mtime).toLocaleString()}
`; } return new Response(page(), { headers: { "Content-Type": "text/html", ...corsheaders, }, }); /* Custom Terbium way lol return new Response(JSON.stringify(entries), { headers: { "Content-Type": "application/json", ...corsheaders, }, }); */ } const type = mime.default.getType(path) || "application/octet-stream"; return new Response(await fs.promises.readFile(path, "arraybuffer"), { headers: { "Content-Type": type, "Content-Disposition": `inline; filename="${path.split("/").pop()}"`, ...corsheaders, }, }); } catch (e) { return new Response(JSON.stringify({ error: e.message, code: e.code, status: 404 }), { status: 404, headers: { "Content-Type": "application/json", ...corsheaders, }, }); } } async function updateFile(path, data) { const { fs, sh } = await currentFs(); switch (data.action) { case "write": await sh.promises.mkdirp(path.replace(/[^/]*$/g, "")); await fs.promises.writeFile(path, data.contents); return new Response( JSON.stringify({ status: "ok", }), { headers: { "Content-Type": "application/json", ...corsheaders, }, }, ); case "delete": await sh.promises.rm(path, { recursive: true }); return new Response( JSON.stringify({ status: "ok", }), { headers: { "Content-Type": "application/json", ...corsheaders, }, }, ); case "touch": await sh.promises.touch(path); return new Response( JSON.stringify({ status: "ok", }), { headers: { "Content-Type": "application/json", ...corsheaders, }, }, ); case "mkdir": await sh.promises.mkdirp(path); return new Response( JSON.stringify({ status: "ok", }), { headers: { "Content-Type": "application/json", ...corsheaders, }, }, ); } } const fsRegex = /\/fs(\/.*)/; const corsheaders = { "Cross-Origin-Embedder-Policy": "require-corp", "Access-Control-Allow-Origin": "*", "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Resource-Policy": "same-site", }; workbox.routing.registerRoute( fsRegex, async ({ url }) => { let path = url.pathname.match(fsRegex)[1]; path = decodeURI(path); return serveFile(path); }, "GET", ); workbox.routing.registerRoute( fsRegex, async ({ url, request }) => { let path = url.pathname.match(fsRegex)[1]; const action = request.headers.get("x-fs-action") || url.searchParams.get("action"); if (!action) { return new Response( JSON.stringify({ error: "No action specified", status: 400, }), { status: 400, headers: { "Content-Type": "application/json", ...corsheaders, }, }, ); } path = decodeURI(path); const body = await request.arrayBuffer(); return updateFile(path, { action, contents: Buffer.from(body), }); }, "POST", ); workbox.routing.registerRoute(/^(?!.*(\/config.json|\/MILESTONE|\/x86images\/|\/service\/))/, async ({ url, request }) => { if (cacheenabled === undefined) { console.debug("retrieving cache value"); const result = await idbKeyval.get("cacheenabled"); if (result !== undefined || result !== null) { cacheenabled = result; } } if ((!cacheenabled && url.pathname === "/" && !navigator.onLine) || (!cacheenabled && url.pathname === "/index.html" && !navigator.onLine)) { return new Response(offlineError(), { status: 500, headers: { "content-type": "text/html" }, }); } if (!cacheenabled) { const fetchResponse = await fetch(request); return new Response(await fetchResponse.arrayBuffer(), { headers: { ...Object.fromEntries(fetchResponse.headers.entries()), ...corsheaders, }, }); return fetchResponse; } if (url.pathname === "/") { url.pathname = "/index.html"; } if (url.password) return new Response("", { headers: { "content-type": "text/html" } }); const basepath = "/anura_files"; const path = decodeURI(url.pathname); // Force Filer to be used in cache routes, as it does not require waiting for anura to be connected const fs = self.opfs || filerfs; const sh = self.opfssh || filersh; // Terbium already has its own way for caching files to the file system so doing it again is just a waste of space /* const response = await serveFile(`${basepath}${path}`, fs, sh); if (response.ok) { return response; } else { */ try { const fetchResponse = await fetch(request); // Promise so that we can return the response before we cache it, for faster response times return new Promise(async resolve => { const corsResponse = new Response(await fetchResponse.clone().arrayBuffer(), { headers: { ...Object.fromEntries(fetchResponse.headers.entries()), ...corsheaders, }, }); resolve(corsResponse); /* if (fetchResponse.ok) { const buffer = await fetchResponse.clone().arrayBuffer(); await sh.promises.mkdirp( `${basepath}${path.replace(/[^/]*$/g, "")}`, ); // Explicitly use Filer's fs here, as // Buffers lose their inheritance when passed // to anura's fs, causing them to be treated as // strings await fs.promises.writeFile( `${basepath}${path}`, Buffer.from(buffer), ); }*/ }).catch(e => { console.error("I hate this bug: ", e); }); } catch (e) { return new Response( JSON.stringify({ error: e.message, status: 500, }), { status: 500, headers: { "Content-Type": "application/json", ...corsheaders, }, }, ); } }); importScripts("/uv/uv.bundle.js"); importScripts("/uv/uv.config.js"); importScripts("/uv/uv.sw.js"); importScripts("/scram/scramjet.all.js"); const { ScramjetServiceWorker } = $scramjetLoadWorker(); const scramjet = new ScramjetServiceWorker(); const uv = new UVServiceWorker(); const methods = ["GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS", "PATCH"]; function sanitizeDownloadFilename(filename) { const sanitized = String(filename || "") .replace(/[\\/:*?"<>|]/g, "_") .trim(); return sanitized || `download-${Date.now()}`; } function splitFilename(name) { const lastDot = name.lastIndexOf("."); if (lastDot <= 0 || lastDot === name.length - 1) { return { base: name, ext: "" }; } return { base: name.slice(0, lastDot), ext: name.slice(lastDot), }; } async function getUniqueDownloadPath(fsPromises, dirPath, filename) { const { base, ext } = splitFilename(filename); let attempt = 0; while (true) { const candidate = `${dirPath}/${attempt === 0 ? `${base}${ext}` : `${base} (${attempt})${ext}`}`; try { await fsPromises.stat(candidate); attempt += 1; } catch { return candidate; } } } async function saveTO(initialFilename) { const id = crypto.randomUUID(); const clients = (await self.clients.matchAll({ type: "window", includeUncontrolled: true })).filter(v => new URL(v.url).pathname === "/"); if (clients.length < 1) return null; const client = clients[0]; client.postMessage({ anura_target: "anura.filepicker", regex: sanitizeDownloadFilename(initialFilename || "download.bin"), id, type: "folder", }); const resp = await new Promise(resolve => { filepickerCallbacks[id] = resolve; }); delete filepickerCallbacks[id]; if (!resp || resp.cancelled) return null; const folder = Array.isArray(resp.folders) ? resp.folders[0] : null; if (!folder || typeof folder !== "string") return null; return folder.replace(/\/$/, "") || "/"; } async function saveFP(response, request, proxyName) { try { const { fs } = await currentFs(); const downloadHelper = window.DownloadHandler; if (!downloadHelper || !response || !request) return response; if (response.status >= 300 && response.status < 400) return response; if (!downloadHelper.isDownload(response.headers, request.destination || "")) return response; const contentDisposition = response.headers.get("content-disposition"); const parsedName = downloadHelper.parseDownloadFilename(contentDisposition, request.url || response.url); const filename = sanitizeDownloadFilename(parsedName || "download.bin"); const selectedDir = await saveTO(filename); const path = await getUniqueDownloadPath(fs.promises, selectedDir, filename); const buffer = await response.clone().arrayBuffer(); await fs.promises.writeFile(path, Buffer.from(buffer)); console.info(`[${proxyName}] Download saved to ${path}`); return new Response(null, { status: 204, statusText: "No Content", }); } catch (error) { console.error(`[${proxyName}] Failed to save download`, error); return response; } } methods.forEach(method => { workbox.routing.registerRoute( /\/uv\/service\//, async event => { console.debug("Got UV req"); uv.on("request", event => { event.data.headers["user-agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0 Safari/537.36 Terbium-Browser/2.3.0"; }); return await uv.fetch(event); }, method, ); }); // Route w-corp-staticblitz.com and subdomains through BareMux, so that the Node.js subsystem doesn't get blocked by filters methods.forEach(method => { workbox.routing.registerRoute( ({ url }) => { return url.hostname === "w-corp-staticblitz.com" || url.hostname.endsWith(".w-corp-staticblitz.com"); }, async event => { try { // Clone the request const bareRequest = new Request(event.url.href, { method: event.request.method, headers: event.request.headers, body: event.request.body, mode: event.request.mode, credentials: event.request.credentials, cache: event.request.cache, redirect: event.request.redirect, referrer: event.request.referrer, integrity: event.request.integrity, }); return await bareClient.fetch(bareRequest); } catch (error) { console.error("BareMux *.w-corp-staticblitz.com proxy fetch failed", error); return new Response("Failed to fetch through BareMux", { status: 500, }); } }, method, ); }); scramjet.loadConfig(); methods.forEach(method => { workbox.routing.registerRoute( /\/service\//, async ({ event }) => { console.log("Got SJ req"); await scramjet.loadConfig(); if (scramjet.route(event)) { const response = await scramjet.fetch(event); return await saveFP(response, event.request, "Scramjet"); } return fetch(event.request); }, method, ); }); // have to put this here because no cache function offlineError() { return `

Offline Error

Try refreshing the page if you are connected to the internet

`; } async function initSw() { for (const client of await self.clients.matchAll()) { client.postMessage({ anura_target: "anura.sw.reinit", }); } } initSw(); ================================================ FILE: public/apps/about.tapp/app.css ================================================ @font-face { font-family: Inter; src: url(/fonts/Inter.ttf); } h1 { font-family: Inter; font-weight: 700; } h4 { margin-top: 4px; margin-bottom: 4px; } html, body { height: 100%; width: 100%; margin: 0; color: #ffffff; font-family: Inter; position: relative; overflow: hidden; } body { display: flex; flex-direction: column; justify-content: start; align-items: start; padding: 20px; width: calc(100% - 20px); height: calc(100% - 14px); padding-top: 4px; } .centered-image { width: 180px; margin-left: -34px; margin-top: -14px; } a { color: #5088ff; text-decoration: none; } a:hover { text-decoration: underline; } ================================================ FILE: public/apps/about.tapp/index.html ================================================ About Terbium TB Logo

Terbium WebOS

Developers: SNOOT, XSTARS, IllusionTBA, Rafflesia, EndlessVortex, ironswordX

© Copyright 2026 TerbiumOS

Licensed under the AGPL 3.0 License

================================================ FILE: public/apps/about.tapp/index.json ================================================ { "name": "About", "config": { "title": "About", "icon": "/fs/apps/system/about.tapp/icon.svg", "src": "/fs/apps/system/about.tapp/index.html" } } ================================================ FILE: public/apps/app store.tapp/index.html ================================================ Terbium App Store

Add Repo

Featured App of the Day

Apps
PWAs
================================================ FILE: public/apps/app store.tapp/index.js ================================================ let currRepo; let viewType = "apps"; /** * Loads the repos content * @param {string} url */ async function loadRepo(url) { const repo = await window.parent.tb.libcurl.fetch(url); let data = await repo.json(); let type = "Terbium"; if (data.maintainer) { type = "Anura"; const list = await window.parent.tb.libcurl.fetch(url.replace("manifest.json", "list.json")); currRepo = { name: data.name, url: url, }; data = await list.json(); } else if (data.title) { type = "Xen"; } else { currRepo = data.repo.name; } document.querySelector(".app-prev").classList.remove("flex"); document.querySelector(".app-prev").classList.add("hidden"); document.querySelector(".main").classList.remove("hidden"); document.querySelector(".main").classList.add("flex"); const featured = document.querySelector(".featured"); switch (type) { case "Terbium": const featuredList1 = data.apps; const randomIndex1 = Math.floor(Math.random() * featuredList1.length); data.featured = featuredList1[randomIndex1] || {}; const icn1 = await window.parent.tb.libcurl.fetch(data.featured.icon); const blob1 = await icn1.blob(); const icnurl1 = URL.createObjectURL(blob1); featured.classList.forEach(cls => { if (cls.startsWith("bg-[url")) { featured.classList.remove(cls); } }); featured.classList.add(`bg-[url('${icnurl1 || "/tb.svg"}')]`); featured.onclick = () => { loadApp(data.featured, type); }; featured.querySelector("h3").textContent = data.featured.name; if (data.featured.version) { featured.querySelector("h4:nth-child(2)").textContent = `Version ${data.featured.version}`; } else { featured.querySelector("h4:nth-child(2)").textContent = `Progressive Web App`; } featured.querySelector("h4:nth-child(3)").textContent = `By ${data.featured.developer || "Unknown"}`; const appCards1 = await Promise.all( data.apps.map(async app => { const icn1 = await window.parent.tb.libcurl.fetch(app.icon); const blob1 = await icn1.blob(); const icnurl1 = URL.createObjectURL(blob1); const displayName = app.name && app.name.length > 10 ? app.name.slice(0, 10) + "..." : app.name || "Unknown"; const cardHtml = `
App Icon ${displayName}
`; return { html: cardHtml, hasWmArgs: !!app.wmArgs }; }), ); const pwaCards = appCards1.filter(card => card.hasWmArgs).map(card => card.html); const appCards = appCards1.filter(card => !card.hasWmArgs).map(card => card.html); if (pwaCards.length === 0) { pwaCards.push(`

There are no PWA apps available in this repo

`); } if (appCards.length === 0) { appCards.push(`

There are no app card apps available in this repo

`); } document.querySelector(".pwa-list").innerHTML = pwaCards.join(""); document.querySelector(".apps-list").innerHTML = appCards.join(""); document.querySelectorAll(".app-card").forEach(card => { card.addEventListener("click", function () { const idx = parseInt(this.getAttribute("data-app-index"), 10); loadApp(data.apps[idx], type); }); }); break; case "Anura": const featuredList2 = data.apps; const randomIndex2 = Math.floor(Math.random() * featuredList2.length); data.featured = featuredList2[randomIndex2] || {}; const icn2 = await window.parent.tb.libcurl.fetch(`${url.replace("manifest.json", "")}/apps/${data.featured.package}/${data.featured.icon}`); const blob2 = await icn2.blob(); const icnurl2 = URL.createObjectURL(blob2); featured.classList.forEach(cls => { if (cls.startsWith("bg-[url")) { featured.classList.remove(cls); } }); featured.classList.add(`bg-[url('${icnurl2 || "/tb.svg"}')]`); featured.onclick = () => { loadApp(data.featured, type); }; featured.querySelector("h3").textContent = data.featured.name; if (data.featured.version) { featured.querySelector("h4:nth-child(2)").textContent = `Version ${data.featured.version}`; } else { featured.querySelector("h4:nth-child(2)").textContent = `Progressive Web App`; } featured.querySelector("h4:nth-child(3)").textContent = `Anura Application`; const appCards2 = await Promise.all( data.apps.map(async app => { const icn1 = await window.parent.tb.libcurl.fetch(`${url.replace("manifest.json", "")}/apps/${app.package}/${app.icon}`); const blob1 = await icn1.blob(); const icnurl1 = URL.createObjectURL(blob1); const displayName = app.name && app.name.length > 10 ? app.name.slice(0, 10) + "..." : app.name || "Unknown"; const cardHtml = `
App Icon ${displayName}
`; return { html: cardHtml, hasWmArgs: !!app.wmArgs }; }), ); const pwaCards2 = appCards2.filter(card => card.hasWmArgs).map(card => card.html); const appCards_2 = appCards2.filter(card => !card.hasWmArgs).map(card => card.html); if (pwaCards2.length === 0) { pwaCards2.push(`

There are no PWA apps available in this repo

`); } if (appCards_2.length === 0) { appCards_2.push(`

There are no app card apps available in this repo

`); } document.querySelector(".pwa-list").innerHTML = pwaCards2.join(""); document.querySelector(".apps-list").innerHTML = appCards_2.join(""); document.querySelectorAll(".app-card").forEach(card => { card.addEventListener("click", function () { const idx = parseInt(this.getAttribute("data-app-index"), 10); loadApp(data.apps[idx], type); }); }); break; case "Xen": console.log("Xen repo not implemented yet."); document.querySelector(".featured h3").textContent = "Xen App Store"; break; } } /** * Loads the app content in the preview * @param {Object} app - The app to load * @param {string} type - The type of app (Terbium, Anura, Xen) */ async function loadApp(app, type) { document.querySelector(".app-prev").classList.remove("hidden"); document.querySelector(".app-prev").classList.add("flex"); document.querySelector(".main").classList.remove("flex"); document.querySelector(".main").classList.add("hidden"); let icnUrl; let isInstalled = false; let uptodate = true; const installedApps = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/installed.json`, "utf8")); if (installedApps.some(a => a.name === app.name)) { isInstalled = true; const config = JSON.parse(await window.parent.tb.fs.promises.readFile(`${installedApps.find(a => a.name === app.name).config}`, "utf8")); if (app.version && config.version && semverCompare(app.version, config.version) > 0) { uptodate = false; } } if (app.wmArgs) { type = "tb-PWA"; } else if ("anura-pkg" in app) { type = "tb-liq"; } switch (type) { case "Terbium": case "tb-PWA": const icn1 = await window.parent.tb.libcurl.fetch(app.icon); const blob1 = await icn1.blob(); icnUrl = URL.createObjectURL(blob1); break; case "Anura": case "tb-liq": let icn2; if (currRepo.url) { icn2 = await window.parent.tb.libcurl.fetch(`${currRepo.url.replace("manifest.json", "")}/apps/${app.package}/${app.icon}`); } else { icn2 = await window.parent.tb.libcurl.fetch(app.icon); } if (!icn2.ok) { icn2 = await window.parent.tb.libcurl.fetch("https://terbiumon.top/favicon.ico"); } const blob2 = await icn2.blob(); icnUrl = URL.createObjectURL(blob2); break; case "Xen": break; } document.querySelector(".app-prev").innerHTML = `

${typeof currRepo === "object" ? currRepo.name : currRepo} → ${app.name}

About ${app.name}

${app.description || "No description available."}

  • Version: ${app.version || "1.0.0"}
  • Developer: ${app.developer || "Unknown"}
  • License: ${app.license || "N/A"}
  • Scanned: ${app.scanned ? `${new URL(app.scanned).hostname.replace(/^www\./, "")}` : "N/A"}
  • Size: ${app.size || "N/A"}
    • Requirements:

    • OS: ${(app.requirements && app.requirements.os) || "Any"}
    • Proxy: ${(app.requirements && app.requirements.proxy) || "Any"}

Images

${ app.images ? app.images .map( img => `
App Image
`, ) .join("") : "

No images available.

" }
`; const addBtns = () => { const insBtn = document.querySelector(".ins-btn"); const updBtn = document.querySelector(".upd-btn"); const unsBtn = document.querySelector(".uns-btn"); if (insBtn) { insBtn.addEventListener("click", async function handler() { insBtn.disabled = true; insBtn.textContent = "Installing..."; insBtn.classList.remove("bg-[#5DD881]", "text-black"); insBtn.classList.add("bg-[#4d4d4d]", "text-white"); const success = await install(app, type); if (success) { insBtn.outerHTML = ``; addBtns(); } else { insBtn.disabled = false; insBtn.textContent = "Install"; insBtn.classList.remove("bg-[#4d4d4d]", "text-white"); insBtn.classList.add("bg-[#5DD881]", "text-black"); } }); } if (updBtn) { updBtn.addEventListener("click", async function handler() { updBtn.disabled = true; updBtn.textContent = "Updating..."; updBtn.classList.remove("bg-[#5DD881]", "text-black"); updBtn.classList.add("bg-[#4d4d4d]", "text-white"); await uninstall(app, type); const success = await install(app, type); if (success) { updBtn.outerHTML = ``; addBtns(); } else { updBtn.disabled = false; updBtn.textContent = "Update"; updBtn.classList.remove("bg-[#4d4d4d]", "text-white"); updBtn.classList.add("bg-[#5DD881]", "text-black"); } }); } if (unsBtn) { unsBtn.addEventListener("click", async function handler() { await uninstall(app, type); unsBtn.outerHTML = ``; addBtns(); }); } }; addBtns(); } /** * Loads the current list of repos */ async function loadRepos() { const repoList = document.querySelector(".repo-list"); repoList.innerHTML = ""; const repos = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/app store/repos.json`, "utf8")); for (const repo of repos) { const repoinfo = await window.parent.tb.libcurl.fetch(repo.url); if (!repoinfo.ok) { const displayName = repo.name && repo.name.length > 8 ? repo.name.slice(0, 8) + "..." : repo.name || "Unknown"; const repoCard = document.createElement("div"); repoCard.className = "repo-card flex flex-row items-center bg-[#00000032] rounded-lg h-[50px] p-1 gap-1"; repoCard.onclick = () => loadRepo(repo.url); repoCard.innerHTML = ` Featured App

${displayName}

`; repoCard.addEventListener("contextmenu", function (e) { e.preventDefault(); window.parent.tb.contextmenu.create({ x: e.clientX, y: e.clientY, options: [ { text: "Load Repo", click: () => loadRepo(repo.url) }, { text: "Remove Repo", click: () => { repoList.removeChild(repoCard); const index = repos.findIndex(r => r.url === repo.url); if (index !== -1) { repos.splice(index, 1); } window.parent.tb.fs.promises.writeFile(`/apps/user/${sessionStorage.getItem("currAcc")}/app store/repos.json`, JSON.stringify(repos, null, 2)); }, }, ], }); }); repoList.appendChild(repoCard); continue; } const data = await repoinfo.json(); if (data.maintainer) { const icn = await window.parent.tb.libcurl.fetch(repo.icon); const blob = await icn.blob(); const icnurl = URL.createObjectURL(blob); const displayName = data.name && data.name.length > 8 ? data.name.slice(0, 8) + "..." : data.name || "Unknown"; const repoCard = document.createElement("div"); repoCard.className = "repo-card flex flex-row items-center bg-[#00000032] rounded-lg h-[50px] p-1 gap-1"; repoCard.onclick = () => loadRepo(repo.url); repoCard.innerHTML = ` Featured App

${displayName}

`; repoCard.addEventListener("contextmenu", function (e) { e.preventDefault(); window.parent.tb.contextmenu.create({ x: e.clientX, y: e.clientY, options: [ { text: "Load Repo", click: () => loadRepo(repo.url) }, { text: "Remove Repo", click: () => { repoList.removeChild(repoCard); const index = repos.findIndex(r => r.url === repo.url); if (index !== -1) { repos.splice(index, 1); } window.parent.tb.fs.promises.writeFile(`/apps/user/${sessionStorage.getItem("currAcc")}/app store/repos.json`, JSON.stringify(repos, null, 2)); }, }, ], }); }); repoList.appendChild(repoCard); } else if (data.title) { throw new Error("Xen repo not implemented yet."); } else { const icn = await window.parent.tb.libcurl.fetch(data.repo.icon); const blob = await icn.blob(); const icnurl = URL.createObjectURL(blob); const displayName = data.repo.name && data.repo.name.length > 8 ? data.repo.name.slice(0, 8) + "..." : data.repo.name || "Unknown"; const repoCard = document.createElement("div"); repoCard.className = "repo-card flex flex-row items-center bg-[#00000032] rounded-lg h-[50px] p-1 gap-1"; repoCard.onclick = () => loadRepo(repo.url); repoCard.innerHTML = ` Featured App

${displayName}

`; repoCard.addEventListener("contextmenu", function (e) { e.preventDefault(); window.parent.tb.contextmenu.create({ x: e.clientX, y: e.clientY, options: [ { text: "Load Repo", click: () => loadRepo(repo.url) }, { text: "Remove Repo", click: () => { repoList.removeChild(repoCard); const index = repos.findIndex(r => r.url === repo.url); if (index !== -1) { repos.splice(index, 1); } window.parent.tb.fs.promises.writeFile(`/apps/user/${sessionStorage.getItem("currAcc")}/app store/repos.json`, JSON.stringify(repos, null, 2)); }, }, ], }); }); repoList.appendChild(repoCard); } } } /** * Changes the view between apps and PWAs * @param {string} type - The type of view ("apps" or "pwa") */ function view(type) { if (type === "apps") { viewType = "apps"; document.querySelector(".apps-list").classList.remove("hidden"); document.querySelector(".pwa-list").classList.add("hidden"); document.querySelector(".apps-list").classList.remove("flex"); document.querySelector(".apps-list").classList.add("grid"); document.querySelector(".pwa-list").classList.remove("grid"); document.querySelector(".pwa-list").classList.add("hidden"); document.querySelector(".app-togg").classList.add("bg-[#5DD88122]"); document.querySelector(".app-togg").classList.remove("bg-[#00000022]"); document.querySelector(".pwa-togg").classList.remove("bg-[#5DD88122]"); document.querySelector(".pwa-togg").classList.add("bg-[#00000022]"); } else if (type === "pwa") { viewType = "pwa"; document.querySelector(".pwa-list").classList.remove("hidden"); document.querySelector(".apps-list").classList.add("hidden"); document.querySelector(".pwa-list").classList.remove("flex"); document.querySelector(".pwa-list").classList.add("grid"); document.querySelector(".apps-list").classList.remove("grid"); document.querySelector(".apps-list").classList.add("hidden"); document.querySelector(".app-togg").classList.remove("bg-[#5DD88122]"); document.querySelector(".app-togg").classList.add("bg-[#00000022]"); document.querySelector(".pwa-togg").classList.add("bg-[#5DD88122]"); document.querySelector(".pwa-togg").classList.remove("bg-[#00000022]"); } } /** * Adds a new repo to the repo list */ async function addRepo() { window.parent.tb.dialog.Message({ title: "Enter a Repo URL", onOk: async value => { const res = await window.parent.tb.libcurl.fetch(value); const meta = await res.json(); const repos = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/app store/repos.json`, "utf8")); if (meta.maintainer) { const list = await window.parent.tb.libcurl.fetch(value.replace("manifest.json", "list.json")); if (list.ok) { repos.push({ name: meta.name || "Unknown", url: value, icon: "https://anura.pro/icon.png", }); } else { window.parent.tb.notification.Toast({ message: "Failed to add repo. The URL does not point to a valid Anura repo manifest", application: "App Store", iconSrc: "/fs/apps/system/app store.tapp/icon.svg", time: 5000, }); } } else if (meta.title) { repos.push({ name: meta.title || "Unknown", url: value, icon: "https://raw.githubusercontent.com/NebulaServices/XenOS/refs/heads/main/public/assets/logo.svg", }); } else { repos.push({ name: meta.repo.name || "Unknown", url: value, icon: meta.repo.icon, }); } await window.parent.tb.fs.promises.writeFile(`/apps/user/${sessionStorage.getItem("currAcc")}/app store/repos.json`, JSON.stringify(repos, null, 2)); loadRepos(); }, }); } /** * Searches for the apps that are loaded in * @param {string} input - The search input */ async function search(input) { if (viewType === "apps") { const applist = document.querySelector(".apps-list"); applist.querySelectorAll(".app-card").forEach(card => { const appName = card.querySelector("span").textContent.toLowerCase(); if (appName.includes(input.toLowerCase())) { card.classList.remove("hidden"); } else { card.classList.add("hidden"); } }); } else { const pwaList = document.querySelector(".pwa-list"); pwaList.querySelectorAll(".app-card").forEach(card => { const appName = card.querySelector("span").textContent.toLowerCase(); if (appName.includes(input.toLowerCase())) { card.classList.remove("hidden"); } else { card.classList.add("hidden"); } }); } } /** * Compares two semantic version strings. * @param {string} a - The first version string. * @param {string} b - The second version string. * @returns {number} - Returns 1 if a > b, -1 if a < b, 0 if they are equal. */ const semverCompare = (a, b) => { const pa = a.split(/[-.]/); const pb = b.split(/[-.]/); for (let i = 0; i < Math.max(pa.length, pb.length); i++) { const na = pa[i] || "0"; const nb = pb[i] || "0"; if (!isNaN(na) && !isNaN(nb)) { if (+na > +nb) return 1; if (+na < +nb) return -1; } else { if (na > nb) return 1; if (na < nb) return -1; } } return 0; }; /** * Installs the requested app * @param {string} type - The type of app (Terbium, tb-PWA, Anura, Xen) * @returns {Promise} - Returns true if the installation was successful, false otherwise */ async function install(app, type) { if (app.requirements) { if (app.requirements.os) { if (semverCompare(window.parent.tb.system.version(), app.requirements.os.replace(/^v/, "")) < 0) { window.parent.tb.notification.Toast({ message: `Failed to install ${app.name}. Your version of Terbium does not meet the minimum requirements.`, application: "App Store", iconSrc: "/fs/apps/system/app store.tapp/icon.svg", time: 5000, }); return false; } } if (app.requirements.proxy && app.requirements.proxy !== window.parent.tb.proxy.get()) { window.parent.tb.notification.Toast({ message: `Failed to install ${app.name}. The current selected proxy does not meet the minimum requirements.`, application: "App Store", iconSrc: "/fs/apps/system/app store.tapp/icon.svg", time: 5000, }); return false; } } switch (type) { case "Terbium": try { await window.parent.tb.system.download(app["pkg-download"], `/apps/system/${app.name}.zip`); await unzip(`/apps/system/${app.name}.zip`, `/apps/system/${app.name}.tapp/`); await window.parent.tb.fs.promises.unlink(`/apps/system/${app.name}.zip`); const appConf = await window.parent.tb.fs.promises.readFile(`/apps/system/${app.name}.tapp/.tbconfig`, "utf8"); const appData = JSON.parse(appConf); await window.parent.tb.launcher.addApp({ title: typeof appData.wmArgs.title === "object" ? { text: appData.wmArgs.title.text, weight: appData.wmArgs.title.weight, html: appData.wmArgs.title.html, } : appData.wmArgs.title, name: appData.title, icon: `/fs/apps/system/${app.name}.tapp/${appData.icon}`, src: `/fs/apps/system/${app.name}.tapp/${appData.wmArgs.src}`, size: { width: appData.wmArgs.size.width, height: appData.wmArgs.size.height, }, single: appData.wmArgs.single, resizable: appData.wmArgs.resizable, controls: appData.wmArgs.controls, message: appData.wmArgs.message, snapable: appData.wmArgs.snapable, }); try { let apps = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/installed.json`, "utf8")); apps.push({ name: app.name, user: await window.parent.tb.user.username(), config: `/apps/system/${app.name}.tapp/.tbconfig`, }); await window.parent.tb.fs.promises.writeFile(`/apps/installed.json`, JSON.stringify(apps)); } catch { await window.parent.tb.fs.promises.writeFile( `/apps/installed.json`, JSON.stringify([ { name: app.name, user: await window.parent.tb.user.username(), config: `/apps/system/${app.name}.tapp/.tbconfig`, }, ]), ); } window.parent.tb.notification.Toast({ message: `${app.name} has been installed!`, application: "App Store", iconSrc: "/fs/apps/system/app store.tapp/icon.svg", time: 5000, onOk: () => { window.parent.tb.system.openApp(app.name); }, }); return true; } catch (e) { console.error("Error installing the app:", e); await window.parent.tb.sh.promises.rm(`/apps/system/${app.name}.tapp`, { recursive: true }); window.parent.tb.notification.Toast({ message: `Failed to install ${app.name}. Check the console for details.`, application: "App Store", iconSrc: "/fs/apps/system/app store.tapp/icon.svg", time: 5000, }); return false; } case "tb-PWA": const web_apps = JSON.parse(await window.parent.tb.fs.promises.readFile("/apps/web_apps.json", "utf8")); web_apps.apps.push(app.name.toLowerCase()); await window.parent.tb.fs.promises.writeFile("/apps/web_apps.json", JSON.stringify(web_apps)); await window.parent.tb.launcher.addApp({ title: app["wmArgs"]["title"], name: app.name, icon: app.icon, src: app["wmArgs"]["src"], size: { width: app["wmArgs"]["size"]["width"], height: app["wmArgs"]["size"]["height"], }, single: app["wmArgs"]["single"], resizable: app["wmArgs"]["resizable"], controls: app["wmArgs"]["controls"], message: app["wmArgs"]["message"], proxy: app["wmArgs"]["proxy"], snapable: app["wmArgs"]["snapable"], }); await window.parent.tb.fs.promises.mkdir(`/apps/user/${await window.parent.tb.user.username()}/${app.name}`); await window.parent.tb.fs.promises.writeFile(`/apps/user/${await window.parent.tb.user.username()}/${app.name}/index.json`, JSON.stringify(app)); try { let apps = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/installed.json`, "utf8")); apps.push({ name: app.name, user: await window.parent.tb.user.username(), config: `/apps/user/${await window.parent.tb.user.username()}/${app.name}/index.json`, }); await window.parent.tb.fs.promises.writeFile(`/apps/installed.json`, JSON.stringify(apps)); } catch { await window.parent.tb.fs.promises.writeFile( `/apps/installed.json`, JSON.stringify([ { name: app.name, user: await window.parent.tb.user.username(), config: `/apps/user/${await window.parent.tb.user.username()}/${app.name}/index.json`, }, ]), ); } window.parent.tb.notification.Toast({ message: `${app.name} has been installed!`, application: "App Store", iconSrc: "/fs/apps/system/app store.tapp/icon.svg", time: 5000, onOk: () => { window.parent.tb.system.openApp(app.name); }, }); return true; case "tb-liq": case "Anura": try { if (type === "tb-liq") { await window.parent.tb.system.download(app["anura-pkg"], `/apps/anura/${app.name}.zip`); } else { await window.parent.tb.system.download(`${currRepo.url.replace("manifest.json", "")}/apps/${app.package}/${app.data}`, `/apps/anura/${app.name}.zip`); } await unzip(`/apps/anura/${app.name}.zip`, `/apps/anura/${app.name}/`); await window.parent.tb.fs.promises.unlink(`/apps/anura/${app.name}.zip`); const appConf = await window.parent.tb.fs.promises.readFile(`/apps/anura/${app.name}/manifest.json`, "utf8"); const appData = JSON.parse(appConf); console.log(appData); await window.parent.tb.launcher.addApp({ name: appData.name, title: appData.wininfo.title, icon: `/fs/apps/anura/${app.name}/${appData.icon}`, src: `/fs/apps/anura/${app.name}/${appData.index}`, size: { width: appData.wininfo.width, height: appData.wininfo.height, }, single: appData.wininfo.allowMultipleInstance, }); window.parent.anura.apps[appData.package] = { title: appData.name, icon: appData.icon, id: appData.package, }; try { let apps = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/installed.json`, "utf8")); apps.push({ name: appData.name, user: await window.parent.tb.user.username(), config: `/apps/anura/${app.name}/manifest.json`, }); await window.parent.tb.fs.promises.writeFile(`/apps/installed.json`, JSON.stringify(apps)); } catch { await window.parent.tb.fs.promises.writeFile( `/apps/installed.json`, JSON.stringify([ { name: appData.name, user: await window.parent.tb.user.username(), config: `/apps/anura/${app.name}/manifest.json`, }, ]), ); } window.parent.tb.notification.Toast({ message: `${app.name} has been installed!`, application: "App Store", iconSrc: "/fs/apps/system/app store.tapp/icon.svg", time: 5000, onOk: () => { window.parent.tb.system.openApp(app.name); }, }); return true; } catch (e) { console.error("Error installing the app:", e); await window.parent.tb.sh.promises.rm(`/apps/anura/${app.name}`, { recursive: true }); window.parent.tb.notification.Toast({ message: `Failed to install ${app.name}. Check the console for details.`, application: "App Store", iconSrc: "/fs/apps/system/app store.tapp/icon.svg", time: 5000, }); return false; } case "Xen": throw new Error("Xen repo not implemented yet."); } } /** * Uninstalls the requested app * @param {Object} app - The app to uninstall * @param {string} type - The type of app (Terbium, tb-PWA, Anura, Xen) */ async function uninstall(app, type) { switch (type) { case "Terbium": if (await dirExists(`/apps/system/${app.name}.tapp`)) { await window.parent.tb.fs.shell.promises.rm(`/apps/system/${app.name}.tapp`, { recursive: true }); } else { await window.parent.tb.fs.shell.promises.rm(`/apps/user/${sessionStorage.getItem("currAcc")}/${app.name}.tapp`, { recursive: true }); } try { let installedApps = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/installed.json`, "utf8")); installedApps = installedApps.filter(a => a.name !== app.name); await window.parent.tb.fs.promises.writeFile(`/apps/installed.json`, JSON.stringify(installedApps)); } catch { throw new Error("Failed to update the installed app list"); } window.parent.tb.launcher.removeApp(app.name); window.parent.tb.notification.Toast({ message: `Successfully uninstalled ${app.name}.`, application: "App Store", iconSrc: "/fs/apps/system/app store.tapp/icon.svg", time: 5000, }); break; case "tb-PWA": const web_apps = JSON.parse(await window.parent.tb.fs.promises.readFile("/apps/web_apps.json", "utf8")); const index = web_apps.apps.indexOf(app.name.toLowerCase()); if (index > -1) { web_apps.apps.splice(index, 1); } await window.parent.tb.fs.promises.writeFile("/apps/web_apps.json", JSON.stringify(web_apps)); window.parent.tb.launcher.removeApp(app.name); await window.parent.tb.fs.promises.unlink(`/apps/user/${await window.parent.tb.user.username()}/${app.name}/index.json`); await window.parent.tb.sh.promises.rm(`/apps/user/${await window.parent.tb.user.username()}/${app.name}`, { recursive: true }); try { let installedApps = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/installed.json`, "utf8")); installedApps = installedApps.filter(a => a.name !== app.name); await window.parent.tb.fs.promises.writeFile(`/apps/installed.json`, JSON.stringify(installedApps)); } catch { throw new Error("Failed to update the installed app list"); } window.parent.tb.notification.Toast({ message: `Successfully uninstalled ${app.name}.`, application: "App Store", iconSrc: "/fs/apps/system/app store.tapp/icon.svg", time: 5000, }); break; case "Anura": case "tb-liq": await window.parent.tb.fs.shell.promises.rm(`/apps/anura/${app.name}`, { recursive: true }); try { let installedApps = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/installed.json`, "utf8")); installedApps = installedApps.filter(a => a.name !== app.name); await window.parent.tb.fs.promises.writeFile(`/apps/installed.json`, JSON.stringify(installedApps)); } catch { throw new Error("Failed to update the installed app list"); } window.parent.tb.launcher.removeApp(app.name); delete window.parent.anura.apps[app.package]; window.parent.tb.notification.Toast({ message: `Successfully uninstalled ${app.name}.`, application: "App Store", iconSrc: "/fs/apps/system/app store.tapp/icon.svg", time: 5000, }); break; } } /** * Unzips the requested file to the target dir * @param {string} path * @param {string} target */ async function unzip(path, target) { const runUnzip = async () => { const response = await fetch("/fs/" + path); const zipFileContent = await response.arrayBuffer(); if (!(await dirExists(target))) { await window.parent.tb.fs.promises.mkdir(target, { recursive: true }); } const compressedFiles = window.parent.tb.fflate.unzipSync(new Uint8Array(zipFileContent)); for (const [relativePath, content] of Object.entries(compressedFiles)) { const fullPath = `${target}/${relativePath}`; const pathParts = fullPath.split("/"); let currentPath = ""; for (let i = 0; i < pathParts.length; i++) { currentPath += pathParts[i] + "/"; if (i === pathParts.length - 1 && !relativePath.endsWith("/")) { try { console.log(`touch ${currentPath.slice(0, -1)}`); await window.parent.tb.fs.promises.writeFile(currentPath.slice(0, -1), window.parent.tb.buffer.from(content)); } catch { console.log(`Cant make ${currentPath.slice(0, -1)}`); } } else if (!(await dirExists(currentPath))) { try { console.log(`mkdir ${currentPath}`); await window.parent.tb.fs.promises.mkdir(currentPath); } catch { console.log(`Cant make ${currentPath}`); } } } if (relativePath.endsWith("/")) { try { console.log(`mkdir fp ${fullPath}`); await window.parent.tb.fs.promises.mkdir(fullPath); } catch { console.log(`Cant make ${fullPath}`); } } } return "Done!"; }; return window.parent.tb.notification.Installing( { message: "Installing package files...", application: "App Store", iconSrc: "/fs/apps/system/app store.tapp/icon.svg", }, runUnzip(), null, { message: "Failed to extract package", application: "App Store", iconSrc: "/fs/apps/system/app store.tapp/icon.svg", time: 5500, }, ); } /** * Resolves if a directory exists * @param {string} path * @returns {Promise} */ const dirExists = async path => { return new Promise(resolve => { window.parent.tb.fs.stat(path, (err, stats) => { if (err) { if (err.code === "ENOENT") { resolve(false); } else { console.error(err); resolve(false); } } else { const exists = stats.type === "DIRECTORY"; resolve(exists); } }); }); }; window.addEventListener("load", async () => { await loadRepo("https://raw.githubusercontent.com/TerbiumOS/tb-repo/refs/heads/main/manifest.json"); loadRepos(); window.parent.document.querySelector(".app-search").addEventListener("input", e => { search(e.target.value); }); }); window.addEventListener("contextmenu", e => { e.preventDefault(); }); ================================================ FILE: public/apps/app store.tapp/index.json ================================================ { "name": "App Store", "config": { "title": { "text": "App Store", "html": "
" }, "icon": "/fs/apps/system/app store.tapp/icon.svg", "src": "/fs/apps/system/app store.tapp/index.html", "size": { "width": 775, "height": 500 } } } ================================================ FILE: public/apps/browser.tapp/index.css ================================================ @font-face { font-family: Inter; src: url("/fonts/Inter.ttf"); } :root { --shell-primary: #ffffff34; --shell-secondary: #00000078; } body { font-family: Inter; font-size: 14px; font-weight: 400; line-height: 20px; color: #ffffff; background-color: transparent; margin: 0; padding: 0; overflow: hidden; border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; } body, html { height: 100%; } .topbar { width: 100%; display: flex; flex-direction: column; background-color: #000000a3; } .topbar .tab-container { padding: 4px; display: flex; align-items: center; width: calc(100% - 8px); gap: 4px; } .topbar .controls { display: flex; flex-direction: row; align-items: center; gap: 4px; width: calc(100% - calc(16px + 8px)); margin-left: 4px; margin-bottom: 4px; height: 24px; background-color: var(--shell-primary); padding: 8px; border-radius: 8px; } .topbar .controls .searchbars { width: 100%; padding: 3px 0 4px 11px; box-shadow: inset 0 0 0 1px #ffffff28; border-radius: 8px; transition: 150ms ease-in-out; } .topbar .controls .searchbars.focus { box-shadow: inset 0 0 0 2px #ffffff; } .topbar .controls input { width: 100%; height: 100%; border: none; border-radius: 6px; background-color: transparent; color: #ffffff; font-size: 13px; font-weight: 800; font-family: Inter; line-height: 0; padding: 0; outline: none; } .topbar .controls input:not(.active) { display: none; } .topbar .controls .navigation-button { width: 16px !important; height: 16px !important; min-height: 16px !important; min-width: 16px !important; stroke: #ffffff; stroke-width: 1.5px; padding: 6px; border-radius: 20px; transition: 150ms ease-in-out; } .topbar .controls .navigation-button.left-arrow { padding-right: calc(6px + 1.5px); } .topbar .controls .navigation-button.right-arrow { padding-left: calc(6px + 1.5px); } .topbar .controls .navigation-button:hover { background-color: #ffffff28; } .topbar .controls .navigation-button:not(.disabled) { cursor: var(--cursor-pointer); } .topbar .controls .navigation-button.disabled { opacity: 0.5; } .topbar .tabs { display: flex; gap: 4px; overflow: hidden; max-width: calc(100% - 38px); } .topbar .tabs .tab { padding: 6px; padding-left: 8px; width: 100px; height: 24px; display: flex; flex-direction: row; align-items: center; justify-content: space-between; border-radius: 6px; transition-property: opacity; transition-duration: 150ms; transition-timing-function: ease-in-out; } .topbar .tabs .tab.active { background-color: var(--shell-primary); } .topbar .tabs .tab:not(.active) { background-color: transparent; } .topbar .tabs .tab:not(.active):hover { opacity: 0.8; } .topbar .tabs .tab .tab-title { font-weight: 400; line-height: 0; color: #ffffff; pointer-events: none; user-select: none; } .topbar .tabs .tab .tab-close { width: 18px; height: 18px; border-radius: 50%; padding: 2px; transition: 150ms ease-in-out; } .topbar .tabs .tab .tab-close:hover { background-color: #ffffff28; } .topbar .new-tab { width: 26px; height: 26px; border-radius: 6px; transition: 150ms ease-in-out; } .topbar .new-tab:hover { background-color: #ffffff28; } main { width: 100%; height: calc(100% - calc(var(--topbar-height) + 10px)); display: flex; flex-direction: column; align-items: center; justify-content: center; overflow: hidden; } main .tab-content { width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; gap: 8px; overflow: hidden; border: none; } main .tab-content:not(.active) { display: none; } .sidebar { width: 200px; height: 100%; padding: 10px 8px 8px 10px; flex-direction: column; gap: 6px; } .set-search { width: 150px; background-color: rgba(0, 0, 0, 0.396); color: rgb(255, 255, 255); font-size: 14px; font-weight: 800; font-family: Inter; line-height: 0; border-width: initial; border-style: none; border-color: initial; border-image: initial; border-radius: 6px; padding: 4px 0px 6px 12px; outline: none; } .sidebar ul { list-style: none; padding: 0; } .sidebar li { margin-bottom: 10px; cursor: var(--cursor-pointer); } .content { margin-left: 250px; padding: 20px; margin-top: -54%; } .section { display: none; transition: opacity 0.5s ease; opacity: 0; } .section.active { display: block; opacity: 1; animation: fade-in 0.5s ease-in-out forwards; } button { background-color: #ffffff28; border-radius: 6px; padding: 6px 8px; border: none; color: #ffffff; font-weight: 600; font-size: 14px; font-family: Inter; cursor: var(--cursor-pointer); transition: 150ms ease-in-out; } ================================================ FILE: public/apps/browser.tapp/index.html ================================================ Terbium Browser
================================================ FILE: public/apps/browser.tapp/index.js ================================================ const Filer = window.parent.tb.fs; const IS_URL = /^(https?:\/\/)?(www\.)?([-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}|localhost)\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; const create_new_id = () => { const id = Math.random().toString(36).substr(2, 9); if (document.getElementById(id)) { return create_new_id(); } return id; }; function customEncode(input) { if (input) { let str = input.toString(); let charArray = str.split(""); let encodedArray = charArray.map((char, index) => { if (index % 2) { return String.fromCharCode(2 ^ char.charCodeAt()); } else { return char; } }); let encodedString = encodedArray.join(""); let finalResult = encodeURIComponent(encodedString); return finalResult; } else { return input; } } function customDecode(encodedString) { if (!encodedString) return encodedString; let [firstPart, ...restParts] = encodedString.split("?"); let decodedString = decodeURIComponent(firstPart) .split("") .map((char, index) => (index % 2 ? String.fromCharCode(2 ^ char.charCodeAt(0)) : char)) .join(""); let finalResult = decodedString + (restParts.length ? "?" + restParts.join("?") : ""); return finalResult; } const topbar_height = document.querySelector(".topbar").getBoundingClientRect().height; document.body.style.setProperty(`--topbar-height`, `${document.querySelector(".topbar").getBoundingClientRect().height}px`); const new_tab = document.querySelector(".new-tab"); new_tab.addEventListener("click", () => { newTab(); }); function closeTab(id) { const tab = document.getElementById(id); const tab_content = document.getElementById(id + "-content"); tab.remove(); window.parent.tb.mediaplayer.hide(); tab_content.remove(); const lastTab = document.querySelector(".tab:last-child"); const lastTabContent = document.querySelector(".tab-content:last-child"); if (lastTab && lastTabContent) { lastTab.classList.add("active"); lastTabContent.classList.add("active"); } if (!document.querySelector(".tab")) { newTab(); } } function newTab() { let updateTab = false; let interval; const id = create_new_id(); const tab = document.createElement("div"); tab.classList.add("tab"); tab.id = id; const tab_title = document.createElement("div"); tab_title.classList.add("tab-title"); tab_title.innerHTML = "New Tab"; tab.appendChild(tab_title); const tab_close = document.createElement("img"); tab_close.classList.add("tab-close"); tab_close.src = "/apps/browser.tapp/close.svg"; tab.appendChild(tab_close); document.querySelectorAll(".tab").forEach(otab => { if (otab.id != tab.id) { otab.classList.remove("active"); } }); if (!localStorage.getItem("defUrl")) { localStorage.setItem("defUrl", "about:newtab"); } tab.classList.add("active"); document.querySelector(".tabs").appendChild(tab); const urlbar = document.createElement("input"); const user = window.parent.sessionStorage.getItem("currAcc"); urlbar.classList.add("urlbar"); urlbar.addEventListener("focus", e => { document.querySelector(".searchbars").classList.add("focus"); }); urlbar.addEventListener("blur", e => { document.querySelector(".searchbars").classList.remove("focus"); }); urlbar.id = id + "-urlbar"; urlbar.type = "text"; urlbar.placeholder = "Search or enter a URL"; urlbar.autocomplete = "off"; urlbar.spellcheck = false; urlbar.addEventListener("focus", () => { urlbar.select(); }); document.querySelector(".searchbars").appendChild(urlbar); document.querySelectorAll(".urlbar").forEach(ourlbar => { if (ourlbar.id != urlbar.id) { ourlbar.classList.remove("active"); } }); urlbar.classList.add("active"); urlbar.addEventListener("click", () => { updateTab = true; }); urlbar.addEventListener("keydown", e => { if (e.key === "Enter") { updateTab = false; const url = urlbar.value.trim(); const activeTab = document.querySelector(".tab.active"); const activeTabContent = document.querySelector(".tab-content.active"); const localhostRegex = /^(https?:\/\/)?localhost(:\d+)?(\/.*)?$/i; const localhostMatch = url.match(localhostRegex); if (localhostMatch) { const port = localhostMatch[2] ? parseInt(localhostMatch[2].substring(1)) : 80; const serverUrl = window.parent.tb.node.servers.get(port); if (serverUrl) { activeTabContent.src = serverUrl; return; } } switch (url) { case "about:settings": activeTabContent.src = "/apps/browser.tapp/settings.html"; break; case "about:newtab": activeTabContent.src = "/apps/browser.tapp/newtab.html"; break; case "about:userscripts": activeTabContent.src = "/apps/browser.tapp/userscripts.html"; break; default: const input = url; Filer.promises.readFile(`/home/${user}/settings.json`, "utf8").then(async data => { let settings = JSON.parse(data); const searchEngine = localStorage.getItem("sEngine") || "https://google.com/search?q="; const isUrl = /^(https?:\/\/)|(localhost(:\d+)?([\/?]|$))|([a-z0-9\-]+\.[a-z]{2,})/i.test(input) && !/\s/.test(input); let targetUrl; if (isUrl) { targetUrl = input.startsWith("http") ? input : `https://${input}`; } else { targetUrl = `${searchEngine}${encodeURIComponent(input)}`; } if (settings.proxy === "Scramjet") { activeTabContent.src = `${window.location.origin}/service/${await window.parent.tb.proxy.encode(targetUrl, "XOR")}`; } else { activeTabContent.src = `${window.location.origin}/uv/service/${await window.parent.tb.proxy.encode(targetUrl, "XOR")}`; } }); break; } activeTabContent.onload = () => { const pageTitle = activeTabContent.contentDocument.title || "Untitled"; const maxTitleLength = 8; const tabTitle = pageTitle.length > maxTitleLength ? pageTitle.substring(0, maxTitleLength) + "..." : pageTitle; activeTab.querySelector(".tab-title").innerHTML = tabTitle; }; } }); const tab_content = document.createElement("iframe"); Filer.promises.readFile(`/home/${user}/settings.json`, "utf8").then(data => { let settings = JSON.parse(data); let proxy = settings["proxy"]; console.log(proxy); console.log(localStorage.getItem("defUrl")); if (localStorage.getItem("defUrl") === "about:newtab") { urlbar.value = "about:newtab"; updateTab = true; tab_content.src = "/apps/browser.tapp/newtab.html"; tab_content.addEventListener("load", () => { tab_content.contentWindow.addEventListener("updTab", () => { updateTab = false; tab_content.contentWindow.removeEventListener("updTab", arguments.callee); }); }); } else { if (proxy === "Ultraviolet") { tab_content.src = parent.window.location.origin + "/uv/service/" + customEncode(localStorage.getItem("defUrl") || "about:newtab"); } else if (proxy === "Scramjet") { tab_content.src = parent.window.location.origin + "/service/" + customEncode(localStorage.getItem("defUrl") || "about:newtab"); } } }); const unloadHandler = function () { setTimeout(function () { const pageTitle = tab_content.contentDocument.title || "Untitled"; const maxTitleLength = 8; const tabTitle = pageTitle.length > maxTitleLength ? pageTitle.substring(0, maxTitleLength) + "..." : pageTitle; tab_title.innerHTML = tabTitle; }, 0); }; tab_content.classList.add("tab-content"); tab_content.id = id + "-content"; const inject_engines = () => { if (document.querySelector(".left-arrow").classList.contains("disabled")) { document.querySelector(".left-arrow").classList.remove("disabled"); } if ("__uv$location" in tab_content.contentDocument) { if (updateTab === false) { urlbar.value = tab_content.contentDocument.__uv$location?.href; } } else { if (updateTab === false) { window.parent.tb.proxy.decode(tab_content.contentWindow.window.location.href.replace(/^.*\/service\//, ""), "XOR").then(decodedUrl => { urlbar.value = decodedUrl; }); } } if (!tab_content.contentDocument.getElementById("tb-cursor-controller")) { const cursor_controller = document.createElement("script"); cursor_controller.src = "/cursor_changer.js"; cursor_controller.id = "tb-cursor-controller"; tab_content.contentDocument.head.appendChild(cursor_controller); } if (!tab_content.contentDocument.getElementById("tb-media-controller")) { const media_controller = document.createElement("script"); media_controller.src = "/media_interactions.js"; media_controller.id = "tb-media-controller"; tab_content.contentDocument.head.appendChild(media_controller); } if (!tab_content.contentDocument.getElementById("userscript-container")) { const userscript_container = document.createElement("div"); userscript_container.id = "userscript-container"; Filer.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/browser/userscripts.json`).then(async data => { const dat = JSON.parse(data); for (const script of dat) { if (urlbar.value.includes(script.match)) { const user_script = document.createElement("script"); console.log("Script: %a\nScript path: %b", script.name, script.file); user_script.type = "text/javascript"; user_script.text = await Filer.promises.readFile(script.file, "utf8"); user_script.type = "text/javascript"; user_script.id = `userscript-${script.name}`; user_script.setAttribute("data-script-path", script.file); userscript_container.appendChild(user_script); } else if (script.match === "*://*/*") { const user_script = document.createElement("script"); console.log("Script: %a\nScript path: %b", script.name, script.file); user_script.type = "text/javascript"; user_script.text = await Filer.promises.readFile(script.file, "utf8"); user_script.type = "text/javascript"; user_script.id = `userscript-${script.name}`; user_script.setAttribute("data-script-path", script.file); userscript_container.appendChild(user_script); } else { console.log(`Skipping script ${script.name}`); } } }); tab_content.contentDocument.head.appendChild(userscript_container); console.log("Injected userscripts into the frame"); } if (!tab_content.contentDocument._hasKeydownListener) { tab_content.contentDocument.addEventListener("keydown", e => { if (e.altKey && e.key.toLowerCase() === "t") { newTab(); } else if (e.altKey && e.key.toLowerCase() === "w") { closeTab(document.querySelector(".tab.active").id); } else if (e.altKey && e.key.toLowerCase() === "r") { const activeTabContent = document.querySelector(".tab-content.active"); window.parent.tb.mediaplayer.hide(); activeTabContent.contentWindow.location.reload(); } else if (e.altKey && e.key.toLowerCase() === "b") { pwaIns(); } else if (e.altKey && e.key.toLowerCase() === "k") { showTabs(); } else if (e.altKey && e.key.toLowerCase() === "i") { if (typeof window.eruda === "undefined") { eruda.init(); } else { if (window.eruda._isInit) { window.eruda.destroy(); } else { window.eruda.init(); } } } }); tab_content.contentDocument._hasKeydownListener = true; } }; if (!interval) { interval = setInterval(inject_engines, 1000); } tab_content.onload = () => { if (!interval) { interval = setInterval(inject_engines, 1000); } const pageTitle = tab_content.contentDocument.title || "Untitled"; const maxTitleLength = 8; const tabTitle = pageTitle.length > maxTitleLength ? pageTitle.substring(0, maxTitleLength) + "..." : pageTitle; tab_title.innerHTML = tabTitle; let url = tab_content.src; url = url.replace(parent.window.location.origin, ""); url = url.replace("/uv/service/", ""); url = url.replace("/service/", ""); tab_content.contentWindow.removeEventListener("unload", unloadHandler); tab_content.contentWindow.addEventListener("unload", unloadHandler); tab_content.contentWindow.addEventListener("load", unloadHandler); tab_content.contentWindow.removeEventListener("load", unloadHandler); }; document.querySelector("main").appendChild(tab_content); document.querySelectorAll(".tab-content").forEach(otab => { if (otab.id != tab_content.id) { otab.classList.remove("active"); } }); tab_content.classList.add("active"); tab.addEventListener("click", e => { if (e.target.classList.contains("tab-close")) return; if (e.target.classList.contains("tab-close")) { tab.remove(); window.parent.tb.mediaplayer.hide(); tab_content.remove(); clearInterval(interval); } document.querySelectorAll(".tab").forEach(otab => { if (otab.id != tab.id) { otab.classList.remove("active"); } }); document.querySelectorAll(".tab-content").forEach(otab => { if (otab.id != tab_content.id) { otab.classList.remove("active"); } }); tab.classList.add("active"); tab_content.classList.add("active"); document.querySelectorAll(".urlbar").forEach(ourlbar => { if (ourlbar.id != urlbar.id) { ourlbar.classList.remove("active"); } }); urlbar.classList.add("active"); let url = tab_content.src; url = url.replace(parent.window.location.origin, ""); url = url.replace("/uv/service/", ""); url = url.replace("/service/", ""); if (!url.includes("about:")) { urlbar.value = customDecode(url); } }); tab_close.addEventListener("click", () => { closeTab(id); clearInterval(interval); }); } window.addEventListener("keypress", e => { if (e.shiftKey && e.key === "W") { e.preventDefault(); newTab(); } else if (e.shiftKey && e.key === "Q") { e.preventDefault(); const tabId = document.querySelector(".tab.active").id; closeTab(tabId); } }); window.onload = () => { newTab(); let tabs = document.querySelector(".tabs"); tabs.addEventListener("wheel", e => { if (e.deltaY > 0) { tabs.scrollLeft += 100; } else { tabs.scrollLeft -= 100; } }); }; document.querySelector(".refresh-button").addEventListener("click", () => { const activeTabContent = document.querySelector(".tab-content.active"); window.parent.tb.mediaplayer.hide(); activeTabContent.contentWindow.location.reload(); }); document.querySelector(".navigate-back").addEventListener("click", () => { if (window.history.canGoBack) { document.querySelector(".right-arrow").classList.remove("disabled"); window.parent.tb.mediaplayer.hide(); window.history.back(); } }); document.querySelector(".ext-btn").addEventListener("click", () => { const activeTabContent = document.querySelector(".tab-content.active"); activeTabContent.contentWindow.location.href = "/apps/browser.tapp/userscripts.html"; const activeUrlbar = document.querySelector(".urlbar.active"); activeUrlbar.value = "about:userscripts"; }); document.querySelector(".navigate-forward").addEventListener("click", () => { if (window.history.canGoForward) { window.parent.tb.mediaplayer.hide(); window.history.forward(); } }); document.querySelector(".fav-button").addEventListener("click", async () => { const activeTabContent = document.querySelector(".tab-content.active"); const dat = JSON.parse(await Filer.promises.readFile(`/apps/user/${await window.parent.tb.user.username()}/browser/favorites.json`, "utf8")); const favicon = activeTabContent.contentDocument.querySelector("link[rel~='icon']")?.href || activeTabContent.contentDocument.querySelector("link[rel='shortcut icon']")?.href || "/apps/browser.tapp/icon.svg"; dat.push({ title: activeTabContent.contentDocument.title || "Untitled", icon: favicon, url: await tb.proxy.decode(activeTabContent.src.replace(window.location.origin, "").replace(/\/uv\/service\/|\/service\//, ""), "XOR"), }); await Filer.promises.writeFile(`/apps/user/${await window.parent.tb.user.username()}/browser/favorites.json`, JSON.stringify(dat)); }); const pwaIns = async () => { const activeTabContent = document.querySelector(".tab-content.active"); const pageTitle = activeTabContent.contentDocument.title || "Untitled"; const maxTitleLength = 8; let tabTitle; if (pageTitle.includes(" - ")) { tabTitle = pageTitle.split(" - ")[0].trim(); } else if (pageTitle.includes("|")) { tabTitle = pageTitle.split("|")[0].trim(); } else { tabTitle = pageTitle.split(" ")[0].trim(); } if (!tabTitle) { tabTitle = pageTitle.length > maxTitleLength ? pageTitle.substring(0, maxTitleLength) : pageTitle; } const favicon = activeTabContent.contentDocument.querySelector("link[rel~='icon']")?.href || activeTabContent.contentDocument.querySelector("link[rel='shortcut icon']")?.href || "/apps/browser.tapp/icon.svg"; let data = JSON.parse(await Filer.promises.readFile("/apps/web_apps.json", "utf8")); await Filer.exists(`/apps/user/${await window.parent.tb.user.username()}/${tabTitle}/index.json`, async exists => { let apps = data.apps; if (apps.includes(tabTitle.toLowerCase()) || exists) { let index = apps.indexOf(tabTitle.toLowerCase()); apps.splice(index, 1); data.apps = apps; await Filer.promises.writeFile("/apps/web_apps.json", JSON.stringify(data)); window.parent.tb.launcher.removeApp(tabTitle); await new Filer.Shell().promises.rm(`/apps/user/${await window.parent.tb.user.username()}/${tabTitle}`, { recursive: true }); } else { apps.push(tabTitle.toLowerCase()); await Filer.promises.writeFile("/apps/web_apps.json", JSON.stringify(data)); window.parent.tb.notification.Toast({ message: `${tabTitle} has been installed!`, application: "App Store", iconSrc: "/fs/apps/system/app store.tapp/icon.svg", time: 5000, }); await window.parent.tb.launcher.addApp({ title: tabTitle, name: tabTitle, icon: `${window.location.origin}/uv/service/${await window.parent.tb.proxy.encode(favicon, "XOR")}`, src: await window.parent.tb.proxy.decode( document .querySelector(".tab-content.active") .src.replace(window.location.origin, "") .replace(/\/uv\/service\/|\/service\//, ""), "XOR", ), size: { width: 600, height: 500, }, proxy: true, }); console.log(tabTitle); } }); }; const showTabs = () => { if (document.querySelector(".tab-container").style.display === "none") { document.querySelector(".tab-container").style.display = "flex"; document.querySelector("main").style.height = `calc(100% - calc(var(--topbar-height) + 10px))`; document.querySelector(".controls").style.marginTop = "0px"; } else { document.querySelector(".controls").style.marginTop = "5px"; document.querySelector("main").style.height = `100%`; document.querySelector(".tab-container").style.display = "none"; } }; const newengine = () => { window.parent.tb.dialog.Message({ title: "Enter a new search engine", defaultValue: "https://google.com/search?q=", onOk: value => { localStorage.setItem("sEngine", value); }, }); }; const nt = () => { window.parent.tb.dialog.Message({ title: "Enter a new start page", defaultValue: "about:newtab", onOk: value => { localStorage.setItem("defUrl", value); }, }); }; document.querySelector(".opt-menu").addEventListener("click", () => { const rect = document.querySelector(".opt-menu").getBoundingClientRect(); window.parent.tb.contextmenu.create({ x: rect.x - 150, y: rect.y + 125, options: [ { text: "Toggle Tabs", click: () => showTabs(), }, { text: "Add site as PWA (beta)", click: async () => pwaIns(), }, { text: "Change default search engine", click: async () => newengine(), }, { text: "Change new tab page", click: async () => nt(), }, { text: "Toggle Eruda", click: () => { if (typeof window.eruda === "undefined") { eruda.init(); } else { if (window.eruda._isInit) { window.eruda.destroy(); } else { window.eruda.init(); } } }, }, ], }); }); window.addEventListener("keydown", e => { if (e.altKey && e.key.toLowerCase() === "t") { newTab(); } else if (e.altKey && e.key.toLowerCase() === "w") { closeTab(document.querySelector(".tab.active").id); } else if (e.altKey && e.key.toLowerCase() === "r") { const activeTabContent = document.querySelector(".tab-content.active"); window.parent.tb.mediaplayer.hide(); activeTabContent.contentWindow.location.reload(); } else if (e.altKey && e.key.toLowerCase() === "b") { pwaIns(); } else if (e.altKey && e.key.toLowerCase() === "k") { showTabs(); } else if (e.altKey && e.key.toLowerCase() === "i") { if (typeof window.eruda === "undefined") { eruda.init(); } else { if (window.eruda._isInit) { window.eruda.destroy(); } else { window.eruda.init(); } } } }); ================================================ FILE: public/apps/browser.tapp/index.json ================================================ { "name": "Browser", "proxy": false, "config": { "title": "Browser", "icon": "/apps/browser.tapp/icon.svg", "src": "/apps/browser.tapp/index.html" } } ================================================ FILE: public/apps/browser.tapp/newtab.html ================================================ New Tab
Terbium Browser
================================================ FILE: public/apps/browser.tapp/userscripts.html ================================================ Userscripts
UserScripts (beta)
================================================ FILE: public/apps/calculator.tapp/index.css ================================================ @font-face { font-family: Inter; src: url("/fonts/Inter.ttf"); } html, body { height: 100%; margin: 0; padding: 0; overflow: hidden; } body { font-family: Inter; font-size: 14px; font-weight: 400; line-height: 20px; color: #ffffff; background-color: transparent; } .topbar { height: 96px; padding: 0 10px; display: flex; flex-direction: column; justify-content: center; } .eq { width: 100%; background-color: transparent; border: none; outline: none; pointer-events: none; color: #ffffff; font-family: Inter; font-size: 48px; font-weight: 800; line-height: 48px; text-align: right; } .buttons { display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(5, 1fr); background-color: #ffffff28; gap: 6px; padding: 8px; border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; overflow: hidden; } .buttons button { width: 76px; height: 76px; background-color: transparent; border: none; border-radius: 10px; outline: none; font-family: Inter; font-size: 20px; font-weight: 800; line-height: 20px; padding: 0; color: #ffffff; cursor: var(--cursor-pointer); transition: 150ms ease-in-out; } .buttons button svg { width: 26px; height: 26px; } .buttons button:hover { background-color: #ffffff42; } .buttons button.two-space { grid-column: span 2; width: 158px; } ================================================ FILE: public/apps/calculator.tapp/index.html ================================================ Calculator
================================================ FILE: public/apps/calculator.tapp/index.js ================================================ document.querySelectorAll(".cbtn").forEach(btn => { btn.addEventListener("click", () => { const display = document.querySelector(".eq"); const value = btn.getAttribute("data-value"); switch (value) { default: const displayValue = display.value; const newValue = displayValue + value; if (display.value === "0") { if (value === ".") return; if (value === "0") return; if (value === "+") return; if (value === "-") return; if (value === "*") return; if (value === "/") return; } if (display.getAttribute("is-equal") === "true") { display.value = ""; display.value = value; display.setAttribute("is-equal", "false"); } else { if (display.value === "0") { display.value = value; } else { display.value = newValue; } } break; case "clear": display.value = "0"; display.setAttribute("is-equal", "false"); break; case "del": if (display.value.length === 1) { display.value = "0"; display.setAttribute("is-equal", "false"); break; } else { const delValue = display.value.slice(0, -1); display.value = delValue; display.setAttribute("is-equal", "false"); } break; case "equal": const eqValue = math.evaluate(display.value); display.value = eqValue; display.setAttribute("is-equal", "true"); break; case "np": const npValue = display.value * -1; display.value = npValue; display.setAttribute("is-equal", "false"); break; } }); }); window.addEventListener("keydown", e => { let availableKeys = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "+", "-", "*", "/", "%", "Backspace", "Enter"]; if (availableKeys.includes(e.key)) { const display = document.querySelector(".eq"); const value = e.key; switch (value) { default: const displayValue = display.value; const newValue = displayValue + value; if (display.getAttribute("is-equal") == "true") { display.value = ""; display.value = value; display.setAttribute("is-equal", "false"); } else { if (display.value === "0") { console.log("zero"); display.value = value; } else { display.value = newValue; } } break; case "-": if (display.value === "0") return; display.value = display.value + value; if (display.getAttribute("is-equal") == "true") display.setAttribute("is-equal", "false"); break; case "+": if (display.value === "0") return; display.value = display.value + value; if (display.getAttribute("is-equal") == "true") display.setAttribute("is-equal", "false"); break; case "*": if (display.value === "0") return; display.value = display.value + value; if (display.getAttribute("is-equal") == "true") display.setAttribute("is-equal", "false"); break; case "/": e.preventDefault(); if (display.value === "0") return; display.value = display.value + value; if (display.getAttribute("is-equal") == "true") display.setAttribute("is-equal", "false"); break; case "Backspace": if (display.value.length === 1) { display.value = "0"; display.setAttribute("is-equal", "false"); break; } else { const delValue = display.value.slice(0, -1); display.value = delValue; display.setAttribute("is-equal", "false"); } break; case "Enter": e.preventDefault(); const eqValue = math.evaluate(display.value); display.value = eqValue; display.setAttribute("is-equal", "true"); break; } } }); ================================================ FILE: public/apps/calculator.tapp/index.json ================================================ { "name": "Calculator", "config": { "title": "Calculator", "icon": "/fs/apps/system/calculator.tapp/icon.svg", "src": "/fs/apps/system/calculator.tapp/index.html", "snapable": false, "maximizable": false, "size": { "width": 338, "height": 556 } } } ================================================ FILE: public/apps/feedback.tapp/index.json ================================================ { "name": "Feedback", "config": { "title": "Feedback", "icon": "/fs/apps/system/feedback.tapp/icon.svg", "src": "https://forms.gle/m664xxmrugWQADQt9", "proxy": true, "size": { "width": 600, "height": 500 } } } ================================================ FILE: public/apps/files.tapp/cm.css ================================================ @keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes fade-out { 0% { opacity: 1; } 100% { opacity: 0; } } .fade-in { animation: fade-in 150ms ease-in-out forwards; } .fade-out { animation: fade-out 150ms ease-in-out forwards; } body .context-menu { background-color: #ffffff1f; color: #ffffff; } body.blurry .context-menu { backdrop-filter: blur(100px); } .context-menu { position: absolute; background-color: #ffffff1f; color: #fff; z-index: 1; display: flex; flex-direction: column; width: 200px; border-radius: 8px; overflow: hidden; backdrop-filter: blur(10px); } .context-menu-button { background-color: transparent; display: flex; font-size: 15.5px; font-weight: 700; line-height: 18px; padding: 8px 10px; border-color: transparent; transition: 150ms ease-in-out; cursor: var(--cursor-pointer); } body .context-menu-button:hover { background-color: #ffffff1f; } ================================================ FILE: public/apps/files.tapp/extensions.json ================================================ { "image": [ "ase", "art", "bmp", "blp", "cd5", "cit", "cpt", "cr2", "cut", "dds", "dib", "djvu", "egt", "exif", "gif", "gpl", "grf", "icns", "ico", "iff", "jng", "jpeg", "jpg", "jfif", "jp2", "jps", "lbm", "max", "miff", "mng", "msp", "nef", "nitf", "ota", "pbm", "pc1", "pc2", "pc3", "pcf", "pcx", "pdn", "pgm", "PI1", "PI2", "PI3", "pict", "pct", "pnm", "pns", "ppm", "psb", "psd", "pdd", "psp", "px", "pxm", "pxr", "qfx", "raw", "rle", "sct", "sgi", "rgb", "int", "bw", "tga", "tiff", "tif", "vtf", "xbm", "xcf", "xpm", "3dv", "amf", "ai", "awg", "cgm", "cdr", "cmx", "dxf", "e2d", "egt", "eps", "fs", "gbr", "odg", "svg", "stl", "vrml", "x3d", "sxd", "v2d", "vnd", "wmf", "emf", "art", "xar", "png", "webp", "jxr", "hdp", "wdp", "cur", "ecw", "iff", "lbm", "liff", "nrrd", "pam", "pcx", "pgf", "sgi", "rgb", "rgba", "bw", "int", "inta", "sid", "ras", "sun", "tga", "heic", "heif" ], "video": ["3g2", "3gp", "aaf", "asf", "avchd", "avi", "drc", "flv", "m2v", "m3u8", "m4p", "m4v", "mkv", "mng", "mov", "mp2", "mp4", "mpe", "mpeg", "mpg", "mpv", "mxf", "nsv", "ogg", "ogv", "qt", "rm", "rmvb", "roq", "svi", "vob", "webm", "wmv", "yuv"], "audio": ["aac", "aiff", "ape", "it", "m3u", "m4a", "mid", "mod", "mp2", "mp3", "mpa", "oga", "ogg", "opus", "s3m", "sid", "wav", "weba", "xm"], "extractables": ["zip", "gz", "deflate"], "text": ["json", "ts", "js", "css", "cpp", "py3", "py", "java", "c", "env", "txt", "text"], "animated": ["gif", "apng", "webp", "flif", "mng"], "pdf": ["pdf", "pdfa", "pdfx"] } ================================================ FILE: public/apps/files.tapp/files.com.js ================================================ const tb = parent.window.tb; const tb_island = tb.window.island; const tb_window = tb.window; const tb_context_menu = tb.context_menu; const tb_dialog = tb.dialog; const appIsland = window.parent.document.querySelector(".app_island"); tb_island.addControl({ text: "File", appname: "Files", id: "files_file", click: () => { let isTrash = document.querySelector(".exp").getAttribute("path") === "/home/trash" ? true : false; tb.contextmenu.create({ x: 6, y: appIsland.clientHeight + 12, iframe: false, options: [ isTrash ? null : { text: "New File", click: async () => { try { const response = await tb.dialog.Message({ title: "Enter a name for the new file", defaultValue: "", onOk: async fileName => { const path = document.querySelector(".exp").getAttribute("path"); const createFile = async (path, fileName) => { await window.parent.tb.fs.exists(`${path}/${fileName}`, async exists => { if (exists) { const ask = await tb.dialog.Message({ title: `This file already exists. Enter a new name for ${fileName}`, defaultValue: "", }); if (ask !== undefined && ask !== "") { await createFile(path, ask); } } else { let sh = tb.sh; await sh.touch(`${path}/${fileName}`, ""); openPath(path); } }); }; await createFile(path, fileName); }, }); } catch (error) { console.error(error); } }, }, isTrash ? null : { text: "New Folder", click: async () => { const response = await tb.dialog.Message({ title: "Enter a name for the new folder", defaultValue: "", onOk: async response => { const path = document.querySelector(".exp").getAttribute("path"); const createUniqueFolder = async (path, folderName, number = null) => { const folderPath = `${path}/${folderName}${number !== null ? ` (${number})` : ""}`; const exists = await window.parent.tb.fs.promises.exists(folderPath); if (exists) { return createUniqueFolder(path, folderName, number !== null ? number + 1 : 2); } else { await window.parent.tb.fs.promises.mkdir(folderPath); } }; await createUniqueFolder(path, response); openPath(path); }, }); }, }, isTrash ? null : { text: "Upload from Computer", click: () => { const fauxput = document.createElement("input"); fauxput.type = "file"; fauxput.multiple = true; fauxput.onchange = async e => { for (const file of e.target.files) { const content = await file.arrayBuffer(); const path = document.querySelector(".exp").getAttribute("path"); const filePath = `${path}/${file.name}`; if (await window.parent.tb.fs.promises.exists(filePath)) { await tb.dialog.Message({ title: `File "${file.name}" already exists`, defaultValue: file.name, onOk: async newFileName => { if (newFileName !== null && newFileName !== "") { await window.parent.tb.fs.promises.writeFile(`${path}/${newFileName}`, window.parent.tb.buffer.from(content), "arraybuffer"); } }, }); } else { await window.parent.tb.fs.promises.writeFile(filePath, window.parent.tb.buffer.from(content), "arraybuffer"); } } openPath(document.querySelector(".nav-input.dir").value); }; fauxput.click(); }, }, ], }); }, }); tb_island.addControl({ text: "View", appname: "Files", id: "files_view", click: () => { const options = [ // { // text: "Settings", // click: () => { // window.tb.window.create({ // title: `Settings`, // icon: "/apps/files.tapp/icon.svg", // src: "/apps/files.tapp/settings.tapp/index.html", // controls: ["minimize", "close"] // }) // } // }, { text: "Go To", click: async () => { const response = await tb.dialog.Message({ title: "Enter the directory to navigate to", defaultValue: "", onOk: async response => { try { openPath(response); } catch { tb.dialog.Alert({ title: "Error", message: `Cannot find ${response}. Check your spelling and try again.`, }); } }, }); }, }, ]; tb.contextmenu.create({ x: 6, y: appIsland.clientHeight + 12, iframe: false, options: options, }); }, }); tb_island.addControl({ text: "Network Storage", appname: "Files", id: "files_net", click: () => { const options = [ { text: "Map Dav Drive", click: async () => { await tb.dialog.Message({ title: "Enter a name for the Dav Drive", defaultValue: "", onOk: async res1 => { await tb.dialog.Message({ title: "Enter the URL for the Dav Drive", defaultValue: "", onOk: async res2 => { await tb.dialog.WebAuth({ title: "Enter the credentials for the Dav Drive", onOk: async (username, password) => { const davjson = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${await tb.user.username()}/files/davs.json`, "utf8")); davjson.push({ name: res1, url: res2, username: username, password: password, }); await window.parent.tb.fs.promises.writeFile(`/apps/user/${await tb.user.username()}/files/davs.json`, JSON.stringify(davjson, null, 2)); const config = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${await tb.user.username()}/files/config.json`, "utf8")); config.drives[res1] = `/mnt/${res1}/`; await window.parent.tb.fs.promises.writeFile(`/apps/user/${await tb.user.username()}/files/config.json`, JSON.stringify(config, null, 2)); await tb.notification.Toast({ application: "System", iconSrc: "/fs/apps/system/about.tapp/icon.svg", message: "New Dav Device has been added", }); openPath(document.querySelector(".nav-input.dir").value); window.dispatchEvent(new Event("updcfg")); }, }); }, }); }, }); }, }, { text: "UnMap Dav Drive", click: async () => { await tb.dialog.Message({ title: "Enter the name of the Dav Drive to remove", defaultValue: "", onOk: async response => { const davjson = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${await tb.user.username()}/files/davs.json`, "utf8")); const index = davjson.findIndex(entry => entry.name.toLowerCase() === response.toLowerCase()); if (index !== -1) { davjson.splice(index, 1); await window.parent.tb.fs.promises.writeFile(`/apps/user/${await tb.user.username()}/files/davs.json`, JSON.stringify(davjson, null, 2)); const config = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${await tb.user.username()}/files/config.json`, "utf8")); delete config.drives[response.toLowerCase()]; await window.parent.tb.fs.promises.writeFile(`/apps/user/${await tb.user.username()}/files/config.json`, JSON.stringify(config, null, 2)); await tb.notification.Toast({ application: "System", iconSrc: "/fs/apps/system/about.tapp/icon.svg", message: "Dav Drive has been removed", }); openPath(document.querySelector(".nav-input.dir").value); window.dispatchEvent(new Event("updcfg")); } else { await tb.notification.Toast({ application: "System", iconSrc: "/fs/apps/system/about.tapp/icon.svg", message: "Dav Drive not found", }); } }, }); }, }, ]; tb.contextmenu.create({ x: 6, y: appIsland.clientHeight + 12, iframe: false, options: options, }); }, }); tb_island.addControl({ text: "Empty Trash", appname: "Files", id: "files-et", click: () => { emptyTrash(); }, }); parent.document.querySelector(`[control-id="files-et"]`)?.classList.add("hidden"); ================================================ FILE: public/apps/files.tapp/icons.json ================================================ { "ext-to-name": { "js": "JavaScript", "html": "HTML", "css": "CSS", "php": "PHP", "py": "Python", "java": "Java", "c": "C", "h": "C", "c++": "C++", "cpp": "C++", "cxx": "C++", "cc": "C++", "c#": "C#", "cs": "C#", "rb": "Ruby", "swift": "Swift", "kt": "Kotlin", "kts": "Kotlin", "ts": "TypeScript", "tsx": "TypeScript", "rs": "Rust", "dart": "Dart", "hs": "Haskell", "ex": "Elixir", "exs": "Elixir", "clj": "Clojure", "cljs": "Clojure", "cljr": "Clojure", "cljc": "Clojure", "edn": "Clojure", "lua": "Lua", "json": "JSON", "xml": "XML", "yaml": "YAML", "yml": "YAML", "zip": "ZIP", "zig": "Zig", "ml": "OCaml", "mli": "OCaml", "env": "ENV", "config": "ENV", "jsx": "JavaScript", "cjs": "JavaScript", "mjs": "JavaScript", "jar": "Java", "png": "Image", "jpg": "Image", "jpeg": "Image", "gif": "Image", "bmp": "Image", "svg": "Image", "webp": "Image", "mp3": "Music", "wav": "Music", "flac": "Music", "ogg": "Music", "mp4": "Video", "mov": "Video", "avi": "Video", "mkv": "Video", "webm": "Video", "txt": "Document", "text": "Document", "md": "Markdown", "tbconfig": "JSON", "lnk": "Shortcut" }, "name-to-path": { "JavaScript": "", "HTML": "", "CSS": "", "PHP": "", "Python": "", "Java": "", "C": "", "C++": "", "C#": "", "Ruby": "", "Swift": "", "Kotlin": "", "TypeScript": "", "Rust": "", "Dart": "", "Haskell": "", "Elixir": "", "Clojure": "", "Lua": "", "JSON": "", "XML": "", "YAML": "", "ZIP": "", "Zig": "", "OCaml": "", "ENV": "", "Image": "", "Music": "", "Document": "", "Video": "", "Unknown": "", "Markdown": "", "Shortcut": "" } } ================================================ FILE: public/apps/files.tapp/index.css ================================================ @font-face { font-family: Inter; src: url(/fonts/Inter.ttf); } html, body { height: 100%; margin: 0; padding: 0; } body { display: flex; flex-direction: column; font-family: Inter; color: #ffffff; } .topbar { position: relative; color: #ffffff; width: calc(100%); display: flex; flex-direction: row; align-items: center; gap: 8px; padding: 8px 8px 8px 8px; } .nav-buttons { display: flex; flex-direction: row; align-items: center; justify-content: flex-end; gap: 8px; } .nav-button { width: 18px; height: 18px; stroke-width: 2px; stroke: currentColor; cursor: var(--cursor-pointer) !important; } .nav-input { outline: none; border: none; background-color: #00000028; color: #ffffff; border-radius: 8px; padding: 6px 10px; font-weight: 700; font-family: Inter; font-size: 14.5px; } .nav-input.dir { width: 100%; } button { background-color: transparent; border-color: transparent; color: #ffffff; cursor: var(--cursor-pointer) !important; } button.path-button { display: flex; flex-direction: row; align-items: center; justify-content: center; } button.path-button svg { width: 18px; height: 18px; } .icon-button { width: 18px; height: 18px; stroke-width: 2px; stroke: currentColor; cursor: var(--cursor-pointer) !important; } .sidebar { position: relative; width: var(--sidebar-width); height: 100%; display: flex; flex-direction: column; align-items: stretch; padding: 8px; padding-right: 0; overflow: auto; scrollbar-width: thin; scrollbar-color: #ffffff30 transparent; } .sidebar::-webkit-scrollbar { width: 8px; } .sidebar::-webkit-scrollbar-track { background-color: transparent; } .sidebar::-webkit-scrollbar-thumb { background-color: #ffffff30; border-radius: 8px; border: 2px solid transparent; } .absolute-path-item { width: max-content; display: flex; flex-direction: row; align-items: center; gap: 8px; padding: 4px 8px 4px 6px; border-radius: 6px; cursor: var(--cursor-pointer) !important; transition: 150ms ease-in-out; user-select: none; } .absolute-path-item:hover { background-color: #ffffff30; } .absolute-path-item .icon { display: flex; align-items: center; justify-content: center; } .absolute-path-item svg { width: 18px; height: 18px; } .absolute-path-item .title { font-size: 16px; font-weight: 500; margin: 0; padding: 0; } .absolute-path-item .title, .absolute-path-item .icon, .absolute-path-item .icon svg, .absolute-path-item .icon svg path { pointer-events: none; } .collapsible-path .title { display: flex; flex-direction: row; align-items: center; gap: 8px; width: max-content; cursor: var(--cursor-pointer) !important; padding: 4px 6px; border-radius: 6px; transition: 150ms ease-in-out; } .collapsible-path .title h2 { font-size: 18px; font-weight: 800; margin: 0; padding: 0; cursor: default; } .collapsible-path .title .collapsible-icon { display: flex; align-items: center; justify-content: center; } .collapsible-path .title svg.collapsed { transform: rotate(-90deg); } .collapsible-path .title svg, .collapsible-path .title svg path, .collapsible-path .title h2 { pointer-events: none; } .collapsible-path .paths { display: flex; flex-direction: column; padding-left: 10px; width: max-content; } .collapsible-path .paths.collapsed { display: none; } .collapsible-path .path-item { display: flex; flex-direction: row; align-items: center; width: max-content; gap: 8px; padding: 4px 8px 4px 6px; border-radius: 6px; cursor: var(--cursor-pointer) !important; transition: 150ms ease-in-out; user-select: none; } .collapsible-path .path-item:hover { background-color: #ffffff30; } .collapsible-path .path-item svg { width: 18px; height: 18px; } .collapsible-path .path-item span { font-size: 16px; font-weight: 500; margin: 0; padding: 0; cursor: default; } .collapsible-path .path-item svg, .collapsible-path .path-item svg path, .collapsible-path .path-item span { pointer-events: none; } main { display: flex; flex-direction: row; height: calc(100% - var(--topbar-height)); } .exp { position: relative; width: calc(100% - var(--sidebar-width)); height: 100%; display: flex; flex-direction: column; padding-bottom: 8px; overflow: auto; scrollbar-width: thin; scrollbar-color: #ffffff30 transparent; } .exp::-webkit-scrollbar { width: 8px; } .exp::-webkit-scrollbar-track { background-color: transparent; } .exp::-webkit-scrollbar-thumb { background-color: #ffffff30; border-radius: 8px; border: 2px solid transparent; } .exp .path-item { width: max-content; display: flex; flex-direction: row; align-items: center; gap: 8px; border: 1px solid transparent; padding: 4px 8px 4px 6px; border-radius: 6px; cursor: var(--cursor-pointer) !important; transition: 150ms ease-in-out; transition-property: background-color; user-select: none; } .exp .path-item.selected { border: 1px solid #ffffff; background-color: #ffffff20; } .exp .path-item:hover { background-color: #ffffff30; } .exp .path-item svg { width: 24px; height: 24px; } .exp .path-item img { width: 24px; height: 24px; } .exp .path-item .icon { display: flex; align-items: center; justify-content: center; } .exp .path-item .title { font-size: 16px; font-weight: 600; margin: 0; padding: 0; } .exp .path-item .title, .exp .path-item .icon, .exp .path-item .icon svg, .exp .path-item .icon svg path { pointer-events: none; } .exp .sd-items { display: flex; flex-direction: row; flex-wrap: wrap; gap: 8px; } .exp .sd-items .sd-item { width: max-content; height: max-content; display: flex; flex-direction: row; align-items: center; gap: 8px; padding: 8px; border-radius: 6px; transition: 150ms ease-in-out; user-select: none; background-color: #ffffff30; } .exp .sd-items .sd-item .icon { display: flex; align-items: center; justify-content: center; } .exp .sd-items .sd-item .icon svg { width: 42px; height: 42px; } .exp .sd-items .sd-item .icon img { width: 42px; height: 42px; } .exp .info { display: flex; flex-direction: column; gap: 4px; } .exp .info .title { font-size: 16px; font-weight: 500; margin: 0; padding: 0; } .exp .info .title, .exp .info .icon, .exp .info .icon svg, .exp .info .icon svg path { pointer-events: none; } .exp .info .percent-container { width: 160px; height: 8px; background-color: #ffffff30; border-radius: 16px; border: 1px solid #ffffff30; } .exp .info .percent { display: block; height: 8px; background-color: #ffffff; border-radius: 4px; } .exp .info .percent.low { background-color: #df3a3a; } ================================================ FILE: public/apps/files.tapp/index.html ================================================ Terbium File Manager
================================================ FILE: public/apps/files.tapp/index.js ================================================ const Filer = window.Filer; const user = sessionStorage.getItem("currAcc"); window.addEventListener("load", event => { const dirInput = document.querySelector(".nav-input.dir"); if (sessionStorage.getItem("ldir")) { dirInput.value = sessionStorage.getItem("ldir"); openPath(sessionStorage.getItem("ldir")); sessionStorage.removeItem("ldir"); } else { dirInput.value = `/home/${user}`; openPath(`/home/${user}`); } let currentDir = "/home"; dirInput.addEventListener("keydown", e => { if (e.key === "Enter") { if (e.target.value === "") { dirInput.value = currentDir; return; } if (e.target.value !== currentDir) { openPath(e.target.value); currentDir = e.target.value; } } }); }); const back = async () => { const dirInput = document.querySelector(".nav-input.dir"); sessionStorage.setItem("lastDir", dirInput.value); if (dirInput.value === "/home") { openPath("//"); } else { const input = dirInput.value.trim(); const parts = input.split("/"); parts.pop(); const inp = parts.join("/") + "/"; openPath(inp); } }; const forward = async () => { const dir = sessionStorage.getItem("lastDir"); openPath(dir); }; document.getElementById("back").addEventListener("click", back); document.getElementById("forward").addEventListener("click", forward); document.getElementById("reload").addEventListener("click", () => { openPath(document.querySelector(".nav-input.dir").value); }); const emptyTrash = async () => { await window.parent.tb.fs.promises.readdir("/system/trash").then(async files => { if (files.length > 0) { for (let file of files) { const filePath = `/system/trash/${file}`; window.parent.tb.fs.promises.stat(filePath, async (err, stats) => { if (err) { console.error(err); return; } if (stats.isFile()) { window.parent.tb.fs.promises.unlink(filePath); } else if (stats.isDirectory()) { await window.parent.tb.sh.promises.rm(filePath, { recursive: true }); } if (document.querySelector(".exp").getAttribute("path") === "/system/trash") { document.querySelectorAll(".exp .path-item").forEach(item => { item.remove(); }); } }); } } }); }; const createCollapsible = async (title, id, opened, children) => { const collapsible = document.createElement("div"); collapsible.classList.add("collapsible-path"); collapsible.id = id; const collapsibleTitleContainer = document.createElement("div"); collapsibleTitleContainer.classList.add("title"); const pathTitle = document.createElement("h2"); pathTitle.textContent = title; const collaspeIcon = document.createElement("div"); collaspeIcon.classList.add("collapsible-icon"); collaspeIcon.innerHTML = ` `; collapsibleTitleContainer.appendChild(pathTitle); collapsibleTitleContainer.appendChild(collaspeIcon); collapsible.appendChild(collapsibleTitleContainer); const paths = document.createElement("div"); paths.classList.add("paths"); if (opened) { paths.classList.remove("collapsed"); collaspeIcon.querySelector("svg").classList.remove("collapsed"); } else if (opened === undefined || opened === false) { paths.classList.add("collapsed"); collaspeIcon.querySelector("svg").classList.add("collapsed"); } for (let title in children) { const path = document.createElement("div"); path.classList.add("path-item"); if (title.toLocaleLowerCase().endsWith(".tapp")) { try { const data = await window.parent.tb.fs.promises.readFile(`${path}/icon.svg`, "utf8"); path.innerHTML = data; } catch { icon.innerHTML = ` `; } } else { switch (title.toLowerCase()) { case "desktop": path.innerHTML = ` `; path.setAttribute("system-folder", "true"); path.setAttribute("oneclick", "true"); break; case "documents": path.innerHTML = ` `; path.setAttribute("system-folder", "true"); path.setAttribute("oneclick", "true"); break; case "images": path.innerHTML = ` `; path.setAttribute("system-folder", "true"); path.setAttribute("oneclick", "true"); break; case "videos": path.innerHTML = ` `; path.setAttribute("system-folder", "true"); path.setAttribute("oneclick", "true"); break; case "music": path.innerHTML = ` `; path.setAttribute("system-folder", "true"); path.setAttribute("oneclick", "true"); break; case "trash": path.innerHTML = ` `; path.setAttribute("system-folder", "true"); path.setAttribute("oneclick", "true"); break; case "file system": path.innerHTML = ` `; path.setAttribute("system-folder", "true"); path.setAttribute("oneclick", "true"); break; default: path.innerHTML = ` `; path.setAttribute("system-folder", "true"); path.setAttribute("oneclick", "true"); path.id = `f-${title.toLowerCase()}`; break; } } const pathTitle = document.createElement("span"); pathTitle.textContent = title; path.appendChild(pathTitle); paths.appendChild(path); function click() { openPath(children[title]); } path.getAttribute("oneclick") === "true" ? path.addEventListener("click", e => click()) : path.addEventListener("dblclick", e => click()); } collapsible.appendChild(paths); collapsibleTitleContainer.addEventListener("click", async e => { const icon = collapsible.querySelector(".collapsible-icon svg"); const paths = collapsible.querySelector(".paths"); let qcdata = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/config.json`, "utf8")); if (qcdata["open-collapsibles"]) { if (qcdata["open-collapsibles"][id]) { if (qcdata["open-collapsibles"][id] === true) { qcdata["open-collapsibles"][id] = false; } else if (qcdata["open-collapsibles"]["quick-center"] === false) { qcdata["open-collapsibles"][id] = true; } } else { if (collapsible.classList.contains("collapsed")) { qcdata["open-collapsibles"][id] = false; } else { qcdata["open-collapsibles"][id] = true; } } await window.parent.tb.fs.promises.writeFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/config.json`, JSON.stringify(qcdata)); } if (icon.classList.contains("collapsed")) { icon.classList.remove("collapsed"); paths.classList.remove("collapsed"); collapsible.classList.remove("collapsed"); } else { icon.classList.add("collapsed"); paths.classList.add("collapsed"); collapsible.classList.add("collapsed"); } }); document.querySelector(".sidebar").appendChild(collapsible); return true; }; const createStorageDeviceCard = (type, davInfo) => { const item = document.createElement("div"); item.classList.add("sd-item"); const icon = document.createElement("div"); icon.classList.add("icon"); icon.innerHTML = ` `; item.appendChild(icon); const info = document.createElement("div"); info.classList.add("info"); const title = document.createElement("span"); title.classList.add("title"); info.appendChild(title); const percentContainer = document.createElement("span"); percentContainer.classList.add("percent-container"); const percent = document.createElement("span"); percent.classList.add("percent"); percent.style.width = "100%"; percentContainer.appendChild(percent); info.appendChild(percentContainer); const size = document.createElement("div"); size.classList.add("size"); info.appendChild(size); item.addEventListener("dblclick", () => { switch (type) { case "local": openPath("local storage"); break; case "fs": openPath("//"); break; case "dav": openPath(`/mnt/${davInfo.name}/`); break; } }); switch (type) { case "local": title.textContent = "Local Storage"; const maxStorage = 10 * 1024 * 1024; const warningThreshold = 90; const usedSize = Object.keys(localStorage).reduce((total, key) => { const item = localStorage.getItem(key); const itemSize = JSON.stringify(item).length * 2; const keySize = key.length * 2; total += itemSize + keySize; return total; }, 0); const usedPercentage = (usedSize / maxStorage) * 100; let formattedSize; if (usedSize >= 1024 * 1024 * 1024) { formattedSize = `${(usedSize / (1024 * 1024 * 1024)).toFixed(2)} GB`; } else { formattedSize = `${(usedSize / (1024 * 1024)).toFixed(2)} MB`; } size.textContent = `${formattedSize} of ${maxStorage / (1024 * 1024)} MB`; const minWidth = 8; const maxWidth = 165; const calculatedWidth = (Math.min(usedPercentage, 100) * (maxWidth - minWidth)) / 100 + minWidth; percent.style.width = `${Math.min(calculatedWidth, 160)}px`; if (usedPercentage >= warningThreshold) { percent.style.backgroundColor = "#D8645D"; } else { percent.style.backgroundColor = "#5D78D8"; } break; case "fs": title.textContent = "File System"; if ("navigator" in window && "storage" in navigator) { navigator.storage.estimate().then(estimate => { const totalSize = estimate.quota; const usedSize = estimate.usage; const usedPercentage = (usedSize / totalSize) * 100; let formattedUsedSize, formattedTotalSize; if (usedSize >= 1024 * 1024 * 1024) { formattedUsedSize = `${(usedSize / (1024 * 1024 * 1024)).toFixed(2)} GB`; } else { formattedUsedSize = `${(usedSize / (1024 * 1024)).toFixed(2)} MB`; } if (totalSize >= 1024 * 1024 * 1024) { formattedTotalSize = `${(totalSize / (1024 * 1024 * 1024)).toFixed(2)} GB`; } else { formattedTotalSize = `${Math.round((totalSize / (1024 * 1024)).toFixed(2))} MB`; } size.textContent = `${formattedUsedSize} of ${formattedTotalSize}`; const minWidth = 8; const maxWidth = 165; const calculatedWidth = (Math.min(usedPercentage, 100) * (maxWidth - minWidth)) / 100 + minWidth; percent.style.width = `${calculatedWidth}px`; if (usedPercentage >= 90) { percent.style.backgroundColor = "#D8645D"; } else { percent.style.backgroundColor = "#5D78D8"; } }); } break; case "dav": title.textContent = davInfo.name || "Dav Drive"; const displayText = davInfo.url || "http://localhost:3001/dav/"; size.textContent = displayText.length > 18 ? displayText.slice(0, 18) + "..." : displayText; item.id = `f-${davInfo.name.toLocaleLowerCase()}`; percent.style.width = "100%"; const test = async () => { const servers = window.parent.tb.vfs.servers; if (servers.has(davInfo.name) && servers.get(davInfo.name).connected === true) { const icn = ` `; document.getElementById(`f-${davInfo.name.toLocaleLowerCase()}`).innerHTML = ` ${icn} ${davInfo.name} `; icon.innerHTML = icn; percent.style.backgroundColor = "#5DD881"; } else { const icn = ` `; document.getElementById(`f-${davInfo.name.toLocaleLowerCase()}`).innerHTML = ` ${icn} ${davInfo.name} `; icon.innerHTML = icn; percent.style.backgroundColor = "#D8645D"; } }; test(); break; } item.appendChild(info); return item; }; const showStorageDevices = () => { const exp = document.querySelector(".exp"); exp.innerHTML = ""; let fscard = createStorageDeviceCard("fs"); let lscard = createStorageDeviceCard("local"); const sd_items = document.createElement("div"); sd_items.classList.add("sd-items"); sd_items.appendChild(fscard); sd_items.appendChild(lscard); const getdav = async () => { 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 si = createStorageDeviceCard("dav", { name: dav.name, url: dav.url, user: dav.username, pass: dav.password }); sd_items.appendChild(si); } }; getdav(); exp.appendChild(sd_items); document.querySelector(".drive-modal").style.display = "none"; const dirInput = document.querySelector(".nav-input.dir"); dirInput.value = "storage devices"; }; const showLS = async () => { const keys = Object.keys(localStorage); const exp = document.querySelector(".exp"); exp.innerHTML = ""; const dirInput = document.querySelector(".nav-input.dir"); dirInput.value = "local storage"; keys.forEach(key => { const pathItem = document.createElement("div"); pathItem.classList.add("path-item", "ls-item"); const icon = document.createElement("div"); icon.classList.add("icon"); icon.innerHTML = ` `; pathItem.appendChild(icon); const title = document.createElement("span"); title.classList.add("title"); title.textContent = key; pathItem.appendChild(title); pathItem.setAttribute("path", `local storage/${key}`); exp.appendChild(pathItem); pathItem.addEventListener("dblclick", e => { let lsitem = localStorage.getItem(key); tb.dialog.Message({ title: `Change the key for ${key}`, defaultValue: lsitem, onOk: async newKey => { if (newKey !== null && newKey !== "" && newKey !== lsitem) { localStorage.setItem(key, newKey); } }, }); }); }); }; const useDavClient = async path => { const client = window.parent.tb.vfs.currentServer.connection.client; let filePath; if (path.startsWith("http")) { const match = path.match(/^https?:\/\/[^\/]+\/dav\/([^\/]+\/)?(.+)$/); filePath = match ? "/" + match[2] : path; } else { filePath = path.replace(davUrl, "/"); } let mntPath; if (path.startsWith("/mnt/")) { mntPath = path; } else { mntPath = window.parent.tb.fs.normalizePath(`/mnt/${window.parent.tb.vfs.currentServer.name}/${filePath}`); } return { client, filePath, mntPath }; }; const getItemDetails = async path => { if (path.includes("http")) { const { client, filePath, mntPath } = await useDavClient(path); const stats = await client.stat(filePath, { depth: 1 }); let message = JSON.stringify({ type: "item-details", path: mntPath, details: { name: stats.basename, type: stats.mime, size: stats.size, created: null, modified: stats.lastmod, accessed: null, owner: "WebDav", mode: null, version: stats.etag, }, }); parent.window.tb.window.create({ title: `Properties`, icon: "/fs/apps/system/files.tapp/icon.svg", src: "/fs/apps/system/files.tapp/properties/index.html", size: { width: 350, height: 275, }, controls: ["minimize", "close"], message: message, }); } else { window.parent.tb.fs.stat(path, (err, stats) => { if (err) return console.error(err); let name = stats.name; let type = stats.isFile() ? "File" : stats.isDirectory() ? "Folder" : "Symbolic Link"; let size = stats.size; let created = stats.ctime; let modified = stats.mtime; let accessed = stats.atime; let owner = stats.uid; let mode = stats.mode; let version = stats.version; let mime = stats.mime; let message = JSON.stringify({ type: "item-details", path: path, details: { name: name, type: type, size: size, mime: mime, created: created, modified: modified, accessed: accessed, owner: owner, mode: mode, version: version, }, }); parent.window.tb.window.create({ title: `Properties`, icon: "/fs/apps/system/files.tapp/icon.svg", src: "/fs/apps/system/files.tapp/properties/index.html", size: { width: 350, height: 275, }, controls: ["minimize", "close"], message: message, }); }); } }; let copied = null; let cut = null; const cm = async e => { e.preventDefault(); if (document.querySelector(".context-menu")) document.querySelector(".context-menu").remove(); const context = document.createElement("div"); context.classList.add("context-menu"); context.classList.add("fade-in"); setTimeout(() => { context.classList.remove("fade-in"); }, 200); let options = []; let isTrash = document.querySelector(".exp").getAttribute("path") === "/system/trash" ? true : false; if (e.target.getAttribute("type") === "file") { options = [ { text: "Open", click: async () => { const name = e.target.getAttribute("name"); const parts = name.split("."); let ext; if (parts.length > 2) { ext = parts.slice(-2).join("."); } else { ext = parts.slice(-1).join("."); } const data = JSON.parse(await window.parent.tb.fs.promises.readFile("/apps/system/files.tapp/extensions.json", "utf8")); if (data["image"].includes(ext)) { parent.window.tb.file.handler.openFile(e.target.getAttribute("path"), "image"); } else if (data["video"].includes(ext)) { parent.window.tb.file.handler.openFile(e.target.getAttribute("path"), "video"); } else if (data["audio"].includes(ext)) { parent.window.tb.file.handler.openFile(e.target.getAttribute("path"), "audio"); } else if (data["pdf"].includes(ext)) { parent.window.tb.file.handler.openFile(e.target.getAttribute("path"), "pdf"); } else if (ext.toLowerCase() === "tapp.zip") { try { const path = e.target.getAttribute("path"); await window.parent.tb.dialog.Permissions({ title: "Install application", message: `Would you like to install the application: ${path}?`, onOk: async () => { const appPath = `/fs/${path}`.replace("//", "/"); const appName = path .replace(`/home/${window.parent.sessionStorage.getItem("currAcc")}/`, "") .replace(/\//g, ".") .replace(/\.zip$/, "") + ""; window.parent.tb.notification.Installing({ message: `Installing ${appName}...`, application: "Files", iconSrc: "/fs/apps/system/files.tapp/icon.svg", time: 500, }); try { const zipFilePath = e.target.getAttribute("path"); const targetDirectory = `/apps/user/${window.parent.sessionStorage.getItem("currAcc")}/${appName}`; await unzip(zipFilePath, targetDirectory, true); console.log("Done!"); const appConf = await window.parent.tb.fs.promises.readFile(`/apps/user/${window.parent.sessionStorage.getItem("currAcc")}/${appName}/.tbconfig`, "utf8"); const appData = JSON.parse(appConf); console.log(appData); await window.parent.tb.launcher.addApp({ name: appData.title, icon: appData.icon.includes("http") ? appData.icon : `/fs/apps/user/${window.parent.sessionStorage.getItem("currAcc")}/${appName}/${appData.icon}`, title: typeof appData.wmArgs.title === "object" ? { text: appData.wmArgs.title.text, weight: appData.wmArgs.title.weight, html: appData.wmArgs.title.html, } : appData.wmArgs.title, src: `/fs/apps/user/${window.parent.sessionStorage.getItem("currAcc")}/${appName}/${appData.wmArgs.src}`, size: { width: appData.wmArgs.size.width, height: appData.wmArgs.size.height, }, single: appData.wmArgs.single, resizable: appData.wmArgs.resizable, controls: appData.wmArgs.controls, message: appData.wmArgs.message, snapable: appData.wmArgs.snapable, user: window.parent.sessionStorage.getItem("currAcc"), }); try { let apps = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/installed.json`, "utf8")); apps.push({ name: appName, user: await window.parent.tb.user.username(), config: `/apps/system/${appName}.tapp/.tbconfig`, }); await window.parent.tb.fs.promises.writeFile(`/apps/installed.json`, JSON.stringify(apps)); } catch { await window.parent.tb.fs.promises.writeFile( `/apps/installed.json`, JSON.stringify([ { name: appName, user: await window.parent.tb.user.username(), config: `/apps/system/${appName}.tapp/.tbconfig`, }, ]), ); } window.parent.tb.notification.Toast({ message: `${appName} has been installed!`, application: "Files", iconSrc: "/fs/apps/system/files.tapp/icon.svg", time: 5000, }); } catch (e) { console.error("Error installing the app:", e); window.parent.tb.notification.Toast({ message: `Failed to install ${appName}. Check the console for details.`, application: "Files", iconSrc: "/fs/apps/system/files.tapp/icon.svg", time: 5000, }); } }, }); } catch (e) { window.parent.tb.dialog.Alert({ title: "Unexpected Error", message: `❌ An Unexpected error occurred when trying to install the app: ${path} Error: ${e}`, }); } } else if (data["extractables"].includes(ext) || ext.toLowerCase() === "app.zip" || ext.toLowerCase() === "lib.zip") { const zipFilePath = e.target.getAttribute("path"); const path = item.getAttribute("path").replace(".zip", ""); const targetDirectory = `${path}`; await unzip(zipFilePath, targetDirectory); openPath(document.querySelector(".nav-input.dir").value); } else if (data["text"].includes(ext)) { parent.window.tb.file.handler.openFile(e.target.getAttribute("path"), "text"); } else { const path = e.target.getAttribute("path"); const name = e.target.getAttribute("name"); const parts = name.split("."); const extKey = parts.length > 2 ? parts.slice(-2).join(".").toLowerCase() : parts.slice(-1).join(".").toLowerCase(); const allHandlers = JSON.parse(await window.parent.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8"))["fileAssociatedApps"] || {}; if (allHandlers[extKey]) { parent.window.tb.file.handler.openFile(path, extKey); return; } let handlers = Object.entries(allHandlers).filter(([type, app]) => { return !(type === "text" && app === "text-editor") && !(type === "image" && app === "media-viewer") && !(type === "video" && app === "media-viewer") && !(type === "audio" && app === "media-viewer"); }); let hands = []; for (const [type, app] of handlers) { hands.push({ text: app, value: type }); } await tb.dialog.Select({ title: `Select a application to open: ${path.split("/").pop()}`, options: [ { text: "Text Editor", value: "text", }, { text: "Media Viewer", value: "media", }, { text: "Webview", value: "webview", }, ...hands, { text: "Other", value: "other", }, ], onOk: async val => { switch (val) { case "text": parent.window.tb.file.handler.openFile(path, "text"); break; case "media": const ext = name.split(".").pop(); if (data["image"].includes(ext)) { parent.window.tb.file.handler.openFile(path, "image"); } else if (data["video"].includes(ext)) { parent.window.tb.file.handler.openFile(path, "video"); } else if (data["audio"].includes(ext)) { parent.window.tb.file.handler.openFile(path, "audio"); } else if (data["pdf"].includes(ext)) { parent.window.tb.file.handler.openFile(path, "pdf"); } break; case "webview": parent.window.tb.file.handler.openFile(path, "webpage"); break; case "other": parent.window.tb.dialog.DirectoryBrowser({ title: "Select a application", filter: ".tapp", onOk: async val => { const app = JSON.parse(await window.parent.tb.fs.promises.readFile(`${val}/.tbconfig`, "utf8")); window.parent.tb.window.create({ ...app.wmArgs, message: { type: "process", path: item.item } }); }, }); break; default: if (hands.length === 0) { parent.window.tb.file.handler.openFile(path, "text"); } else { parent.window.tb.file.handler.openFile(path, val); } break; } }, }); } }, }, { text: "Open With", click: async () => { let handlers = JSON.parse(await window.parent.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8"))["fileAssociatedApps"]; handlers = Object.entries(handlers).filter(([type, app]) => { return !(type === "text" && app === "text-editor") && !(type === "image" && app === "media-viewer") && !(type === "video" && app === "media-viewer") && !(type === "audio" && app === "media-viewer"); }); let hands = []; for (const [type, app] of handlers) { hands.push({ text: app, value: type }); } const data = JSON.parse(await window.parent.window.parent.tb.fs.promises.readFile("/apps/system/files.tapp/extensions.json", "utf8")); await tb.dialog.Select({ title: `Select a application to open: ${e.target.getAttribute("path").split("/").pop()}`, options: [ { text: "Text Editor", value: "text", }, { text: "Media Viewer", value: "media", }, { text: "Webview", value: "webview", }, ...hands, { text: "Other", value: "other", }, ], onOk: async val => { switch (val) { case "text": parent.window.tb.file.handler.openFile(e.target.getAttribute("path"), "text"); break; case "media": const ext = e.target.getAttribute("name").split(".").pop(); if (data["image"].includes(ext)) { parent.window.tb.file.handler.openFile(e.target.getAttribute("path"), "image"); } else if (data["video"].includes(ext)) { parent.window.tb.file.handler.openFile(e.target.getAttribute("path"), "video"); } else if (data["audio"].includes(ext)) { parent.window.tb.file.handler.openFile(e.target.getAttribute("path"), "audio"); } else if (data["pdf"].includes(ext)) { parent.window.tb.file.handler.openFile(e.target.getAttribute("path"), "pdf"); } break; case "webview": parent.window.tb.file.handler.openFile(e.target.getAttribute("path"), "webpage"); break; case "other": parent.window.tb.dialog.DirectoryBrowser({ title: "Select a application", filter: ".tapp", onOk: async val => { const app = JSON.parse(await window.parent.tb.fs.promises.readFile(`${val}/.tbconfig`, "utf8")); window.parent.tb.window.create({ ...app.wmArgs, message: { type: "process", path: item.item } }); }, }); break; default: if (hands.length === 0) { parent.window.tb.file.handler.openFile(e.target.getAttribute("path"), "text"); } else { parent.window.tb.file.handler.openFile(e.target.getAttribute("path"), val); } break; } }, }); }, }, { text: "Rename", click: async () => { try { const path = e.target.getAttribute("path"); const currentFileName = e.target.querySelector(".title").textContent; await tb.dialog.Message({ title: `Enter a new name for ${currentFileName}`, defaultValue: currentFileName, onOk: async newFileName => { if (newFileName === currentFileName) { return; } if (path.includes("http")) { const { client, filePath } = await useDavClient(path); await client.moveFile(filePath, `${path}/${newFileName}`); openPath(document.querySelector(".nav-input.dir").value); } else { try { await window.parent.tb.fs.promises.rename(path, `${e.target.getAttribute("parent-path")}/${newFileName}`); const type = e.target.classList.contains("file-item") ? "file" : "folder"; document.querySelector(`[path="${path}"]`).remove(); const item = document.createElement("div"); item.classList.add(`${type}-item`, "path-item"); const icon = document.createElement("div"); icon.classList.add("icon"); let ext = newFileName.split(".").pop(); const data = await fetch(`/fs//system/etc/terbium/file-icons.json`).then(res => res.json()); let iconName = data["ext-to-name"][ext]; let iconPath = data["name-to-path"][iconName]; let unknown = data["name-to-path"]["Unknown"]; if (iconPath) { icon.innerHTML = iconPath; } else { icon.innerHTML = unknown; } item.appendChild(icon); const itemTitle = document.createElement("span"); itemTitle.classList.add("title"); itemTitle.textContent = newFileName; item.appendChild(itemTitle); item.setAttribute("path", `${e.target.getAttribute("parent-path")}/${newFileName}`); item.setAttribute("name", newFileName); let pbs = path.split("/"); pbs.pop(); pbs = pbs.join("/"); item.setAttribute("parent-path", pbs); document.querySelector(".exp").appendChild(item); } catch (error) { console.log(error); const ask = await tb.dialog.Message({ title: `This file already exists. Enter a new name for ${newFileName}`, defaultValue: newFileName, }); if (ask !== undefined && ask !== "") { await rename(path, ask); } } } }, }); } catch (error) { console.error(error); } }, }, { text: "Copy", click: async () => { copied = { path: e.target.getAttribute("path"), name: e.target.getAttribute("name") }; }, }, { text: "Cut", click: async () => { copied = { path: e.target.getAttribute("path"), name: e.target.getAttribute("name") }; e.target.classList.add("opacity-50"); cut = true; }, }, { text: isTrash ? "Delete" : e.target.getAttribute("path").includes("http") ? "Delete File" : "Move To Trash", click: async () => { const path = e.target.getAttribute("path"); if (path.includes("http")) { const { client, filePath } = await useDavClient(path); client.deleteFile(filePath); document.querySelector(".exp").removeChild(e.target); } else { if (document.querySelector(".exp").getAttribute("path") === "/system/trash") { await window.parent.tb.fs.promises.unlink(path); document.querySelector(".exp").removeChild(e.target); } else { let data = await window.parent.tb.fs.promises.readFile(path, "utf8"); await window.parent.tb.fs.promises.writeFile(`/system/trash/${e.target.getAttribute("name")}`, data); await window.parent.tb.fs.promises.unlink(path); document.querySelector(".exp").removeChild(e.target); } } }, }, { text: "Download to Computer", click: async () => { const path = e.target.getAttribute("path"); const name = e.target.getAttribute("name"); const lk = document.createElement("a"); lk.download = name; if (path.includes("http")) { const { client, filePath } = await useDavClient(path); const blob = await client.getFileContents(filePath); const stats = await client.stat(filePath); const fileBlob = new Blob([blob], { type: stats.mime }); const url = URL.createObjectURL(fileBlob); lk.href = url; lk.click(); URL.revokeObjectURL(url); } else { fetch(`${window.location.origin}/fs/${path}`) .then(response => response.blob()) .then(blob => { const extension = path.split(".").pop().toLowerCase(); let mimeType; switch (extension) { case "txt": mimeType = "text/plain"; break; case "html": mimeType = "text/html"; break; case "jpg": case "jpeg": mimeType = "image/jpeg"; break; case "png": mimeType = "image/png"; break; case "mp4": mimeType = "video/mp4"; break; case "mp3": mimeType = "audio/mp3"; break; default: mimeType = "application/octet-stream"; } const fileBlob = new Blob([blob], { type: mimeType }); const url = URL.createObjectURL(fileBlob); lk.href = url; lk.click(); URL.revokeObjectURL(url); }) .catch(error => { console.error(error); }); } }, }, { text: "Properties", click: () => { getItemDetails(e.target.getAttribute("path")); }, }, ]; } else if (e.target.getAttribute("type") === "folder") { options = [ { text: "Open", click: () => { openPath(e.target.getAttribute("path")); }, }, e.target.getAttribute("path") === "/system/trash" || isTrash ? null : { text: "New File", click: () => {}, }, e.target.getAttribute("path") === "/system/trash" || isTrash ? null : { text: "New Folder", click: async () => { await tb.dialog.Message({ title: "Enter a name for the new folder", defaultValue: "", onOk: async response => { const path = document.querySelector(".exp").getAttribute("path"); const createUniqueFolder = async (path, folderName, number = null) => { const folderPath = `${path}/${folderName}${number !== null ? ` (${number})` : ""}`; const exists = await window.parent.tb.fs.promises.exists(folderPath); if (exists) { return createUniqueFolder(path, folderName, number !== null ? number + 1 : 2); } else { await window.parent.tb.fs.promises.mkdir(folderPath); } }; await createUniqueFolder(path, response); openPath(path); }, }); }, }, /* To be finished soon e.target.getAttribute("path") === "/system/trash" || isTrash ? null : { text: "ZIP Folder", click: async () => { self.t = e let zip = {}; async function addzip(inp, basePath = '') { const files = await window.parent.tb.fs.promises.readdir(inp); for (const file of files) { const fullPath = `${inp}/${file}`; const stats = await window.parent.tb.fs.promises.stat(fullPath); const zipPath = `${basePath}${file}`; if (stats.isDirectory()) { await addzip(fullPath, `${zipPath}/`); } else { const fileData = await window.parent.tb.fs.promises.readFile(fullPath); zip[zipPath] = new Uint8Array(fileData); } } } await addzip(e.target.getAttribute("path")); await tb.dialog.Select({ title: "Where do you want to save the ZIP?", options: [{ text: "File System", value: "fs" }, { text: "Computer", value: "pc" }], onOk: async (perm) => { const zipped = window.parent.tb.fflate.zipSync(zip); if (perm === "fs") { await tb.dialog.SaveFile({ title: "Enter a name for the ZIP file", defualtDir: `/home/${window.parent.sessionStorage.getItem("currAcc")}/`, filename: `${e.target.getAttribute("name")}.zip`, onOk: async (value) => { const zipBlob = new Blob([zipped.buffer], { type: 'application/zip' }); const ab = await zipBlob.arrayBuffer(); await window.parent.tb.fs.promises.writeFile(value, new Uint8Array(ab)); window.parent.tb.notification.Toast({ message: `ZIP file created at ${value}`, application: "Files", iconSrc: "/fs/apps/system/files.tapp/icon.svg", time: 5000, }); }, }); } else if (perm === "pc") { const zipBlob = new Blob([zipped.buffer], { type: 'application/zip' }); const url = URL.createObjectURL(zipBlob); const link = document.createElement('a'); link.href = url; link.download = `${e.target.getAttribute("name")}.zip`; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); } } }); } },*/ e.target.getAttribute("path") === "/system/trash" || isTrash || e.target.getAttribute("system-folder") === "true" ? null : { text: "Rename", click: async () => { let path = e.target.getAttribute("path"); tb.dialog.Message({ title: `Enter a new name for folder ${e.target.querySelector(".title").textContent}`, defaultValue: e.target.getAttribute("name"), onOk: async newName => { if (newName !== null && newName !== "" && newName !== e.target.querySelector(".title").textContent) { const rename = async (path, fileName) => { try { await window.parent.tb.fs.promises.rename(path, `${e.target.getAttribute("parent-path")}/${fileName}`); const type = e.target.classList.contains("file-item") ? "file" : "folder"; document.querySelector(`[path="${path}"]`).remove(); const item = document.createElement("div"); item.classList.add(`${type}-item`, "path-item"); const icon = document.createElement("div"); icon.classList.add("icon"); switch (type) { case "file": icon.innerHTML = ` `; break; case "folder": icon.innerHTML = ` `; item.addEventListener("dblclick", e => { openPath(e.target.getAttribute("path")); }); break; } item.appendChild(icon); const itemTitle = document.createElement("span"); itemTitle.classList.add("title"); itemTitle.textContent = fileName; item.appendChild(itemTitle); item.setAttribute("path", `${e.target.getAttribute("parent-path")}/${fileName}`); let pbs = path.split("/"); pbs.pop(); pbs = pbs.join("/"); item.setAttribute("parent-path", pbs); document.querySelector(".exp").appendChild(item); } catch (error) { console.log(error); tb.dialog.Message({ title: `Enter a new name for ${fileName}`, defaultValue: fileName, onOk: async ask => { if (ask !== null && ask !== "") { await rename(path, ask); } }, }); } }; await rename(path, newName); } }, }); }, }, // isTrash ? { // text: "Restore", // click: async () => {} // } : null, e.target.getAttribute("path") === "/system/trash" || e.target.getAttribute("system-folder") === "true" ? null : { text: isTrash ? "Delete" : "Move To Trash", click: async () => { const path = e.target.getAttribute("path"); if (document.querySelector(".exp").getAttribute("path") === "/system/trash") { await window.parent.tb.fs.promises.readdir(path, async (err, files) => { if (err) { console.error(err); return; } if (files.length > 0) { for (const file of files) { const filePath = `${path}/${file}`; await window.parent.tb.fs.unlink(filePath); } await window.parent.tb.fs.promises.rmdir(path); document.querySelector(".exp").removeChild(e.target); } else { await window.parent.tb.fs.promises.rmdir(path); document.querySelector(".exp").removeChild(e.target); } }); } else { await window.parent.tb.fs.promises.readdir(path, async (err, files) => { if (err) { console.error(err); return; } if (files.length > 0) { await window.parent.tb.fs.promises.mkdir("/system/trash/" + path.split("/").pop()); for (const file of files) { const filePath = `${path}/${file}`; let data = await window.parent.tb.fs.readFile(filePath, "utf8"); await window.parent.tb.fs.promises.writeFile(`/system/trash/${path.split("/").pop()}/${file}`, data); await window.parent.tb.fs.promises.unlink(filePath); } await window.parent.tb.fs.promises.rmdir(path); document.querySelector(".exp").removeChild(e.target); } else { await window.parent.tb.fs.promises.rmdir(path); await window.parent.tb.fs.promises.mkdir("/system/trash/" + path.split("/").pop()); document.querySelector(".exp").removeChild(e.target); } }); } }, }, // e.target.getAttribute("path") === "/system/trash" ? { // text: "Restore All", // click: () => {} // } : null, e.target.getAttribute("path") === "/system/trash" ? { text: "Empty Trash", click: async () => emptyTrash(), } : null, // { // text: "Properties", // click: () => {} // } ]; } else { options = [ isTrash ? null : { text: "New File", click: async () => { try { await tb.dialog.Message({ title: "Enter a name for the new file", defaultValue: "", onOk: async fileName => { const path = document.querySelector(".exp").getAttribute("path"); const createFile = async (path, fileName) => { if (e.target.getAttribute("path").includes("http")) { const { client, filePath } = await useDavClient(path); const exists = await client.exists(`${filePath}/${fileName}`); if (exists) { const ask = await tb.dialog.Message({ title: `This file already exists. Enter a new name for ${fileName}`, defaultValue: "", }); if (ask !== undefined && ask !== "") { await createFile(path, ask); } } else { client.putFileContents(`${filePath}/${fileName}`, ""); createPath(fileName, `${filePath}/${fileName}`, "file"); } } else { await window.parent.tb.fs.exists(`${path}/${fileName}`, async exists => { if (exists) { const ask = await tb.dialog.Message({ title: `This file already exists. Enter a new name for ${fileName}`, defaultValue: "", }); if (ask !== undefined && ask !== "") { await createFile(path, ask); } } else { let sh = window.parent.tb.sh; await sh.touch(`${path}/${fileName}`, ""); createPath(fileName, `${path}/${fileName}`, "file"); } }); } }; await createFile(path, fileName); }, }); } catch (error) { console.error(error); } }, }, isTrash ? null : { text: "New Folder", click: async () => { await tb.dialog.Message({ title: "Enter a name for the new folder", defaultValue: "", onOk: async response => { const path = document.querySelector(".exp").getAttribute("path"); if (path.includes("http")) { const { client, filePath } = await useDavClient(path); const exists = await client.exists(`${filePath}/${response}`); if (exists) { const ask = await tb.dialog.Message({ title: `This folder already exists. Enter a new name for ${response}`, defaultValue: "", }); if (ask !== undefined && ask !== "") { await createFile(path, ask); } } else { client.createDirectory(`${filePath}/${response}`); } } else { const createUniqueFolder = async (path, folderName, number = null) => { const folderPath = `${path}/${folderName}${number !== null ? ` (${number})` : ""}`; const exists = await window.parent.tb.fs.promises.exists(folderPath); if (exists) { return createUniqueFolder(path, folderName, number !== null ? number + 1 : 2); } else { await window.parent.tb.fs.promises.mkdir(folderPath); } }; await createUniqueFolder(path, response); } createPath(response, `${path}/${response}`, "folder"); }, }); }, }, isTrash ? null : copied || cut ? { text: "Paste", click: async () => { await window.parent.tb.fs.promises.writeFile(`${document.querySelector(".exp").getAttribute("path")}/${copied.name}`, await window.parent.tb.fs.promises.readFile(copied.path, "utf8")); if (cut) { await window.parent.tb.fs.promises.unlink(copied.path); cut = false; } copied = null; openPath(`${document.querySelector(".exp").getAttribute("path")}`); }, } : null, isTrash ? null : { text: "Upload from Computer", click: () => { const fauxput = document.createElement("input"); fauxput.type = "file"; fauxput.multiple = true; fauxput.onchange = async e => { const path = document.querySelector(".exp").getAttribute("path"); if (path.includes("http")) { for (const file of e.target.files) { const content = await file.arrayBuffer(); const { client, filePath } = await useDavClient(path); const exists = await client.exists(`${filePath}/${file.name}`); if (exists) { await tb.dialog.Message({ title: `File "${file.name}" already exists`, defaultValue: file.name, onOk: async newFileName => { if (newFileName !== null && newFileName !== "") { client.putFileContents(`${filePath}/${newFileName}`, window.parent.tb.buffer.from(content)); } }, }); } else { client.putFileContents(`${filePath}/${file.name}`, window.parent.tb.buffer.from(content)); } } } else { for (const file of e.target.files) { const content = await file.arrayBuffer(); const filePath = `${path}/${file.name}`; const exists = await window.parent.tb.fs.promises.exists(filePath); if (exists) { await tb.dialog.Message({ title: `File "${file.name}" already exists`, defaultValue: file.name, onOk: async newFileName => { if (newFileName !== null && newFileName !== "") { await window.parent.tb.fs.promises.writeFile(`${path}/${newFileName}`, window.parent.tb.buffer.from(content), "arraybuffer"); } }, }); } else { await window.parent.tb.fs.promises.writeFile(filePath, window.parent.tb.buffer.from(content), "arraybuffer"); } } } openPath(document.querySelector(".nav-input.dir").value); }; fauxput.click(); }, }, isTrash ? null : !showHidden ? { text: "Show hidden files", click: async () => { const config = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${user}/files/config.json`, "utf8")); config["show-hidden-files"] = true; await window.parent.tb.fs.promises.writeFile(`/apps/user/${user}/files/config.json`, JSON.stringify(config)); openPath(document.querySelector(".nav-input.dir").value); }, } : { text: "Hide hidden files", click: async () => { const config = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${user}/files/config.json`, "utf8")); config["show-hidden-files"] = false; await window.parent.tb.fs.promises.writeFile(`/apps/user/${user}/files/config.json`, JSON.stringify(config)); openPath(document.querySelector(".nav-input.dir").value); }, }, // isTrash ? null : { // text: "Paste", // click: () => {} // }, isTrash ? { text: "Restore All", click: () => {}, } : null, isTrash ? { text: "Empty Trash", click: async () => emptyTrash(), } : null, // { // text: "Properties", // click: () => {} // }, ]; } for (let option of options) { if (option === null) continue; const optionEl = document.createElement("div"); optionEl.classList.add("context-menu-button"); optionEl.textContent = option.text; optionEl.addEventListener("click", option.click); context.appendChild(optionEl); } document.body.appendChild(context); if (e.clientX + context.offsetWidth > window.innerWidth) { context.style.left = `${e.clientX - context.offsetWidth}px`; } else { context.style.left = `${e.clientX}px`; } if (e.clientY + context.offsetHeight > window.innerHeight) { context.style.top = `${e.clientY - context.offsetHeight}px`; } else { context.style.top = `${e.clientY}px`; } window.addEventListener("click", e => { if (e.button === 0) { if (!e.target.classList.contains("context-menu")) { if (document.querySelector(".context-menu")) document.querySelector(".context-menu").remove(); } } }); }; window.addEventListener("contextmenu", cm); window.addEventListener("touchhold", cm); let showHidden = false; const createPath = async (title, path, type) => { const config = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${user}/files/config.json`, "utf8")); if (config["show-hidden-files"] === false && title.startsWith(".")) return; showHidden = config["show-hidden-files"]; let item = document.createElement("div"); item.classList.add("path-item"); item.setAttribute("path", path); item.setAttribute("name", title); item.setAttribute("type", type); let pbs = path.split("/"); pbs.pop(); pbs = pbs.join("/"); item.setAttribute("parent-path", pbs); const icon = document.createElement("div"); icon.classList.add("icon"); let itemTitle = document.createElement("span"); itemTitle.classList.add("title"); itemTitle.textContent = title; if (type === "file") { item.classList.add("file-item"); let ext = path.split(".").pop(); const data = JSON.parse(await window.parent.window.parent.tb.fs.promises.readFile("/system/etc/terbium/file-icons.json")); let iconName = data["ext-to-name"][ext]; let iconPath = data["name-to-path"][iconName]; let unknown = data["name-to-path"]["Unknown"]; if (iconPath) { const imgData = await window.parent.tb.fs.promises.readFile(iconPath, "utf8"); icon.innerHTML = imgData; } else { const imgData = await window.parent.tb.fs.promises.readFile(unknown, "utf8"); icon.innerHTML = imgData; } if (copied && copied.name === item.getAttribute("name").toLowerCase() && cut) { item.classList.add("opacity-50"); } item.ondblclick = async e => { let ext = path.split("."); if (ext.length > 2) { ext = ext.slice(-2).join("."); } else { ext = ext.slice(-1).join("."); } const data = JSON.parse(await window.parent.window.parent.tb.fs.promises.readFile("/apps/system/files.tapp/extensions.json", "utf8")); if (data["image"].includes(ext)) { parent.window.tb.file.handler.openFile(item.getAttribute("path"), "image"); } else if (data["video"].includes(ext)) { parent.window.tb.file.handler.openFile(item.getAttribute("path"), "video"); } else if (data["audio"].includes(ext)) { parent.window.tb.file.handler.openFile(item.getAttribute("path"), "audio"); } else if (data["pdf"].includes(ext)) { parent.window.tb.file.handler.openFile(item.getAttribute("path"), "pdf"); } else if (ext.toLowerCase() === "tapp.zip") { try { const path = e.target.getAttribute("path"); await window.parent.tb.dialog.Permissions({ title: "Install application", message: `Would you like to install the application: ${path}?`, onOk: async () => { const appPath = `/fs/${path}`.replace("//", "/"); const appName = path .replace(`/home/${window.parent.sessionStorage.getItem("currAcc")}/`, "") .replace(/\//g, ".") .replace(/\.zip$/, "") + ""; window.parent.tb.notification.Installing({ message: `Installing ${appName}...`, application: "Files", iconSrc: "/fs/apps/system/files.tapp/icon.svg", time: 500, }); try { const zipFilePath = e.target.getAttribute("path"); const targetDirectory = `/apps/user/${window.parent.sessionStorage.getItem("currAcc")}/${appName}`; await unzip(zipFilePath, targetDirectory, true); console.log("Done!"); const appConf = await window.parent.tb.fs.promises.readFile(`/apps/user/${window.parent.sessionStorage.getItem("currAcc")}/${appName}/.tbconfig`, "utf8"); const appData = JSON.parse(appConf); console.log(appData); await window.parent.tb.launcher.addApp({ name: appData.title, icon: appData.icon.includes("http") ? appData.icon : `/fs/apps/user/${window.parent.sessionStorage.getItem("currAcc")}/${appName}/${appData.icon}`, title: typeof appData.wmArgs.title === "object" ? { text: appData.wmArgs.title.text, weight: appData.wmArgs.title.weight, html: appData.wmArgs.title.html, } : appData.wmArgs.title, src: `/fs/apps/user/${window.parent.sessionStorage.getItem("currAcc")}/${appName}/${appData.wmArgs.src}`, size: { width: appData.wmArgs.size.width, height: appData.wmArgs.size.height, }, single: appData.wmArgs.single, resizable: appData.wmArgs.resizable, controls: appData.wmArgs.controls, message: appData.wmArgs.message, snapable: appData.wmArgs.snapable, user: window.parent.sessionStorage.getItem("currAcc"), }); try { let apps = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/installed.json`, "utf8")); apps.push({ name: appName, user: await window.parent.tb.user.username(), config: `/apps/user/${await window.parent.tb.user.username()}/${appName}/.tbconfig`, }); await window.parent.tb.fs.promises.writeFile(`/apps/installed.json`, JSON.stringify(apps)); } catch { await window.parent.tb.fs.promises.writeFile( `/apps/installed.json`, JSON.stringify([ { name: appName, user: await window.parent.tb.user.username(), config: `/apps/user/${await window.parent.tb.user.username()}/${appName}/.tbconfig`, }, ]), ); } window.parent.tb.notification.Toast({ message: `${appName} has been installed!`, application: "Files", iconSrc: "/fs/apps/system/files.tapp/icon.svg", time: 5000, }); } catch (e) { console.error("Error installing the app:", e); window.parent.tb.notification.Toast({ message: `Failed to install ${appName}. Check the console for details.`, application: "Files", iconSrc: "/fs/apps/system/files.tapp/icon.svg", time: 5000, }); } }, }); } catch (e) { window.parent.tb.dialog.Alert({ title: "Unexpected Error", message: `❌ An Unexpected error occurred when trying to install the app: ${path} Error: ${e}`, }); } } else if (data["extractables"].includes(ext) || ext.toLowerCase() === "app.zip" || ext.toLowerCase() === "lib.zip") { const zipFilePath = e.target.getAttribute("path"); const path = item.getAttribute("path").replace(".zip", ""); const targetDirectory = `${path}`; await unzip(zipFilePath, targetDirectory); openPath(document.querySelector(".nav-input.dir").value); } else if (data["text"].includes(ext)) { parent.window.tb.file.handler.openFile(item.getAttribute("path"), "text"); } else { const path = item.getAttribute("path"); const name = item.getAttribute("name"); const parts = name.split("."); const extKey = parts.length > 2 ? parts.slice(-2).join(".").toLowerCase() : parts.slice(-1).join(".").toLowerCase(); const allHandlers = JSON.parse(await window.parent.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8"))["fileAssociatedApps"] || {}; if (allHandlers[extKey]) { parent.window.tb.file.handler.openFile(path, extKey); return; } let handlers = Object.entries(allHandlers).filter(([type, app]) => { return !(type === "text" && app === "text-editor") && !(type === "image" && app === "media-viewer") && !(type === "video" && app === "media-viewer") && !(type === "audio" && app === "media-viewer"); }); let hands = []; for (const [type, app] of handlers) { hands.push({ text: app, value: type }); } await tb.dialog.Select({ title: `Select a application to open: ${path.split("/").pop()}`, options: [ { text: "Text Editor", value: "text", }, { text: "Media Viewer", value: "media", }, { text: "Webview", value: "webview", }, ...hands, { text: "Other", value: "other", }, ], onOk: async val => { switch (val) { case "text": parent.window.tb.file.handler.openFile(path, "text"); break; case "media": const ext = name.split(".").pop(); if (data["image"].includes(ext)) { parent.window.tb.file.handler.openFile(path, "image"); } else if (data["video"].includes(ext)) { parent.window.tb.file.handler.openFile(path, "video"); } else if (data["audio"].includes(ext)) { parent.window.tb.file.handler.openFile(path, "audio"); } else if (data["pdf"].includes(ext)) { parent.window.tb.file.handler.openFile(path, "pdf"); } break; case "webview": parent.window.tb.file.handler.openFile(path, "webpage"); break; case "other": parent.window.tb.dialog.DirectoryBrowser({ title: "Select a application", filter: ".tapp", onOk: async val => { const app = JSON.parse(await window.parent.tb.fs.promises.readFile(`${val}/.tbconfig`, "utf8")); window.parent.tb.window.create({ ...app.wmArgs, message: { type: "process", path: item.item } }); }, }); break; default: if (hands.length === 0) { parent.window.tb.file.handler.openFile(path, "text"); } else { parent.window.tb.file.handler.openFile(path, val); } break; } }, }); } }; } else if (type === "folder") { if (title.toLocaleLowerCase().endsWith(".tapp")) { try { if (await window.parent.tb.vfs.whatFS(path).promises.exists(`${path}/.tbconfig`)) { const appData = JSON.parse(await window.parent.tb.vfs.whatFS(path).promises.readFile(`${path}/.tbconfig`, "utf8")); const iconPath = appData.icon.includes("http") ? appData.icon : `${path}/${appData.icon}`; if (iconPath.startsWith("http")) { icon.innerHTML = `${appData.title || `; } else if (iconPath.toLowerCase().endsWith(".svg")) { const imgData = await window.parent.tb.vfs.whatFS(path).promises.readFile(iconPath, "utf8"); icon.innerHTML = imgData; } else { const bin = await window.parent.tb.vfs.whatFS(path).promises.readFile(iconPath); const b64 = window.parent.tb.buffer.from(bin).toString("base64"); let mime = "application/octet-stream"; if (iconPath.toLowerCase().endsWith(".png")) mime = "image/png"; else if (iconPath.toLowerCase().endsWith(".jpg") || iconPath.toLowerCase().endsWith(".jpeg")) mime = "image/jpeg"; else if (iconPath.toLowerCase().endsWith(".webp")) mime = "image/webp"; icon.innerHTML = `${appData.title || `; } } else if (await window.parent.tb.vfs.whatFS(path).promises.exists(`${path}/index.json`)) { const appData = JSON.parse(await window.parent.tb.vfs.whatFS(path).promises.readFile(`${path}/index.json`, "utf8")); const iconPath = appData.config.icon.includes("http") ? appData.config.icon : `${appData.config.icon.replace("/fs/", "/")}`; const imgData = await window.parent.tb.vfs.whatFS(path).promises.readFile(iconPath, "utf8"); icon.innerHTML = imgData; } else { const data = await window.parent.tb.vfs.whatFS(path).promises.readFile(`${path}/icon.svg`, "utf8"); icon.innerHTML = data; } } catch { icon.innerHTML = ` `; } } else if (title.toLocaleLowerCase().endsWith(".app")) { try { if (await window.parent.tb.vfs.whatFS(path).promises.exists(`${path}/manifest.json`)) { const appData = JSON.parse(await window.parent.tb.vfs.whatFS(path).promises.readFile(`${path}/manifest.json`, "utf8")); const iconPath = appData.icon.includes("http") ? appData.icon : `${path}/${appData.icon}`; const imgData = await window.parent.tb.vfs.whatFS(path).promises.readFile(iconPath, "utf8"); icon.innerHTML = imgData; } } catch { icon.innerHTML = ` `; } } else { switch (title.toLowerCase()) { case "desktop": icon.innerHTML = ` `; break; case "documents": icon.innerHTML = ` `; break; case "images": icon.innerHTML = ` `; break; case "videos": icon.innerHTML = ` `; break; case "music": icon.innerHTML = ` `; break; case "trash": icon.innerHTML = ` `; break; default: icon.innerHTML = ` `; break; } } switch (pbs) { case "/home": switch (title.toLowerCase()) { case "documents": title = "Documents"; item.setAttribute("system-folder", "true"); break; case "images": title = "Images"; item.setAttribute("system-folder", "true"); break; case "videos": title = "Videos"; item.setAttribute("system-folder", "true"); break; case "music": title = "Music"; item.setAttribute("system-folder", "true"); break; case "trash": title = "Trash"; item.setAttribute("system-folder", "true"); break; } } item.addEventListener("dblclick", e => { openPath(path); const dirInput = document.querySelector(".nav-input.dir"); dirInput.value = path; }); } item.appendChild(icon); item.appendChild(itemTitle); document.querySelector(".exp").appendChild(item); }; const openPath = async (path, override = false) => { if (path === "/system/trash") { if (parent.document.querySelector(`[control-id="files-et"]`)) { parent.document.querySelector(`[control-id="files-et"]`).classList.remove("hidden"); } } else if (path === "cmd") { const path = document.querySelector(".exp").getAttribute("path"); let message = JSON.stringify({ type: "open-path", path: path, }); document.querySelector(".nav-input.dir").value = path; parent.window.tb.window.create({ title: "Terminal", icon: "/fs/apps/system/terminal.tapp/icon.svg", src: "/fs/apps/system/terminal.tapp/index.html", size: { width: 438, height: 326, }, single: true, message: message, }); return; } else { if (parent.document.querySelector(`[control-id="files-et"]`)) { parent.document.querySelector(`[control-id="files-et"]`).classList.add("hidden"); } } if (path === "storage devices") { showStorageDevices(); return; } if (path === "local storage") { showLS(); const modal = document.querySelector(".drive-modal"); modal.style.display = "flex"; modal.innerHTML = ` LFS `; return; } if (path.includes("mnt")) { window.parent.tb.vfs.setServer(path.split("/")[2]); let davConfig = window.parent.tb.vfs.currentServer; console.log("Loading webdav: " + davConfig.url + path.split("/")[2]); let relPath = path.replace(`/mnt/${davConfig.name}/`, "/"); console.log(relPath); const exp = document.querySelector(".exp"); exp.innerHTML = ""; exp.setAttribute("path", davConfig.url + relPath); const dirInput = document.querySelector(".nav-input.dir"); dirInput.value = path; exp.innerHTML = `
Loading WebDAV...
`; const modal = document.querySelector(".drive-modal"); try { const client = window.parent.tb.vfs.currentServer.connection.client; const contents = await client.getDirectoryContents(relPath); exp.innerHTML = ""; document.getElementById(`f-${davConfig.name.toLocaleLowerCase()}`).innerHTML = ` ${davConfig.name} `; modal.style.display = "flex"; modal.innerHTML = ` WebDav `; for (const item of contents) { const name = item.basename; const itemPath = `${davConfig.url}${relPath}/${name}`; if (item.type === "directory" && item.filename === path.replace(`/mnt/${davConfig.name}/`, "")) continue; const type = item.type === "directory" ? "folder" : "file"; const el = document.createElement("div"); el.classList.add("path-item", type === "folder" ? "folder-item" : "file-item"); el.setAttribute("path", itemPath); el.setAttribute("name", name); el.setAttribute("type", type); const icon = document.createElement("div"); icon.classList.add("icon"); if (type === "folder") { icon.innerHTML = ` `; } else { const data = JSON.parse(await window.parent.window.parent.tb.fs.promises.readFile("/system/etc/terbium/file-icons.json")); const ext = itemPath.split(".").pop(); const iconName = data["ext-to-name"][ext]; let iconPath = data["name-to-path"][iconName]; let unknown = data["name-to-path"]["Unknown"]; if (iconPath) { icon.innerHTML = await window.parent.window.parent.tb.fs.promises.readFile(iconPath, "utf8"); } else { icon.innerHTML = await window.parent.window.parent.tb.fs.promises.readFile(unknown, "utf8"); } } el.appendChild(icon); const itemTitle = document.createElement("span"); itemTitle.classList.add("title"); itemTitle.textContent = name; el.appendChild(itemTitle); if (type === "folder") { el.addEventListener("dblclick", () => openPath(itemPath.replace(davConfig.url, `/mnt/${davConfig.name}`))); } else { el.addEventListener("dblclick", async () => { let handlers = JSON.parse(await window.parent.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8"))["fileAssociatedApps"]; handlers = Object.entries(handlers).filter(([type, app]) => { return !(type === "text" && app === "text-editor") && !(type === "image" && app === "media-viewer") && !(type === "video" && app === "media-viewer") && !(type === "audio" && app === "media-viewer"); }); let hands = []; for (const [type, app] of handlers) { hands.push({ text: app, value: type }); } const data = JSON.parse(await window.parent.tb.fs.promises.readFile("/apps/system/files.tapp/extensions.json", "utf8")); await tb.dialog.Select({ title: `Select a application to open: ${itemPath.split("/").pop()}`, options: [ { text: "Text Editor", value: "text", }, { text: "Media Viewer", value: "media", }, ...hands, { text: "Other", value: "other", }, ], onOk: async val => { switch (val) { case "text": parent.window.tb.file.handler.openFile(itemPath, "text"); break; case "media": const ext = itemPath.split(".").pop(); if (data["image"].includes(ext)) { parent.window.tb.file.handler.openFile(itemPath, "image"); } else if (data["video"].includes(ext)) { parent.window.tb.file.handler.openFile(itemPath, "video"); } else if (data["audio"].includes(ext)) { parent.window.tb.file.handler.openFile(itemPath, "audio"); } else if (data["pdf"].includes(ext)) { parent.window.tb.file.handler.openFile(itemPath, "pdf"); } break; case "webview": parent.window.tb.file.handler.openFile(itemPath, "webpage"); break; case "other": parent.window.tb.dialog.DirectoryBrowser({ title: "Select a application", filter: ".tapp", onOk: async val => { const app = JSON.parse(await window.parent.tb.fs.promises.readFile(`${val}/.tbconfig`, "utf8")); window.parent.tb.window.create({ ...app.wmArgs, message: { type: "process", path: item.item } }); }, }); break; default: if (hands.length === 0) { parent.window.tb.file.handler.openFile(itemPath, "text"); } else { parent.window.tb.file.handler.openFile(itemPath, val); } break; } }, }); }); } exp.appendChild(el); } } catch (e) { console.error(e); exp.innerHTML = `

Failed to load WebDAV: ${e}

`; document.getElementById(`f-${davConfig.name.toLocaleLowerCase()}`).innerHTML = ` ${davConfig.name} `; modal.style.display = "flex"; modal.innerHTML = ` WebDav `; } const search = document.querySelector(".nav-input.search"); search.setAttribute("placeholder", `Search ${path.split("/").pop()}`); return; } else { document.querySelector(".drive-modal").style.display = "none"; } if (path.split("/").pop() === "") { path = path.substring(0, path.length - 1); } await window.parent.tb.fs.exists(path, async exists => { if (!exists) { console.error("Path does not exist"); return; } }); const dirInput = document.querySelector(".nav-input.dir"); dirInput.value = path; const exp = document.querySelector(".exp"); exp.innerHTML = ""; exp.setAttribute("path", path); if (path.toLowerCase().endsWith(".app") && override !== true) { tb.dialog.Select({ title: `Sideload App`, message: `Do you want to sideload the anura app found at ${path}?`, options: [ { text: "Yes", value: "yes" }, { text: "View Source", value: "source" }, { text: "No", value: "no" }, ], onOk: async val => { if (val === "yes") { sideloadApp(path); } else if (val === "source") { openPath(path, true); } else { openPath("/home/" + user); } }, }); const sideloadApp = async path => { try { const anura = window.parent.anura; const appPath = `/fs${path}`.replace("//", "/"); await anura.registerExternalApp(appPath); } catch (e) { window.parent.tb.dialog.Alert({ title: "Unexpected Error", message: `❌ An Unexpected error occurred when trying to sideload the anura app: ${path} Error: ${e}`, }); } openPath(document.getElementById(".nav-input")); }; } else { window.parent.tb.fs.readdir(path, async (err, files) => { if (err) return console.error(err); for (let file of files) { await window.parent.tb.fs.stat(path + "/" + file, (err, stats) => { if (err) return console.error(err); if (stats.isDirectory()) { createPath(file, path + "/" + file, "folder"); } else if (stats.isFile()) { createPath(file, path + "/" + file, "file"); } }); } }); } const search = document.querySelector(".nav-input.search"); search.setAttribute("placeholder", `Search ${path.split("/").pop()}`); }; // For use elsewhere like the app island self.openPath = openPath; self.emptyTrash = emptyTrash; async function unzip(path, target, app) { const runUnzip = async () => { const response = await fetch("/fs/" + path); const zipFileContent = await response.arrayBuffer(); if (!(await dirExists(target))) { await window.parent.tb.fs.promises.mkdir(target, { recursive: true }); } const compressedFiles = window.parent.tb.fflate.unzipSync(new Uint8Array(zipFileContent)); for (const [relativePath, content] of Object.entries(compressedFiles)) { const fullPath = `${target}/${relativePath}`; const pathParts = fullPath.split("/"); let currentPath = ""; for (let i = 0; i < pathParts.length; i++) { currentPath += pathParts[i] + "/"; if (i === pathParts.length - 1 && !relativePath.endsWith("/")) { try { console.log(`touch ${currentPath.slice(0, -1)}`); await window.parent.tb.fs.promises.writeFile(currentPath.slice(0, -1), window.parent.tb.buffer.from(content), "arraybuffer"); } catch { console.log(`Cant make ${currentPath.slice(0, -1)}`); } } else if (!(await dirExists(currentPath))) { try { console.log(`mkdir ${currentPath}`); await window.parent.tb.fs.promises.mkdir(currentPath); } catch { console.log(`Cant make ${currentPath}`); } } } if (relativePath.endsWith("/")) { try { console.log(`mkdir fp ${fullPath}`); await window.parent.tb.fs.promises.mkdir(fullPath); } catch { console.log(`Cant make ${fullPath}`); } } } return "Done!"; }; if (!app) { return window.parent.tb.notification.Installing( { message: "Unzipping...", application: "Files", iconSrc: "/fs/apps/system/files.tapp/icon.svg", }, runUnzip(), { message: `Finished unzipping ${path}`, application: "Files", iconSrc: "/fs/apps/system/files.tapp/icon.svg", time: 3500, }, { message: `Failed to unzip ${path}`, application: "Files", iconSrc: "/fs/apps/system/files.tapp/icon.svg", time: 2500, }, ); } return runUnzip(); } const dirExists = async path => { return new Promise(resolve => { window.parent.tb.fs.stat(path, (err, stats) => { if (err) { if (err.code === "ENOENT") { resolve(false); } else { console.error(err); resolve(false); } } else { const exists = stats.type === "DIRECTORY"; resolve(exists); } }); }); }; const clearSearchButton = document.querySelector(".clear-search"); const search = document.querySelector(".nav-input.search"); search.addEventListener("input", e => { const exp = document.querySelector(".exp"); let path = exp.getAttribute("path"); if (search.value === "") { openPath(path); clearSearchButton.classList.add("opacity-0", "pointer-events-none"); return; } exp.innerHTML = ""; exp.setAttribute("path", path); window.parent.tb.fs.readdir(path, async (err, files) => { if (err) { console.error(err); return; } for (let file of files) { await window.parent.tb.fs.stat(path + "/" + file, (err, stats) => { if (err) { console.error(err); return; } if (clearSearchButton.classList.contains("opacity-0")) { clearSearchButton.classList.remove("opacity-0", "pointer-events-none"); } if (stats.isDirectory()) { if (file.toLowerCase().includes(search.value.toLowerCase())) { createPath(file, path + "/" + file, "folder"); } } else if (stats.isFile()) { if (file.toLowerCase().includes(search.value.toLowerCase())) { createPath(file, path + "/" + file, "file"); } } }); } }); }); const cfgload = async () => { const search = document.querySelector(".nav-input.search"); document.querySelector(".sidebar").innerHTML = ""; if (search.value !== "") { search.value = ""; } const topbarheight = document.querySelector(".topbar").offsetHeight; document.querySelector("main").style.setProperty("--topbar-height", `${topbarheight}px`); let config = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/config.json`, "utf8")); if (config["quick-center"] === true) { await createCollapsible("Quick Center", "quick-center", config["open-collapsibles"]["quick-center"], JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/quick-center.json`, "utf8"))["paths"]); } if (config["drives"]) { await createCollapsible("Drives", "drives", config["open-collapsibles"]["drives"], config["drives"]); } if (config["storage"]) { const storageDevices = document.createElement("div"); storageDevices.classList.add("absolute-path-item"); const icon = document.createElement("div"); icon.classList.add("icon"); icon.innerHTML = ` `; storageDevices.appendChild(icon); const itemTitle = document.createElement("span"); itemTitle.classList.add("title"); itemTitle.textContent = "Storage Devices"; storageDevices.appendChild(itemTitle); document.querySelector(".sidebar").appendChild(storageDevices); storageDevices.addEventListener("click", e => showStorageDevices()); } const sidebarwidth = document.querySelector(".sidebar").offsetWidth; document.querySelector("main").style.setProperty("--sidebar-width", `${sidebarwidth}px`); }; window.addEventListener("load", cfgload); window.addEventListener("updcfg", cfgload); ================================================ FILE: public/apps/files.tapp/index.json ================================================ { "name": "Files", "config": { "title": "Files", "icon": "/fs/apps/system/files.tapp/icon.svg", "src": "/fs/apps/system/files.tapp/index.html", "size": { "width": 600, "height": 500 } } } ================================================ FILE: public/apps/files.tapp/properties/index.html ================================================ Properties ================================================ FILE: public/apps/files.tapp/properties/index.js ================================================ window.addEventListener("message", e => { let data = JSON.parse(e.data); let file_name = data.details.name; let tof = data.details.type; let loc = data.path; let size = data.details.size; if (size > 1000000000) { size = `${(size / 1000000000).toFixed(2)} GB`; } else if (size > 1000000) { size = `${(size / 1000000).toFixed(2)} MB`; } else if (size > 1000) { size = `${(size / 1000).toFixed(2)} KB`; } else { size = `${size} B`; } let created = data.details.created; let createdDate = new Date(created); created = `${createdDate.getFullYear()}-${createdDate.getMonth()}-${createdDate.getDate()}`; let modified = data.details.modified; let modifiedDate = new Date(modified); modified = `${modifiedDate.getFullYear()}-${modifiedDate.getMonth()}-${modifiedDate.getDate()}`; let accessed = data.details.accessed; let accessedDate = new Date(accessed); accessed = `${accessedDate.getFullYear()}-${accessedDate.getMonth()}-${accessedDate.getDate()}`; document.body.innerHTML = `
Name:
${file_name}
Type of file:
${tof}
${ data.details.mime ? `
MIME:
${data.details.mime}
` : `` }
Location:
${loc}
Size:
${size}
Created:
${created}
Modified:
${modified}
Accessed:
${accessed}
`; }); ================================================ FILE: public/apps/files.tapp/webdav.js ================================================ /*! For license information please see webdav.js.LICENSE.txt */ var t={0:()=>{},80:(t,e)=>{const n=":A-Za-z_\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD",r="["+n+"]["+n+"\\-.\\d\\u00B7\\u0300-\\u036F\\u203F-\\u2040]*",o=new RegExp("^"+r+"$");e.isExist=function(t){return void 0!==t},e.isEmptyObject=function(t){return 0===Object.keys(t).length},e.merge=function(t,e,n){if(e){const r=Object.keys(e),o=r.length;for(let i=0;i{const{buildOptions:r}=n(785),o=n(385),{prettify:i}=n(978),s=n(993);t.exports=class{constructor(t){this.externalEntities={},this.options=r(t)}parse(t,e){if("string"==typeof t);else{if(!t.toString)throw new Error("XML data is accepted in String or Bytes[] form.");t=t.toString()}if(e){!0===e&&(e={});const n=s.validate(t,e);if(!0!==n)throw Error(`${n.err.msg}:${n.err.line}:${n.err.col}`)}const n=new o(this.options);n.addExternalEntities(this.externalEntities);const r=n.parseXml(t);return this.options.preserveOrder||void 0===r?r:i(r,this.options)}addEntity(t,e){if(-1!==e.indexOf("&"))throw new Error("Entity value can't have '&'");if(-1!==t.indexOf("&")||-1!==t.indexOf(";"))throw new Error("An entity must be set without '&' and ';'. Eg. use '#xD' for ' '");if("&"===e)throw new Error("An entity with value '&' is not permitted");this.externalEntities[t]=e}}},173:(t,e,n)=>{const r=n(993),o=n(156),i=n(179);t.exports={XMLParser:o,XMLValidator:r,XMLBuilder:i}},179:(t,e,n)=>{const r=n(829),o=n(940),i={attributeNamePrefix:"@_",attributesGroupName:!1,textNodeName:"#text",ignoreAttributes:!0,cdataPropName:!1,format:!1,indentBy:" ",suppressEmptyNode:!1,suppressUnpairedNode:!0,suppressBooleanAttributes:!0,tagValueProcessor:function(t,e){return e},attributeValueProcessor:function(t,e){return e},preserveOrder:!1,commentPropName:!1,unpairedTags:[],entities:[{regex:new RegExp("&","g"),val:"&"},{regex:new RegExp(">","g"),val:">"},{regex:new RegExp("<","g"),val:"<"},{regex:new RegExp("'","g"),val:"'"},{regex:new RegExp('"',"g"),val:"""}],processEntities:!0,stopNodes:[],oneListGroup:!1};function s(t){this.options=Object.assign({},i,t),!0===this.options.ignoreAttributes||this.options.attributesGroupName?this.isAttribute=function(){return!1}:(this.ignoreAttributesFn=o(this.options.ignoreAttributes),this.attrPrefixLen=this.options.attributeNamePrefix.length,this.isAttribute=c),this.processTextOrObjNode=a,this.options.format?(this.indentate=u,this.tagEndChar=">\n",this.newLine="\n"):(this.indentate=function(){return""},this.tagEndChar=">",this.newLine="")}function a(t,e,n,r){const o=this.j2x(t,n+1,r.concat(e));return void 0!==t[this.options.textNodeName]&&1===Object.keys(t).length?this.buildTextValNode(t[this.options.textNodeName],e,o.attrStr,n):this.buildObjectNode(o.val,e,o.attrStr,n)}function u(t){return this.options.indentBy.repeat(t)}function c(t){return!(!t.startsWith(this.options.attributeNamePrefix)||t===this.options.textNodeName)&&t.substr(this.attrPrefixLen)}s.prototype.build=function(t){return this.options.preserveOrder?r(t,this.options):(Array.isArray(t)&&this.options.arrayNodeName&&this.options.arrayNodeName.length>1&&(t={[this.options.arrayNodeName]:t}),this.j2x(t,0,[]).val)},s.prototype.j2x=function(t,e,n){let r="",o="";const i=n.join(".");for(let s in t)if(Object.prototype.hasOwnProperty.call(t,s))if(void 0===t[s])this.isAttribute(s)&&(o+="");else if(null===t[s])this.isAttribute(s)||s===this.options.cdataPropName?o+="":"?"===s[0]?o+=this.indentate(e)+"<"+s+"?"+this.tagEndChar:o+=this.indentate(e)+"<"+s+"/"+this.tagEndChar;else if(t[s]instanceof Date)o+=this.buildTextValNode(t[s],s,"",e);else if("object"!=typeof t[s]){const n=this.isAttribute(s);if(n&&!this.ignoreAttributesFn(n,i))r+=this.buildAttrPairStr(n,""+t[s]);else if(!n)if(s===this.options.textNodeName){let e=this.options.tagValueProcessor(s,""+t[s]);o+=this.replaceEntitiesValue(e)}else o+=this.buildTextValNode(t[s],s,"",e)}else if(Array.isArray(t[s])){const r=t[s].length;let i="",a="";for(let u=0;u"+t+o}},s.prototype.closeTag=function(t){let e="";return-1!==this.options.unpairedTags.indexOf(t)?this.options.suppressUnpairedNode||(e="/"):e=this.options.suppressEmptyNode?"/":`>`+this.newLine;if(!1!==this.options.commentPropName&&e===this.options.commentPropName)return this.indentate(r)+`\x3c!--${t}--\x3e`+this.newLine;if("?"===e[0])return this.indentate(r)+"<"+e+n+"?"+this.tagEndChar;{let o=this.options.tagValueProcessor(e,t);return o=this.replaceEntitiesValue(o),""===o?this.indentate(r)+"<"+e+n+this.closeTag(e)+this.tagEndChar:this.indentate(r)+"<"+e+n+">"+o+"0&&this.options.processEntities)for(let e=0;e{function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},e(t)}function n(t){var e="function"==typeof Map?new Map:void 0;return n=function(t){if(null===t||(n=t,-1===Function.toString.call(n).indexOf("[native code]")))return t;var n;if("function"!=typeof t)throw new TypeError("Super expression must either be null or a function");if(void 0!==e){if(e.has(t))return e.get(t);e.set(t,s)}function s(){return r(t,arguments,i(this).constructor)}return s.prototype=Object.create(t.prototype,{constructor:{value:s,enumerable:!1,writable:!0,configurable:!0}}),o(s,t)},n(t)}function r(t,e,n){return r=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(t){return!1}}()?Reflect.construct:function(t,e,n){var r=[null];r.push.apply(r,e);var i=new(Function.bind.apply(t,r));return n&&o(i,n.prototype),i},r.apply(null,arguments)}function o(t,e){return o=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t},o(t,e)}function i(t){return i=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)},i(t)}var s=function(t){function n(t){var r;return function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,n),(r=function(t,n){return!n||"object"!==e(n)&&"function"!=typeof n?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):n}(this,i(n).call(this,t))).name="ObjectPrototypeMutationError",r}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&o(t,e)}(n,t),n}(n(Error));function a(t,n){for(var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){},o=n.split("."),i=o.length,s=function(e){var n=o[e];if(!t)return{v:void 0};if("+"===n){if(Array.isArray(t))return{v:t.map(function(n,i){var s=o.slice(e+1);return s.length>0?a(n,s.join("."),r):r(t,i,o,e)})};var i=o.slice(0,e).join(".");throw new Error("Object at wildcard (".concat(i,") is not an array"))}t=r(t,n,o,e)},u=0;u2&&void 0!==arguments[2]?arguments[2]:{};if("object"!=e(t)||null===t)return!1;if(void 0===n)return!1;if("number"==typeof n)return n in t;try{var o=!1;return a(t,n,function(t,e,n,i){if(!u(n,i))return t&&t[e];o=r.own?t.hasOwnProperty(e):e in t}),o}catch(t){return!1}},hasOwn:function(t,e,n){return this.has(t,e,n||{own:!0})},isIn:function(t,n,r){var o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};if("object"!=e(t)||null===t)return!1;if(void 0===n)return!1;try{var i=!1,s=!1;return a(t,n,function(t,n,o,a){return i=i||t===r||!!t&&t[n]===r,s=u(o,a)&&"object"===e(t)&&n in t,t&&t[n]}),o.validPath?i&&s:i}catch(t){return!1}},ObjectPrototypeMutationError:s}},214:t=>{var e,n;e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n={rotl:function(t,e){return t<>>32-e},rotr:function(t,e){return t<<32-e|t>>>e},endian:function(t){if(t.constructor==Number)return 16711935&n.rotl(t,8)|4278255360&n.rotl(t,24);for(var e=0;e0;t--)e.push(Math.floor(256*Math.random()));return e},bytesToWords:function(t){for(var e=[],n=0,r=0;n>>5]|=t[n]<<24-r%32;return e},wordsToBytes:function(t){for(var e=[],n=0;n<32*t.length;n+=8)e.push(t[n>>>5]>>>24-n%32&255);return e},bytesToHex:function(t){for(var e=[],n=0;n>>4).toString(16)),e.push((15&t[n]).toString(16));return e.join("")},hexToBytes:function(t){for(var e=[],n=0;n>>6*(3-i)&63)):n.push("=");return n.join("")},base64ToBytes:function(t){t=t.replace(/[^A-Z0-9+\/]/gi,"");for(var n=[],r=0,o=0;r>>6-2*o);return n}},t.exports=n},221:(t,e,n)=>{!function(){var e=n(214),r=n(706).utf8,o=n(552),i=n(706).bin,s=function(t,n){t.constructor==String?t=n&&"binary"===n.encoding?i.stringToBytes(t):r.stringToBytes(t):o(t)?t=Array.prototype.slice.call(t,0):Array.isArray(t)||t.constructor===Uint8Array||(t=t.toString());for(var a=e.bytesToWords(t),u=8*t.length,c=1732584193,l=-271733879,h=-1732584194,p=271733878,f=0;f>>24)|4278255360&(a[f]<<24|a[f]>>>8);a[u>>>5]|=128<>>9<<4)]=u;var g=s._ff,d=s._gg,m=s._hh,y=s._ii;for(f=0;f>>0,l=l+b>>>0,h=h+w>>>0,p=p+x>>>0}return e.endian([c,l,h,p])};s._ff=function(t,e,n,r,o,i,s){var a=t+(e&n|~e&r)+(o>>>0)+s;return(a<>>32-i)+e},s._gg=function(t,e,n,r,o,i,s){var a=t+(e&r|n&~r)+(o>>>0)+s;return(a<>>32-i)+e},s._hh=function(t,e,n,r,o,i,s){var a=t+(e^n^r)+(o>>>0)+s;return(a<>>32-i)+e},s._ii=function(t,e,n,r,o,i,s){var a=t+(n^(e|~r))+(o>>>0)+s;return(a<>>32-i)+e},s._blocksize=16,s._digestsize=16,t.exports=function(t,n){if(null==t)throw new Error("Illegal argument "+t);var r=e.wordsToBytes(s(t,n));return n&&n.asBytes?r:n&&n.asString?i.bytesToString(r):e.bytesToHex(r)}}()},320:(t,e)=>{var n=Object.prototype.hasOwnProperty;function r(t){try{return decodeURIComponent(t.replace(/\+/g," "))}catch(t){return null}}function o(t){try{return encodeURIComponent(t)}catch(t){return null}}e.stringify=function(t,e){e=e||"";var r,i,s=[];for(i in"string"!=typeof e&&(e="?"),t)if(n.call(t,i)){if((r=t[i])||null!=r&&!isNaN(r)||(r=""),i=o(i),r=o(r),null===i||null===r)continue;s.push(i+"="+r)}return s.length?e+s.join("&"):""},e.parse=function(t){for(var e,n=/([^=?#&]+)=?([^&]*)/g,o={};e=n.exec(t);){var i=r(e[1]),s=r(e[2]);null===i||null===s||i in o||(o[i]=s)}return o}},345:()=>{},385:(t,e,n)=>{const r=n(80),o=n(611),i=n(519),s=n(701),a=n(940);function u(t){const e=Object.keys(t);for(let n=0;n0)){s||(t=this.replaceEntitiesValue(t));const r=this.options.tagValueProcessor(e,t,n,o,i);return null==r?t:typeof r!=typeof t||r!==t?r:this.options.trimValues||t.trim()===t?x(t,this.options.parseTagValue,this.options.numberParseOptions):t}}function l(t){if(this.options.removeNSPrefix){const e=t.split(":"),n="/"===t.charAt(0)?"/":"";if("xmlns"===e[0])return"";2===e.length&&(t=n+e[1])}return t}const h=new RegExp("([^\\s=]+)\\s*(=\\s*(['\"])([\\s\\S]*?)\\3)?","gm");function p(t,e,n){if(!0!==this.options.ignoreAttributes&&"string"==typeof t){const n=r.getAllMatches(t,h),o=n.length,i={};for(let t=0;t",a,"Closing Tag is not closed.");let o=t.substring(a+2,e).trim();if(this.options.removeNSPrefix){const t=o.indexOf(":");-1!==t&&(o=o.substr(t+1))}this.options.transformTagName&&(o=this.options.transformTagName(o)),n&&(r=this.saveTextToParentTag(r,n,s));const i=s.substring(s.lastIndexOf(".")+1);if(o&&-1!==this.options.unpairedTags.indexOf(o))throw new Error(`Unpaired tag can not be used as closing tag: `);let u=0;i&&-1!==this.options.unpairedTags.indexOf(i)?(u=s.lastIndexOf(".",s.lastIndexOf(".")-1),this.tagsNodeStack.pop()):u=s.lastIndexOf("."),s=s.substring(0,u),n=this.tagsNodeStack.pop(),r="",a=e}else if("?"===t[a+1]){let e=b(t,a,!1,"?>");if(!e)throw new Error("Pi Tag is not closed.");if(r=this.saveTextToParentTag(r,n,s),this.options.ignoreDeclaration&&"?xml"===e.tagName||this.options.ignorePiTags);else{const t=new o(e.tagName);t.add(this.options.textNodeName,""),e.tagName!==e.tagExp&&e.attrExpPresent&&(t[":@"]=this.buildAttributesMap(e.tagExp,s,e.tagName)),this.addChild(n,t,s)}a=e.closeIndex+1}else if("!--"===t.substr(a+1,3)){const e=v(t,"--\x3e",a+4,"Comment is not closed.");if(this.options.commentPropName){const o=t.substring(a+4,e-2);r=this.saveTextToParentTag(r,n,s),n.add(this.options.commentPropName,[{[this.options.textNodeName]:o}])}a=e}else if("!D"===t.substr(a+1,2)){const e=i(t,a);this.docTypeEntities=e.entities,a=e.i}else if("!["===t.substr(a+1,2)){const e=v(t,"]]>",a,"CDATA is not closed.")-2,o=t.substring(a+9,e);r=this.saveTextToParentTag(r,n,s);let i=this.parseTextData(o,n.tagname,s,!0,!1,!0,!0);null==i&&(i=""),this.options.cdataPropName?n.add(this.options.cdataPropName,[{[this.options.textNodeName]:o}]):n.add(this.options.textNodeName,i),a=e+2}else{let i=b(t,a,this.options.removeNSPrefix),u=i.tagName;const c=i.rawTagName;let l=i.tagExp,h=i.attrExpPresent,p=i.closeIndex;this.options.transformTagName&&(u=this.options.transformTagName(u)),n&&r&&"!xml"!==n.tagname&&(r=this.saveTextToParentTag(r,n,s,!1));const f=n;if(f&&-1!==this.options.unpairedTags.indexOf(f.tagname)&&(n=this.tagsNodeStack.pop(),s=s.substring(0,s.lastIndexOf("."))),u!==e.tagname&&(s+=s?"."+u:u),this.isItStopNode(this.options.stopNodes,s,u)){let e="";if(l.length>0&&l.lastIndexOf("/")===l.length-1)"/"===u[u.length-1]?(u=u.substr(0,u.length-1),s=s.substr(0,s.length-1),l=u):l=l.substr(0,l.length-1),a=i.closeIndex;else if(-1!==this.options.unpairedTags.indexOf(u))a=i.closeIndex;else{const n=this.readStopNodeData(t,c,p+1);if(!n)throw new Error(`Unexpected end of ${c}`);a=n.i,e=n.tagContent}const r=new o(u);u!==l&&h&&(r[":@"]=this.buildAttributesMap(l,s,u)),e&&(e=this.parseTextData(e,u,s,!0,h,!0,!0)),s=s.substr(0,s.lastIndexOf(".")),r.add(this.options.textNodeName,e),this.addChild(n,r,s)}else{if(l.length>0&&l.lastIndexOf("/")===l.length-1){"/"===u[u.length-1]?(u=u.substr(0,u.length-1),s=s.substr(0,s.length-1),l=u):l=l.substr(0,l.length-1),this.options.transformTagName&&(u=this.options.transformTagName(u));const t=new o(u);u!==l&&h&&(t[":@"]=this.buildAttributesMap(l,s,u)),this.addChild(n,t,s),s=s.substr(0,s.lastIndexOf("."))}else{const t=new o(u);this.tagsNodeStack.push(n),u!==l&&h&&(t[":@"]=this.buildAttributesMap(l,s,u)),this.addChild(n,t,s),n=t}r="",a=p}}else r+=t[a];return e.child};function g(t,e,n){const r=this.options.updateTag(e.tagname,n,e[":@"]);!1===r||("string"==typeof r?(e.tagname=r,t.addChild(e)):t.addChild(e))}const d=function(t){if(this.options.processEntities){for(let e in this.docTypeEntities){const n=this.docTypeEntities[e];t=t.replace(n.regx,n.val)}for(let e in this.lastEntities){const n=this.lastEntities[e];t=t.replace(n.regex,n.val)}if(this.options.htmlEntities)for(let e in this.htmlEntities){const n=this.htmlEntities[e];t=t.replace(n.regex,n.val)}t=t.replace(this.ampEntity.regex,this.ampEntity.val)}return t};function m(t,e,n,r){return t&&(void 0===r&&(r=0===e.child.length),void 0!==(t=this.parseTextData(t,e.tagname,n,!1,!!e[":@"]&&0!==Object.keys(e[":@"]).length,r))&&""!==t&&e.add(this.options.textNodeName,t),t=""),t}function y(t,e,n){const r="*."+n;for(const n in t){const o=t[n];if(r===o||e===o)return!0}return!1}function v(t,e,n,r){const o=t.indexOf(e,n);if(-1===o)throw new Error(r);return o+e.length-1}function b(t,e,n){const r=function(t,e){let n,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:">",o="";for(let i=e;i3&&void 0!==arguments[3]?arguments[3]:">");if(!r)return;let o=r.data;const i=r.index,s=o.search(/\s/);let a=o,u=!0;-1!==s&&(a=o.substring(0,s),o=o.substring(s+1).trimStart());const c=a;if(n){const t=a.indexOf(":");-1!==t&&(a=a.substr(t+1),u=a!==r.data.substr(t+1))}return{tagName:a,tagExp:o,closeIndex:i,attrExpPresent:u,rawTagName:c}}function w(t,e,n){const r=n;let o=1;for(;n",n,`${e} is not closed`);if(t.substring(n+2,i).trim()===e&&(o--,0===o))return{tagContent:t.substring(r,n),i};n=i}else if("?"===t[n+1])n=v(t,"?>",n+1,"StopNode is not closed.");else if("!--"===t.substr(n+1,3))n=v(t,"--\x3e",n+3,"StopNode is not closed.");else if("!["===t.substr(n+1,2))n=v(t,"]]>",n,"StopNode is not closed.")-2;else{const r=b(t,n,">");r&&((r&&r.tagName)===e&&"/"!==r.tagExp[r.tagExp.length-1]&&o++,n=r.closeIndex)}}function x(t,e,n){if(e&&"string"==typeof t){const e=t.trim();return"true"===e||"false"!==e&&s(t,n)}return r.isExist(t)?t:""}t.exports=class{constructor(t){this.options=t,this.currentNode=null,this.tagsNodeStack=[],this.docTypeEntities={},this.lastEntities={apos:{regex:/&(apos|#39|#x27);/g,val:"'"},gt:{regex:/&(gt|#62|#x3E);/g,val:">"},lt:{regex:/&(lt|#60|#x3C);/g,val:"<"},quot:{regex:/&(quot|#34|#x22);/g,val:'"'}},this.ampEntity={regex:/&(amp|#38|#x26);/g,val:"&"},this.htmlEntities={space:{regex:/&(nbsp|#160);/g,val:" "},cent:{regex:/&(cent|#162);/g,val:"¢"},pound:{regex:/&(pound|#163);/g,val:"£"},yen:{regex:/&(yen|#165);/g,val:"¥"},euro:{regex:/&(euro|#8364);/g,val:"€"},copyright:{regex:/&(copy|#169);/g,val:"©"},reg:{regex:/&(reg|#174);/g,val:"®"},inr:{regex:/&(inr|#8377);/g,val:"₹"},num_dec:{regex:/&#([0-9]{1,7});/g,val:(t,e)=>String.fromCharCode(Number.parseInt(e,10))},num_hex:{regex:/&#x([0-9a-fA-F]{1,6});/g,val:(t,e)=>String.fromCharCode(Number.parseInt(e,16))}},this.addExternalEntities=u,this.parseXml=f,this.parseTextData=c,this.resolveNameSpace=l,this.buildAttributesMap=p,this.isItStopNode=y,this.replaceEntitiesValue=d,this.readStopNodeData=w,this.saveTextToParentTag=m,this.addChild=g,this.ignoreAttributesFn=a(this.options.ignoreAttributes)}}},388:()=>{},519:(t,e,n)=>{const r=n(80);function o(t,e){let n="";for(;e"===t[e]){if(p?"-"===t[e-1]&&"-"===t[e-2]&&(p=!1,r--):r--,0===r)break}else"["===t[e]?h=!0:f+=t[e];else{if(h&&s(t,e)){let r,i;e+=7,[r,i,e]=o(t,e+1),-1===i.indexOf("&")&&(n[l(r)]={regx:RegExp(`&${r};`,"g"),val:i})}else if(h&&a(t,e))e+=8;else if(h&&u(t,e))e+=8;else if(h&&c(t,e))e+=9;else{if(!i)throw new Error("Invalid DOCTYPE");p=!0}r++,f=""}if(0!==r)throw new Error("Unclosed DOCTYPE")}return{entities:n,i:e}}},552:t=>{function e(t){return!!t.constructor&&"function"==typeof t.constructor.isBuffer&&t.constructor.isBuffer(t)}t.exports=function(t){return null!=t&&(e(t)||function(t){return"function"==typeof t.readFloatLE&&"function"==typeof t.slice&&e(t.slice(0,0))}(t)||!!t._isBuffer)}},574:(t,e,n)=>{var r=n(773);t.exports=function(t){return t?("{}"===t.substr(0,2)&&(t="\\{\\}"+t.substr(2)),m(function(t){return t.split("\\\\").join(o).split("\\{").join(i).split("\\}").join(s).split("\\,").join(a).split("\\.").join(u)}(t),!0).map(l)):[]};var o="\0SLASH"+Math.random()+"\0",i="\0OPEN"+Math.random()+"\0",s="\0CLOSE"+Math.random()+"\0",a="\0COMMA"+Math.random()+"\0",u="\0PERIOD"+Math.random()+"\0";function c(t){return parseInt(t,10)==t?parseInt(t,10):t.charCodeAt(0)}function l(t){return t.split(o).join("\\").split(i).join("{").split(s).join("}").split(a).join(",").split(u).join(".")}function h(t){if(!t)return[""];var e=[],n=r("{","}",t);if(!n)return t.split(",");var o=n.pre,i=n.body,s=n.post,a=o.split(",");a[a.length-1]+="{"+i+"}";var u=h(s);return s.length&&(a[a.length-1]+=u.shift(),a.push.apply(a,u)),e.push.apply(e,a),e}function p(t){return"{"+t+"}"}function f(t){return/^-?0\d/.test(t)}function g(t,e){return t<=e}function d(t,e){return t>=e}function m(t,e){var n=[],o=r("{","}",t);if(!o)return[t];var i=o.pre,a=o.post.length?m(o.post,!1):[""];if(/\$$/.test(o.pre))for(var u=0;u=0;if(!x&&!N)return o.post.match(/,(?!,).*\}/)?m(t=o.pre+"{"+o.body+s+o.post):[t];if(x)y=o.body.split(/\.\./);else if(1===(y=h(o.body)).length&&1===(y=m(y[0],!1).map(p)).length)return a.map(function(t){return o.pre+y[0]+t});if(x){var P=c(y[0]),A=c(y[1]),E=Math.max(y[0].length,y[1].length),O=3==y.length?Math.abs(c(y[2])):1,T=g;A0){var I=new Array(C+1).join("0");$=S<0?"-"+I+$.slice(1):I+$}}v.push($)}}else{v=[];for(var k=0;k{t.exports=class{constructor(t){this.tagname=t,this.child=[],this[":@"]={}}add(t,e){"__proto__"===t&&(t="#__proto__"),this.child.push({[t]:e})}addChild(t){"__proto__"===t.tagname&&(t.tagname="#__proto__"),t[":@"]&&Object.keys(t[":@"]).length>0?this.child.push({[t.tagname]:t.child,":@":t[":@"]}):this.child.push({[t.tagname]:t.child})}}},694:(t,e)=>{e.d=function(t){if(!t)return 0;for(var e=(t=t.toString()).length,n=t.length;n--;){var r=t.charCodeAt(n);56320<=r&&r<=57343&&n--,127{var r=n(0),o=function(t){return"string"==typeof t};function i(t,e){for(var n=[],r=0;r=-1&&!e;n--){var r=n>=0?arguments[n]:process.cwd();if(!o(r))throw new TypeError("Arguments to path.resolve must be strings");r&&(t=r+"/"+t,e="/"===r.charAt(0))}return(e?"/":"")+(t=i(t.split("/"),!e).join("/"))||"."},a.normalize=function(t){var e=a.isAbsolute(t),n="/"===t.substr(-1);return(t=i(t.split("/"),!e).join("/"))||e||(t="."),t&&n&&(t+="/"),(e?"/":"")+t},a.isAbsolute=function(t){return"/"===t.charAt(0)},a.join=function(){for(var t="",e=0;e=0&&""===t[n];n--);return e>n?[]:t.slice(e,n+1)}t=a.resolve(t).substr(1),e=a.resolve(e).substr(1);for(var r=n(t.split("/")),o=n(e.split("/")),i=Math.min(r.length,o.length),s=i,u=0;u{const e=/^[-+]?0x[a-fA-F0-9]+$/,n=/^([\-\+])?(0*)([0-9]*(\.[0-9]*)?)$/,r={hex:!0,leadingZeros:!0,decimalPoint:".",eNotation:!0};t.exports=function(t){let o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(o=Object.assign({},r,o),!t||"string"!=typeof t)return t;let i=t.trim();if(void 0!==o.skipLike&&o.skipLike.test(i))return t;if("0"===t)return 0;if(o.hex&&e.test(i))return function(t){if(parseInt)return parseInt(t,16);if(Number.parseInt)return Number.parseInt(t,16);if(window&&window.parseInt)return window.parseInt(t,16);throw new Error("parseInt, Number.parseInt, window.parseInt are not supported")}(i);if(-1!==i.search(/[eE]/)){const e=i.match(/^([-\+])?(0*)([0-9]*(\.[0-9]*)?[eE][-\+]?[0-9]+)$/);if(e){if(o.leadingZeros)i=(e[1]||"")+e[3];else if("0"!==e[2]||"."!==e[3][0])return t;return o.eNotation?Number(i):t}return t}{const e=n.exec(i);if(e){const n=e[1],r=e[2];let a=(s=e[3])&&-1!==s.indexOf(".")?("."===(s=s.replace(/0+$/,""))?s="0":"."===s[0]?s="0"+s:"."===s[s.length-1]&&(s=s.substr(0,s.length-1)),s):s;if(!o.leadingZeros&&r.length>0&&n&&"."!==i[2])return t;if(!o.leadingZeros&&r.length>0&&!n&&"."!==i[1])return t;if(o.leadingZeros&&r===t)return 0;{const e=Number(i),s=""+e;return-1!==s.search(/[eE]/)?o.eNotation?e:t:-1!==i.indexOf(".")?"0"===s&&""===a||s===a||n&&s==="-"+a?e:t:r?a===s||n+a===s?e:t:i===s||i===n+s?e:t}}return t}var s}},706:t=>{var e={utf8:{stringToBytes:function(t){return e.bin.stringToBytes(unescape(encodeURIComponent(t)))},bytesToString:function(t){return decodeURIComponent(escape(e.bin.bytesToString(t)))}},bin:{stringToBytes:function(t){for(var e=[],n=0;n{var r=n(745),o=n(320),i=/^[\x00-\x20\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/,s=/[\n\r\t]/g,a=/^[A-Za-z][A-Za-z0-9+-.]*:\/\//,u=/:\d+$/,c=/^([a-z][a-z0-9.+-]*:)?(\/\/)?([\\/]+)?([\S\s]*)/i,l=/^[a-zA-Z]:/;function h(t){return(t||"").toString().replace(i,"")}var p=[["#","hash"],["?","query"],function(t,e){return d(e.protocol)?t.replace(/\\/g,"/"):t},["/","pathname"],["@","auth",1],[NaN,"host",void 0,1,1],[/:(\d*)$/,"port",void 0,1],[NaN,"hostname",void 0,1,1]],f={hash:1,query:1};function g(t){var e,n=("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{}).location||{},r={},o=typeof(t=t||n);if("blob:"===t.protocol)r=new y(unescape(t.pathname),{});else if("string"===o)for(e in r=new y(t,{}),f)delete r[e];else if("object"===o){for(e in t)e in f||(r[e]=t[e]);void 0===r.slashes&&(r.slashes=a.test(t.href))}return r}function d(t){return"file:"===t||"ftp:"===t||"http:"===t||"https:"===t||"ws:"===t||"wss:"===t}function m(t,e){t=(t=h(t)).replace(s,""),e=e||{};var n,r=c.exec(t),o=r[1]?r[1].toLowerCase():"",i=!!r[2],a=!!r[3],u=0;return i?a?(n=r[2]+r[3]+r[4],u=r[2].length+r[3].length):(n=r[2]+r[4],u=r[2].length):a?(n=r[3]+r[4],u=r[3].length):n=r[4],"file:"===o?u>=2&&(n=n.slice(2)):d(o)?n=r[4]:o?i&&(n=n.slice(2)):u>=2&&d(e.protocol)&&(n=r[4]),{protocol:o,slashes:i||d(o),slashesCount:u,rest:n}}function y(t,e,n){if(t=(t=h(t)).replace(s,""),!(this instanceof y))return new y(t,e,n);var i,a,u,c,f,v,b=p.slice(),w=typeof e,x=this,N=0;for("object"!==w&&"string"!==w&&(n=e,e=null),n&&"function"!=typeof n&&(n=o.parse),i=!(a=m(t||"",e=g(e))).protocol&&!a.slashes,x.slashes=a.slashes||i&&e.slashes,x.protocol=a.protocol||e.protocol||"",t=a.rest,("file:"===a.protocol&&(2!==a.slashesCount||l.test(t))||!a.slashes&&(a.protocol||a.slashesCount<2||!d(x.protocol)))&&(b[3]=[/(.*)/,"pathname"]);N{t.exports=function(t,e){if(e=e.split(":")[0],!(t=+t))return!1;switch(e){case"http":case"ws":return 80!==t;case"https":case"wss":return 443!==t;case"ftp":return 21!==t;case"gopher":return 70!==t;case"file":return!1}return 0!==t}},773:t=>{function e(t,e,o){t instanceof RegExp&&(t=n(t,o)),e instanceof RegExp&&(e=n(e,o));var i=r(t,e,o);return i&&{start:i[0],end:i[1],pre:o.slice(0,i[0]),body:o.slice(i[0]+t.length,i[1]),post:o.slice(i[1]+e.length)}}function n(t,e){var n=e.match(t);return n?n[0]:null}function r(t,e,n){var r,o,i,s,a,u=n.indexOf(t),c=n.indexOf(e,u+1),l=u;if(u>=0&&c>0){if(t===e)return[u,c];for(r=[],i=n.length;l>=0&&!a;)l==u?(r.push(l),u=n.indexOf(t,l+1)):1==r.length?a=[r.pop(),c]:((o=r.pop())=0?u:c;r.length&&(a=[i,s])}return a}t.exports=e,e.range=r},785:(t,e)=>{const n={preserveOrder:!1,attributeNamePrefix:"@_",attributesGroupName:!1,textNodeName:"#text",ignoreAttributes:!0,removeNSPrefix:!1,allowBooleanAttributes:!1,parseTagValue:!0,parseAttributeValue:!1,trimValues:!0,cdataPropName:!1,numberParseOptions:{hex:!0,leadingZeros:!0,eNotation:!0},tagValueProcessor:function(t,e){return e},attributeValueProcessor:function(t,e){return e},stopNodes:[],alwaysCreateTextNode:!1,isArray:()=>!1,commentPropName:!1,unpairedTags:[],processEntities:!0,htmlEntities:!1,ignoreDeclaration:!1,ignorePiTags:!1,transformTagName:!1,transformAttributeName:!1,updateTag:function(t,e,n){return t}};e.buildOptions=function(t){return Object.assign({},n,t)},e.defaultOptions=n},800:()=>{},802:function(t,e,n){var r;t=n.nmd(t),function(){var o=(t&&t.exports,"object"==typeof global&&global);o.global!==o&&o.window;var i=function(t){this.message=t};(i.prototype=new Error).name="InvalidCharacterError";var s=function(t){throw new i(t)},a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",u=/[\t\n\f\r ]/g,c={encode:function(t){t=String(t),/[^\0-\xFF]/.test(t)&&s("The string to be encoded contains characters outside of the Latin1 range.");for(var e,n,r,o,i=t.length%3,u="",c=-1,l=t.length-i;++c>18&63)+a.charAt(o>>12&63)+a.charAt(o>>6&63)+a.charAt(63&o);return 2==i?(e=t.charCodeAt(c)<<8,n=t.charCodeAt(++c),u+=a.charAt((o=e+n)>>10)+a.charAt(o>>4&63)+a.charAt(o<<2&63)+"="):1==i&&(o=t.charCodeAt(c),u+=a.charAt(o>>2)+a.charAt(o<<4&63)+"=="),u},decode:function(t){var e=(t=String(t).replace(u,"")).length;e%4==0&&(e=(t=t.replace(/==?$/,"")).length),(e%4==1||/[^+a-zA-Z0-9/]/.test(t))&&s("Invalid character: the string to be decoded is not correctly encoded.");for(var n,r,o=0,i="",c=-1;++c>(-2*o&6)));return i},version:"1.0.0"};void 0===(r=function(){return c}.call(e,n,e,t))||(t.exports=r)}()},805:()=>{},829:t=>{function e(t,s,a,u){let c="",l=!1;for(let h=0;h`,l=!1;continue}if(f===s.commentPropName){c+=u+`\x3c!--${p[f][0][s.textNodeName]}--\x3e`,l=!0;continue}if("?"===f[0]){const t=r(p[":@"],s),e="?xml"===f?"":u;let n=p[f][0][s.textNodeName];n=0!==n.length?" "+n:"",c+=e+`<${f}${n}${t}?>`,l=!0;continue}let d=u;""!==d&&(d+=s.indentBy);const m=u+`<${f}${r(p[":@"],s)}`,y=e(p[f],s,g,d);-1!==s.unpairedTags.indexOf(f)?s.suppressUnpairedNode?c+=m+">":c+=m+"/>":y&&0!==y.length||!s.suppressEmptyNode?y&&y.endsWith(">")?c+=m+`>${y}${u}`:(c+=m+">",y&&""!==u&&(y.includes("/>")||y.includes("`):c+=m+"/>",l=!0}return c}function n(t){const e=Object.keys(t);for(let n=0;n0&&e.processEntities)for(let n=0;n0&&(r="\n"),e(t,n,"",r)}},940:t=>{t.exports=function(t){return"function"==typeof t?t:Array.isArray(t)?e=>{for(const n of t){if("string"==typeof n&&e===n)return!0;if(n instanceof RegExp&&n.test(e))return!0}}:()=>!1}},978:(t,e)=>{function n(t,e,s){let a;const u={};for(let c=0;c0&&(u[e.textNodeName]=a):void 0!==a&&(u[e.textNodeName]=a),u}function r(t){const e=Object.keys(t);for(let t=0;t{const r=n(80),o={allowBooleanAttributes:!1,unpairedTags:[]};function i(t){return" "===t||"\t"===t||"\n"===t||"\r"===t}function s(t,e){const n=e;for(;e5&&"xml"===r)return g("InvalidXml","XML declaration allowed only at the start of the document.",y(t,e));if("?"==t[e]&&">"==t[e+1]){e++;break}continue}return e}function a(t,e){if(t.length>e+5&&"-"===t[e+1]&&"-"===t[e+2]){for(e+=3;e"===t[e+2]){e+=2;break}}else if(t.length>e+8&&"D"===t[e+1]&&"O"===t[e+2]&&"C"===t[e+3]&&"T"===t[e+4]&&"Y"===t[e+5]&&"P"===t[e+6]&&"E"===t[e+7]){let n=1;for(e+=8;e"===t[e]&&(n--,0===n))break}else if(t.length>e+9&&"["===t[e+1]&&"C"===t[e+2]&&"D"===t[e+3]&&"A"===t[e+4]&&"T"===t[e+5]&&"A"===t[e+6]&&"["===t[e+7])for(e+=8;e"===t[e+2]){e+=2;break}return e}e.validate=function(t,e){e=Object.assign({},o,e);const n=[];let r=!1,u=!1;"\ufeff"===t[0]&&(t=t.substr(1));for(let o=0;o"!==t[o]&&" "!==t[o]&&"\t"!==t[o]&&"\n"!==t[o]&&"\r"!==t[o];o++)d+=t[o];if(d=d.trim(),"/"===d[d.length-1]&&(d=d.substring(0,d.length-1),o--),!m(d)){let e;return e=0===d.trim().length?"Invalid space after '<'.":"Tag '"+d+"' is an invalid name.",g("InvalidTag",e,y(t,o))}const v=l(t,o);if(!1===v)return g("InvalidAttr","Attributes for '"+d+"' have open quote.",y(t,o));let b=v.value;if(o=v.index,"/"===b[b.length-1]){const n=o-b.length;b=b.substring(0,b.length-1);const i=p(b,e);if(!0!==i)return g(i.err.code,i.err.msg,y(t,n+i.err.line));r=!0}else if(h){if(!v.tagClosed)return g("InvalidTag","Closing tag '"+d+"' doesn't have proper closing.",y(t,o));if(b.trim().length>0)return g("InvalidTag","Closing tag '"+d+"' can't have attributes or invalid starting.",y(t,c));if(0===n.length)return g("InvalidTag","Closing tag '"+d+"' has not been opened.",y(t,c));{const e=n.pop();if(d!==e.tagName){let n=y(t,e.tagStartPos);return g("InvalidTag","Expected closing tag '"+e.tagName+"' (opened in line "+n.line+", col "+n.col+") instead of closing tag '"+d+"'.",y(t,c))}0==n.length&&(u=!0)}}else{const i=p(b,e);if(!0!==i)return g(i.err.code,i.err.msg,y(t,o-b.length+i.err.line));if(!0===u)return g("InvalidXml","Multiple possible root nodes found.",y(t,o));-1!==e.unpairedTags.indexOf(d)||n.push({tagName:d,tagStartPos:c}),r=!0}for(o++;o0)||g("InvalidXml","Invalid '"+JSON.stringify(n.map(t=>t.tagName),null,4).replace(/\r?\n/g,"")+"' found.",{line:1,col:1}):g("InvalidXml","Start tag expected.",1)};const u='"',c="'";function l(t,e){let n="",r="",o=!1;for(;e"===t[e]&&""===r){o=!0;break}n+=t[e]}return""===r&&{value:n,index:e,tagClosed:o}}const h=new RegExp("(\\s*)([^\\s=]+)(\\s*=)?(\\s*(['\"])(([\\s\\S])*?)\\5)?","g");function p(t,e){const n=r.getAllMatches(t,h),o={};for(let t=0;t{var e=t&&t.__esModule?()=>t.default:()=>t;return n.d(e,{a:e}),e},n.d=(t,e)=>{for(var r in e)n.o(e,r)&&!n.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),n.nmd=t=>(t.paths=[],t.children||(t.children=[]),t);var r=n(715),o=n.n(r);function i(t){if(!s(t))throw new Error("Parameter was not an error")}function s(t){return!!t&&"object"==typeof t&&"[object Error]"===(e=t,Object.prototype.toString.call(e))||t instanceof Error;var e}class a extends Error{constructor(t,e){const n=[...arguments],{options:r,shortMessage:o}=function(t){let e,n="";if(0===t.length)e={};else if(s(t[0]))e={cause:t[0]},n=t.slice(1).join(" ")||"";else if(t[0]&&"object"==typeof t[0])e=Object.assign({},t[0]),n=t.slice(1).join(" ")||"";else{if("string"!=typeof t[0])throw new Error("Invalid arguments passed to Layerr");e={},n=n=t.join(" ")||""}return{options:e,shortMessage:n}}(n);let i=o;if(r.cause&&(i=`${i}: ${r.cause.message}`),super(i),this.message=i,r.name&&"string"==typeof r.name?this.name=r.name:this.name="Layerr",r.cause&&Object.defineProperty(this,"_cause",{value:r.cause}),Object.defineProperty(this,"_info",{value:{}}),r.info&&"object"==typeof r.info&&Object.assign(this._info,r.info),Error.captureStackTrace){const t=r.constructorOpt||this.constructor;Error.captureStackTrace(this,t)}}static cause(t){return i(t),t._cause&&s(t._cause)?t._cause:null}static fullStack(t){i(t);const e=a.cause(t);return e?`${t.stack}\ncaused by: ${a.fullStack(e)}`:t.stack??""}static info(t){i(t);const e={},n=a.cause(t);return n&&Object.assign(e,a.info(n)),t._info&&Object.assign(e,t._info),e}toString(){let t=this.name||this.constructor.name||this.constructor.prototype.name;return this.message&&(t=`${t}: ${this.message}`),t}}var u=n(699),c=n.n(u);const l="__PATH_SEPARATOR_POSIX__",h="__PATH_SEPARATOR_WINDOWS__";function p(t){try{const e=t.replace(/\//g,l).replace(/\\\\/g,h);return encodeURIComponent(e).split(h).join("\\\\").split(l).join("/")}catch(t){throw new a(t,"Failed encoding path")}}function f(t){return t.startsWith("/")?t:"/"+t}function g(t){let e=t;return"/"!==e[0]&&(e="/"+e),/^.+\/$/.test(e)&&(e=e.substr(0,e.length-1)),e}function d(t){let e=new(o())(t).pathname;return e.length<=0&&(e="/"),g(e)}function m(){for(var t=arguments.length,e=new Array(t),n=0;n1){var n=t.shift();t[0]=n+t[0]}t[0].match(/^file:\/\/\//)?t[0]=t[0].replace(/^([^/:]+):\/*/,"$1:///"):t[0]=t[0].replace(/^([^/:]+):\/*/,"$1://");for(var r=0;r0&&(o=o.replace(/^[\/]+/,"")),o=r0?"?":"")+s.join("&")}("object"==typeof arguments[0]?arguments[0]:[].slice.call(arguments))}(e.reduce((t,e,n)=>((0===n||"/"!==e||"/"===e&&"/"!==t[t.length-1])&&t.push(e),t),[]))}var y=n(221),v=n.n(y);function b(t,e){const n=t.url.replace("//",""),r=-1==n.indexOf("/")?"/":n.slice(n.indexOf("/")),o=t.method?t.method.toUpperCase():"GET",i=!!/(^|,)\s*auth\s*($|,)/.test(e.qop)&&"auth",s=`00000000${e.nc}`.slice(-8),a=function(t,e,n,r,o,i,s){const a=s||v()(`${e}:${n}:${r}`);return t&&"md5-sess"===t.toLowerCase()?v()(`${a}:${o}:${i}`):a}(e.algorithm,e.username,e.realm,e.password,e.nonce,e.cnonce,e.ha1),u=v()(`${o}:${r}`),c=i?v()(`${a}:${e.nonce}:${s}:${e.cnonce}:${i}:${u}`):v()(`${a}:${e.nonce}:${u}`),l={username:e.username,realm:e.realm,nonce:e.nonce,uri:r,qop:i,response:c,nc:s,cnonce:e.cnonce,algorithm:e.algorithm,opaque:e.opaque},h=[];for(const t in l)l[t]&&("qop"===t||"nc"===t||"algorithm"===t?h.push(`${t}=${l[t]}`):h.push(`${t}="${l[t]}"`));return`Digest ${h.join(", ")}`}function w(t){return"digest"===(t.headers&&t.headers.get("www-authenticate")||"").split(/\s/)[0].toLowerCase()}var x=n(802),N=n.n(x);function P(t){return N().decode(t)}function A(t,e){var n;return`Basic ${n=`${t}:${e}`,N().encode(n)}`}const E="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:"undefined"!=typeof window?window:globalThis,O=(E.fetch.bind(E),E.Headers,E.Request),T=E.Response;let j=function(t){return t.Auto="auto",t.Digest="digest",t.None="none",t.Password="password",t.Token="token",t}({}),S=function(t){return t.DataTypeNoLength="data-type-no-length",t.InvalidAuthType="invalid-auth-type",t.InvalidOutputFormat="invalid-output-format",t.LinkUnsupportedAuthType="link-unsupported-auth",t.InvalidUpdateRange="invalid-update-range",t.NotSupported="not-supported",t}({});function $(t,e,n,r,o){switch(t.authType){case j.Auto:e&&n&&(t.headers.Authorization=A(e,n));break;case j.Digest:t.digest=function(t,e,n){return{username:t,password:e,ha1:n,nc:0,algorithm:"md5",hasDigestAuth:!1}}(e,n,o);break;case j.None:break;case j.Password:t.headers.Authorization=A(e,n);break;case j.Token:t.headers.Authorization=`${(i=r).token_type} ${i.access_token}`;break;default:throw new a({info:{code:S.InvalidAuthType}},`Invalid auth type: ${t.authType}`)}var i}function C(t,e){const n=new URL(t,window.location.origin).origin;return window.location.origin===n?(console.log("[WebDav] Libcurl will not be used since this request is local"),window.fetch(t,{mode:"cors",credentials:"include",...e})):window.parent.tb.libcurl.fetch(t,{...e})}n(345),n(800);const I="@@HOTPATCHER",k=()=>{};function R(t){return{original:t,methods:[t],final:!1}}class L{constructor(){this._configuration={registry:{},getEmptyAction:"null"},this.__type__=I}get configuration(){return this._configuration}get getEmptyAction(){return this.configuration.getEmptyAction}set getEmptyAction(t){this.configuration.getEmptyAction=t}control(t){let e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(!t||t.__type__!==I)throw new Error("Failed taking control of target HotPatcher instance: Invalid type or object");return Object.keys(t.configuration.registry).forEach(n=>{this.configuration.registry.hasOwnProperty(n)?e&&(this.configuration.registry[n]=Object.assign({},t.configuration.registry[n])):this.configuration.registry[n]=Object.assign({},t.configuration.registry[n])}),t._configuration=this.configuration,this}execute(t){const e=this.get(t)||k;for(var n=arguments.length,r=new Array(n>1?n-1:0),o=1;o0;)o=[e.shift().apply(i,o)];return o[0]}}(...e.methods)}isPatched(t){return!!this.configuration.registry[t]}patch(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{chain:r=!1}=n;if(this.configuration.registry[t]&&this.configuration.registry[t].final)throw new Error(`Failed patching '${t}': Method marked as being final`);if("function"!=typeof e)throw new Error(`Failed patching '${t}': Provided method is not a function`);if(r)this.configuration.registry[t]?this.configuration.registry[t].methods.push(e):this.configuration.registry[t]=R(e);else if(this.isPatched(t)){const{original:n}=this.configuration.registry[t];this.configuration.registry[t]=Object.assign(R(e),{original:n})}else this.configuration.registry[t]=R(e);return this}patchInline(t,e){this.isPatched(t)||this.patch(t,e);for(var n=arguments.length,r=new Array(n>2?n-2:0),o=2;o1?e-1:0),r=1;r{this.patch(t,e,{chain:!0})}),this}restore(t){if(!this.isPatched(t))throw new Error(`Failed restoring method: No method present for key: ${t}`);if("function"!=typeof this.configuration.registry[t].original)throw new Error(`Failed restoring method: Original method not found or of invalid type for key: ${t}`);return this.configuration.registry[t].methods=[this.configuration.registry[t].original],this}setFinal(t){if(!this.configuration.registry.hasOwnProperty(t))throw new Error(`Failed marking '${t}' as final: No method found for key`);return this.configuration.registry[t].final=!0,this}}let _=null;function M(){return _||(_=new L),_}function U(t){return function(t){if("object"!=typeof t||null===t||"[object Object]"!=Object.prototype.toString.call(t))return!1;if(null===Object.getPrototypeOf(t))return!0;let e=t;for(;null!==Object.getPrototypeOf(e);)e=Object.getPrototypeOf(e);return Object.getPrototypeOf(t)===e}(t)?Object.assign({},t):Object.setPrototypeOf(Object.assign({},t),Object.getPrototypeOf(t))}function F(){for(var t=arguments.length,e=new Array(t),n=0;n0;){const t=o.shift();r=r?D(r,t):U(t)}return r}function D(t,e){const n=U(t);return Object.keys(e).forEach(t=>{n.hasOwnProperty(t)?Array.isArray(e[t])?n[t]=Array.isArray(n[t])?[...n[t],...e[t]]:[...e[t]]:"object"==typeof e[t]&&e[t]?n[t]="object"==typeof n[t]&&n[t]?D(n[t],e[t]):U(e[t]):n[t]=e[t]:n[t]=e[t]}),n}function B(t){const e={};for(const n of t.keys())e[n]=t.get(n);return e}function W(){for(var t=arguments.length,e=new Array(t),n=0;n(Object.keys(e).forEach(n=>{const o=n.toLowerCase();r.hasOwnProperty(o)?t[r[o]]=e[n]:(r[o]=n,t[n]=e[n])}),t),{})}n(805);const V="function"==typeof ArrayBuffer,{toString:z}=Object.prototype;function G(t){return V&&(t instanceof ArrayBuffer||"[object ArrayBuffer]"===z.call(t))}function q(t){return null!=t&&null!=t.constructor&&"function"==typeof t.constructor.isBuffer&&t.constructor.isBuffer(t)}function H(t){return function(){for(var e=[],n=0;ne.patchInline("fetch",C,t.url,function(t){let e={};const n={method:t.method};if(t.headers&&(e=W(e,t.headers)),void 0!==t.data){const[r,o]=function(t){if("string"==typeof t)return[t,{}];if(q(t))return[new Uint8Array(t),{}];if(G(t))return[t,{}];if(t&&"object"==typeof t)return[JSON.stringify(t),{"content-type":"application/json"}];throw new Error("Unable to convert request body: Unexpected body type: "+typeof t)}(t.data);n.body=r,e=W(e,o)}return t.signal&&(n.signal=t.signal),t.withCredentials&&(n.credentials="include"),n.headers=e,n}(t)),t)}var tt=n(574);const et=t=>{if("string"!=typeof t)throw new TypeError("invalid pattern");if(t.length>65536)throw new TypeError("pattern is too long")},nt={"[:alnum:]":["\\p{L}\\p{Nl}\\p{Nd}",!0],"[:alpha:]":["\\p{L}\\p{Nl}",!0],"[:ascii:]":["\\x00-\\x7f",!1],"[:blank:]":["\\p{Zs}\\t",!0],"[:cntrl:]":["\\p{Cc}",!0],"[:digit:]":["\\p{Nd}",!0],"[:graph:]":["\\p{Z}\\p{C}",!0,!0],"[:lower:]":["\\p{Ll}",!0],"[:print:]":["\\p{C}",!0],"[:punct:]":["\\p{P}",!0],"[:space:]":["\\p{Z}\\t\\r\\n\\v\\f",!0],"[:upper:]":["\\p{Lu}",!0],"[:word:]":["\\p{L}\\p{Nl}\\p{Nd}\\p{Pc}",!0],"[:xdigit:]":["A-Fa-f0-9",!1]},rt=t=>t.replace(/[[\]\\-]/g,"\\$&"),ot=t=>t.join(""),it=(t,e)=>{const n=e;if("["!==t.charAt(n))throw new Error("not in a brace expression");const r=[],o=[];let i=n+1,s=!1,a=!1,u=!1,c=!1,l=n,h="";t:for(;ih?r.push(rt(h)+"-"+rt(e)):e===h&&r.push(rt(e)),h="",i++):t.startsWith("-]",i+1)?(r.push(rt(e+"-")),i+=2):t.startsWith("-",i+1)?(h=e,i+=2):(r.push(rt(e)),i++)}else u=!0,i++}else c=!0,i++}if(l1&&void 0!==arguments[1]?arguments[1]:{};return e?t.replace(/\[([^\/\\])\]/g,"$1"):t.replace(/((?!\\).|^)\[([^\/\\])\]/g,"$1$2").replace(/\\([^\/])/g,"$1")},at=new Set(["!","?","+","*","@"]),ut=t=>at.has(t),ct="(?!\\.)",lt=new Set(["[","."]),ht=new Set(["..","."]),pt=new Set("().*{}+?[]^$\\!"),ft=t=>t.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&"),gt="[^/]",dt=gt+"*?",mt=gt+"+?";class yt{type;#t;#e;#n=!1;#r=[];#o;#i;#s;#a=!1;#u;#c;#l=!1;constructor(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};this.type=t,t&&(this.#e=!0),this.#o=e,this.#t=this.#o?this.#o.#t:this,this.#u=this.#t===this?n:this.#t.#u,this.#s=this.#t===this?[]:this.#t.#s,"!"!==t||this.#t.#a||this.#s.push(this),this.#i=this.#o?this.#o.#r.length:0}get hasMagic(){if(void 0!==this.#e)return this.#e;for(const t of this.#r)if("string"!=typeof t&&(t.type||t.hasMagic))return this.#e=!0;return this.#e}toString(){return void 0!==this.#c?this.#c:this.type?this.#c=this.type+"("+this.#r.map(t=>String(t)).join("|")+")":this.#c=this.#r.map(t=>String(t)).join("")}#h(){if(this!==this.#t)throw new Error("should only call on root");if(this.#a)return this;let t;for(this.toString(),this.#a=!0;t=this.#s.pop();){if("!"!==t.type)continue;let e=t,n=e.#o;for(;n;){for(let r=e.#i+1;!n.type&&r"string"==typeof t?t:t.toJSON()):[this.type,...this.#r.map(t=>t.toJSON())];return this.isStart()&&!this.type&&t.unshift([]),this.isEnd()&&(this===this.#t||this.#t.#a&&"!"===this.#o?.type)&&t.push({}),t}isStart(){if(this.#t===this)return!0;if(!this.#o?.isStart())return!1;if(0===this.#i)return!0;const t=this.#o;for(let e=0;e1&&void 0!==arguments[1]?arguments[1]:{};const n=new yt(null,void 0,e);return yt.#p(t,n,0,e),n}toMMPattern(){if(this!==this.#t)return this.#t.toMMPattern();const t=this.toString(),[e,n,r,o]=this.toRegExpSource();if(!(r||this.#e||this.#u.nocase&&!this.#u.nocaseMagicOnly&&t.toUpperCase()!==t.toLowerCase()))return n;const i=(this.#u.nocase?"i":"")+(o?"u":"");return Object.assign(new RegExp(`^${e}$`,i),{_src:e,_glob:t})}get options(){return this.#u}toRegExpSource(t){const e=t??!!this.#u.dot;if(this.#t===this&&this.#h(),!this.type){const n=this.isStart()&&this.isEnd(),r=this.#r.map(e=>{const[r,o,i,s]="string"==typeof e?yt.#f(e,this.#e,n):e.toRegExpSource(t);return this.#e=this.#e||i,this.#n=this.#n||s,r}).join("");let o="";if(this.isStart()&&"string"==typeof this.#r[0]&&(1!==this.#r.length||!ht.has(this.#r[0]))){const n=lt,i=e&&n.has(r.charAt(0))||r.startsWith("\\.")&&n.has(r.charAt(2))||r.startsWith("\\.\\.")&&n.has(r.charAt(4)),s=!e&&!t&&n.has(r.charAt(0));o=i?"(?!(?:^|/)\\.\\.?(?:$|/))":s?ct:""}let i="";return this.isEnd()&&this.#t.#a&&"!"===this.#o?.type&&(i="(?:$|\\/)"),[o+r+i,st(r),this.#e=!!this.#e,this.#n]}const n="*"===this.type||"+"===this.type,r="!"===this.type?"(?:(?!(?:":"(?:";let o=this.#g(e);if(this.isStart()&&this.isEnd()&&!o&&"!"!==this.type){const t=this.toString();return this.#r=[t],this.type=null,this.#e=void 0,[t,st(this.toString()),!1,!1]}let i=!n||t||e?"":this.#g(!0);i===o&&(i=""),i&&(o=`(?:${o})(?:${i})*?`);let s="";return s="!"===this.type&&this.#l?(this.isStart()&&!e?ct:"")+mt:r+o+("!"===this.type?"))"+(!this.isStart()||e||t?"":ct)+dt+")":"@"===this.type?")":"?"===this.type?")?":"+"===this.type&&i?")":"*"===this.type&&i?")?":`)${this.type}`),[s,st(o),this.#e=!!this.#e,this.#n]}#g(t){return this.#r.map(e=>{if("string"==typeof e)throw new Error("string type in extglob ast??");const[n,r,o,i]=e.toRegExpSource(t);return this.#n=this.#n||i,n}).filter(t=>!(this.isStart()&&this.isEnd()&&!t)).join("|")}static#f(t,e){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2],r=!1,o="",i=!1;for(let s=0;s2&&void 0!==arguments[2]?arguments[2]:{};return et(e),!(!n.nocomment&&"#"===e.charAt(0))&&new zt(e,n).match(t)},bt=/^\*+([^+@!?\*\[\(]*)$/,wt=t=>e=>!e.startsWith(".")&&e.endsWith(t),xt=t=>e=>e.endsWith(t),Nt=t=>(t=t.toLowerCase(),e=>!e.startsWith(".")&&e.toLowerCase().endsWith(t)),Pt=t=>(t=t.toLowerCase(),e=>e.toLowerCase().endsWith(t)),At=/^\*+\.\*+$/,Et=t=>!t.startsWith(".")&&t.includes("."),Ot=t=>"."!==t&&".."!==t&&t.includes("."),Tt=/^\.\*+$/,jt=t=>"."!==t&&".."!==t&&t.startsWith("."),St=/^\*+$/,$t=t=>0!==t.length&&!t.startsWith("."),Ct=t=>0!==t.length&&"."!==t&&".."!==t,It=/^\?+([^+@!?\*\[\(]*)?$/,kt=t=>{let[e,n=""]=t;const r=Mt([e]);return n?(n=n.toLowerCase(),t=>r(t)&&t.toLowerCase().endsWith(n)):r},Rt=t=>{let[e,n=""]=t;const r=Ut([e]);return n?(n=n.toLowerCase(),t=>r(t)&&t.toLowerCase().endsWith(n)):r},Lt=t=>{let[e,n=""]=t;const r=Ut([e]);return n?t=>r(t)&&t.endsWith(n):r},_t=t=>{let[e,n=""]=t;const r=Mt([e]);return n?t=>r(t)&&t.endsWith(n):r},Mt=t=>{let[e]=t;const n=e.length;return t=>t.length===n&&!t.startsWith(".")},Ut=t=>{let[e]=t;const n=e.length;return t=>t.length===n&&"."!==t&&".."!==t},Ft="object"==typeof process&&process?"object"==typeof process.env&&process.env&&process.env.__MINIMATCH_TESTING_PLATFORM__||process.platform:"posix";vt.sep="win32"===Ft?"\\":"/";const Dt=Symbol("globstar **");vt.GLOBSTAR=Dt,vt.filter=function(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return n=>vt(n,t,e)};const Bt=function(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return Object.assign({},t,e)};vt.defaults=t=>{if(!t||"object"!=typeof t||!Object.keys(t).length)return vt;const e=vt;return Object.assign(function(n,r){return e(n,r,Bt(t,arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}))},{Minimatch:class extends e.Minimatch{constructor(e){super(e,Bt(t,arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}))}static defaults(n){return e.defaults(Bt(t,n)).Minimatch}},AST:class extends e.AST{constructor(e,n){super(e,n,Bt(t,arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}))}static fromGlob(n){let r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.AST.fromGlob(n,Bt(t,r))}},unescape:function(n){let r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.unescape(n,Bt(t,r))},escape:function(n){let r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.escape(n,Bt(t,r))},filter:function(n){let r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.filter(n,Bt(t,r))},defaults:n=>e.defaults(Bt(t,n)),makeRe:function(n){let r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.makeRe(n,Bt(t,r))},braceExpand:function(n){let r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.braceExpand(n,Bt(t,r))},match:function(n,r){let o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return e.match(n,r,Bt(t,o))},sep:e.sep,GLOBSTAR:Dt})};const Wt=function(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return et(t),e.nobrace||!/\{(?:(?!\{).)*\}/.test(t)?[t]:tt(t)};vt.braceExpand=Wt,vt.makeRe=function(t){return new zt(t,arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).makeRe()},vt.match=function(t,e){const n=new zt(e,arguments.length>2&&void 0!==arguments[2]?arguments[2]:{});return t=t.filter(t=>n.match(t)),n.options.nonull&&!t.length&&t.push(e),t};const Vt=/[?*]|[+@!]\(.*?\)|\[|\]/;class zt{options;set;pattern;windowsPathsNoEscape;nonegate;negate;comment;empty;preserveMultipleSlashes;partial;globSet;globParts;nocase;isWindows;platform;windowsNoMagicRoot;regexp;constructor(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};et(t),e=e||{},this.options=e,this.pattern=t,this.platform=e.platform||Ft,this.isWindows="win32"===this.platform,this.windowsPathsNoEscape=!!e.windowsPathsNoEscape||!1===e.allowWindowsEscape,this.windowsPathsNoEscape&&(this.pattern=this.pattern.replace(/\\/g,"/")),this.preserveMultipleSlashes=!!e.preserveMultipleSlashes,this.regexp=null,this.negate=!1,this.nonegate=!!e.nonegate,this.comment=!1,this.empty=!1,this.partial=!!e.partial,this.nocase=!!this.options.nocase,this.windowsNoMagicRoot=void 0!==e.windowsNoMagicRoot?e.windowsNoMagicRoot:!(!this.isWindows||!this.nocase),this.globSet=[],this.globParts=[],this.set=[],this.make()}hasMagic(){if(this.options.magicalBraces&&this.set.length>1)return!0;for(const t of this.set)for(const e of t)if("string"!=typeof e)return!0;return!1}debug(){}make(){const t=this.pattern,e=this.options;if(!e.nocomment&&"#"===t.charAt(0))return void(this.comment=!0);if(!t)return void(this.empty=!0);this.parseNegate(),this.globSet=[...new Set(this.braceExpand())],e.debug&&(this.debug=function(){return console.error(...arguments)}),this.debug(this.pattern,this.globSet);const n=this.globSet.map(t=>this.slashSplit(t));this.globParts=this.preprocess(n),this.debug(this.pattern,this.globParts);let r=this.globParts.map((t,e,n)=>{if(this.isWindows&&this.windowsNoMagicRoot){const e=!(""!==t[0]||""!==t[1]||"?"!==t[2]&&Vt.test(t[2])||Vt.test(t[3])),n=/^[a-z]:/i.test(t[0]);if(e)return[...t.slice(0,4),...t.slice(4).map(t=>this.parse(t))];if(n)return[t[0],...t.slice(1).map(t=>this.parse(t))]}return t.map(t=>this.parse(t))});if(this.debug(this.pattern,r),this.set=r.filter(t=>-1===t.indexOf(!1)),this.isWindows)for(let t=0;t=2?(t=this.firstPhasePreProcess(t),t=this.secondPhasePreProcess(t)):t=e>=1?this.levelOneOptimize(t):this.adjascentGlobstarOptimize(t),t}adjascentGlobstarOptimize(t){return t.map(t=>{let e=-1;for(;-1!==(e=t.indexOf("**",e+1));){let n=e;for(;"**"===t[n+1];)n++;n!==e&&t.splice(e,n-e)}return t})}levelOneOptimize(t){return t.map(t=>0===(t=t.reduce((t,e)=>{const n=t[t.length-1];return"**"===e&&"**"===n?t:".."===e&&n&&".."!==n&&"."!==n&&"**"!==n?(t.pop(),t):(t.push(e),t)},[])).length?[""]:t)}levelTwoFileOptimize(t){Array.isArray(t)||(t=this.slashSplit(t));let e=!1;do{if(e=!1,!this.preserveMultipleSlashes){for(let n=1;nr&&n.splice(r+1,o-r);let i=n[r+1];const s=n[r+2],a=n[r+3];if(".."!==i)continue;if(!s||"."===s||".."===s||!a||"."===a||".."===a)continue;e=!0,n.splice(r,1);const u=n.slice(0);u[r]="**",t.push(u),r--}if(!this.preserveMultipleSlashes){for(let t=1;tt.length)}partsMatch(t,e){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2],r=0,o=0,i=[],s="";for(;r2&&void 0!==arguments[2]&&arguments[2];const r=this.options;if(this.isWindows){const n="string"==typeof t[0]&&/^[a-z]:$/i.test(t[0]),r=!n&&""===t[0]&&""===t[1]&&"?"===t[2]&&/^[a-z]:$/i.test(t[3]),o="string"==typeof e[0]&&/^[a-z]:$/i.test(e[0]),i=r?3:n?0:void 0,s=!o&&""===e[0]&&""===e[1]&&"?"===e[2]&&"string"==typeof e[3]&&/^[a-z]:$/i.test(e[3])?3:o?0:void 0;if("number"==typeof i&&"number"==typeof s){const[n,r]=[t[i],e[s]];n.toLowerCase()===r.toLowerCase()&&(e[s]=n,s>i?e=e.slice(s):i>s&&(t=t.slice(i)))}}const{optimizationLevel:o=1}=this.options;o>=2&&(t=this.levelTwoFileOptimize(t)),this.debug("matchOne",this,{file:t,pattern:e}),this.debug("matchOne",t.length,e.length);for(var i=0,s=0,a=t.length,u=e.length;i>> no match, partial?",t,h,e,p),h!==a))}let o;if("string"==typeof c?(o=l===c,this.debug("string match",c,l,o)):(o=c.test(l),this.debug("pattern match",c,l,o)),!o)return!1}if(i===a&&s===u)return!0;if(i===a)return n;if(s===u)return i===a-1&&""===t[i];throw new Error("wtf?")}braceExpand(){return Wt(this.pattern,this.options)}parse(t){et(t);const e=this.options;if("**"===t)return Dt;if(""===t)return"";let n,r=null;(n=t.match(St))?r=e.dot?Ct:$t:(n=t.match(bt))?r=(e.nocase?e.dot?Pt:Nt:e.dot?xt:wt)(n[1]):(n=t.match(It))?r=(e.nocase?e.dot?Rt:kt:e.dot?Lt:_t)(n):(n=t.match(At))?r=e.dot?Ot:Et:(n=t.match(Tt))&&(r=jt);const o=yt.fromGlob(t,this.options).toMMPattern();return r&&"object"==typeof o&&Reflect.defineProperty(o,"test",{value:r}),o}makeRe(){if(this.regexp||!1===this.regexp)return this.regexp;const t=this.set;if(!t.length)return this.regexp=!1,this.regexp;const e=this.options,n=e.noglobstar?"[^/]*?":e.dot?"(?:(?!(?:\\/|^)(?:\\.{1,2})($|\\/)).)*?":"(?:(?!(?:\\/|^)\\.).)*?",r=new Set(e.nocase?["i"]:[]);let o=t.map(t=>{const e=t.map(t=>{if(t instanceof RegExp)for(const e of t.flags.split(""))r.add(e);return"string"==typeof t?t.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&"):t===Dt?Dt:t._src});return e.forEach((t,r)=>{const o=e[r+1],i=e[r-1];t===Dt&&i!==Dt&&(void 0===i?void 0!==o&&o!==Dt?e[r+1]="(?:\\/|"+n+"\\/)?"+o:e[r]=n:void 0===o?e[r-1]=i+"(?:\\/|"+n+")?":o!==Dt&&(e[r-1]=i+"(?:\\/|\\/"+n+"\\/)"+o,e[r+1]=Dt))}),e.filter(t=>t!==Dt).join("/")}).join("|");const[i,s]=t.length>1?["(?:",")"]:["",""];o="^"+i+o+s+"$",this.negate&&(o="^(?!"+o+").+$");try{this.regexp=new RegExp(o,[...r].join(""))}catch(t){this.regexp=!1}return this.regexp}slashSplit(t){return this.preserveMultipleSlashes?t.split("/"):this.isWindows&&/^\/\/[^\/]+/.test(t)?["",...t.split(/\/+/)]:t.split(/\/+/)}match(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.partial;if(this.debug("match",t,this.pattern),this.comment)return!1;if(this.empty)return""===t;if("/"===t&&e)return!0;const n=this.options;this.isWindows&&(t=t.split("\\").join("/"));const r=this.slashSplit(t);this.debug(this.pattern,"split",r);const o=this.set;this.debug(this.pattern,"set",o);let i=r[r.length-1];if(!i)for(let t=r.length-2;!i&&t>=0;t--)i=r[t];for(let t=0;t1&&void 0!==arguments[1]?arguments[1]:""}Invalid response: ${t.status} ${t.statusText}`);return e.status=t.status,e.response=t,e}function qt(t,e){const{status:n}=e;if(401===n&&t.digest)return e;if(n>=400)throw Gt(e);return e}function Ht(t,e){return arguments.length>2&&void 0!==arguments[2]&&arguments[2]?{data:e,headers:t.headers?B(t.headers):{},status:t.status,statusText:t.statusText}:e}vt.AST=yt,vt.Minimatch=zt,vt.escape=function(t){let{windowsPathsNoEscape:e=!1}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e?t.replace(/[?*()[\]]/g,"[$&]"):t.replace(/[?*()[\]\\]/g,"\\$&")},vt.unescape=st;const Zt=(Xt=function(t,e,n){let r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};const o=K({url:m(t.remoteURL,p(e)),method:"COPY",headers:{Destination:m(t.remoteURL,p(n)),Overwrite:!1===r.overwrite?"F":"T",Depth:r.shallow?"0":"infinity"}},t,r);return s=function(e){qt(t,e)},(i=J(o,t))&&i.then||(i=Promise.resolve(i)),s?i.then(s):i;var i,s},function(){for(var t=[],e=0;e2&&void 0!==arguments[2]?arguments[2]:Qt.Original;const r=Kt().get(t,e);return"array"===n&&!1===Array.isArray(r)?[r]:"object"===n&&Array.isArray(r)?r[0]:r}function ne(t,e){return e=e??{attributeNamePrefix:"@",attributeParsers:[],tagParsers:[te]},new Promise(n=>{n(function(t){const{multistatus:e}=t;if(""===e)return{multistatus:{response:[]}};if(!e)throw new Error("Invalid response: No root multistatus found");const n={multistatus:Array.isArray(e)?e[0]:e};return Kt().set(n,"multistatus.response",ee(n,"multistatus.response",Qt.Array)),Kt().set(n,"multistatus.response",Kt().get(n,"multistatus.response").map(t=>function(t){const e=Object.assign({},t);return e.status?Kt().set(e,"status",ee(e,"status",Qt.Object)):(Kt().set(e,"propstat",ee(e,"propstat",Qt.Object)),Kt().set(e,"propstat.prop",ee(e,"propstat.prop",Qt.Object))),e}(t))),n}(function(t){let{attributeNamePrefix:e,attributeParsers:n,tagParsers:r}=t;return new Yt.XMLParser({allowBooleanAttributes:!0,attributeNamePrefix:e,textNodeName:"text",ignoreAttributes:!1,removeNSPrefix:!0,numberParseOptions:{hex:!0,leadingZeros:!1},attributeValueProcessor(t,e,r){for(const t of n)try{const n=t(r,e);if(n!==e)return n}catch(t){}return e},tagValueProcessor(t,e,n){for(const t of r)try{const r=t(n,e);if(r!==e)return r}catch(t){}return e}})}(e).parse(t)))})}function re(t,e){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const{getlastmodified:r=null,getcontentlength:o="0",resourcetype:i=null,getcontenttype:s=null,getetag:a=null}=t,u=i&&"object"==typeof i&&void 0!==i.collection?"directory":"file",l={filename:e,basename:c().basename(e),lastmod:r,size:parseInt(o,10),type:u,etag:"string"==typeof a?a.replace(/"/g,""):null};return"file"===u&&(l.mime=s&&"string"==typeof s?s.split(";")[0]:""),n&&(void 0!==t.displayname&&(t.displayname=String(t.displayname)),l.props=t),l}function oe(t,e){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2],r=null;try{t.multistatus.response[0].propstat&&(r=t.multistatus.response[0])}catch(t){}if(!r)throw new Error("Failed getting item stat: bad response");const{propstat:{prop:o,status:i}}=r,[s,a,u]=i.split(" ",3),c=parseInt(a,10);if(c>=400){const t=new Error(`Invalid response: ${c} ${u}`);throw t.status=c,t}return re(o,g(e),n)}function ie(t){switch(String(t)){case"-3":return"unlimited";case"-2":case"-1":return"unknown";default:return parseInt(String(t),10)}}function se(t,e,n){return n?e?e(t):t:(t&&t.then||(t=Promise.resolve(t)),e?t.then(e):t)}const ae=function(t){return function(){for(var e=[],n=0;n2&&void 0!==arguments[2]?arguments[2]:{};const{details:r=!1}=n,o=K({url:m(t.remoteURL,p(e)),method:"PROPFIND",headers:{Accept:"text/plain,application/xml",Depth:"0"}},t,n);return se(J(o,t),function(n){return qt(t,n),se(n.text(),function(o){return se(ne(o,t.parsing),function(t){const o=oe(t,e,r);return Ht(n,o,r)})})})});function ue(t,e,n){return n?e?e(t):t:(t&&t.then||(t=Promise.resolve(t)),e?t.then(e):t)}const ce=le(function(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const r=function(t){if(!t||"/"===t)return[];let e=t;const n=[];do{n.push(e),e=c().dirname(e)}while(e&&"/"!==e);return n}(g(e));r.sort((t,e)=>t.length>e.length?1:e.length>t.length?-1:0);let o=!1;return function(t,e,n){if("function"==typeof t[fe]){var r,o,i,s=t[fe]();function l(t){try{for(;!((r=s.next()).done||n&&n());)if((t=e(r.value))&&t.then){if(!me(t))return void t.then(l,i||(i=ge.bind(null,o=new de,2)));t=t.v}o?ge(o,1,t):o=t}catch(t){ge(o||(o=new de),2,t)}}if(l(),s.return){var a=function(t){try{r.done||s.return()}catch(t){}return t};if(o&&o.then)return o.then(a,function(t){throw a(t)});a()}return o}if(!("length"in t))throw new TypeError("Object is not iterable");for(var u=[],c=0;c2&&void 0!==arguments[2]?arguments[2]:{};if(!0===n.recursive)return ce(t,e,n);const r=K({url:m(t.remoteURL,(o=p(e),o.endsWith("/")?o:o+"/")),method:"MKCOL"},t,n);var o;return ue(J(r,t),function(e){qt(t,e)})});var ve=n(388),be=n.n(ve);const we=function(t){return function(){for(var e=[],n=0;n2&&void 0!==arguments[2]?arguments[2]:{};const r={};if("object"==typeof n.range&&"number"==typeof n.range.start){let t=`bytes=${n.range.start}-`;"number"==typeof n.range.end&&(t=`${t}${n.range.end}`),r.Range=t}const o=K({url:m(t.remoteURL,p(e)),method:"GET",headers:r},t,n);return s=function(e){if(qt(t,e),r.Range&&206!==e.status){const t=new Error(`Invalid response code for partial request: ${e.status}`);throw t.status=e.status,t}return n.callback&&setTimeout(()=>{n.callback(e)},0),e.body},(i=J(o,t))&&i.then||(i=Promise.resolve(i)),s?i.then(s):i;var i,s}),xe=()=>{},Ne=function(t){return function(){for(var e=[],n=0;n2&&void 0!==arguments[2]?arguments[2]:{};const r=K({url:m(t.remoteURL,p(e)),method:"DELETE"},t,n);return i=function(e){qt(t,e)},(o=J(r,t))&&o.then||(o=Promise.resolve(o)),i?o.then(i):o;var o,i}),Ae=function(t){return function(){for(var e=[],n=0;n2&&void 0!==arguments[2]?arguments[2]:{};return function(r,o){try{var i=(s=ae(t,e,n),a=function(){return!0},u?a?a(s):s:(s&&s.then||(s=Promise.resolve(s)),a?s.then(a):s))}catch(t){return o(t)}var s,a,u;return i&&i.then?i.then(void 0,o):i}(0,function(t){if(404===t.status)return!1;throw t})});function Ee(t,e,n){return n?e?e(t):t:(t&&t.then||(t=Promise.resolve(t)),e?t.then(e):t)}const Oe=function(t){return function(){for(var e=[],n=0;n2&&void 0!==arguments[2]?arguments[2]:{};const r=K({url:m(t.remoteURL,p(e),"/"),method:"PROPFIND",headers:{Accept:"text/plain,application/xml",Depth:n.deep?"infinity":"1"}},t,n);return Ee(J(r,t),function(r){return qt(t,r),Ee(r.text(),function(o){if(!o)throw new Error("Failed parsing directory contents: Empty response");return Ee(ne(o,t.parsing),function(o){const i=f(e);let s=function(t,e,n){let r=arguments.length>3&&void 0!==arguments[3]&&arguments[3],o=arguments.length>4&&void 0!==arguments[4]&&arguments[4];const i=c().join(e,"/"),{multistatus:{response:s}}=t,u=s.map(t=>{const e=function(t){try{return t.replace(/^https?:\/\/[^\/]+/,"")}catch(t){throw new a(t,"Failed normalising HREF")}}(t.href),{propstat:{prop:n}}=t;return re(n,"/"===i?decodeURIComponent(g(e)):g(c().relative(decodeURIComponent(i),decodeURIComponent(e))),r)});return o?u:u.filter(t=>t.basename&&("file"===t.type||t.filename!==n.replace(/\/$/,"")))}(o,f(t.remoteBasePath||t.remotePath),i,n.details,n.includeSelf);return n.glob&&(s=function(t,e){return t.filter(t=>vt(t.filename,e,{matchBase:!0}))}(s,n.glob)),Ht(r,s,n.details)})})})});function Te(t){return function(){for(var e=[],n=0;n2&&void 0!==arguments[2]?arguments[2]:{};const r=K({url:m(t.remoteURL,p(e)),method:"GET",headers:{Accept:"text/plain"},transformResponse:[Ie]},t,n);return Se(J(r,t),function(e){return qt(t,e),Se(e.text(),function(t){return Ht(e,t,n.details)})})});function Se(t,e,n){return n?e?e(t):t:(t&&t.then||(t=Promise.resolve(t)),e?t.then(e):t)}const $e=Te(function(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const r=K({url:m(t.remoteURL,p(e)),method:"GET"},t,n);return Se(J(r,t),function(e){let r;return qt(t,e),function(t,e){var n=t();return n&&n.then?n.then(e):e()}(function(){return Se(e.arrayBuffer(),function(t){r=t})},function(){return Ht(e,r,n.details)})})}),Ce=Te(function(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{format:r="binary"}=n;if("binary"!==r&&"text"!==r)throw new a({info:{code:S.InvalidOutputFormat}},`Invalid output format: ${r}`);return"text"===r?je(t,e,n):$e(t,e,n)}),Ie=t=>t;function ke(t){return new Yt.XMLBuilder({attributeNamePrefix:"@_",format:!0,ignoreAttributes:!1,suppressEmptyNode:!0}).build(Re({lockinfo:{"@_xmlns:d":"DAV:",lockscope:{exclusive:{}},locktype:{write:{}},owner:{href:t}}},"d"))}function Re(t,e){const n={...t};for(const t in n)n.hasOwnProperty(t)&&(n[t]&&"object"==typeof n[t]&&-1===t.indexOf(":")?(n[`${e}:${t}`]=Re(n[t],e),delete n[t]):!1===/^@_/.test(t)&&(n[`${e}:${t}`]=n[t],delete n[t]));return n}function Le(t,e,n){return n?e?e(t):t:(t&&t.then||(t=Promise.resolve(t)),e?t.then(e):t)}function _e(t){return function(){for(var e=[],n=0;n3&&void 0!==arguments[3]?arguments[3]:{};const o=K({url:m(t.remoteURL,p(e)),method:"UNLOCK",headers:{"Lock-Token":n}},t,r);return Le(J(o,t),function(e){if(qt(t,e),204!==e.status&&200!==e.status)throw Gt(e)})}),Ue=_e(function(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{refreshToken:r,timeout:o=Fe}=n,i={Accept:"text/plain,application/xml",Timeout:o};r&&(i.If=r);const s=K({url:m(t.remoteURL,p(e)),method:"LOCK",headers:i,data:ke(t.contactHref)},t,n);return Le(J(s,t),function(e){return qt(t,e),Le(e.text(),function(t){const n=(i=t,new Yt.XMLParser({removeNSPrefix:!0,parseAttributeValue:!0,parseTagValue:!0}).parse(i)),r=Kt().get(n,"prop.lockdiscovery.activelock.locktoken.href"),o=Kt().get(n,"prop.lockdiscovery.activelock.timeout");var i;if(!r)throw Gt(e,"No lock token received: ");return{token:r,serverTimeout:o}})})}),Fe="Infinite, Second-4100000000";function De(t,e,n){return n?e?e(t):t:(t&&t.then||(t=Promise.resolve(t)),e?t.then(e):t)}const Be=function(t){return function(){for(var e=[],n=0;n1&&void 0!==arguments[1]?arguments[1]:{};const n=e.path||"/",r=K({url:m(t.remoteURL,n),method:"PROPFIND",headers:{Accept:"text/plain,application/xml",Depth:"0"}},t,e);return De(J(r,t),function(n){return qt(t,n),De(n.text(),function(r){return De(ne(r,t.parsing),function(t){const r=function(t){try{const[e]=t.multistatus.response,{propstat:{prop:{"quota-used-bytes":n,"quota-available-bytes":r}}}=e;return void 0!==n&&void 0!==r?{used:parseInt(String(n),10),available:ie(r)}:null}catch(t){}return null}(t);return Ht(n,r,e.details)})})})});function We(t,e,n){return n?e?e(t):t:(t&&t.then||(t=Promise.resolve(t)),e?t.then(e):t)}const Ve=function(t){return function(){for(var e=[],n=0;n2&&void 0!==arguments[2]?arguments[2]:{};const{details:r=!1}=n,o=K({url:m(t.remoteURL,p(e)),method:"SEARCH",headers:{Accept:"text/plain,application/xml","Content-Type":t.headers["Content-Type"]||"application/xml; charset=utf-8"}},t,n);return We(J(o,t),function(n){return qt(t,n),We(n.text(),function(o){return We(ne(o,t.parsing),function(t){const o=function(t,e,n){const r={truncated:!1,results:[]};return r.truncated=t.multistatus.response.some(t=>"507"===(t.status||t.propstat?.status).split(" ",3)?.[1]&&t.href.replace(/\/$/,"").endsWith(p(e).replace(/\/$/,""))),t.multistatus.response.forEach(t=>{if(void 0===t.propstat)return;const e=t.href.split("/").map(decodeURIComponent).join("/");r.results.push(re(t.propstat.prop,e,n))}),r}(t,e,r);return Ht(n,o,r)})})})}),ze=function(t){return function(){for(var e=[],n=0;n3&&void 0!==arguments[3]?arguments[3]:{};const o=K({url:m(t.remoteURL,p(e)),method:"MOVE",headers:{Destination:m(t.remoteURL,p(n)),Overwrite:!1===r.overwrite?"F":"T"}},t,r);return s=function(e){qt(t,e)},(i=J(o,t))&&i.then||(i=Promise.resolve(i)),s?i.then(s):i;var i,s});var Ge=n(694);function qe(t){if(G(t))return t.byteLength;if(q(t))return t.length;if("string"==typeof t)return(0,Ge.d)(t);throw new a({info:{code:S.DataTypeNoLength}},"Cannot calculate data length: Invalid type")}const He=function(t){return function(){for(var e=[],n=0;n3&&void 0!==arguments[3]?arguments[3]:{};const{contentLength:o=!0,overwrite:i=!0}=r,s={"Content-Type":"application/octet-stream"};!1===o||(s["Content-Length"]="number"==typeof o?`${o}`:`${qe(n)}`),i||(s["If-None-Match"]="*");const a=K({url:m(t.remoteURL,p(e)),method:"PUT",headers:s,data:n},t,r);return c=function(e){try{qt(t,e)}catch(t){const e=t;if(412!==e.status||i)throw e;return!1}return!0},(u=J(a,t))&&u.then||(u=Promise.resolve(u)),c?u.then(c):u;var u,c}),Ze=function(t){return function(){for(var e=[],n=0;n2&&void 0!==arguments[2]?arguments[2]:{};const r=K({url:m(t.remoteURL,p(e)),method:"OPTIONS"},t,n);return i=function(e){try{qt(t,e)}catch(t){throw t}return{compliance:(e.headers.get("DAV")??"").split(",").map(t=>t.trim()),server:e.headers.get("Server")??""}},(o=J(r,t))&&o.then||(o=Promise.resolve(o)),i?o.then(i):o;var o,i});function Xe(t,e,n){return n?e?e(t):t:(t&&t.then||(t=Promise.resolve(t)),e?t.then(e):t)}const Ye=Qe(function(t,e,n,r,o){let i=arguments.length>5&&void 0!==arguments[5]?arguments[5]:{};if(n>r||n<0)throw new a({info:{code:S.InvalidUpdateRange}},`Invalid update range ${n} for partial update`);const s={"Content-Type":"application/octet-stream","Content-Length":""+(r-n+1),"Content-Range":`bytes ${n}-${r}/*`},u=K({url:m(t.remoteURL,p(e)),method:"PUT",headers:s,data:o},t,i);return Xe(J(u,t),function(e){qt(t,e)})});function Je(t,e){var n=t();return n&&n.then?n.then(e):e(n)}const Ke=Qe(function(t,e,n,r,o){let i=arguments.length>5&&void 0!==arguments[5]?arguments[5]:{};if(n>r||n<0)throw new a({info:{code:S.InvalidUpdateRange}},`Invalid update range ${n} for partial update`);const s={"Content-Type":"application/x-sabredav-partialupdate","Content-Length":""+(r-n+1),"X-Update-Range":`bytes=${n}-${r}`},u=K({url:m(t.remoteURL,p(e)),method:"PATCH",headers:s,data:o},t,i);return Xe(J(u,t),function(e){qt(t,e)})});function Qe(t){return function(){for(var e=[],n=0;n5&&void 0!==arguments[5]?arguments[5]:{};return Xe(Ze(t,e,i),function(s){let u=!1;return Je(function(){if(s.compliance.includes("sabredav-partialupdate"))return Xe(Ke(t,e,n,r,o,i),function(t){return u=!0,t})},function(c){let l=!1;return u?c:Je(function(){if(s.server.includes("Apache")&&s.compliance.includes(""))return Xe(Ye(t,e,n,r,o,i),function(t){return l=!0,t})},function(t){if(l)return t;throw new a({info:{code:S.NotSupported}},"Not supported")})})})}),en="https://github.com/perry-mitchell/webdav-client/blob/master/LOCK_CONTACT.md";function nn(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{authType:n=null,remoteBasePath:r,contactHref:o=en,ha1:i,headers:s={},httpAgent:u,httpsAgent:c,password:l,token:h,username:f,withCredentials:g}=e;let y=n;y||(y=f||l?j.Password:j.None);const v={authType:y,remoteBasePath:r,contactHref:o,ha1:i,headers:Object.assign({},s),httpAgent:u,httpsAgent:c,password:l,parsing:{attributeNamePrefix:e.attributeNamePrefix??"@",attributeParsers:[],tagParsers:[te]},remotePath:d(t),remoteURL:t,token:h,username:f,withCredentials:g};return $(v,v.username,v.password,v.token,v.ha1),{copyFile:(t,e,n)=>Zt(v,t,e,n),createDirectory:(t,e)=>ye(v,t,e),createReadStream:(t,e)=>function(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const r=new(0,be().PassThrough);return we(t,e,n).then(t=>{t.pipe(r)}).catch(t=>{r.emit("error",t)}),r}(v,t,e),createWriteStream:(t,e,n)=>function(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:xe;const o=new(0,be().PassThrough),i={};!1===n.overwrite&&(i["If-None-Match"]="*");const s=K({url:m(t.remoteURL,p(e)),method:"PUT",headers:i,data:o,maxRedirects:0},t,n);return J(s,t).then(e=>qt(t,e)).then(t=>{setTimeout(()=>{r(t)},0)}).catch(t=>{o.emit("error",t)}),o}(v,t,e,n),customRequest:(t,e)=>Ne(v,t,e),deleteFile:(t,e)=>Pe(v,t,e),exists:(t,e)=>Ae(v,t,e),getDirectoryContents:(t,e)=>Oe(v,t,e),getFileContents:(t,e)=>Ce(v,t,e),getFileDownloadLink:t=>function(t,e){let n=m(t.remoteURL,p(e));const r=/^https:/i.test(n)?"https":"http";switch(t.authType){case j.None:break;case j.Password:{const e=P(t.headers.Authorization.replace(/^Basic /i,"").trim());n=n.replace(/^https?:\/\//,`${r}://${e}@`);break}default:throw new a({info:{code:S.LinkUnsupportedAuthType}},`Unsupported auth type for file link: ${t.authType}`)}return n}(v,t),getFileUploadLink:t=>function(t,e){let n=`${m(t.remoteURL,p(e))}?Content-Type=application/octet-stream`;const r=/^https:/i.test(n)?"https":"http";switch(t.authType){case j.None:break;case j.Password:{const e=P(t.headers.Authorization.replace(/^Basic /i,"").trim());n=n.replace(/^https?:\/\//,`${r}://${e}@`);break}default:throw new a({info:{code:S.LinkUnsupportedAuthType}},`Unsupported auth type for file link: ${t.authType}`)}return n}(v,t),getHeaders:()=>Object.assign({},v.headers),getQuota:t=>Be(v,t),lock:(t,e)=>Ue(v,t,e),moveFile:(t,e,n)=>ze(v,t,e,n),putFileContents:(t,e,n)=>He(v,t,e,n),partialUpdateFileContents:(t,e,n,r,o)=>tn(v,t,e,n,r,o),getDAVCompliance:t=>Ze(v,t),search:(t,e)=>Ve(v,t,e),setHeaders:t=>{v.headers=Object.assign({},t)},stat:(t,e)=>ae(v,t,e),unlock:(t,e,n)=>Me(v,t,e,n),registerAttributeParser:t=>{v.parsing.attributeParsers.push(t)},registerTagParser:t=>{v.parsing.tagParsers.push(t)}}}export{j as AuthType,S as ErrorCode,O as Request,T as Response,qe as calculateDataLength,nn as createClient,M as getPatcher,oe as parseStat,ne as parseXML,re as prepareFileFromProps,Ht as processResponsePayload,ie as translateDiskSpace}; ================================================ FILE: public/apps/fsapp.app/GUI.js ================================================ // implementing it myself was too hard so i just stole it from https://codepen.io/adam-lynch/pen/GaqgXP // This context menu is for files and folders const newcontextmenu = new anura.ContextMenu(); // This context menu is for applications and libraries const appcontextmenu = new anura.ContextMenu(); // This context menu is for when no files are selected const emptycontextmenu = new anura.ContextMenu(); // Helper to add context menu items to both menus function addContextMenuItem(name, func) { newcontextmenu.addItem(name, func); appcontextmenu.addItem(name, func); } // addContextMenuItem("Get Info", function () {}); // addContextMenuItem("Pin to Shelf", function () {}); addContextMenuItem("Cut", function () { cut(); }); addContextMenuItem("Copy", function () { copy(); }); addContextMenuItem("Paste", function () { paste(); }); addContextMenuItem("Delete", function () { deleteFile(); }); addContextMenuItem("Rename", function () { rename(); }); addContextMenuItem("Refresh", function () { reload(); }); appcontextmenu.addItem("Install (Session)", function () { // While this is the same as double clicking, it's still useful to have the verbosely named option installSession(); }); appcontextmenu.addItem("Install (Permanent)", function () { // This is not the same as double clicking, as it will install the app permanently installPermanent(); }); appcontextmenu.addItem("Navigate", function () { // Normally, double clicking a folder will navigate into it, but for apps and libs, this is not the case navigate(); }); emptycontextmenu.addItem("Upload from PC", function () { upload(); }); emptycontextmenu.addItem("New folder", function () { newFolder(); }); emptycontextmenu.addItem("New file", function () { newFile(); }); emptycontextmenu.addItem("Paste", function () { paste(); }); emptycontextmenu.addItem("Refresh", function () { reload(); }); const min = 150; // The max (fr) values for grid-template-columns const columnTypeToRatioMap = { icon: 0.1, name: 3, size: 1, type: 1, modified: 1, }; const table = document.querySelector("table"); /* The following will soon be filled with column objects containing the header element and their size value for grid-template-columns */ const columns = []; let headerBeingResized; // The next three functions are mouse event callbacks // Where the magic happens. I.e. when they're actually resizing const onMouseMove = (e) => requestAnimationFrame(() => { console.log("onMouseMove"); (window.getSelection ? window.getSelection() : document.selection ).empty(); // Calculate the desired width horizontalScrollOffset = document.documentElement.scrollLeft; const width = horizontalScrollOffset + e.clientX - headerBeingResized.offsetLeft; // Update the column object with the new size value const column = columns.find( ({ header }) => header === headerBeingResized, ); column.size = Math.max(min, width) + "px"; // Enforce our minimum // For the other headers which don't have a set width, fix it to their computed width columns.forEach((column) => { if (column.size.startsWith("minmax")) { // isn't fixed yet (it would be a pixel value otherwise) column.size = parseInt(column.header.clientWidth, 10) + "px"; } }); /* Update the column sizes Reminder: grid-template-columns sets the width for all columns in one value */ table.style.gridTemplateColumns = columns .map(({ header, size }) => size) .join(" "); }); // Clean up event listeners, classes, etc. const onMouseUp = () => { console.log("onMouseUp"); window.removeEventListener("mousemove", onMouseMove); window.removeEventListener("mouseup", onMouseUp); headerBeingResized.classList.remove("header--being-resized"); headerBeingResized = null; }; // Get ready, they're about to resize const initResize = ({ target }) => { console.log("initResize"); headerBeingResized = target.parentNode; window.addEventListener("mousemove", onMouseMove); window.addEventListener("mouseup", onMouseUp); headerBeingResized.classList.add("header--being-resized"); }; document.querySelectorAll("th").forEach((header) => { const max = columnTypeToRatioMap[header.dataset.type] + "fr"; columns.push({ header, size: `minmax(${min}px, ${max})`, }); header .querySelector(".resize-handle") .addEventListener("mousedown", initResize); }); document.addEventListener("contextmenu", (e) => { if (e.shiftKey) { return; } e.preventDefault(); const boundingRect = window.frameElement.getBoundingClientRect(); const containsApps = currentlySelected .map( (item) => item.getAttribute("data-path").split(".").slice("-1")[0], ) .filter((item) => item == "app" || item == "lib").length > 0; if (containsApps) { appcontextmenu.show(e.pageX + boundingRect.x, e.pageY + boundingRect.y); newcontextmenu.hide(); emptycontextmenu.hide(); } else if (currentlySelected.length != 0) { newcontextmenu.show(e.pageX + boundingRect.x, e.pageY + boundingRect.y); appcontextmenu.hide(); emptycontextmenu.hide(); } else { emptycontextmenu.show( e.pageX + boundingRect.x, e.pageY + boundingRect.y, ); newcontextmenu.hide(); appcontextmenu.hide(); } }); document.addEventListener("click", (e) => { newcontextmenu.hide(); appcontextmenu.hide(); emptycontextmenu.hide(); }); ================================================ FILE: public/apps/fsapp.app/appview.html ================================================ App Info Viewer

================================================ FILE: public/apps/fsapp.app/components/File.mjs ================================================ function formatBytes(bytes, decimals = 2) { if (bytes === 0) return "0 Bytes"; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; } export function File() { this.mount = async () => { this.absolutePath = `${this.path}/${this.file}`; this.icon = anura.files.fallbackIcon; try { const iconURL = await anura.files.getIcon(this.absolutePath); this.icon = iconURL; } catch (e) { console.error(e); } this.description = "Anura File"; try { const fileType = await anura.files.getFileType(this.absolutePath); this.description = fileType; } catch (e) { console.error(e); } }; return html` { e.currentTarget.classList.add("hover"); }} on:mouseleave=${(e) => { e.currentTarget.classList.remove("hover"); }} on:contextmenu=${(e) => { if (currentlySelected.length > 0) { return; } e.currentTarget.classList.add("selected"); currentlySelected = [e.currentTarget]; }} on:click=${(e) => { if (currentlySelected.includes(e.currentTarget)) { if ( self.filePicker && self.filePicker?.type === "file" && e.currentTarget.getAttribute("data-type") === "file" ) { selectAction(currentlySelected); } else { fileAction(currentlySelected); } currentlySelected.forEach((row) => { row.classList.remove("selected"); }); currentlySelected = []; return; } if (!e.shiftKey) { if (!e.ctrlKey) { currentlySelected.forEach((row) => { row.classList.remove("selected"); }); currentlySelected = []; } e.currentTarget.classList.add("selected"); currentlySelected.push(e.currentTarget); } else { if (currentlySelected.length == 0) { e.currentTarget.classList.add("selected"); currentlySelected.push(e.currentTarget); } else { var arr = Array.from( document.querySelectorAll("tr"), ).filter( (row) => row.parentNode.nodeName.toLowerCase() !== "thead", ); var firstI = arr.indexOf( currentlySelected[currentlySelected.length - 1], ); var lastI = arr.indexOf(e.currentTarget); var first = Math.min(firstI, lastI); var last = Math.max(firstI, lastI); for (var i = first; i <= last; i++) { if (!currentlySelected.includes(arr[i])) { currentlySelected.push(arr[i]); arr[i].classList.add("selected"); } } } } }} data-type="file" data-path=${use(this.absolutePath)} >
${this.file} ${formatBytes(this.stats.size)} ${use(this.description)} ${new Date(this.stats.mtime).toLocaleString()}
`; } ================================================ FILE: public/apps/fsapp.app/components/Folder.mjs ================================================ export function Folder() { this.mount = async () => { this.absolutePath = `${this.path}/${this.file}`; this.description = "Folder"; try { let manifestPath = `${this.absolutePath}/manifest.json`; console.log(manifestPath); const data = await fs.promises.readFile(manifestPath); let manifest = JSON.parse(data); let folderExt = this.file.split(".").slice("-1")[0]; this.icon = `/fs${this.absolutePath}/${manifest.icon}`; console.log(this.icon); this.description = `Anura ${folderExt == "app" ? "Application" : "Library"}`; } catch (error) { console.log(error); this.icon = anura.files.folderIcon; } }; return html` { e.currentTarget.classList.add("hover"); }} on:mouseleave=${(e) => { e.currentTarget.classList.remove("hover"); }} on:contextmenu=${(e) => { if (self.currentlySelected.length > 0) { return; } e.currentTarget.classList.add("selected"); self.currentlySelected = [e.currentTarget]; }} on:click=${(e) => { if (currentlySelected.includes(e.currentTarget)) { if ( self.filePicker && self.filePicker?.type === "file" && e.currentTarget.getAttribute("data-type") === "file" ) { selectAction(currentlySelected); } else { fileAction(currentlySelected); } currentlySelected.forEach((row) => { row.classList.remove("selected"); }); currentlySelected = []; return; } if (!e.shiftKey) { if (!e.ctrlKey) { currentlySelected.forEach((row) => { row.classList.remove("selected"); }); currentlySelected = []; } e.currentTarget.classList.add("selected"); currentlySelected.push(e.currentTarget); } else { if (currentlySelected.length == 0) { e.currentTarget.classList.add("selected"); currentlySelected.push(e.currentTarget); } else { var arr = Array.from( document.querySelectorAll("tr"), ).filter( (row) => row.parentNode.nodeName.toLowerCase() !== "thead", ); var firstI = arr.indexOf( currentlySelected[currentlySelected.length - 1], ); var lastI = arr.indexOf(e.currentTarget); var first = Math.min(firstI, lastI); var last = Math.max(firstI, lastI); for (var i = first; i <= last; i++) { if (!currentlySelected.includes(arr[i])) { currentlySelected.push(arr[i]); arr[i].classList.add("selected"); } } } } }} data-type="dir" data-path=${use(this.absolutePath)} >
${this.file}/ N/A ${use(this.description)} ${new Date(this.stats.mtime).toLocaleString()}
`; } ================================================ FILE: public/apps/fsapp.app/components/Selector.mjs ================================================ export function Selector() { this.css = ` margin-top: 0.3em; margin-right: 1em; display: flex; flex-direction: row; align-items: center; position: fixed; bottom: 0; left: 0; width: 100%; justify-content: flex-end; padding: 0.5em; button { background: var(--theme-accent); margin: 1rem 0.5rem; padding: 1.5em; display: flex; align-items: center; border-radius: 9999px; } `; return html`
`; } ================================================ FILE: public/apps/fsapp.app/components/SideBar.mjs ================================================ export function SideBar() { this.css = ` display: flex; flex-direction: column; flex: 0 0 15em; margin-right: 3em; button { height: 3em; border-bottom-right-radius: 5em; border-top-right-radius: 5em; background-color: var(--theme-bg); border: none; text-align: left; display: flex; align-items: center; } button:hover { background-color: var(--theme-secondary-bg); } button:active { background-color: color-mix( var(--theme-bg), var(--theme-secondary-bg), 0.5 ); } i { margin-right: 1em; margin-left: 0.5em; } `; return html`


`; } ================================================ FILE: public/apps/fsapp.app/components/TopBar.mjs ================================================ export function TopBar() { this.css = ` margin-top: 0.3em; margin-right: 1em; display: flex; flex-direction: row; align-items: center; button { border-radius: 1em; border: none; background-color: var(--theme-bg); height: 2.25em; transition: background-color 0.1s; } button > i { padding: 0.25em; } button:hover { transition: background-color 0.1s; background-color: var(--theme-secondary-bg); } button:active { background-color: color-mix( var(--theme-bg), var(--theme-secondary-bg), 0.5 ); } .sep { flex-grow: 1; } .breadcrumbs button { font-size: 16px; margin-right: 0.25em; margin-left: 0.25em; border-radius: 0; display: inline-block; } .breadcrumbs button:hover { background-color: transparent; } `; return html`
`; } ================================================ FILE: public/apps/fsapp.app/filemanager.css ================================================ @font-face { font-family: Roboto; src: url("/assets/fonts/Roboto-Regular.ttf") format("truetype"); } :root { --theme-fg: #FFFFFF; --theme-secondary-fg: #C1C1C1; --theme-border: #444444; --material-border: #444444; --theme-dark-border: #000000; --theme-bg: #202124; --material-bg: #202124; --theme-secondary-bg: #383838; --theme-dark-bg: #161616; --theme-accent: #4285F4; --matter-helper-theme: #4285F4; } * { color: var(--theme-fg); font-family: "Roboto", RobotoDraft, "Droid Sans", Arial, Helvetica, -apple-system, BlinkMacSystemFont, system-ui, sans-serif; user-select: none; } body { margin: 0; } *::-webkit-scrollbar { width: 8px; } *::-webkit-scrollbar-thumb { background-color: var(--theme-secondary-bg); border-radius: 8px; } *::-webkit-scrollbar-button { display: none; } .container { background-color: var(--theme-bg); width: 100%; height: 100%; display: flex; flex-direction: row; } .sidebar { display: flex; flex-direction: column; flex: 0 0 15em; margin-right: 3em; } .sidebar button { height: 3em; border-bottom-right-radius: 5em; border-top-right-radius: 5em; background-color: var(--theme-bg); border: none; text-align: left; display: flex; align-items: center; cursor: var(--cursor-pointer); } .sidebar button:hover { background-color: var(--theme-secondary-bg); } .sidebar button:active { background-color: color-mix( var(--theme-bg), var(--theme-secondary-bg), 0.5 ); } .sidebar i { margin-right: 1em; margin-left: 0.5em; } .fileView { flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; } .resize-handle { position: absolute; top: 0; right: 0; bottom: 0; background: var(--theme-secondary-fg); opacity: 0; width: 2.5px; cursor: col-resize; transition: opacity 0.1s; margin-block: 0.2em; } .hidden-resize-handle { opacity: 0; transition: opacity 0.1s; } .resize-handle:hover, .header--being-resized .resize-handle { opacity: 0.5; } th:hover .resize-handle { opacity: 0.3; } table { transition: background-color 0.1s; flex-grow: 1; overflow: scroll; display: grid; grid-template-columns: min-content 3fr 1fr 1fr 1fr; grid-auto-rows: min-content; overflow-x: hidden; overflow-y: auto; } tr, thead, tbody { display: contents; } th { position: relative; } th > tr { margin: 0; } tr > * { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-top: 0.5em; padding-bottom: 0.5em; } .iconContainer { display: flex; flex-direction: row; align-items: center; justify-content: center; } .icon { height: 1em; width: 1em; } .selected > * { transition: background-color 0.1s; background-color: color-mix( in srgb, var(--theme-bg) 50%, var(--theme-accent) 50% ) !important; } .hover > * { transition: background-color 0.1s; background-color: var(--theme-secondary-bg); } .topbar { margin-top: 0.3em; margin-right: 1em; display: flex; flex-direction: row; align-items: center; } .topbar button { border-radius: 1em; border: none; background-color: var(--theme-bg); height: 2.25em; transition: background-color 0.1s; } .topbar button > i { padding: 0.25em; } .topbar button:hover { transition: background-color 0.1s; background-color: var(--theme-secondary-bg); } .topbar button:active { background-color: color-mix( var(--theme-bg), var(--theme-secondary-bg), 0.5 ); } .topbar .sep { flex-grow: 1; } .topbar .breadcrumbs button { font-size: 16px; margin-right: 0.25em; margin-left: 0.25em; border-radius: 0; display: inline-block; } .topbar .breadcrumbs button:hover { background-color: transparent; } hr { color: transparent; border-bottom: 1px solid var(--theme-border); opacity: 0.25; width: 100%; } ================================================ FILE: public/apps/fsapp.app/index.html ================================================ ================================================ FILE: public/apps/fsapp.app/index.mjs ================================================ // importing libaries self.fflate = window.parent.tb.fflate; self.mime = await anura.import("npm:mime"); self.currentlySelected = []; self.clipboard = []; self.removeAfterPaste = false; self.fs = anura.fs; self.filePicker = false; self.Buffer = Filer.Buffer; self.sh = new anura.fs.Shell(); // components import { File } from "./components/File.mjs"; import { Folder } from "./components/Folder.mjs"; import { TopBar } from "./components/TopBar.mjs"; import { SideBar } from "./components/SideBar.mjs"; import { Selector } from "./components/Selector.mjs"; const url = new URL(window.location.href); if (url.searchParams.get("picker")) { const picker = window.parent.ExternalApp.deserializeArgs(url.searchParams.get("picker")); if (picker) { self.filePicker = {}; self.filePicker.regex = new RegExp(picker[0]); self.filePicker.type = picker[1]; self.filePicker.multiple = picker[2]; self.filePicker.id = picker[3]; } } function App() { this.css = ` background-color: var(--theme-bg); width: 100%; height: 100%; display: flex; flex-direction: row; .fileView { flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; } `; return html`
<${SideBar}>
<${TopBar}>
{ if (e.currentTarget === e.target) { currentlySelected.forEach((row) => { row.classList.remove("selected"); }); currentlySelected = []; } }}> `; } self.loadPath = async (path) => { console.debug("loading path: ", path); const files = await fs.promises.readdir(path + "/"); files.sort(); console.debug("files: ", files); setBreadcrumbs(path); const table = document.querySelector("tbody"); table.innerHTML = ""; for (const file of files) { console.log(file); const stats = await fs.promises.stat(`${path}/${file}`); if (stats.isDirectory()) { const element = html`<${Folder} path=${path} file=${file} stats=${stats}>`; // oh my god this is horrid table.appendChild(element.children[1].children[0]); } else { const ext = file.split("/").pop().split(".").pop(); const element = html`<${File} path=${path} file=${file} stats=${stats}>`; console.log(element); if ( self.filePicker && self.filePicker.type !== "dir" && self.filePicker.regex.test(ext) ) { table.appendChild(element.children[1].children[0]); } else if (!self.filePicker) { console.log(element.children[1].children[0]); table.appendChild(element.children[1].children[0]); } } } }; document.body.appendChild(html`<${App} />`); if (filePicker) { document .getElementById("app") .appendChild(html`<${Selector}>`); } loadPath("/"); ================================================ FILE: public/apps/fsapp.app/manifest.json ================================================ { "name": "Anura File Manager", "type": "auto", "package": "anura.fsapp", "index": "index.html", "icon": "files.png", "wininfo": { "title": "Anura File Manager", "width": "700px", "height": "500px" } } ================================================ FILE: public/apps/fsapp.app/operations.js ================================================ const fs = window.parent.anura.fs async function selectAction(selected) { currentlySelected.forEach((row) => { row.classList.remove("selected"); }); currentlySelected = []; if (selected.length == 1) { var fileSelected = selected[0]; if (fileSelected.getAttribute("data-type") == filePicker.type) { let fileData = { message: "FileSelected", id: filePicker.id, filePath: fileSelected .getAttribute("data-path") .replace(/(\/)\1+/g, "$1"), }; window.callback({ data: fileData }); // window.parent.postMessage(fileData, "*"); } } else if (selected.length > 1 && filePicker.multiple) { let dataPaths = []; for (var i = 0; i < selected.length; i++) { var dataType = selected[i].getAttribute("data-type"); var dataPath = selected[i].getAttribute("data-path"); if (dataType !== filePicker.type) { return; } if (dataPath !== null) { dataPaths.push(dataPath.replace(/(\/)\1+/g, "$1")); } } let fileData = { message: "FileSelected", id: filePicker.id, filePath: dataPaths, }; window.callback({ data: fileData }); // window.parent.postMessage(fileData, "*"); } else if (selected.length == 0) { if (filePicker.type == "dir") { let fileData = { message: "FileSelected", id: filePicker.id, filePath: document .querySelector(".breadcrumbs") .getAttribute("data-current-path"), }; window.callback({ data: fileData }); } } } async function fileAction(selected) { const instance = { hidden: false, icon: "apps/fsapp.app/files.png", manifest: {name: 'Anura File Manager', type: 'auto', package: 'anura.fsapp', index: 'index.html', icon: 'files.png'}, name: "Anura File Manager", title: "Anura File Manager", package: "anura.fsapp", source: "apps/fsapp.app", windows: [] } if (selected.length == 1) { // SINGLE FILE SELECTION // var fileSelected = selected[0]; if (fileSelected.getAttribute("data-type") == "file") { console.debug("Clicked on file"); if ( fileSelected .getAttribute("data-path") .split(".") .slice("-2") .join(".") == "app.zip" ) { console.log("App Archive detected, extracting"); let data = await fs.promises.readFile( fileSelected.getAttribute("data-path"), ); const path = fileSelected .getAttribute("data-path") .split(".") .slice(0, -1) .join("."); const zip = await unzip(new Uint8Array(data)); const manifest = JSON.parse( new TextDecoder().decode(zip["manifest.json"]), ); const icon = new Blob([zip[manifest.icon]], { //type: mime.default.getType(manifest.icon), }); const win = await anura.wm.create(instance, { title: "Anura File Manager", width: "450px", height: "525px", }); const iframe = document.createElement("iframe"); iframe.setAttribute( "src", document.location.href.split("/").slice(0, -1).join("/") + "/appview.html?manifest=" + window.parent.ExternalApp.serializeArgs([ JSON.stringify(manifest), URL.createObjectURL(icon), "app", ]), ); iframe.style = "top:0; left:0; bottom:0; right:0; width:100%; height:100%; border:none; margin:0; padding:0;"; win.content.appendChild(iframe); Object.assign(iframe.contentWindow, { anura, ExternalApp, instance, instanceWindow: win, install: { session: async () => { anura.notifications.add({ title: "Application Installing for Session", description: `Application ${path.replace( "//", "/", )} is being installed, please wait`, timeout: 50000, }); await fs.mkdir(`${path.replace("//", "/")}`); try { for (const [ relativePath, content, ] of Object.entries(zip)) { if (relativePath.endsWith("/")) { fs.mkdir(`${path}/${relativePath}`); } else { console.log(`${path}/${relativePath}`); fs.writeFile( `${path}/${relativePath}`, await Buffer.from(content), ); } } await anura.registerExternalApp( `/fs${path}`.replace("//", "/"), ); anura.notifications.add({ title: "Application Installed for Session", description: `Application ${path.replace( "//", "/", )} has been installed temporarily, it will go away on refresh`, timeout: 50000, }); } catch (e) { console.error(e); } }, permanent: async () => { anura.notifications.add({ title: "Application Installing", description: `Application ${path.replace( "//", "/", )} is being installed, please wait`, timeout: 50000, }); await fs.promises.mkdir( anura.settings.get("directories")["apps"] + "/" + path.split("/").slice("-1")[0], ); try { for (const [ relativePath, content, ] of Object.entries(zip)) { if (relativePath.endsWith("/")) { await fs.promises.mkdir( `${anura.settings.get("directories")["apps"]}/${path.split("/").slice("-1")[0]}/${relativePath}`, ); } else { await fs.promises.writeFile( `${anura.settings.get("directories")["apps"]}/${path.split("/").slice("-1")[0]}/${relativePath}`, Buffer.from(content), ); } } await anura.registerExternalApp( `/fs${anura.settings.get("directories")["apps"]}/${path.split("/").slice("-1")[0]}`.replace( "//", "/", ), ); anura.notifications.add({ title: "Application Installed", description: `Application ${path.replace( "//", "/", )} has been installed permanently`, timeout: 50000, }); } catch (e) { console.error(e); } }, }, }); iframe.contentWindow.addEventListener("load", () => { const matter = document.createElement("link"); matter.setAttribute("rel", "stylesheet"); matter.setAttribute("href", "/assets/matter.css"); iframe.contentDocument.head.appendChild(matter); }); } else if ( fileSelected .getAttribute("data-path") .split(".") .slice("-2") .join(".") == "lib.zip" ) { console.log("Library Archive detected, extracting"); const data = await fs.promises.readFile( fileSelected.getAttribute("data-path"), ); const path = fileSelected .getAttribute("data-path") .split(".") .slice(0, -1) .join("."); const zip = await unzip(new Uint8Array(data)); console.log(zip); const manifest = JSON.parse( new TextDecoder().decode(zip["manifest.json"]), ); const icon = new Blob([zip[manifest.icon]], { //type: mime.default.getType(manifest.icon), }); const win = await anura.wm.create(instance, { title: "Anura File Manager", width: "450px", height: "525px", }); const iframe = document.createElement("iframe"); iframe.setAttribute( "src", document.location.href.split("/").slice(0, -1).join("/") + "/appview.html?manifest=" + window.parent.ExternalApp.serializeArgs([ JSON.stringify(manifest), URL.createObjectURL(icon), "lib", ]), ); iframe.style = "top:0; left:0; bottom:0; right:0; width:100%; height:100%; border:none; margin:0; padding:0;"; win.content.appendChild(iframe); Object.assign(iframe.contentWindow, { anura, ExternalApp, instance, instanceWindow: win, install: { session: async () => { anura.notifications.add({ title: "Library Installing for Session", description: `Library ${path.replace( "//", "/", )} is being installed, please wait`, timeout: 50000, }); await fs.promises.mkdir(`${path}`); let filesRemaining = Object.keys(zip).length; Object.entries(zip).forEach( async ([relativePath, content]) => { if (relativePath.endsWith("/")) { await fs.promises.mkdir( `${path}/${relativePath}`, ); } else { await fs.promises.writeFile( `${path}/${relativePath}`, Buffer.from(content), ); } filesRemaining--; console.log(filesRemaining); if (filesRemaining == 0) { await anura.registerExternalLib( `/fs/${path}`.replace("//", "/"), ); anura.notifications.add({ title: "Library Installed for Session", description: `Library ${path.replace( "//", "/", )} has been installed temporarily, it will go away on refresh`, timeout: 50000, }); } }, function (e) { console.error(e); }, ); }, permanent: async () => { anura.notifications.add({ title: "Library Installing", description: `Library ${path.replace( "//", "/", )} is being installed`, timeout: 50000, }); await fs.mkdir( anura.settings.get("directories")["libs"] + "/" + path.split("/").slice("-1")[0], ); let filesRemaining = Object.keys(zip).length; Object.entries(zip).forEach( async ([relativePath, content]) => { if (relativePath.endsWith("/")) { await fs.promises.mkdir( `${anura.settings.get("directories")["libs"]}/${path.split("/").slice("-1")[0]}/${relativePath}`, ); } else { await fs.promises.writeFile( `${anura.settings.get("directories")["libs"]}/${path.split("/").slice("-1")[0]}/${relativePath}`, Buffer.from(content), ); } filesRemaining--; console.log(filesRemaining); if (filesRemaining == 0) { await anura.registerExternalLib( `/fs${anura.settings.get("directories")["libs"]}/${path.split("/").slice("-1")[0]}`.replace( "//", "/", ), ); anura.notifications.add({ title: "Library Installed", description: `Library ${path.replace( "//", "/", )} has been installed permanently`, timeout: 50000, }); } }, function (e) { console.error(e); }, ); }, }, }); iframe.contentWindow.addEventListener("load", () => { const matter = document.createElement("link"); matter.setAttribute("rel", "stylesheet"); matter.setAttribute("href", "/assets/matter.css"); iframe.contentDocument.head.appendChild(matter); }); } else { anura.files.open(fileSelected.getAttribute("data-path")); } } else if (fileSelected.getAttribute("data-type") == "dir") { if ( fileSelected .getAttribute("data-path") .split(".") .slice("-1")[0] == "app" ) { try { let data; try { data = await fs.promises.readFile( `${fileSelected.getAttribute("data-path")}/manifest.json`, ); } catch { console.debug( "Changing folder to ", fileSelected.getAttribute("data-path"), ); loadPath(fileSelected.getAttribute("data-path")); return; } const manifest = JSON.parse(data); if (anura.apps[manifest.package]) { anura.apps[manifest.package].open(); return; } const iconData = await fs.promises.readFile( `${fileSelected.getAttribute("data-path")}/${manifest.icon}`, ); const icon = new Blob([iconData]); const win = await anura.wm.create(instance, { title: "Anura File Manager", width: "450px", height: "525px", }); const iframe = document.createElement("iframe"); iframe.setAttribute( "src", document.location.href .split("/") .slice(0, -1) .join("/") + "/appview.html?manifest=" + window.parent.ExternalApp.serializeArgs([ data.toString(), URL.createObjectURL(icon), "app", ]), ); iframe.style = "top:0; left:0; bottom:0; right:0; width:100%; height:100%; border:none; margin:0; padding:0;"; win.content.appendChild(iframe); Object.assign(iframe.contentWindow, { anura, ExternalApp, instance, instanceWindow: win, install: { session: async () => { await anura.registerExternalApp( `/fs${fileSelected.getAttribute("data-path")}`.replace( "//", "/", ), ); anura.notifications.add({ title: "Application Installed for Session", description: `Application ${fileSelected .getAttribute("data-path") .replace( "//", "/", )} has been installed temporarily, it will go away on refresh`, timeout: 50000, }); win.close(); }, permanent: async () => { await fs.promises.rename( fileSelected.getAttribute("data-path"), anura.settings.get("directories")["apps"] + "/" + fileSelected .getAttribute("data-path") .split("/") .slice("-1")[0], ); await anura.registerExternalApp( `/fs${anura.settings.get("directories")["apps"]}/${fileSelected.getAttribute("data-path").split("/").slice("-1")[0]}`.replace( "//", "/", ), ); anura.notifications.add({ title: "Application Installed", description: `Application ${fileSelected .getAttribute("data-path") .replace( "//", "/", )} has been installed permanently`, timeout: 50000, }); win.close(); }, }, }); iframe.contentWindow.addEventListener("load", () => { const matter = document.createElement("link"); matter.setAttribute("rel", "stylesheet"); matter.setAttribute("href", "/assets/matter.css"); iframe.contentDocument.head.appendChild(matter); }); } catch (e) { anura.dialog.alert( `There was an error: ${e}`, "Error installing app", ); } } else if ( fileSelected .getAttribute("data-path") .split(".") .slice("-1")[0] == "lib" ) { try { let data; try { data = await fs.promises.readFile( `${fileSelected.getAttribute("data-path")}/manifest.json`, ); } catch { console.debug( "Changing folder to ", fileSelected.getAttribute("data-path"), ); loadPath(fileSelected.getAttribute("data-path")); return; } const manifest = JSON.parse(data); if (anura.libs[manifest.package]) { return; } const iconData = await fs.promises.readFile( `${fileSelected.getAttribute("data-path")}/${manifest.icon}`, ); const icon = new Blob([iconData]); const win = await anura.wm.create(instance, { title: "Anura File Manager", width: "450px", height: "525px", }); const iframe = document.createElement("iframe"); iframe.setAttribute( "src", document.location.href .split("/") .slice(0, -1) .join("/") + "/appview.html?manifest=" + window.parent.ExternalApp.serializeArgs([ data.toString(), URL.createObjectURL(icon), "lib", ]), ); iframe.style = "top:0; left:0; bottom:0; right:0; width:100%; height:100%; border:none; margin:0; padding:0;"; win.content.appendChild(iframe); Object.assign(iframe.contentWindow, { anura, ExternalApp, instance, instanceWindow: win, install: { session: async () => { await anura.registerExternalLib( `/fs${fileSelected.getAttribute("data-path")}`.replace( "//", "/", ), ); anura.notifications.add({ title: "Library Installed for Session", description: `Library ${fileSelected .getAttribute("data-path") .replace( "//", "/", )} has been installed temporarily, it will go away on refresh`, timeout: 50000, }); win.close(); }, permanent: async () => { await fs.promises.rename( fileSelected.getAttribute("data-path"), anura.settings.get("directories")["libs"] + "/" + fileSelected .getAttribute("data-path") .split("/") .slice("-1")[0], ); await anura.registerExternalLib( `/fs${anura.settings.get("directories")["libs"]}/${fileSelected.getAttribute("data-path").split("/").slice("-1")[0]}`.replace( "//", "/", ), ); anura.notifications.add({ title: "Library Installed", description: `Library ${fileSelected .getAttribute("data-path") .replace( "//", "/", )} has been installed permanently`, timeout: 50000, }); win.close(); }, }, }); iframe.contentWindow.addEventListener("load", () => { const matter = document.createElement("link"); matter.setAttribute("rel", "stylesheet"); matter.setAttribute("href", "/assets/matter.css"); iframe.contentDocument.head.appendChild(matter); }); } catch (e) { anura.notifications.add({ title: "Library Install Error", description: `Library had an error installing: ${e}`, timeout: 50000, }); } } else { console.debug( "Changing folder to ", fileSelected.getAttribute("data-path"), ); loadPath(fileSelected.getAttribute("data-path")); } } else { console.warn( "Unknown filetype ", fileSelected.getAttribute("data-type"), " doing nothing!", ); } } else { // MULTIPLE FILE SELECTION // console.error("raff please implement"); } } function setBreadcrumbs(path) { path = path.replace(/(\/)\1+/g, "$1"); var pathSplit = path.split("/"); pathSplit[0] = "My files"; var breadcrumbs = document.querySelector(".breadcrumbs"); breadcrumbs.setAttribute("data-current-path", path); breadcrumbs.innerHTML = ""; if ( pathSplit.length == 2 && pathSplit[0] == "My files" && pathSplit[1] == "" ) { var breadcrumb = document.createElement("button"); breadcrumb.innerText = "My files"; breadcrumb.addEventListener("click", () => { loadPath("/"); }); breadcrumbs.appendChild(breadcrumb); return; } for (var i = 0; i < pathSplit.length; i++) { console.log(i); var breadcrumb = document.createElement("button"); breadcrumb.innerText = pathSplit[i]; var index = i; breadcrumb.addEventListener("click", () => { loadPath("/" + pathSplit.slice(1, index).join("/")); }); breadcrumbs.appendChild(breadcrumb); if (pathSplit[i] !== pathSplit[pathSplit.length - 1]) { var breadcrumbSpan = document.createElement("span"); breadcrumbSpan.innerText = ">"; breadcrumbs.appendChild(breadcrumbSpan); } } } async function newFolder(path) { if (path === undefined) { let folderName = await anura.dialog.prompt("Folder Name: "); if (folderName) { path = document .querySelector(".breadcrumbs") .getAttribute("data-current-path") + "/" + folderName; } } if (path) { fs.mkdir(path); reload(); } } async function newFile(path) { if (path === undefined) { let fileName = await anura.dialog.prompt("File Name: "); console.log(fileName) if (fileName) { path = document .querySelector(".breadcrumbs") .getAttribute("data-current-path") + "/" + fileName; } } await fs.promises.writeFile(path, ""); reload(); } function reload() { loadPath( document .querySelector(".breadcrumbs") .getAttribute("data-current-path"), ); } function reload() { loadPath( document .querySelector(".breadcrumbs") .getAttribute("data-current-path"), ); } function upload() { let fauxput = document.createElement("input"); // fauxput - fake input that isn't shown or ever added to page TODO: think of a better name for this variable fauxput.type = "file"; fauxput.onchange = async (e) => { const file = await e.target.files[0]; const content = await file.arrayBuffer(); fs.writeFile( `${document .querySelector(".breadcrumbs") .getAttribute("data-current-path")}/${file.name}`, Buffer.from(content), function (err) { reload(); }, ); }; fauxput.click(); } function deleteFile() { if (currentlySelected.length == 0) { anura.notifications.add({ title: "Filesystem app", description: "BUG: You have no files selected, right clicking does not select files", timeout: 5000, }); } currentlySelected.forEach(async (item) => { await sh.rm( item.getAttribute("data-path"), { recursive: true, }, function (err) { if (err) throw err; reload(); }, ); }); } function copy() { clipboard = currentlySelected; removeAfterPaste = false; } function cut() { clipboard = currentlySelected; removeAfterPaste = true; } async function paste() { const path = document .querySelector(".breadcrumbs") .getAttribute("data-current-path"); if (!removeAfterPaste) { for (item of clipboard) { if (item.attributes["data-type"].value == "dir") { //INPUT let newPath = path; let oldPath = item.attributes["data-path"].value; // Normalize (remove trailing slash, replace // with /) if (oldPath.endsWith("/")) oldPath = oldPath.slice(0, -1); if (newPath.endsWith("/")) newPath = newPath.slice(0, -1); newPath = newPath.replace("//", "/"); oldPath = oldPath.replace("//", "/"); const oldFolderName = oldPath.split("/").pop(); // Search const files = await sh.promises.ls(oldPath, { recursive: true, }); console.log(files); // Apply for (file of files) { // Creating the relative path string let path = file.split("/"); const filename = path.pop(); path = path.join("/"); path = path.substring(oldPath.length); await sh.promises.mkdirp( `${newPath}/${oldFolderName}${path}`, ); const data = await fs.promises.readFile( `${oldPath}${path}/${filename}`, ); await fs.promises.writeFile( `${newPath}/${oldFolderName}${path}/${filename}`, data, ); } } else { let origin = item.attributes["data-path"].value; fs.promises.writeFile( `${path}/${origin.split("/").slice("-1")[0]}`, await fs.promises.readFile(origin), ); } } clipboard = []; reload(); } if (removeAfterPaste) { // cut for (const item of clipboard) { itemName = item.getAttribute("data-path"); await fs.promises.rename( itemName, `${path}/${itemName.split("/").slice("-1")[0]}`, ); reload(); } clipboard = []; } } async function rename() { const path = document .querySelector(".breadcrumbs") .getAttribute("data-current-path"); if (currentlySelected.length == 0) { anura.notifications.add({ title: "Filesystem app", description: "BUG: You have no files selected, right clicking does not select files", timeout: 5000, }); return; } if (currentlySelected.length > 1) { anura.notifications.add({ title: "Filesystem app", description: "Renaming only works with one file", timeout: 5000, }); return; } let filename = await anura.dialog.prompt("Filename:"); if (filename) { fs.rename( currentlySelected[0].getAttribute("data-path"), `${path}/${filename}`, function (err) { if (err) throw err; reload(); }, ); } } function installSession() { if (currentlySelected.length == 0) { anura.notifications.add({ title: "Filesystem app", description: "BUG: You have no files selected, right clicking does not select files", timeout: 5000, }); return; } currentlySelected.forEach(async (item) => { const path = item.getAttribute("data-path"); const ext = path.split(".").slice("-1")[0]; fs.stat(path, async function (err, stats) { if (stats.isDirectory()) { if (ext == "app") { try { await anura.registerExternalApp( `/fs${path}`.replace("//", "/"), ); anura.notifications.add({ title: "Application Installed for Session", description: `Application ${path.replace( "//", "/", )} has been installed temporarily, it will go away on refresh`, timeout: 50000, }); } catch (e) { anura.dialog.alert( `There was an error: ${e}`, "Error installing app", ); } } if (ext == "lib") { try { await anura.registerExternalLib( `/fs${path}`.replace("//", "/"), ); anura.notifications.add({ title: "Library Installed for Session", description: `Library ${path.replace( "//", "/", )} has been installed temporarily, it will go away on refresh`, timeout: 50000, }); } catch (e) { anura.dialog.alert( `There was an error: ${e}`, "Error installing library", ); } } } }); }); } function installPermanent() { if (currentlySelected.length == 0) { anura.notifications.add({ title: "Filesystem app", description: "BUG: You have no files selected, right clicking does not select files", timeout: 5000, }); return; } currentlySelected.forEach(async (item) => { const path = item.getAttribute("data-path"); const ext = path.split(".").slice("-1")[0]; fs.stat(path, async function (err, stats) { if (stats.isDirectory()) { if (ext == "app") { const destination = anura.settings.get("directories")["apps"]; try { await fs.promises.rename( path, destination + "/" + path.split("/").slice("-1")[0], ); await anura.registerExternalApp( `/fs${destination}/${path.split("/").slice("-1")[0]}`.replace( "//", "/", ), ); } catch (e) { anura.notifications.add({ title: "Application Install Error", description: `Application had an error installing: ${e}`, timeout: 50000, }); } } if (ext == "lib") { const destination = anura.settings.get("directories")["libs"]; try { sh.ls( path, { recursive: true, }, async function (err, entries) { if (err) throw err; let items = []; let dirs = []; entries.forEach((entry) => { function recurse(dirnode, path) { dirnode.contents.forEach((entry) => { if (entry.type === "DIRECTORY") { recurse( entry, path + "/" + entry.name, ); dirs.push( path + "/" + entry.name, ); } else { items.push( path + "/" + entry.name, ); } }); } const topLevelFolder = path; dirs.push(path); if (entry.type === "DIRECTORY") { recurse(entry, path + "/" + entry.name); dirs.push(path + "/" + entry.name); } else { items.push(path + "/" + entry.name); } }); destItems = []; destDirs = []; numberToSubBy = path.length - path.split("/").pop().length; for (item in items) { destItems.push( destination + "/" + items[item].slice(numberToSubBy), ); } for (dir in dirs) { destDirs.push( destination + "/" + dirs[dir].slice(numberToSubBy), ); } console.log("initials"); console.log(items); console.log("destinations"); console.log(destItems); console.log("directories to mkdir -p "); console.log(destDirs); for (dir in destDirs) { await new Promise((resolve, reject) => { sh.mkdirp( destDirs[dir], function (err) { if (err) { reject(err); console.error(err); } resolve(); }, ); }); } for (item in items) { await new Promise((resolve, reject) => { fs.readFile( items[item], function (err, data) { fs.writeFile( destItems[item], data, function (err) { if (err) { reject(err); console.error(err); } resolve(); }, ); }, ); }); } console.log("finished copying files???"); await anura.registerExternalLib( `/fs${destination}/${path.split("/").slice("-1")[0]}`.replace( "//", "/", ), ); anura.notifications.add({ title: "Library Installed", description: `Library ${path.replaceAll( "/", "", )} has been installed permanently.`, timeout: 50000, }); reload(); }, ); } catch (e) { anura.notifications.add({ title: "Library Install Error", description: `Library had an error installing: ${e}`, timeout: 50000, }); } } } }); }); } // Context menu version of the loadPath function // Used to enter app and lib folders, as double // clicking on them will install them. function navigate() { if (currentlySelected.length == 1) { loadPath(currentlySelected[0].getAttribute("data-path")); } // Can't navigate to multiple folders } function unzip(zip) { return new Promise((res, rej) => { window.parent.tb.fflate.unzip(zip, (err, unzipped) => { if (err) rej(err); else res(unzipped); }); }); } ================================================ FILE: public/apps/libfilepicker.lib/GUI.js ================================================ // This context menu is for files and folders const newcontextmenu = new parent.anura.ContextMenu(); // This context menu is for applications and libraries const appcontextmenu = new parent.anura.ContextMenu(); // This context menu is for when no files are selected const emptycontextmenu = new parent.anura.ContextMenu(); // Helper to add context menu items to both menus function addContextMenuItem(name, func) { newcontextmenu.addItem(name, func); appcontextmenu.addItem(name, func); } // addContextMenuItem("Get Info", function () {}); // addContextMenuItem("Pin to Shelf", function () {}); addContextMenuItem("Cut", function () { cut(); }); addContextMenuItem("Copy", function () { copy(); }); addContextMenuItem("Paste", function () { paste(); }); addContextMenuItem("Delete", function () { deleteFile(); }); addContextMenuItem("Rename", function () { rename(); }); appcontextmenu.addItem("Install (Session)", function () { // While this is the same as double clicking, it's still useful to have the verbosely named option installSession(); }); appcontextmenu.addItem("Install (Permanent)", function () { // This is not the same as double clicking, as it will install the app permanently installPermanent(); }); appcontextmenu.addItem("Navigate", function () { // Normally, double clicking a folder will navigate into it, but for apps and libs, this is not the case navigate(); }); emptycontextmenu.addItem("Upload from PC", function () { upload(); }); emptycontextmenu.addItem("New folder", function () { newFolder(); }); emptycontextmenu.addItem("New file", function () { newFile(); }) emptycontextmenu.addItem("Refresh", function () { reload(); }); const min = 150; // The max (fr) values for grid-template-columns const columnTypeToRatioMap = { icon: 0.1, name: 3, size: 1, type: 1, modified: 1, }; const table = document.querySelector("table"); /* The following will soon be filled with column objects containing the header element and their size value for grid-template-columns */ const columns = []; let headerBeingResized; // The next three functions are mouse event callbacks // Where the magic happens. I.e. when they're actually resizing const onMouseMove = (e) => requestAnimationFrame(() => { console.log("onMouseMove"); (window.getSelection ? window.getSelection() : document.selection ).empty(); // Calculate the desired width horizontalScrollOffset = document.documentElement.scrollLeft; const width = horizontalScrollOffset + e.clientX - headerBeingResized.offsetLeft; // Update the column object with the new size value const column = columns.find( ({ header }) => header === headerBeingResized, ); column.size = Math.max(min, width) + "px"; // Enforce our minimum // For the other headers which don't have a set width, fix it to their computed width columns.forEach((column) => { if (column.size.startsWith("minmax")) { // isn't fixed yet (it would be a pixel value otherwise) column.size = parseInt(column.header.clientWidth, 10) + "px"; } }); /* Update the column sizes Reminder: grid-template-columns sets the width for all columns in one value */ table.style.gridTemplateColumns = columns .map(({ header, size }) => size) .join(" "); }); // Clean up event listeners, classes, etc. const onMouseUp = () => { console.log("onMouseUp"); window.removeEventListener("mousemove", onMouseMove); window.removeEventListener("mouseup", onMouseUp); headerBeingResized.classList.remove("header--being-resized"); headerBeingResized = null; }; // Get ready, they're about to resize const initResize = ({ target }) => { console.log("initResize"); headerBeingResized = target.parentNode; window.addEventListener("mousemove", onMouseMove); window.addEventListener("mouseup", onMouseUp); headerBeingResized.classList.add("header--being-resized"); }; // Let's populate that columns array and add listeners to the resize handles document.querySelectorAll("th").forEach((header) => { const max = columnTypeToRatioMap[header.dataset.type] + "fr"; columns.push({ header, // The initial size value for grid-template-columns: size: `minmax(${min}px, ${max})`, }); header .querySelector(".resize-handle") .addEventListener("mousedown", initResize); }); ================================================ FILE: public/apps/libfilepicker.lib/README.md ================================================ # Usage Adding the library to your app: ```html ``` Picking a file: ```js // Synchronously: selectFile().then((filePath) => { // implement your logic here }); // Asynchronously: await selectFile() // Parameters: // selectFile(fileExtension {can use regex or just be a file extension}) // Example: selectFile("txt") // Returning: // One file "/example.txt" // Multiple files ["/example.txt", "/example1.txt"] ``` Picking a folder: ```js // Synchronously: selectFolder().then((filePath) => { // implement your logic here }); // Asynchronously: await selectFolder() // Returns: // One folder "/folder" // Multiple folders ["/folder", "/folder2"] ``` ================================================ FILE: public/apps/libfilepicker.lib/file.html ================================================

Name Size Type Date modified
Icon Name Size Type Date modified
nya.rs 1 MB Rust source code Today 17:00
================================================ FILE: public/apps/libfilepicker.lib/filemanager.css ================================================ @font-face { font-family: Roboto; src: url("/assets/fonts/Roboto-Regular.ttf") format("truetype"); } * { color: #ffffff; font-family: "Roboto", sans-serif; user-select: none; } body { margin: 0; } .container { background-color: #202124; width: 100%; height: 100%; display: flex; flex-direction: row; } .sidebar { display: flex; flex-direction: column; flex: 0 0 15em; margin-right: 3em; } .sidebar button { height: 3em; border-bottom-right-radius: 5em; border-top-right-radius: 5em; background-color: #202124; border: none; text-align: left; display: flex; align-items: center; } .sidebar button:hover { background-color: #2d2e31; } .sidebar button:active { background-color: #3f4d63; } .sidebar i { margin-right: 1em; margin-left: 0.5em; } .fileView { flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; } .resize-handle { position: absolute; top: 0; right: 0; bottom: 0; background: black; opacity: 0; width: 3px; cursor: col-resize; } .hidden-resize-handle { display: none; } .resize-handle:hover, .header--being-resized .resize-handle { opacity: 0.5; } th:hover .resize-handle { opacity: 0.3; } table { flex-grow: 1; overflow: scroll; display: grid; grid-template-columns: min-content 3fr 1fr 1fr 1fr; grid-auto-rows: min-content; overflow-x: hidden; overflow-y: auto; } tr, thead, tbody { display: contents; } th { position: relative; } th > tr { margin: 0; } tr > * { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-top: 0.5em; padding-bottom: 0.5em; } .iconContainer { display: flex; flex-direction: row; align-items: center; justify-content: center; } .icon { height: 1em; width: 1em; } .selected > * { background-color: color-mix(in srgb, #8aadf4, #303446) !important; } .hover > * { background-color: #414559; } .topbar { margin-top: 0.3em; margin-right: 1em; display: flex; flex-direction: row; align-items: center; } .topbar button { border-radius: 1em; border: none; background-color: #202124; height: 2.25em; } .topbar button > i { padding: 0.25em; } .topbar button:hover { background-color: #292c3c; } .topbar button:active { background-color: #232634 !important; } .topbar .sep { flex-grow: 1; } .topbar .breadcrumbs button { font-size: 16px; margin-right: 0.25em; margin-left: 0.25em; border-radius: 0; display: inline-block; } .topbar .breadcrumbs button:hover { background-color: transparent; } hr { color: transparent; border-bottom: 1px solid #414559; opacity: 0.25; width: 100%; } ================================================ FILE: public/apps/libfilepicker.lib/folder.html ================================================

Icon Name Size Type Date modified
nya.rs 1 MB Rust source code Today 17:00
================================================ FILE: public/apps/libfilepicker.lib/handler.js ================================================ export function selectFile(options) { return new Promise(async (resolve, reject) => { await window.tb.dialog.FileBrowser({ title: "Select a File", onOk: async (val) => { resolve(val) }, onCancel: () => { reject('User Rejected') } }) }) } export function selectFolder(options) { return new Promise(async (resolve, reject) => { await window.tb.dialog.DirectoryBrowser({ title: "Select a Directory", onOk: async (val) => { resolve(val) }, onCancel: () => { reject('User Rejected') } }) }) } ================================================ FILE: public/apps/libfilepicker.lib/install.js ================================================ // Runs on every boot as the lib is installed export default async function install(_, filePickerLib) { const { selectFile, selectFolder } = await filePickerLib.getImport(); top.navigator.serviceWorker.addEventListener("message", async (event) => { if (event.data.anura_target === "anura.filepicker") { if (event.data.type === "folder") { let folders; let cancelled = false; try { folders = await selectFolder({ regex: event.data.regex }); if (typeof folders === "string") { folders = [folders]; } } catch (e) { folders = []; cancelled = true; } top.navigator.serviceWorker.controller.postMessage({ anura_target: "anura.filepicker.result", id: event.data.id, value: { folders, cancelled, }, }); return; } else { let files; let cancelled = false; try { files = await selectFile({ regex: event.data.regex }); if (typeof files === "string") { files = [files]; } } catch (e) { files = []; cancelled = true; } top.navigator.serviceWorker.controller.postMessage({ anura_target: "anura.filepicker.result", id: event.data.id, value: { files, cancelled, }, }); } } }); } ================================================ FILE: public/apps/libfilepicker.lib/manifest.json ================================================ { "name": "File Picker", "icon": "files.png", "package": "anura.filepicker", "versions": { "1.0.0": "handler.js" }, "currentVersion": "1.0.0", "installHook": "install.js" } ================================================ FILE: public/apps/libfilepicker.lib/operations.js ================================================ var currentlySelected = []; var clipboard = []; var removeAfterPaste = false; window.fs = parent.anura.fs; window.anura = parent.anura; window.Buffer = Filer.Buffer; let sh = new fs.Shell(); function loadPath(path) { console.debug("loading path: ", path); fs.readdir(path, (err, files) => { if (err) throw err; setBreadcrumbs(path); let table = document.querySelector("tbody"); table.innerHTML = ""; files.forEach((file) => { let row = document.createElement("tr"); let iconContainer = document.createElement("td"); let icon = document.createElement("img"); let name = document.createElement("td"); let size = document.createElement("td"); let description = document.createElement("td"); let date = document.createElement("td"); let type = document.createElement("td"); iconContainer.className = "iconContainer"; icon.className = "icon"; fs.stat(`${path}/${file}`, function (err, stats) { if (err) throw err; if (stats.isDirectory()) { name.innerText = `${file}/`; description.innerText = "Folder"; date.innerText = new Date(stats.mtime).toLocaleString(); size.innerText = stats.size; let folderExt = file.split(".").slice("-1")[0] if (folderExt == "app" | folderExt == "lib" && file !== "lib") { let manifestPath = `${path}/${file}/manifest.json`; fs.readFile(manifestPath, function (err, data) { if (err) { icon.src = anura.files.folderIcon; } let manifest = JSON.parse(data); icon.src = `/fs${path}/${file}/${manifest.icon}`; icon.onerror = () => { icon.src = anura.files.folderIcon; }; description.innerText = `Anura ${folderExt == "app" ? "Application" : "Library"}`; }); } else { icon.src = anura.files.folderIcon; } iconContainer.appendChild(icon); row.appendChild(iconContainer); row.appendChild(name); row.appendChild(size); row.appendChild(description); row.appendChild(date); row.setAttribute("data-type", "dir"); row.setAttribute("data-path", `${path}/${file}`); } else { if (selectorType !== "dir") { let ext = file.split("/").pop().split(".").pop(); if (fileRegex.test(ext)) { name.innerText = `${file}`; description.innerText = "Anura File"; anura.files.getFileType(`${path}/${file}`).then((type) => { description.innerText = type; }); date.innerText = new Date(stats.mtime).toLocaleString(); size.innerText = stats.size; anura.files.getIcon(`${path}/${file}`).then((iconURL) => { icon.src = iconURL; }).catch((e) => { icon.src = anura.files.fallbackIcon; console.error(e); }); iconContainer.appendChild(icon); row.appendChild(iconContainer); row.appendChild(name); row.appendChild(size); row.appendChild(description); row.appendChild(date); row.setAttribute("data-type", "file"); row.setAttribute("data-path", `${path}/${file}`); } } } console.debug("appending"); table.appendChild(row); if (files[files.length - 1] === file) { reloadListeners(); } }); }); }); } function reloadListeners() { console.debug("reloading listeners"); console.debug(document.querySelectorAll("tr")); document.querySelectorAll("tr").forEach((row) => { if (row.parentNode.nodeName.toLowerCase() !== "thead") { console.debug("adding listeners to ", row); row.addEventListener("mouseenter", (e) => { e.currentTarget.classList.add("hover"); }); row.addEventListener("mouseleave", (e) => { e.currentTarget.classList.remove("hover"); }); row.addEventListener("contextmenu", (e) => { if (currentlySelected.length > 0) { return; } e.currentTarget.classList.add("selected"); currentlySelected = [e.currentTarget]; }); row.addEventListener("click", (e) => { if (currentlySelected.includes(e.currentTarget)) { fileAction(currentlySelected); currentlySelected.forEach((row) => { row.classList.remove("selected"); }); currentlySelected = []; return; } if (!e.shiftKey) { if (!e.ctrlKey) { currentlySelected.forEach((row) => { row.classList.remove("selected"); }); currentlySelected = []; } e.currentTarget.classList.add("selected"); currentlySelected.push(e.currentTarget); } else { if (currentlySelected.length == 0) { e.currentTarget.classList.add("selected"); currentlySelected.push(e.currentTarget); } else { var arr = Array.from( document.querySelectorAll("tr"), ).filter( (row) => row.parentNode.nodeName.toLowerCase() !== "thead", ); var firstI = arr.indexOf( currentlySelected[currentlySelected.length - 1], ); var lastI = arr.indexOf(e.currentTarget); var first = Math.min(firstI, lastI); var last = Math.max(firstI, lastI); for (var i = first; i <= last; i++) { if (!currentlySelected.includes(arr[i])) { currentlySelected.push(arr[i]); arr[i].classList.add("selected"); } } } } }); } }); } async function selectAction(selected) { currentlySelected.forEach((row) => { row.classList.remove("selected"); }); currentlySelected = []; if (selected.length == 1) { var fileSelected = selected[0]; if (fileSelected.getAttribute("data-type") == selectorType) { let fileData = { message: 'FileSelected', filePath: fileSelected.getAttribute("data-path").replace(/(\/)\1+/g, "$1") }; window.parent.postMessage(fileData, '*'); } } else if (selected.length > 1) { let dataPaths = []; for (var i = 0; i < selected.length; i++) { var dataType = selected[i].getAttribute("data-type"); var dataPath = selected[i].getAttribute("data-path"); if (dataType !== selectorType ) { return; } if (dataPath !== null) { dataPaths.push(dataPath.replace(/(\/)\1+/g, "$1")); } } let fileData = { message: 'FileSelected', filePath: dataPaths }; window.parent.postMessage(fileData, '*'); } else if (selected.length == 0) { if (selectorType == "dir") { let fileData = { message: 'FileSelected', filePath: document.querySelector(".breadcrumbs").getAttribute("data-current-path") }; window.parent.postMessage(fileData, '*'); } } } async function fileAction(selected) { if (selected.length == 1) { var fileSelected = selected[0]; if (fileSelected.getAttribute("data-type") == "file") { if (selectorType == "file") { let fileData = { message: 'FileSelected', filePath: fileSelected.getAttribute("data-path").replace(/(\/)\1+/g, "$1") }; window.parent.postMessage(fileData, '*'); } } else if (fileSelected.getAttribute("data-type") == "dir") { console.debug( "Changing folder to ", fileSelected.getAttribute("data-path"), ); loadPath(fileSelected.getAttribute("data-path")); } else { console.warn( "Unknown filetype ", fileSelected.getAttribute("data-type"), " doing nothing!", ); } } else if (selected.length > 1) { let dataPaths = []; for (var i = 0; i < selected.length; i++) { var dataType = selected[i].getAttribute("data-type"); var dataPath = selected[i].getAttribute("data-path"); if (dataType !== selectorType ) { return; } if (dataPath !== null) { dataPaths.push(dataPath.replace(/(\/)\1+/g, "$1")); } } let fileData = { message: 'FileSelected', filePath: dataPaths }; window.parent.postMessage(fileData, '*'); } } function setBreadcrumbs(path) { path = path.replace(/(\/)\1+/g, "$1"); var pathSplit = path.split("/"); pathSplit[0] = "My files"; var breadcrumbs = document.querySelector(".breadcrumbs"); breadcrumbs.setAttribute("data-current-path", path); breadcrumbs.innerHTML = ""; if ( pathSplit.length == 2 && pathSplit[0] == "My files" && pathSplit[1] == "" ) { var breadcrumb = document.createElement("button"); breadcrumb.innerText = "My files"; breadcrumb.addEventListener("click", () => { loadPath("/"); }); breadcrumbs.appendChild(breadcrumb); return; } for (var i = 0; i < pathSplit.length; i++) { console.log(i); var breadcrumb = document.createElement("button"); breadcrumb.innerText = pathSplit[i]; var index = i; breadcrumb.addEventListener("click", () => { loadPath("/" + pathSplit.slice(1, index).join("/")); }); breadcrumbs.appendChild(breadcrumb); if (pathSplit[i] !== pathSplit[pathSplit.length - 1]) { var breadcrumbSpan = document.createElement("span"); breadcrumbSpan.innerText = ">"; breadcrumbs.appendChild(breadcrumbSpan); } } } document.querySelector("table").addEventListener("click", (e) => { if (e.currentTarget === e.target) { currentlySelected.forEach((row) => { row.classList.remove("selected"); }); currentlySelected = []; } }); document.addEventListener("contextmenu", (e) => { if (e.shiftKey) { return; } e.preventDefault(); const boundingRect = window.frameElement.getBoundingClientRect(); // var contextmenu = document.querySelector("#contextMenu"); // contextmenu.style.left = e.pageX + "px"; // contextmenu.style.top = e.pageY + "px"; // const hasSelection = currentlySelected.length > 0; // for (const elt of contextmenu.getElementsByClassName("needs-selection")) { // elt.ariaDisabled = !hasSelection; // elt.onclick = hasSelection ? elt.onclick : null; // } // contextmenu.style.removeProperty("display"); const containsApps = currentlySelected.map((item) => item.getAttribute("data-path").split(".").slice("-1")[0]).filter((item) => item == "app" || item == "lib").length > 0; if (containsApps) { appcontextmenu.show(e.pageX + boundingRect.x, e.pageY + boundingRect.y); newcontextmenu.hide(); emptycontextmenu.hide(); } else if (currentlySelected.length != 0) { newcontextmenu.show(e.pageX + boundingRect.x, e.pageY + boundingRect.y); appcontextmenu.hide(); emptycontextmenu.hide(); } else { emptycontextmenu.show(e.pageX + boundingRect.x, e.pageY + boundingRect.y); newcontextmenu.hide(); appcontextmenu.hide(); } }); document.addEventListener("click", (e) => { if ( !document.querySelector("#contextMenu").contains(e.target) || !e.target.ariaDisabled ) { // document.querySelector("#contextMenu").style.setProperty("display", "none"); newcontextmenu.hide(); appcontextmenu.hide(); emptycontextmenu.hide(); } }); function newFolder(path) { if (path === undefined) { path = document .querySelector(".breadcrumbs") .getAttribute("data-current-path") + "/" + prompt("Folder Name: "); } fs.mkdir(path); reload(); } function newFile(path) { if (path === undefined) { path = document .querySelector(".breadcrumbs") .getAttribute("data-current-path") + "/" + prompt("File Name: "); } fs.writeFile(path, ""); reload(); } function reload() { loadPath( document .querySelector(".breadcrumbs") .getAttribute("data-current-path"), ); } function reload() { loadPath( document .querySelector(".breadcrumbs") .getAttribute("data-current-path"), ); } function upload() { let fauxput = document.createElement("input"); // fauxput - fake input that isn't shown or ever added to page TODO: think of a better name for this variable fauxput.type = "file"; fauxput.onchange = async (e) => { const file = await e.target.files[0]; const content = await file.arrayBuffer(); fs.writeFile( `${document .querySelector(".breadcrumbs") .getAttribute("data-current-path")}/${file.name}`, Buffer.from(content), function (err) { reload(); }, ); }; fauxput.click(); } function deleteFile() { if (currentlySelected.length == 0) { anura.notifications.add({ title: "Filesystem app", description: "BUG: You have no files selected, right clicking does not select files", timeout: 5000, }); } currentlySelected.forEach(async (item) => { await sh.rm( item.getAttribute("data-path"), { recursive: true, }, function (err) { if (err) throw err; reload(); }, ); }); } function copy() { clipboard = currentlySelected; removeAfterPaste = false; } function cut() { clipboard = currentlySelected; removeAfterPaste = true; } function paste() { const path = document .querySelector(".breadcrumbs") .getAttribute("data-current-path"); if (!removeAfterPaste) { // copy destination = path; clipboard.forEach((item) => { origin = item.getAttribute("data-path"); fs.stat(origin, function (err, data) { if (data.isDirectory()) { // Ok so you are about to be in for a ride sh.ls( origin, { recursive: true, }, async function (err, entries) { if (err) throw err; let items = []; let dirs = []; entries.forEach((entry) => { function recurse(dirnode, path) { dirnode.contents.forEach((entry) => { if (entry.type === "DIRECTORY") { recurse( entry, path + "/" + entry.name, ); dirs.push(path + "/" + entry.name); } else { items.push(path + "/" + entry.name); } }); } const topLevelFolder = origin; dirs.push(origin); if (entry.type === "DIRECTORY") { recurse(entry, origin + "/" + entry.name); dirs.push(origin + "/" + entry.name); } else { items.push(origin + "/" + entry.name); } }); destItems = []; destDirs = []; numberToSubBy = origin.length - origin.split("/").pop().length; for (item in items) { destItems.push( destination + "/" + items[item].slice(numberToSubBy), ); } for (dir in dirs) { destDirs.push( destination + "/" + dirs[dir].slice(numberToSubBy), ); } console.log("initials"); console.log(items); console.log("destinations"); console.log(destItems); console.log("directories to mkdir -p "); console.log(destDirs); for (dir in destDirs) { await new Promise((resolve, reject) => { sh.mkdirp(destDirs[dir], function (err) { if (err) { reject(err); console.error(err); } resolve(); }); }); } for (item in items) { fs.readFile(items[item], function (err, data) { fs.writeFile(destItems[item], data); }); } reload(); }, ); } else { fs.readFile(origin, function (err, data) { if (err) throw err; fs.writeFile( `${path}/${origin.split("/").slice("-1")[0]}`, data ); reload(); }); } }); }); } if (removeAfterPaste) { // cut clipboard.forEach((item) => { itemName = item.getAttribute("data-path"); fs.rename( itemName, `${path}/${itemName.split("/").slice("-1")[0]}`, function (err) { if (err) throw err; reload(); }, ); }); } } function rename() { const path = document .querySelector(".breadcrumbs") .getAttribute("data-current-path"); if (currentlySelected.length == 0) { anura.notifications.add({ title: "Filesystem app", description: "BUG: You have no files selected, right clicking does not select files", timeout: 5000, }); return; } if (currentlySelected.length > 1) { anura.notifications.add({ title: "Filesystem app", description: "Renaming only works with one file", timeout: 5000, }); return; } fs.rename( currentlySelected[0].getAttribute("data-path"), `${path}/${prompt("filename:")}`, function (err) { if (err) throw err; reload(); }, ); } // Context menu version of the loadPath function // Used to enter app and lib folders, as double // clicking on them will install them. function navigate() { if (currentlySelected.length == 1) { loadPath(currentlySelected[0].getAttribute("data-path")); } // Can't navigate to multiple folders } loadPath("/"); ================================================ FILE: public/apps/libfileview.lib/fileHandler.js ================================================ const icons = await (await fetch(localPathToURL("icons.json"))).json(); export function openFile(path) { const fs = anura.fs || tb.fs; function openImage(path, mimetype) { fs.readFile(path, function (err, data) { tb.file.handler.openFile(path, 'image') }); } function openPDF(path) { fs.readFile(path, function (err, data) { tb.file.handler.openFile(path, 'pdf') }); } function openAudio(path, mimetype) { fs.readFile(path, function (err, data) { tb.file.handler.openFile(path, 'audio') }); } function openVideo(path, mimetype) { fs.readFile(path, function (err, data) { tb.file.handler.openFile(path, 'video') }); } function openText(path) { fs.readFile(path, function (err, data) { tb.file.handler.openFile(path, 'text') }); } function openHTML(path) { fs.readFile(path, function (err, data) { tb.file.handler.openFile(path, 'webpage') }); } let ext = path.split(".").slice("-1")[0]; switch (ext) { case "txt": case "js": case "mjs": case "cjs": case "json": case "css": openText(path); break; case "ajs": anura.processes.execute(path); break; case "mp3": openAudio(path, "audio/mpeg"); break; case "flac": openAudio(path, "audio/flac"); break; case "wav": openAudio(path, "audio/wav"); break; case "ogg": openAudio(path, "audio/ogg"); break; case "mp4": openVideo(path, "video/mp4"); break; case "mov": openVideo(path, "video/mp4"); break; case "webm": openVideo(path, "video/webm"); break; case "gif": openImage(path, "image/gif"); break; case "png": openImage(path, "image/png"); break; case "svg": openImage(path, "image/svg+xml"); break; case "jpg": case "jpeg": openImage(path, "image/jpeg"); break; case "pdf": openPDF(path); break; case "html": openHTML(path); break; default: openText(path); break; } } export function getIcon(path) { let ext = path.split(".").slice("-1")[0]; let iconObject = icons.files.find((icon) => icon.ext == ext); if (iconObject) { return localPathToURL(iconObject.icon); } return localPathToURL(icons.default); } export function getFileType(path) { let ext = path.split(".").slice("-1")[0]; let iconObject = icons.files.find((icon) => icon.ext == ext); if (iconObject) { return iconObject.type; } return "Anura File"; } function localPathToURL(path) { return ( import.meta.url.substring(0, import.meta.url.lastIndexOf("/")) + "/" + path ); } ================================================ FILE: public/apps/libfileview.lib/icons.json ================================================ { "files": [ { "ext": "txt", "icon": "icons/txt.svg", "source": "papirus/Papirus/16x16/mimetypes/text-plain.svg", "type": "Text File" }, { "ext": "mp3", "icon": "icons/mp3.svg", "source": "papirus/Papirus/16x16/mimetypes/audio-mpeg.svg", "type": "MPEG Audio" }, { "ext": "flac", "icon": "icons/flac.svg", "source": "papirus/Papirus/16x16/mimetypes/audio-flac.svg", "type": "FLAC Audio" }, { "ext": "wav", "icon": "icons/wav.svg", "source": "papirus/Papirus/16x16/mimetypes/audio-x-wav.svg", "type": "WAV Audio" }, { "ext": "ogg", "icon": "icons/ogg.svg", "source": "papirus/Papirus/16x16/mimetypes/audio-x-generic.svg", "type": "OGG Audio" }, { "ext": "mp4", "icon": "icons/mp4.svg", "source": "papirus/Papirus/16x16/mimetypes/video-mp4.svg", "type": "MPEG Video" }, { "ext": "mov", "icon": "icons/mov.svg", "source": "papirus/Papirus/16x16/mimetypes/video.svg", "type": "Quicktime Video" }, { "ext": "webm", "icon": "icons/webm.svg", "source": "papirus/Papirus/16x16/mimetypes/video-webm.svg", "type": "WebM Video" }, { "ext": "gif", "icon": "icons/gif.svg", "source": "papirus/Papirus/16x16/mimetypes/image-gif.svg", "type": "GIF Image" }, { "ext": "png", "icon": "icons/png.svg", "source": "papirus/Papirus/16x16/mimetypes/image-png.svg", "type": "PNG Image" }, { "ext": "jpg", "icon": "icons/jpeg.svg", "source": "papirus/Papirus/16x16/mimetypes/image-jpeg.svg", "type": "JPEG Image" }, { "ext": "jpeg", "icon": "icons/jpeg.svg", "source": "papirus/Papirus/16x16/mimetypes/image-jpeg.svg", "type": "JPEG Image" }, { "ext": "svg", "icon": "icons/svg.svg", "source": "papirus/Papirus/16x16/mimetypes/image-svg+xml.svg", "type": "SVG Image" }, { "ext": "pdf", "icon": "icons/pdf.svg", "source": "papirus/Papirus/16x16/mimetypes/application-pdf.svg", "type": "PDF Document" }, { "ext": "py", "icon": "icons/py.svg", "source": "papirus/Papirus/16x16/mimetypes/application-x-python-bytecode.svg", "type": "Python Script" }, { "ext": "js", "icon": "icons/js.svg", "source": "papirus/Papirus/16x16/mimetypes/application-javascript.svg", "type": "JavaScript Code" }, { "ext": "mjs", "icon": "icons/js.svg", "source": "papirus/Papirus/16x16/mimetypes/application-javascript.svg", "type": "JavaScript Module" }, { "ext": "cjs", "icon": "icons/js.svg", "source": "papirus/Papirus/16x16/mimetypes/application-javascript.svg", "type": "JavaScript CommonJS Module" }, { "ext": "ajs", "icon": "icons/js.svg", "source": "papirus/Papirus/16x16/mimetypes/application-javascript.svg", "type": "Anura JavaScript Binary" }, { "ext": "json", "icon": "icons/json.svg", "source": "papirus/Papirus/16x16/mimetypes/application-json.svg", "type": "JSON Data" }, { "ext": "html", "icon": "icons/html.svg", "source": "papirus/Papirus/16x16/mimetypes/text-html.svg", "type": "HTML Document" }, { "ext": "css", "icon": "icons/css.svg", "source": "papirus/Papirus/16x16/mimetypes/text-css.svg", "type": "CSS Stylesheet" } ], "default": "icons/txt.svg", "defaultSource": "papirus/Papirus/16x16/mimetypes/text-plain.svg", "folder": "icons/folder.svg", "folderSource": "papirus/Papirus/16x16/places/folder-white.svg" } ================================================ FILE: public/apps/libfileview.lib/install.js ================================================ const icons = await (await fetch(localPathToURL("icons.json"))).json(); // This constant is our own ID. It is used when registering the library. const HANDLER = "anura.fileviewer"; // This is the list of file extensions that we will handle const defaultHandlers = [...icons.files.map((file) => file.ext), "default"]; // The install function is called when the library is registered on boot. // If you want to detect the first install, you can set an anura registry // key and retrieve it later. // Here, we set the file handler for a few common file types to ourself. // If you want to restore to this file handler after an override, you can // set the `libfileview.reset` key to true. // following code in the console: // anura.settings.set('libfileview.reset', true) // If you want to disable the default file handler entirely, you can set // the `libfileview.disable` key to true. // anura.settings.set('libfileview.disable', true) export default function install(anura) { if (anura.settings.get("libfileview.disable")) { return; } anura.files.setFolderIcon(localPathToURL(icons.folder)); const exts = anura.settings.get("FileExts") || {}; const resetMode = anura.settings.get("libfileview.reset"); const externalHandlers = Object.keys(exts).filter( (ext) => exts[ext].id !== HANDLER, ); defaultHandlers.forEach((ext) => { if (!externalHandlers.includes(ext) || resetMode) { anura.files.setModule(HANDLER, ext); } }); anura.settings.set("libfileview.reset", false); } function localPathToURL(path) { return ( import.meta.url.substring(0, import.meta.url.lastIndexOf("/")) + "/" + path ); } ================================================ FILE: public/apps/libfileview.lib/manifest.json ================================================ { "name": "File Viewer", "icon": "files.png", "package": "anura.fileviewer", "installHook": "install.js", "versions": { "1.0.0": "fileHandler.js" }, "currentVersion": "1.0.0" } ================================================ FILE: public/apps/libpersist.lib/install.js ================================================ export default function install(anura) { const directories = anura.settings.get("directories"); anura.fs.exists(directories["opt"] + "/anura.persistence", (exists) => { if (exists) return; anura.fs.mkdir(directories["opt"] + "/anura.persistence"); anura.fs.mkdir(directories["opt"] + "/anura.persistence/providers"); anura.fs.mkdir( directories["opt"] + "/anura.persistence/providers/anureg", ); anura.fs.writeFile( directories["opt"] + "/anura.persistence/providers/anureg/manifest.json", JSON.stringify({ name: "anureg", vendor: "[[internal]]", description: "Anura's default persistance provider, using a simple JSON file", handler: "index.js", }), ); anura.fs.writeFile( directories["opt"] + "/anura.persistence/providers/anureg/index.js", `const { PersistenceProvider } = await anura.import("anura.persistence"); export default class Anureg extends PersistenceProvider { cache = {}; fs; basepath; file; config; constructor(anura, config, fs, basepath) { super(anura); this.fs = fs; this.basepath = basepath; this.config = config; this.file = config.path || (this.basepath + (config.filename || "//system/etc/anura/anura_settings.json")); } async init() { this.fs.exists(this.basepath, async (exists) => { if (!exists) { await this.fs.promises.mkdir(this.basepath); } }); try { const text = await this.fs.promises.readFile(this.file); this.cache = JSON.parse(text); } catch (e) { this.fs.writeFile(this.file, JSON.stringify(this.cache)); } } async get(prop) { return this.cache[prop]; } async has(prop) { return prop in this.cache; } async set(prop, val) { this.cache[prop] = val; return new Promise((r) => this.fs.writeFile(this.file, JSON.stringify(this.cache), r)); } createStoreFn(stateful, win) { return async ( target, ident, _backing ) => { target = (await this.get("dreamland." + ident)) || target; win.addEventListener("close", () => { console.info("[dreamland.js]: saving " + ident); this.set("dreamland." + ident, target); }); return stateful(target); } } } export const using = ["fs", "basepath"]; export const lifecycle = ["init"];`, ); }); } ================================================ FILE: public/apps/libpersist.lib/manifest.json ================================================ { "name": "AnuraOS Persistent Storage", "icon": "icon.svg", "package": "anura.persistence", "versions": { "1.0.0": "src/index.js" }, "installHook": "install.js", "cache": true, "currentVersion": "1.0.0" } ================================================ FILE: public/apps/libpersist.lib/src/index.js ================================================ /** * @typedef Anura * @type {any} */ /** * Base class for persisting data * This class is meant to be extended * by a specific implementation, such as * Anureg. It is not meant to be manually * used, however it can technically be * used as a memory-based cache. However, * this is mostly useless as the cache * is not actually persisted. */ export class PersistenceProvider { cache = {}; constructor(anura) { this.anura = anura; } async init() { console.log("init"); } async get(prop) { return this.cache[prop]; } async has(prop) { return prop in this.cache; } async set(prop, val) { this.cache[prop] = val; } createStoreFn(_stateful, _win) { return function () { // Not implemented for generic provider throw new Error("Not implemented"); }; } toProxy() { return new Proxy(this, { get: (target, prop) => { return target.get(prop); }, set: (target, prop, val) => { target.set(prop, val); return true; }, }); } } export class ProviderLoader { fs; anura; basepath; constructor(anura, fs, basepath) { this.fs = fs; this.anura = anura; this.basepath = basepath; this.providers = {}; } async locate() { const providers = await this.fs.promises.readdir(this.basepath); for (const provider of providers) { const manifest = JSON.parse( await this.fs.promises.readFile( this.basepath + "/" + provider + "/manifest.json", ), ); let mod = await import( "/fs/" + this.basepath + "/" + provider + "/" + manifest.handler ); this.providers[manifest.name] = { manifest, mod, }; } } /** * Build a new persistenceProvider * @param {Anura} anura - The Anura instance * @param {Object} app - The app instance * @param {Object} config - The configuration object * @param {string} provider - The provider name * @returns {PersistenceProvider} The provider */ async build(app, config = {}, provider = "anureg") { let args = [config, this.anura]; let using = this.providers[provider].mod.using || []; let lifecycle = this.providers[provider].mod.lifecycle || []; for (let i = 0; i < using.length; i++) { switch (using[i]) { case "fs": args.push(this.fs); break; case "basepath": args.push( this.anura.settings.get("directories")["opt"] + "/" + app.package, ); break; default: throw new Error("Unknown dependency: " + using[i]); } } let providerInstance = new this.providers[provider].mod.default( ...args, ); if (lifecycle.includes("init")) { await providerInstance.init(); } return providerInstance; } } export function buildLoader(anura, basepath) { if (!basepath) { basepath = anura.settings.get("directories")["opt"] + "/anura.persistence/providers"; } return new ProviderLoader(anura, anura.fs, basepath); } ================================================ FILE: public/apps/media viewer.tapp/index.css ================================================ @font-face { font-family: Inter; src: url(/fonts/inter.ttf); } html, body { font-family: Inter; font-size: 14px; font-weight: 400; line-height: 20px; color: #ffffff; background-color: transparent; margin: 0; padding: 0; overflow: hidden; height: 100%; } .media { overflow: auto; height: 100%; width: 100%; scrollbar-width: thin; scrollbar-color: #ffffff44 transparent; display: flex; flex-direction: column; justify-content: center; align-items: center; background-image: url(./bg.png); background-repeat: repeat; background-position: center; background-size: 5%; } .media::-webkit-scrollbar { width: 8px; height: 8px; } .media::-webkit-scrollbar-corner { background-color: #ffffff; } .media::-webkit-scrollbar-track { background-color: #ffffff; width: 8px; height: 8px; } .media::-webkit-scrollbar-thumb { background-color: #00000028; border: none; width: 8px; height: 8px; } .media img { width: max-content; height: max-content; max-width: 100%; max-height: 100%; } ================================================ FILE: public/apps/media viewer.tapp/index.html ================================================ Terbium Media Viewer
================================================ FILE: public/apps/media viewer.tapp/index.js ================================================ import * as id3 from "https://unpkg.com/id3js@latest/lib/id3.js"; import * as pdfjsLib from "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/5.4.149/pdf.min.mjs"; if (pdfjsLib && pdfjsLib.GlobalWorkerOptions) { console.log("Setting up PDF.js worker"); pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/5.4.149/pdf.worker.min.mjs"; } window.addEventListener("load", async () => { parent.postMessage(JSON.stringify({ type: "ready" }), "*"); setTimeout(() => { if (!asked) { showFileBrowser(); } }, 100); }); async function openFile(url, ext, fileName, dav) { let exts = JSON.parse(await window.parent.tb.fs.promises.readFile("/apps/system/files.tapp/extensions.json", "utf8")); if (exts["animated"].includes(ext)) { let imgObj = new Image(); imgObj.src = url; imgObj.setAttribute("draggable", false); document.querySelector(".media").innerHTML = ""; document.querySelector(".media").appendChild(imgObj); let scale = 1; window.addEventListener("wheel", function (e) { const zoomSpeed = 0.1; e.preventDefault(); if (e.deltaY < 0) { scale *= 1 + zoomSpeed; } else { scale /= 1 + zoomSpeed; } imgObj.style.transform = `scale(${scale})`; }); window.addEventListener("resize", () => { imgObj.style.transform = `scale(${scale})`; }); imgObj.addEventListener("load", () => { imgObj.style.transform = `scale(${scale})`; }); } else if (exts["image"].includes(ext)) { let canvas = document.createElement("canvas"); let ctx = canvas.getContext("2d"); document.querySelector(".media").innerHTML = ""; document.querySelector(".media").appendChild(canvas); let isDragging = false; let isMouseDown = false; let startCoords = { x: 0, y: 0 }; let offset = { x: 0, y: 0 }; let scale = 0.5; let imgObj = new Image(); if (dav) { try { const davInstances = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/davs.json`, "utf8")); 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.parent.tb.vfs.servers.get(dav.name); let filePath; if (url.startsWith("http")) { const match = url.match(/^https?:\/\/[^\/]+\/dav\/([^\/]+\/)?(.+)$/); filePath = match ? "/" + match[2] : url; } else { filePath = url.replace(davUrl, "/"); } const response = await client.getFileContents(filePath); const blob = new Blob([response], { type: "image/" + ext }); imgObj.src = URL.createObjectURL(blob); } catch (err) { window.tb.dialog.Alert({ title: "Failed to read dav file", message: err, }); } } else { imgObj.src = url; } imgObj.onload = () => { initializeCanvas(); }; function initializeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; drawImageWithOffsetAndScale(); } window.addEventListener("resize", function () { initializeCanvas(); }); canvas.addEventListener("mousedown", function (e) { isDragging = true; isMouseDown = true; startCoords = { x: e.clientX, y: e.clientY }; }); window.addEventListener("mouseup", function () { isDragging = false; isMouseDown = false; }); canvas.addEventListener("mousemove", function (e) { if (isDragging) { const deltaX = e.clientX - startCoords.x; const deltaY = e.clientY - startCoords.y; offset.x += deltaX; offset.y += deltaY; startCoords = { x: e.clientX, y: e.clientY }; drawImageWithOffsetAndScale(); } }); canvas.addEventListener("touchstart", e => { isDragging = true; isMouseDown = true; startCoords = { x: e.touches[0].clientX, y: e.touches[0].clientY }; }); window.addEventListener("touchend", () => { isDragging = false; isMouseDown = false; }); canvas.addEventListener("touchmove", e => { if (isDragging) { const deltaX = e.touches[0].clientX - startCoords.x; const deltaY = e.touches[0].clientY - startCoords.y; offset.x += deltaX; offset.y += deltaY; startCoords = { x: e.touches[0].clientX, y: e.touches[0].clientY }; drawImageWithOffsetAndScale(); } }); canvas.addEventListener("mouseleave", () => { isDragging = false; }); canvas.addEventListener("mouseenter", () => { if (isMouseDown) { isDragging = true; } }); canvas.addEventListener("wheel", function (e) { const zoomSpeed = 0.1; e.preventDefault(); if (e.deltaY < 0) { scale *= 1 + zoomSpeed; } else { scale /= 1 + zoomSpeed; } drawImageWithOffsetAndScale(); }); function drawImageWithOffsetAndScale() { ctx.clearRect(0, 0, canvas.width, canvas.height); const newWidth = imgObj.width * scale; const newHeight = imgObj.height * scale; const x = offset.x + (canvas.width - newWidth) / 2; const y = offset.y + (canvas.height - newHeight) / 2; ctx.drawImage(imgObj, x, y, newWidth, newHeight); } } else if (exts["pdf"].includes(ext)) { document.querySelector(".media").innerHTML = ""; const pdfContainer = document.createElement("div"); pdfContainer.className = "pdf-container"; pdfContainer.style.display = "flex"; pdfContainer.style.flexDirection = "column"; pdfContainer.style.alignItems = "center"; pdfContainer.style.gap = "20px"; pdfContainer.style.overflow = "auto"; pdfContainer.style.width = "100%"; pdfContainer.style.height = "100%"; pdfContainer.style.transformOrigin = "top center"; pdfContainer.style.paddingTop = "56px"; document.querySelector(".media").appendChild(pdfContainer); const loadingTask = pdfjsLib.getDocument({ url }); const pdf = await loadingTask.promise; for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { const page = await pdf.getPage(pageNum); const viewport = page.getViewport({ scale: 1 }); const canvas = document.createElement("canvas"); canvas.style.maxWidth = "75%"; canvas.style.height = "auto"; canvas.width = viewport.width; canvas.height = viewport.height; const context = canvas.getContext("2d"); const renderContext = { canvasContext: context, viewport: viewport, }; await page.render(renderContext).promise; pdfContainer.appendChild(canvas); } const controls = document.createElement("div"); controls.className = "pdf-controls"; controls.innerHTML = `
1 / ${pdf.numPages}
`; document.querySelector(".media").appendChild(controls); const canvases = pdfContainer.querySelectorAll("canvas"); let currentPage = 1; const prevBtn = controls.querySelector(".prev-btn"); const nextBtn = controls.querySelector(".next-btn"); const pageIndicator = controls.querySelector(".page-indicator"); const pageInput = controls.querySelector(".page-input"); const goBtn = controls.querySelector(".go-btn"); function updateControls() { pageIndicator.textContent = `${currentPage} / ${canvases.length}`; pageInput.value = currentPage; prevBtn.disabled = currentPage <= 1; nextBtn.disabled = currentPage >= canvases.length; } function showPage(page) { page = Math.max(1, Math.min(page, canvases.length)); const target = canvases[page - 1]; if (!target) return; pdfContainer.scrollTo({ top: target.offsetTop - 10, behavior: "smooth" }); currentPage = page; updateControls(); } prevBtn.addEventListener("click", () => showPage(currentPage - 1)); nextBtn.addEventListener("click", () => showPage(currentPage + 1)); goBtn.addEventListener("click", () => showPage(parseInt(pageInput.value, 10))); pageInput.addEventListener("keydown", e => { if (e.key === "Enter") showPage(parseInt(pageInput.value, 10)); }); pdfContainer.addEventListener("scroll", () => { const containerRect = pdfContainer.getBoundingClientRect(); const containerCenter = containerRect.top + containerRect.height / 2; let closestIndex = 0; let closestDist = Infinity; canvases.forEach((c, idx) => { const r = c.getBoundingClientRect(); const cCenter = r.top + r.height / 2; const dist = Math.abs(cCenter - containerCenter); if (dist < closestDist) { closestDist = dist; closestIndex = idx; } }); if (currentPage !== closestIndex + 1) { currentPage = closestIndex + 1; updateControls(); } }); updateControls(); showPage(1); window.addEventListener("keydown", e => { if (e.key === "ArrowLeft") showPage(currentPage - 1); if (e.key === "ArrowRight") showPage(currentPage + 1); }); } else if (exts["video"].includes(ext)) { const videoPlayerHTML = `
0:00 / 0:00
`; document.querySelector(".media").innerHTML = videoPlayerHTML; const videoContainer = document.querySelector(".custom-video-player"); const videoElem = videoContainer.querySelector("video"); const playPauseBtn = videoContainer.querySelector(".play-pause-btn"); const seekBar = videoContainer.querySelector(".seek-bar"); const timeDisplay = videoContainer.querySelector(".time-display"); const volumeBar = videoContainer.querySelector(".volume-bar"); const fullscreenBtn = videoContainer.querySelector(".fullscreen-btn"); const titleOverlay = videoContainer.querySelector(".title-overlay"); if (dav) { try { const davInstances = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/davs.json`, "utf8")); 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.parent.tb.vfs.servers.get(dav.name); let filePath; if (url.startsWith("http")) { const match = url.match(/^https?:\/\/[^\/]+\/dav\/([^\/]+\/)?(.+)$/); filePath = match ? "/" + match[2] : url; } else { filePath = url.replace(davUrl, "/"); } const response = await client.getFileContents(filePath); const blob = new Blob([response], { type: "video/" + ext }); videoElem.src = URL.createObjectURL(blob); } catch (err) { window.tb.dialog.Alert({ title: "Failed to read dav file", message: err, }); } } else { videoElem.src = url; } let scale = 1; titleOverlay.textContent = fileName || "Video.mp4"; function formatTime(seconds) { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, "0")}`; } videoElem.addEventListener("click", () => { if (videoElem.paused) { videoElem.play(); playPauseBtn.textContent = "⏸"; } else { videoElem.pause(); playPauseBtn.textContent = "▶"; } }); playPauseBtn.addEventListener("click", () => { if (videoElem.paused) { videoElem.play(); playPauseBtn.textContent = "⏸"; } else { videoElem.pause(); playPauseBtn.textContent = "▶"; } }); videoElem.addEventListener("timeupdate", () => { seekBar.value = (videoElem.currentTime / videoElem.duration) * 100 || 0; timeDisplay.textContent = `${formatTime(videoElem.currentTime)} / ${formatTime(videoElem.duration)}`; }); seekBar.addEventListener("input", () => { const time = (seekBar.value / 100) * videoElem.duration; videoElem.currentTime = time; }); volumeBar.addEventListener("input", () => { videoElem.volume = volumeBar.value / 100; }); fullscreenBtn.addEventListener("click", () => { if (videoContainer.requestFullscreen) { videoContainer.requestFullscreen(); } }); videoElem.play(); window.addEventListener("wheel", function (e) { const zoomSpeed = 0.1; e.preventDefault(); if (e.deltaY < 0) { scale *= 1 + zoomSpeed; } else { scale /= 1 + zoomSpeed; } videoContainer.style.transform = `scale(${scale})`; }); window.addEventListener("resize", () => { videoContainer.style.transform = `scale(${scale})`; }); videoElem.addEventListener("load", () => { videoContainer.style.transform = `scale(${scale})`; }); } else if (exts["audio"].includes(ext)) { const audioPlayerHTML = `
0:00 / 0:00
`; document.querySelector(".media").innerHTML = audioPlayerHTML; const audioContainer = document.querySelector(".custom-audio-player"); const audioElem = audioContainer.querySelector("audio"); const playPauseBtn = audioContainer.querySelector(".play-pause-btn"); const seekBar = audioContainer.querySelector(".seek-bar"); const timeDisplay = audioContainer.querySelector(".time-display"); const volumeBar = audioContainer.querySelector(".volume-bar"); const audioVisual = audioContainer.querySelector(".audio-visual"); const titleOverlay = audioContainer.querySelector(".title-overlay"); if (dav) { try { const davInstances = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/davs.json`, "utf8")); 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.parent.tb.vfs.servers.get(dav.name); let filePath; if (url.startsWith("http")) { const match = url.match(/^https?:\/\/[^\/]+\/dav\/([^\/]+\/)?(.+)$/); filePath = match ? "/" + match[2] : url; } else { filePath = url.replace(davUrl, "/"); } const response = await client.getFileContents(filePath); const blob = new Blob([response], { type: "audio/" + ext }); audioElem.src = URL.createObjectURL(blob); } catch (err) { window.tb.dialog.Alert({ title: "Failed to read dav file", message: err, }); } } else { audioElem.src = url; } let scale = 1; titleOverlay.textContent = fileName || "Audio.mp3"; function formatTime(seconds) { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, "0")}`; } audioVisual.addEventListener("click", () => { if (audioElem.paused) { audioElem.play(); playPauseBtn.textContent = "⏸"; } else { audioElem.pause(); playPauseBtn.textContent = "▶"; } }); playPauseBtn.addEventListener("click", () => { if (audioElem.paused) { audioElem.play(); playPauseBtn.textContent = "⏸"; } else { audioElem.pause(); playPauseBtn.textContent = "▶"; } }); audioElem.addEventListener("timeupdate", () => { seekBar.value = (audioElem.currentTime / audioElem.duration) * 100 || 0; timeDisplay.textContent = `${formatTime(audioElem.currentTime)} / ${formatTime(audioElem.duration)}`; }); seekBar.addEventListener("input", () => { const time = (seekBar.value / 100) * audioElem.duration; audioElem.currentTime = time; }); volumeBar.addEventListener("input", () => { audioElem.volume = volumeBar.value / 100; }); const p = async () => { const ext = await window.parent.tb.mediaplayer.isExisting(); if (ext === false) { try { const response = await fetch(url); const blob = await response.blob(); const tags = await id3.fromFile(new File([blob], "audio.mp3", { type: blob.type })); let image = null; if (tags.images && tags.images.length > 0) { const imageData = tags.images[0]; try { if (image && typeof image === "string" && image.startsWith("blob:")) { URL.revokeObjectURL(image); image = null; } let data = imageData.data; let uint8; if (data instanceof ArrayBuffer) { uint8 = new Uint8Array(data); } else if (ArrayBuffer.isView(data)) { uint8 = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); } else if (Array.isArray(data)) { uint8 = new Uint8Array(data); } else if (typeof data === "string") { const b64 = data.replace(/^data:\w+\/[a-zA-Z+]+;base64,/, ""); const raw = atob(b64); uint8 = new Uint8Array(raw.length); for (let i = 0; i < raw.length; ++i) uint8[i] = raw.charCodeAt(i); } else if (window.parent && window.parent.tb && window.parent.tb.buffer && typeof window.parent.tb.buffer.from === "function") { const buf = window.parent.tb.buffer.from(data); uint8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); } else { throw new Error("Unknown image data type: " + Object.prototype.toString.call(data)); } const mime = imageData.format || imageData.mime || "image/jpeg"; const signatures = [ { name: "jpeg", sig: [0xff, 0xd8, 0xff] }, { name: "png", sig: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] }, { name: "gif", sig: [0x47, 0x49, 0x46, 0x38] }, ]; let startIndex = 0; for (const s of signatures) { const sig = s.sig; for (let i = 0; i <= uint8.length - sig.length; i++) { let ok = true; for (let j = 0; j < sig.length; j++) { if (uint8[i + j] !== sig[j]) { ok = false; break; } } if (ok) { startIndex = i; break; } } if (startIndex) break; } if (startIndex > 0) { uint8 = uint8.subarray(startIndex); console.warn("Sliced album art buffer to skip", startIndex, "bytes of leading data"); } const blob = new Blob([uint8], { type: mime }); image = URL.createObjectURL(blob); audioVisual.style.backgroundImage = `url("${image}")`; audioVisual.style.backgroundSize = "cover"; audioVisual.style.backgroundPosition = "center"; audioVisual.innerHTML = ""; const uint8ToBase64 = u8 => { const CHUNK = 0x8000; let index = 0; let result = []; while (index < u8.length) { result.push(String.fromCharCode.apply(null, u8.subarray(index, Math.min(index + CHUNK, u8.length)))); index += CHUNK; } return btoa(result.join("")); }; const testImg = new Image(); testImg.onload = () => { if (audioVisual.contains(testImg)) audioVisual.removeChild(testImg); }; testImg.onerror = () => { console.warn("Album art blob failed to load; trying base64 fallback"); try { const b64 = uint8ToBase64(uint8); const dataUrl = `data:${mime};base64,${b64}`; testImg.src = dataUrl; audioVisual.style.backgroundImage = `url("${dataUrl}")`; } catch (err) { console.warn("Base64 fallback failed", err); } }; testImg.src = image; } catch (err) { console.warn("Failed to decode album art:", err, imageData); } } const title = tags.title || fileName || "Unknown Title"; const artist = tags.artist || "Unknown Artist"; titleOverlay.textContent = `${title} - ${artist}`; window.parent.tb.mediaplayer.music({ track_name: title, artist: artist, endtime: Math.trunc(audioElem.duration), background: image, onPausePlay: () => { if (audioElem.paused) { audioElem.play(); } else { audioElem.pause(); } }, onSeek: time => { audioElem.currentTime = time; }, }); } catch (e) { console.log("Error reading ID3 tags:", e); } } }; audioElem.addEventListener("play", p); p(); audioElem.play(); window.addEventListener("wheel", function (e) { const zoomSpeed = 0.1; e.preventDefault(); if (e.deltaY < 0) { scale *= 1 + zoomSpeed; } else { scale /= 1 + zoomSpeed; } audioContainer.style.transform = `scale(${scale})`; }); window.addEventListener("resize", () => { audioContainer.style.transform = `scale(${scale})`; }); audioElem.addEventListener("load", () => { audioContainer.style.transform = `scale(${scale})`; }); } } function showFileBrowser() { if (asked) return; asked = true; if (!(window.parent && window.parent.tb && window.parent.tb.dialog && window.parent.tb.fs && window.parent.tb.window)) return; window.parent.tb.dialog.FileBrowser({ title: "Open a file", onOk: async file => { const ext = file.split(".").pop(); let json = JSON.parse(await window.parent.tb.fs.promises.readFile("/apps/system/files.tapp/extensions.json", "utf8")); if (file.includes("http")) { openFile(file, ext, file.split("/").pop(), true); } else if (json["image"].includes(ext)) { let img = await window.parent.tb.fs.promises.readFile(file); let blob = new Blob([img], { type: "image/" + ext }); let url = URL.createObjectURL(blob); openFile(url, ext, file.split("/").pop()); } else if (json["animated"].includes(ext)) { let img = await window.parent.tb.fs.promises.readFile(file); let blob = new Blob([img], { type: "image/" + ext }); let url = URL.createObjectURL(blob); openFile(url, ext, file.split("/").pop()); } else if (json["pdf"].includes(ext)) { let pdf = await window.parent.tb.fs.promises.readFile(file); let blob = new Blob([pdf], { type: "application/pdf" }); let url = URL.createObjectURL(blob); openFile(url, ext, file.split("/").pop()); } else if (json["video"].includes(ext)) { let video = await window.parent.tb.fs.promises.readFile(file); let blob = new Blob([video], { type: "video/" + ext }); let url = URL.createObjectURL(blob); openFile(url, ext, file.split("/").pop()); } else if (json["audio"].includes(ext)) { let audio = await window.parent.tb.fs.promises.readFile(file); let blob = new Blob([audio], { type: "audio/" + ext }); let url = URL.createObjectURL(blob); openFile(url, ext, file.split("/").pop()); } }, onCancel: () => { window.parent.tb.window.close(); }, }); } let asked = false; window.addEventListener("message", async e => { let data; try { data = JSON.parse(e.data); } catch (e) { data = e.data; } if (data === undefined && !asked) { showFileBrowser(); } if (data && data.type === "process") { asked = true; if (data.path) { 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, data.path.split("/").pop(), true); } 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 }); let url = URL.createObjectURL(blob); openFile(url, ext, data.path.split("/").pop()); } else if (json["animated"].includes(ext)) { let img = await window.parent.tb.fs.promises.readFile(data.path); let blob = new Blob([img], { type: "image/" + ext }); let url = URL.createObjectURL(blob); openFile(url, ext, data.path.split("/").pop()); } else if (json["pdf"].includes(ext)) { let pdf = await window.parent.tb.fs.promises.readFile(data.path); let blob = new Blob([pdf], { type: "application/pdf" }); let url = URL.createObjectURL(blob); openFile(url, ext, data.path.split("/").pop()); } else if (json["video"].includes(ext)) { let video = await window.parent.tb.fs.promises.readFile(data.path); let blob = new Blob([video], { type: "video/" + ext }); let url = URL.createObjectURL(blob); openFile(url, ext, data.path.split("/").pop()); } else if (json["audio"].includes(ext)) { let audio = await window.parent.tb.fs.promises.readFile(data.path); let blob = new Blob([audio], { type: "audio/" + ext }); let url = URL.createObjectURL(blob); openFile(url, ext, data.path.split("/").pop()); } } } }); ================================================ FILE: public/apps/media viewer.tapp/index.json ================================================ { "name": "Media Viewer", "config": { "title": "Media Viewer", "icon": "/fs/apps/system/media viewer.tapp/icon.svg", "src": "/fs/apps/system/media viewer.tapp/index.html" } } ================================================ FILE: public/apps/media viewer.tapp/media.com.js ================================================ const tb = parent.window.tb; const tb_island = tb.window.island; const tb_window = tb.window; const tb_context_menu = tb.context_menu; const tb_dialog = tb.dialog; tb_island.addControl({ text: "File", appname: "Media Viewer", id: "media-file", click: () => { const appIsland = parent.document.querySelector(".app_island"); const options = [ { text: "Open File", click: async () => { await tb.dialog.FileBrowser({ title: "Select a file to view", onOk: async file => { const url = `${parent.window.location.origin}/fs/${file}`; const ext = file.split(".").pop(); openFile(url, ext); }, }); }, }, ]; tb.contextmenu.create({ x: appIsland.clientWidth - 110, y: appIsland.clientHeight + 12, iframe: false, options: options, }); }, }); tb_island.addControl({ text: "Computer", appname: "Media Viewer", id: "media-computer", click: () => { const appIsland = parent.document.querySelector(".app_island"); const options = [ { text: "Open File from PC", click: async () => { const file = document.createElement("input"); file.type = "file"; file.accept = "image/*,video/*"; file.onchange = async () => { const url = URL.createObjectURL(file.files[0]); const ext = file.files[0].name.split(".").pop(); openFile(url, ext); }; file.click(); }, }, ]; tb.contextmenu.create({ x: appIsland.clientWidth - 110, y: appIsland.clientHeight + 12, iframe: false, options: options, }); }, }); ================================================ FILE: public/apps/nfsadapter/FileSystemDirectoryHandle.js ================================================ import FileSystemHandle from './FileSystemHandle.js' import { errors } from './util.js' const { GONE, MOD_ERR } = errors const kAdapter = Symbol('adapter') class FileSystemDirectoryHandle extends FileSystemHandle { /** @type {FileSystemDirectoryHandle} */ [kAdapter] constructor (adapter) { super(adapter) this[kAdapter] = adapter } /** * @param {string} name Name of the directory * @param {object} [options] * @param {boolean} [options.create] create the directory if don't exist * @returns {Promise} */ async getDirectoryHandle (name, options = {}) { if (name === '') { throw new TypeError(`Name can't be an empty string.`) } if (name === '.' || name === '..' || name.includes('/')) { throw new TypeError(`Name contains invalid characters.`) } options.create = !!options.create const handle = await this[kAdapter].getDirectoryHandle(name, options) return new FileSystemDirectoryHandle(handle) } /** @returns {AsyncGenerator<[string, FileSystemHandle | FileSystemDirectoryHandle]>} */ async * entries () { const {FileSystemFileHandle} = await import('./FileSystemFileHandle.js') for await (const [_, entry] of this[kAdapter].entries()) yield [entry.name, entry.kind === 'file' ? new FileSystemFileHandle(entry) : new FileSystemDirectoryHandle(entry)] } /** @deprecated use .entries() instead */ async * getEntries() { const {FileSystemFileHandle} = await import('./FileSystemFileHandle.js') console.warn('deprecated, use .entries() instead') for await (let entry of this[kAdapter].entries()) yield entry.kind === 'file' ? new FileSystemFileHandle(entry) : new FileSystemDirectoryHandle(entry) } /** * @param {string} name Name of the file * @param {object} [options] * @param {boolean} [options.create] create the file if don't exist */ async getFileHandle (name, options = {}) { const {FileSystemFileHandle} = await import('./FileSystemFileHandle.js') if (name === '') throw new TypeError(`Name can't be an empty string.`) if (name === '.' || name === '..' || name.includes('/')) { throw new TypeError(`Name contains invalid characters.`) } options.create = !!options.create const handle = await this[kAdapter].getFileHandle(name, options) return new FileSystemFileHandle(handle) } /** * @param {string} name * @param {object} [options] * @param {boolean} [options.recursive] */ async removeEntry (name, options = {}) { if (name === '') { throw new TypeError(`Name can't be an empty string.`) } if (name === '.' || name === '..' || name.includes('/')) { throw new TypeError(`Name contains invalid characters.`) } options.recursive = !!options.recursive // cuz node's fs.rm require boolean return this[kAdapter].removeEntry(name, options) } async resolve (possibleDescendant) { if (await possibleDescendant.isSameEntry(this)) { return [] } const openSet = [{ handle: this, path: [] }] while (openSet.length) { let { handle: current, path } = openSet.pop() for await (const entry of current.values()) { if (await entry.isSameEntry(possibleDescendant)) { return [...path, entry.name] } if (entry.kind === 'directory') { openSet.push({ handle: entry, path: [...path, entry.name] }) } } } return null } async * keys () { for await (const [name] of this[kAdapter].entries()) yield name } async * values () { for await (const [_, entry] of this) yield entry } [Symbol.asyncIterator]() { return this.entries() } } Object.defineProperty(FileSystemDirectoryHandle.prototype, Symbol.toStringTag, { value: 'FileSystemDirectoryHandle', writable: false, enumerable: false, configurable: true }) Object.defineProperties(FileSystemDirectoryHandle.prototype, { getDirectoryHandle: { enumerable: true }, entries: { enumerable: true }, getFileHandle: { enumerable: true }, removeEntry: { enumerable: true } }) if (globalThis.FileSystemDirectoryHandle) { const proto = globalThis.FileSystemDirectoryHandle.prototype proto.resolve = async function resolve (possibleDescendant) { if (await possibleDescendant.isSameEntry(this)) { return [] } const openSet = [{ handle: this, path: [] }] while (openSet.length) { let { handle: current, path } = openSet.pop() for await (const entry of current.values()) { if (await entry.isSameEntry(possibleDescendant)) { return [...path, entry.name] } if (entry.kind === 'directory') { openSet.push({ handle: entry, path: [...path, entry.name] }) } } } return null } // Safari allows us operate on deleted files, // so we need to check if they still exist. // Hope to remove this one day. async function ensureDoActuallyStillExist (handle) { const root = await navigator.storage.getDirectory() const path = await root.resolve(handle) if (path === null) { throw new DOMException(...GONE) } } const entries = proto.entries proto.entries = async function * () { await ensureDoActuallyStillExist(this) yield * entries.call(this) } proto[Symbol.asyncIterator] = async function * () { yield * this.entries() } const removeEntry = proto.removeEntry proto.removeEntry = async function (name, options = {}) { return removeEntry.call(this, name, options).catch(async err => { const unknown = err instanceof DOMException && err.name === 'UnknownError' if (unknown && !options.recursive) { const empty = (await entries.call(this).next()).done if (!empty) { throw new DOMException(...MOD_ERR) } } throw err }) } } export default FileSystemDirectoryHandle export { FileSystemDirectoryHandle } ================================================ FILE: public/apps/nfsadapter/FileSystemFileHandle.js ================================================ const kAdapter$1 = Symbol('adapter'); /** * @typedef {Object} FileSystemHandlePermissionDescriptor * @property {('read'|'readwrite')} [mode='read'] */ class FileSystemHandle { /** @type {FileSystemHandle} */ [kAdapter$1] /** @type {string} */ name /** @type {('file'|'directory')} */ kind /** @param {FileSystemHandle & {writable}} adapter */ constructor (adapter) { this.kind = adapter.kind; this.name = adapter.name; this[kAdapter$1] = adapter; } /** @param {FileSystemHandlePermissionDescriptor} descriptor */ async queryPermission (descriptor = {}) { const { mode = 'read' } = descriptor; const handle = this[kAdapter$1]; if (handle.queryPermission) { return handle.queryPermission({mode}) } if (mode === 'read') { return 'granted' } else if (mode === 'readwrite') { return handle.writable ? 'granted' : 'denied' } else { throw new TypeError(`Mode ${mode} must be 'read' or 'readwrite'`) } } async requestPermission ({mode = 'read'} = {}) { const handle = this[kAdapter$1]; if (handle.requestPermission) { return handle.requestPermission({mode}) } if (mode === 'read') { return 'granted' } else if (mode === 'readwrite') { return handle.writable ? 'granted' : 'denied' } else { throw new TypeError(`Mode ${mode} must be 'read' or 'readwrite'`) } } /** * Attempts to remove the entry represented by handle from the underlying file system. * * @param {object} options * @param {boolean} [options.recursive=false] */ async remove (options = {}) { await this[kAdapter$1].remove(options); } /** * @param {FileSystemHandle} other */ async isSameEntry (other) { if (this === other) return true if ( (!other) || (typeof other !== 'object') || (this.kind !== other.kind) || (!other[kAdapter$1]) ) return false return this[kAdapter$1].isSameEntry(other[kAdapter$1]) } } Object.defineProperty(FileSystemHandle.prototype, Symbol.toStringTag, { value: 'FileSystemHandle', writable: false, enumerable: false, configurable: true }); // Safari safari doesn't support writable streams yet. if (globalThis.FileSystemHandle) { globalThis.FileSystemHandle.prototype.queryPermission ??= function (descriptor) { return 'granted' }; } const config = { ReadableStream: globalThis.ReadableStream, WritableStream: globalThis.WritableStream, TransformStream: globalThis.TransformStream, DOMException: globalThis.DOMException, Blob: globalThis.Blob, File: globalThis.File, }; const { WritableStream } = config; class FileSystemWritableFileStream extends WritableStream { #writer constructor (writer) { super(writer); this.#writer = writer; // Stupid Safari hack to extend native classes // https://bugs.webkit.org/show_bug.cgi?id=226201 Object.setPrototypeOf(this, FileSystemWritableFileStream.prototype); /** @private */ this._closed = false; } async close () { this._closed = true; const w = this.getWriter(); const p = w.close(); w.releaseLock(); return p // return super.close ? super.close() : this.getWriter().close() } /** @param {number} position */ seek (position) { return this.write({ type: 'seek', position }) } /** @param {number} size */ truncate (size) { return this.write({ type: 'truncate', size }) } // The write(data) method steps are: write (data) { if (this._closed) { return Promise.reject(new TypeError('Cannot write to a CLOSED writable stream')) } // 1. Let writer be the result of getting a writer for this. const writer = this.getWriter(); // 2. Let result be the result of writing a chunk to writer given data. const result = writer.write(data); // 3. Release writer. writer.releaseLock(); // 4. Return result. return result } } Object.defineProperty(FileSystemWritableFileStream.prototype, Symbol.toStringTag, { value: 'FileSystemWritableFileStream', writable: false, enumerable: false, configurable: true }); Object.defineProperties(FileSystemWritableFileStream.prototype, { close: { enumerable: true }, seek: { enumerable: true }, truncate: { enumerable: true }, write: { enumerable: true } }); // Safari safari doesn't support writable streams yet. if ( globalThis.FileSystemFileHandle && !globalThis.FileSystemFileHandle.prototype.createWritable && !globalThis.FileSystemWritableFileStream ) { globalThis.FileSystemWritableFileStream = FileSystemWritableFileStream; } const errors = { INVALID: ['seeking position failed.', 'InvalidStateError'], GONE: ['A requested file or directory could not be found at the time an operation was processed.', 'NotFoundError'], MISMATCH: ['The path supplied exists, but was not an entry of requested type.', 'TypeMismatchError'], MOD_ERR: ['The object can not be modified in this way.', 'InvalidModificationError'], SYNTAX: m => [`Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. ${m}`, 'SyntaxError'], SECURITY: ['It was determined that certain files are unsafe for access within a Web application, or that too many calls are being made on file resources.', 'SecurityError'], DISALLOWED: ['The request is not allowed by the user agent or the platform in the current context.', 'NotAllowedError'] }; const { INVALID, SYNTAX, GONE } = errors; const kAdapter = Symbol('adapter'); class FileSystemFileHandle extends FileSystemHandle { /** @type {FileSystemFileHandle} */ [kAdapter] constructor (adapter) { super(adapter); this[kAdapter] = adapter; } /** * @param {Object} [options={}] * @param {boolean} [options.keepExistingData] * @returns {Promise} */ async createWritable (options = {}) { return new FileSystemWritableFileStream( await this[kAdapter].createWritable(options) ) } /** * @returns {Promise} */ async getFile () { return this[kAdapter].getFile() } } Object.defineProperty(FileSystemFileHandle.prototype, Symbol.toStringTag, { value: 'FileSystemFileHandle', writable: false, enumerable: false, configurable: true }); Object.defineProperties(FileSystemFileHandle.prototype, { createWritable: { enumerable: true }, getFile: { enumerable: true } }); // Safari doesn't support async createWritable streams yet. if ( globalThis.FileSystemFileHandle && !globalThis.FileSystemFileHandle.prototype.createWritable ) { const wm = new WeakMap(); let workerUrl; // Worker code that should be inlined (can't use any external functions) const code = () => { let fileHandle, handle; onmessage = async evt => { const port = evt.ports[0]; const cmd = evt.data; switch (cmd.type) { case 'open': const file = cmd.name; let dir = await navigator.storage.getDirectory(); for (const folder of cmd.path) { dir = await dir.getDirectoryHandle(folder); } fileHandle = await dir.getFileHandle(file); handle = await fileHandle.createSyncAccessHandle(); break case 'write': handle.write(cmd.data, { at: cmd.position }); handle.flush(); break case 'truncate': handle.truncate(cmd.size); break case 'abort': case 'close': handle.close(); break } port.postMessage(0); }; }; globalThis.FileSystemFileHandle.prototype.createWritable = async function (options) { // Safari only support writing data in a worker with sync access handle. if (!workerUrl) { const stringCode = `(${code.toString()})()`; const blob = new Blob([stringCode], { type: 'text/javascript' }); workerUrl = URL.createObjectURL(blob); } const worker = new Worker(workerUrl, { type: 'module' }); let position = 0; const textEncoder = new TextEncoder(); let size = await this.getFile().then(file => file.size); const send = message => new Promise((resolve, reject) => { const mc = new MessageChannel(); mc.port1.onmessage = evt => { if (evt.data instanceof Error) reject(evt.data); else resolve(evt.data); mc.port1.close(); mc.port2.close(); mc.port1.onmessage = null; }; worker.postMessage(message, [mc.port2]); }); // Safari also don't support transferable file system handles. // So we need to pass the path to the worker. This is a bit hacky and ugly. const root = await navigator.storage.getDirectory(); const parent = await wm.get(this); const path = await root.resolve(parent); // Should likely never happen, but just in case... if (path === null) throw new DOMException(...GONE) await send({ type: 'open', path, name: this.name }); if (options?.keepExistingData === false) { await send({ type: 'truncate', size: 0 }); size = 0; } const ws = new FileSystemWritableFileStream({ start: ctrl => { }, async write(chunk) { const isPlainObject = chunk?.constructor === Object; if (isPlainObject) { chunk = { ...chunk }; } else { chunk = { type: 'write', data: chunk, position }; } if (chunk.type === 'write') { if (!('data' in chunk)) { await send({ type: 'close' }); throw new DOMException(...SYNTAX('write requires a data argument')) } chunk.position ??= position; if (typeof chunk.data === 'string') { chunk.data = textEncoder.encode(chunk.data); } else if (chunk.data instanceof ArrayBuffer) { chunk.data = new Uint8Array(chunk.data); } else if (!(chunk.data instanceof Uint8Array) && ArrayBuffer.isView(chunk.data)) { chunk.data = new Uint8Array(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength); } else if (!(chunk.data instanceof Uint8Array)) { const ab = await new Response(chunk.data).arrayBuffer(); chunk.data = new Uint8Array(ab); } if (Number.isInteger(chunk.position) && chunk.position >= 0) { position = chunk.position; } position += chunk.data.byteLength; size += chunk.data.byteLength; } else if (chunk.type === 'seek') { if (Number.isInteger(chunk.position) && chunk.position >= 0) { if (size < chunk.position) { throw new DOMException(...INVALID) } console.log('seeking', chunk); position = chunk.position; return // Don't need to enqueue seek... } else { await send({ type: 'close' }); throw new DOMException(...SYNTAX('seek requires a position argument')) } } else if (chunk.type === 'truncate') { if (Number.isInteger(chunk.size) && chunk.size >= 0) { size = chunk.size; if (position > size) { position = size; } } else { await send({ type: 'close' }); throw new DOMException(...SYNTAX('truncate requires a size argument')) } } await send(chunk); }, async close () { await send({ type: 'close' }); worker.terminate(); }, async abort (reason) { await send({ type: 'abort', reason }); worker.terminate(); }, }); return ws }; const orig = FileSystemDirectoryHandle.prototype.getFileHandle; FileSystemDirectoryHandle.prototype.getFileHandle = async function (...args) { const handle = await orig.call(this, ...args); wm.set(handle, this); return handle }; } export { FileSystemFileHandle, FileSystemFileHandle as default }; ================================================ FILE: public/apps/nfsadapter/FileSystemHandle.js ================================================ const kAdapter = Symbol('adapter'); /** * @typedef {Object} FileSystemHandlePermissionDescriptor * @property {('read'|'readwrite')} [mode='read'] */ class FileSystemHandle { /** @type {FileSystemHandle} */ [kAdapter] /** @type {string} */ name /** @type {('file'|'directory')} */ kind /** @param {FileSystemHandle & {writable}} adapter */ constructor (adapter) { this.kind = adapter.kind; this.name = adapter.name; this[kAdapter] = adapter; } /** @param {FileSystemHandlePermissionDescriptor} descriptor */ async queryPermission (descriptor = {}) { const { mode = 'read' } = descriptor; const handle = this[kAdapter]; if (handle.queryPermission) { return handle.queryPermission({mode}) } if (mode === 'read') { return 'granted' } else if (mode === 'readwrite') { return handle.writable ? 'granted' : 'denied' } else { throw new TypeError(`Mode ${mode} must be 'read' or 'readwrite'`) } } async requestPermission ({mode = 'read'} = {}) { const handle = this[kAdapter]; if (handle.requestPermission) { return handle.requestPermission({mode}) } if (mode === 'read') { return 'granted' } else if (mode === 'readwrite') { return handle.writable ? 'granted' : 'denied' } else { throw new TypeError(`Mode ${mode} must be 'read' or 'readwrite'`) } } /** * Attempts to remove the entry represented by handle from the underlying file system. * * @param {object} options * @param {boolean} [options.recursive=false] */ async remove (options = {}) { await this[kAdapter].remove(options); } /** * @param {FileSystemHandle} other */ async isSameEntry (other) { if (this === other) return true if ( (!other) || (typeof other !== 'object') || (this.kind !== other.kind) || (!other[kAdapter]) ) return false return this[kAdapter].isSameEntry(other[kAdapter]) } } Object.defineProperty(FileSystemHandle.prototype, Symbol.toStringTag, { value: 'FileSystemHandle', writable: false, enumerable: false, configurable: true }); // Safari safari doesn't support writable streams yet. if (globalThis.FileSystemHandle) { globalThis.FileSystemHandle.prototype.queryPermission ??= function (descriptor) { return 'granted' }; } export { FileSystemHandle, FileSystemHandle as default }; ================================================ FILE: public/apps/nfsadapter/adapters/anuraadapter.js ================================================ import { errors } from '../util.js' import config from '../config.js' const join = window.Filer.path.join; const fs = window.anura.fs.promises; const Buffer = window.Filer.Buffer; const cbfs = window.anura.fs; // This stands for callback fs but I like to pretend it stands for cock and ball fs. const { DOMException } = config const { INVALID, GONE, MISMATCH, MOD_ERR, SYNTAX } = errors /** * @see https://github.com/node-fetch/fetch-blob/blob/0455796ede330ecffd9eb6b9fdf206cc15f90f3e/index.js#L232 * @param {*} object * @returns {object is Blob} */ function isBlob (object) { return ( object && typeof object === 'object' && typeof object.constructor === 'function' && ( typeof object.stream === 'function' || typeof object.arrayBuffer === 'function' ) && /^(Blob|File)$/.test(object[Symbol.toStringTag]) ) } export class Sink { /** * @param {fs.FileHandle} fileHandle * @param {number} size */ constructor (fileHandle, size) { this._fileHandle = fileHandle this._size = size this._position = 0 } async abort() { const filehandle = this._fileHandle await new Promise((res, rej) => { cbfs.close(filehandle, (err) => { if (!err) res() else rej() }) }) } async write (chunk) { if (typeof chunk === 'object') { if (chunk.type === 'write') { if (Number.isInteger(chunk.position) && chunk.position >= 0) { this._position = chunk.position } if (!('data' in chunk)) { const filehandle = this._fileHandle await new Promise((res, rej) => { cbfs.close(filehandle, (err) => { if (!err) res() else rej() }) }) throw new DOMException(...SYNTAX('write requires a data argument')) } chunk = chunk.data } else if (chunk.type === 'seek') { if (Number.isInteger(chunk.position) && chunk.position >= 0) { if (this._size < chunk.position) { throw new DOMException(...INVALID) } this._position = chunk.position return } else { const filehandle = this._fileHandle await new Promise((res, rej) => { cbfs.close(filehandle, (err) => { if (!err) res() else rej() }) }) throw new DOMException(...SYNTAX('seek requires a position argument')) } } else if (chunk.type === 'truncate') { if (Number.isInteger(chunk.size) && chunk.size >= 0) { console.log("handle:") console.log(this._fileHandle) const filehandle = this._fileHandle await new Promise((res, rej) => { cbfs.ftruncate(filehandle, chunk.size, (err) => { if (!err) res() else rej() }) }) this._size = chunk.size if (this._position > this._size) { this._position = this._size } return } else { const filehandle = this._fileHandle await new Promise((res, rej) => { cbfs.close(filehandle, (err) => { if (!err) res() else rej() }) }) throw new DOMException(...SYNTAX('truncate requires a size argument')) } } } if (chunk instanceof ArrayBuffer) { chunk = new Uint8Array(chunk) } else if (typeof chunk === 'string') { chunk = Buffer.from(chunk) } else if (isBlob(chunk)) { for await (const data of chunk.stream()) { // const res = await this._fileHandle.writev([data], this._position) const res = await new Promise((res, rej) => { cbfs.write(this._fileHandle, Filer.Buffer.from(data), 0, data.length, this._position, (err, nbytes) => { if (err) rej(err) else res(nbytes); })}) this._position += res.bytesWritten this._size += res.bytesWritten } return } const res = await new Promise((res, rej) => { cbfs.write(this._fileHandle, Filer.Buffer.from(chunk), 0, chunk.length, this._position, (err, nbytes) => { if (err) rej(err) else res(nbytes); })}) // const res = await this._fileHandle.writev([chunk], this._position) this._position += res.bytesWritten this._size += res.bytesWritten } async close () { // First make sure we close the handle const filehandle = this._fileHandle await new Promise((res, rej) => { cbfs.close(filehandle, (err) => { if (!err) res() else rej() }) }) } } export class FileHandle { /** * @param {string} path * @param {string} name */ constructor (path, name) { this._path = path this.name = name this.kind = 'file' } async getFile () { await fs.stat(this._path).catch(err => { if (err.code === 'ENOENT') throw new DOMException(...GONE) }) // TODO: replace once https://github.com/nodejs/node/issues/37340 is fixed return new File([await fs.readFile(this._path)], this.name); } async isSameEntry (other) { return this._path === this._getPath.apply(other) } _getPath() { return this._path } /** @param {{ keepExistingData: boolean; }} opts */ async createWritable (opts) { const fileHandle = await fs.open(this._path, opts.keepExistingData ? 'r+' : 'w+').catch(err => { if (err.code === 'ENOENT') throw new DOMException(...GONE) throw err }) const { size } = await fs.stat(this._path); return new Sink(fileHandle, size) } } export class FolderHandle { _path = '' constructor (path = '', name = '') { this.name = name this.kind = 'directory' this._path = path } /** @param {FolderHandle} other */ async isSameEntry (other) { return this._path === other._path } /** @returns {AsyncGenerator<[string, FileHandle | FolderHandle]>} */ async * entries () { const dir = this._path const items = await fs.readdir(dir).catch(err => { if (err.code === 'ENOENT') throw new DOMException(...GONE) throw err }) for (let name of items) { const path = Filer.path.join(dir, name) const stat = await fs.lstat(path) if (stat.isFile()) { yield [name, new FileHandle(path, name)] } else if (stat.isDirectory()) { yield [name, new FolderHandle(path, name)] } } } /** * @param {string} name * @param {{ create: boolean; }} opts */ async getDirectoryHandle (name, opts) { const path = join(this._path, name) const stat = await fs.lstat(path).catch(err => { if (err.code !== 'ENOENT') throw err }) const isDirectory = stat?.isDirectory() if (stat && isDirectory) return new FolderHandle(path, name) if (stat && !isDirectory) throw new DOMException(...MISMATCH) if (!opts.create) throw new DOMException(...GONE) await fs.mkdir(path) return new FolderHandle(path, name) } /** * @param {string} name * @param {{ create: boolean; }} opts */ async getFileHandle (name, opts) { const path = join(this._path, name) const stat = await fs.lstat(path).catch(err => { if (err.code !== 'ENOENT') throw err }) const isFile = stat?.isFile() if (stat && isFile) return new FileHandle(path, name) if (stat && !isFile) throw new DOMException(...MISMATCH) if (!opts.create) throw new DOMException(...GONE) anura.fs.close(await fs.open(path, 'w')) return new FileHandle(path, name) } async queryPermission () { return 'granted' } /** * @param {string} name * @param {{ recursive: boolean; }} opts */ async removeEntry (name, opts) { const path = join(this._path, name) const stat = await fs.lstat(path).catch(err => { if (err.code === 'ENOENT') throw new DOMException(...GONE) throw err }) if (stat.isDirectory()) { if (opts.recursive) { await fs.rm(path, { recursive: true, }).catch(err => { if (err.code === 'ENOTEMPTY') throw new DOMException(...MOD_ERR) throw err }) } else { await fs.rmdir(path).catch(err => { if (err.code === 'ENOTEMPTY') throw new DOMException(...MOD_ERR) throw err }) } } else { await fs.unlink(path) } } } export default path => { return new FolderHandle("/") } ================================================ FILE: public/apps/nfsadapter/adapters/memory.js ================================================ const errors = { INVALID: ['seeking position failed.', 'InvalidStateError'], GONE: ['A requested file or directory could not be found at the time an operation was processed.', 'NotFoundError'], MISMATCH: ['The path supplied exists, but was not an entry of requested type.', 'TypeMismatchError'], MOD_ERR: ['The object can not be modified in this way.', 'InvalidModificationError'], SYNTAX: m => [`Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. ${m}`, 'SyntaxError'], SECURITY: ['It was determined that certain files are unsafe for access within a Web application, or that too many calls are being made on file resources.', 'SecurityError'], DISALLOWED: ['The request is not allowed by the user agent or the platform in the current context.', 'NotAllowedError'] }; const config = { ReadableStream: globalThis.ReadableStream, WritableStream: globalThis.WritableStream, TransformStream: globalThis.TransformStream, DOMException: globalThis.DOMException, Blob: globalThis.Blob, File: globalThis.File, }; const { File, Blob, DOMException } = config; const { INVALID, GONE, MISMATCH, MOD_ERR, SYNTAX, SECURITY, DISALLOWED } = errors; class Sink { /** * @param {FileHandle} fileHandle * @param {File} file */ constructor (fileHandle, file) { this.fileHandle = fileHandle; this.file = file; this.size = file.size; this.position = 0; } write (chunk) { let file = this.file; if (typeof chunk === 'object') { if (chunk.type === 'write') { if (Number.isInteger(chunk.position) && chunk.position >= 0) { this.position = chunk.position; if (this.size < chunk.position) { this.file = new File( [this.file, new ArrayBuffer(chunk.position - this.size)], this.file.name, this.file ); } } if (!('data' in chunk)) { throw new DOMException(...SYNTAX('write requires a data argument')) } chunk = chunk.data; } else if (chunk.type === 'seek') { if (Number.isInteger(chunk.position) && chunk.position >= 0) { if (this.size < chunk.position) { throw new DOMException(...INVALID) } this.position = chunk.position; return } else { throw new DOMException(...SYNTAX('seek requires a position argument')) } } else if (chunk.type === 'truncate') { if (Number.isInteger(chunk.size) && chunk.size >= 0) { file = chunk.size < this.size ? new File([file.slice(0, chunk.size)], file.name, file) : new File([file, new Uint8Array(chunk.size - this.size)], file.name); this.size = file.size; if (this.position > file.size) { this.position = file.size; } this.file = file; return } else { throw new DOMException(...SYNTAX('truncate requires a size argument')) } } } chunk = new Blob([chunk]); let blob = this.file; // Calc the head and tail fragments const head = blob.slice(0, this.position); const tail = blob.slice(this.position + chunk.size); // Calc the padding let padding = this.position - head.size; if (padding < 0) { padding = 0; } blob = new File([ head, new Uint8Array(padding), chunk, tail ], blob.name); this.size = blob.size; this.position += chunk.size; this.file = blob; } close () { if (this.fileHandle._deleted) throw new DOMException(...GONE) this.fileHandle._file = this.file; this.file = this.position = this.size = null; if (this.fileHandle.onclose) { this.fileHandle.onclose(this.fileHandle); } } } class FileHandle { constructor (name = '', file = new File([], name), writable = true) { this._file = file; this.name = name; this.kind = 'file'; this._deleted = false; this.writable = writable; this.readable = true; } async getFile () { if (this._deleted) throw new DOMException(...GONE) return this._file } async createWritable (opts) { if (!this.writable) throw new DOMException(...DISALLOWED) if (this._deleted) throw new DOMException(...GONE) const file = opts.keepExistingData ? await this.getFile() : new File([], this.name); return new Sink(this, file) } async isSameEntry (other) { return this === other } async _destroy () { this._deleted = true; this._file = null; } } class FolderHandle { /** @param {string} name */ constructor (name, writable = true) { this.name = name; this.kind = 'directory'; this._deleted = false; /** @type {Object.} */ this._entries = {}; this.writable = writable; this.readable = true; } /** @returns {AsyncGenerator<[string, FileHandle | FolderHandle]>} */ async * entries () { if (this._deleted) throw new DOMException(...GONE) yield* Object.entries(this._entries); } async isSameEntry (other) { return this === other } /** * @param {string} name * @param {{ create: boolean; }} opts */ async getDirectoryHandle (name, opts) { if (this._deleted) throw new DOMException(...GONE) const entry = this._entries[name]; if (entry) { // entry exist if (entry instanceof FileHandle) { throw new DOMException(...MISMATCH) } else { return entry } } else { if (opts.create) { return (this._entries[name] = new FolderHandle(name)) } else { throw new DOMException(...GONE) } } } /** * @param {string} name * @param {{ create: boolean; }} opts */ async getFileHandle (name, opts) { const entry = this._entries[name]; const isFile = entry instanceof FileHandle; if (entry && isFile) return entry if (entry && !isFile) throw new DOMException(...MISMATCH) if (!entry && !opts.create) throw new DOMException(...GONE) if (!entry && opts.create) { return (this._entries[name] = new FileHandle(name)) } } async removeEntry (name, opts) { const entry = this._entries[name]; if (!entry) throw new DOMException(...GONE) await entry._destroy(opts.recursive); delete this._entries[name]; } async _destroy (recursive) { for (let x of Object.values(this._entries)) { if (!recursive) throw new DOMException(...MOD_ERR) await x._destroy(recursive); } this._entries = {}; this._deleted = true; } } const fs = new FolderHandle(''); var memory = () => fs; export { FileHandle, FolderHandle, Sink, memory as default }; ================================================ FILE: public/apps/nfsadapter/adapters/sandbox.js ================================================ const errors = { INVALID: ['seeking position failed.', 'InvalidStateError'], GONE: ['A requested file or directory could not be found at the time an operation was processed.', 'NotFoundError'], MISMATCH: ['The path supplied exists, but was not an entry of requested type.', 'TypeMismatchError'], MOD_ERR: ['The object can not be modified in this way.', 'InvalidModificationError'], SYNTAX: m => [`Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. ${m}`, 'SyntaxError'], SECURITY: ['It was determined that certain files are unsafe for access within a Web application, or that too many calls are being made on file resources.', 'SecurityError'], DISALLOWED: ['The request is not allowed by the user agent or the platform in the current context.', 'NotAllowedError'] }; /* global Blob, DOMException */ const { DISALLOWED } = errors; class Sink { /** * @param {FileWriter} writer * @param {FileEntry} fileEntry */ constructor (writer, fileEntry) { this.writer = writer; this.fileEntry = fileEntry; } /** * @param {BlobPart | Object} chunk */ async write (chunk) { if (typeof chunk === 'object') { if (chunk.type === 'write') { if (Number.isInteger(chunk.position) && chunk.position >= 0) { this.writer.seek(chunk.position); if (this.writer.position !== chunk.position) { await new Promise((resolve, reject) => { this.writer.onwriteend = resolve; this.writer.onerror = reject; this.writer.truncate(chunk.position); }); this.writer.seek(chunk.position); } } if (!('data' in chunk)) { throw new DOMException('Failed to execute \'write\' on \'UnderlyingSinkBase\': Invalid params passed. write requires a data argument', 'SyntaxError') } chunk = chunk.data; } else if (chunk.type === 'seek') { if (Number.isInteger(chunk.position) && chunk.position >= 0) { this.writer.seek(chunk.position); if (this.writer.position !== chunk.position) { throw new DOMException('seeking position failed', 'InvalidStateError') } return } else { throw new DOMException('Failed to execute \'write\' on \'UnderlyingSinkBase\': Invalid params passed. seek requires a position argument', 'SyntaxError') } } else if (chunk.type === 'truncate') { return new Promise(resolve => { if (Number.isInteger(chunk.size) && chunk.size >= 0) { this.writer.onwriteend = evt => resolve(); this.writer.truncate(chunk.size); } else { throw new DOMException('Failed to execute \'write\' on \'UnderlyingSinkBase\': Invalid params passed. truncate requires a size argument', 'SyntaxError') } }) } } await new Promise((resolve, reject) => { this.writer.onwriteend = resolve; this.writer.onerror = reject; this.writer.write(new Blob([chunk])); }); } close () { return new Promise(this.fileEntry.file.bind(this.fileEntry)) } } class FileHandle { /** @param {FileEntry} file */ constructor (file, writable = true) { this.file = file; this.kind = 'file'; this.writable = writable; this.readable = true; } get name () { return this.file.name } /** * @param {{ file: { toURL: () => string; }; }} other */ isSameEntry (other) { return this.file.toURL() === other.file.toURL() } /** @return {Promise} */ getFile () { return new Promise(this.file.file.bind(this.file)) } /** @return {Promise} */ createWritable (opts) { if (!this.writable) throw new DOMException(...DISALLOWED) return new Promise((resolve, reject) => this.file.createWriter(fileWriter => { if (opts.keepExistingData === false) { fileWriter.onwriteend = evt => resolve(new Sink(fileWriter, this.file)); fileWriter.truncate(0); } else { resolve(new Sink(fileWriter, this.file)); } }, reject) ) } } class FolderHandle { /** @param {DirectoryEntry} dir */ constructor (dir, writable = true) { this.dir = dir; this.writable = writable; this.readable = true; this.kind = 'directory'; this.name = dir.name; } /** @param {FolderHandle} other */ isSameEntry (other) { return this.dir.fullPath === other.dir.fullPath } /** @returns {AsyncGenerator<[string, FileHandle | FolderHandle]>} */ async * entries () { const reader = this.dir.createReader(); const entries = await new Promise(reader.readEntries.bind(reader)); for (const x of entries) { yield [x.name, x.isFile ? new FileHandle(x, this.writable) : new FolderHandle(x, this.writable)]; } } /** * @param {string} name * @param {{ create: boolean; }} opts * @returns {Promise} */ getDirectoryHandle (name, opts) { return new Promise((resolve, reject) => { this.dir.getDirectory(name, opts, dir => { resolve(new FolderHandle(dir)); }, reject); }) } /** * @param {string} name * @param {{ create: boolean; }} opts * @returns {Promise} */ getFileHandle (name, opts) { return new Promise((resolve, reject) => this.dir.getFile(name, opts, file => resolve(new FileHandle(file)), reject) ) } /** * @param {string} name * @param {{ recursive: boolean; }} opts */ async removeEntry (name, opts) { /** @type {Error|FolderHandle|FileHandle} */ const entry = await this.getDirectoryHandle(name, { create: false }).catch(err => err.name === 'TypeMismatchError' ? this.getFileHandle(name, { create: false }) : err ); if (entry instanceof Error) throw entry return new Promise((resolve, reject) => { if (entry instanceof FolderHandle) { opts.recursive ? entry.dir.removeRecursively(() => resolve(), reject) : entry.dir.remove(() => resolve(), reject); } else if (entry.file) { entry.file.remove(() => resolve(), reject); } }) } } var sandbox = (opts = {}) => new Promise((resolve, reject) => window.webkitRequestFileSystem( opts._persistent, 0, e => resolve(new FolderHandle(e.root)), reject ) ); export { FileHandle, FolderHandle, sandbox as default }; ================================================ FILE: public/apps/nfsadapter/config.js ================================================ const config = { ReadableStream: globalThis.ReadableStream, WritableStream: globalThis.WritableStream, TransformStream: globalThis.TransformStream, DOMException: globalThis.DOMException, Blob: globalThis.Blob, File: globalThis.File, } export default config ================================================ FILE: public/apps/nfsadapter/nfsadapter.js ================================================ const e=globalThis.showDirectoryPicker;async function t(t={}){if(e&&!t._preferPolyfill)return e(t);const i=document.createElement("input");i.type="file",i.webkitdirectory=!0,i.multiple=!0,i.style.position="fixed",i.style.top="-100000px",i.style.left="-100000px",document.body.appendChild(i);const r=Promise.resolve().then((function(){return p}));return await new Promise((e=>{i.addEventListener("change",e),i.click()})),r.then((e=>e.getDirHandlesFromInput(i)))}const i={accepts:[]},r=globalThis.showOpenFilePicker;async function n(e={}){const t={...i,...e};if(r&&!e._preferPolyfill)return r(t);const n=document.createElement("input");n.type="file",n.multiple=t.multiple,n.accept=(t.accepts||[]).map((e=>[...(e.extensions||[]).map((e=>"."+e)),...e.mimeTypes||[]])).flat().join(","),Object.assign(n.style,{position:"fixed",top:"-100000px",left:"-100000px"}),document.body.appendChild(n);const s=Promise.resolve().then((function(){return p}));return await new Promise((e=>{n.addEventListener("change",e,{once:!0}),n.click()})),n.remove(),s.then((e=>e.getFileHandlesFromInput(n)))}const s=globalThis.showSaveFilePicker;async function a(e={}){if(s&&!e._preferPolyfill)return s(e);e._name&&(console.warn("deprecated _name, spec now have `suggestedName`"),e.suggestedName=e._name);const{FileSystemFileHandle:t}=await Promise.resolve().then((function(){return P})),{FileHandle:i}=await Promise.resolve().then((function(){return R}));return new t(new i(e.suggestedName))}async function o(e,t={}){if(!e)return globalThis.navigator?.storage?.getDirectory()||globalThis.getOriginPrivateDirectory();const{FileSystemDirectoryHandle:i}=await Promise.resolve().then((function(){return F})),r=await e;return new i(await(r.default?r.default(t):r(t)))}globalThis.DataTransferItem&&!DataTransferItem.prototype.getAsFileSystemHandle&&(DataTransferItem.prototype.getAsFileSystemHandle=async function(){const e=this.webkitGetAsEntry(),[{FileHandle:t,FolderHandle:i},{FileSystemDirectoryHandle:r},{FileSystemFileHandle:n}]=await Promise.all([Promise.resolve().then((function(){return L})),Promise.resolve().then((function(){return F})),Promise.resolve().then((function(){return P}))]);return e.isFile?new n(new t(e,!1)):new r(new i(e,!1))});const l={ReadableStream:globalThis.ReadableStream,WritableStream:globalThis.WritableStream,TransformStream:globalThis.TransformStream,DOMException:globalThis.DOMException,Blob:globalThis.Blob,File:globalThis.File},{WritableStream:c}=l;class d extends c{#e;constructor(e){super(e),this.#e=e,Object.setPrototypeOf(this,d.prototype),this._closed=!1}async close(){this._closed=!0;const e=this.getWriter(),t=e.close();return e.releaseLock(),t}seek(e){return this.write({type:"seek",position:e})}truncate(e){return this.write({type:"truncate",size:e})}write(e){if(this._closed)return Promise.reject(new TypeError("Cannot write to a CLOSED writable stream"));const t=this.getWriter(),i=t.write(e);return t.releaseLock(),i}}Object.defineProperty(d.prototype,Symbol.toStringTag,{value:"FileSystemWritableFileStream",writable:!1,enumerable:!1,configurable:!0}),Object.defineProperties(d.prototype,{close:{enumerable:!0},seek:{enumerable:!0},truncate:{enumerable:!0},write:{enumerable:!0}}),!globalThis.FileSystemFileHandle||globalThis.FileSystemFileHandle.prototype.createWritable||globalThis.FileSystemWritableFileStream||(globalThis.FileSystemWritableFileStream=d);const h=Symbol("adapter");class w{[h];name;kind;constructor(e){this.kind=e.kind,this.name=e.name,this[h]=e}async queryPermission(e={}){const{mode:t="read"}=e,i=this[h];if(i.queryPermission)return i.queryPermission({mode:t});if("read"===t)return"granted";if("readwrite"===t)return i.writable?"granted":"denied";throw new TypeError(`Mode ${t} must be 'read' or 'readwrite'`)}async requestPermission({mode:e="read"}={}){const t=this[h];if(t.requestPermission)return t.requestPermission({mode:e});if("read"===e)return"granted";if("readwrite"===e)return t.writable?"granted":"denied";throw new TypeError(`Mode ${e} must be 'read' or 'readwrite'`)}async remove(e={}){await this[h].remove(e)}async isSameEntry(e){return this===e||!(!e||"object"!=typeof e||this.kind!==e.kind||!e[h])&&this[h].isSameEntry(e[h])}}Object.defineProperty(w.prototype,Symbol.toStringTag,{value:"FileSystemHandle",writable:!1,enumerable:!1,configurable:!0}),globalThis.FileSystemHandle&&(globalThis.FileSystemHandle.prototype.queryPermission??=function(e){return"granted"});const u={INVALID:["seeking position failed.","InvalidStateError"],GONE:["A requested file or directory could not be found at the time an operation was processed.","NotFoundError"],MISMATCH:["The path supplied exists, but was not an entry of requested type.","TypeMismatchError"],MOD_ERR:["The object can not be modified in this way.","InvalidModificationError"],SYNTAX:e=>[`Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. ${e}`,"SyntaxError"],SECURITY:["It was determined that certain files are unsafe for access within a Web application, or that too many calls are being made on file resources.","SecurityError"],DISALLOWED:["The request is not allowed by the user agent or the platform in the current context.","NotAllowedError"]},y={writable:globalThis.WritableStream};var p=Object.freeze({__proto__:null,errors:u,config:y,fromDataTransfer:async function(e){console.warn("deprecated fromDataTransfer - use `dt.items[0].getAsFileSystemHandle()` instead");const[t,i,r]=await Promise.all([Promise.resolve().then((function(){return te})),Promise.resolve().then((function(){return L})),Promise.resolve().then((function(){return F}))]),n=new t.FolderHandle("",!1);return n._entries=e.map((e=>e.isFile?new i.FileHandle(e,!1):new i.FolderHandle(e,!1))),new r.FileSystemDirectoryHandle(n)},getDirHandlesFromInput:async function(e){const{FolderHandle:t,FileHandle:i}=await Promise.resolve().then((function(){return te})),{FileSystemDirectoryHandle:r}=await Promise.resolve().then((function(){return F})),n=Array.from(e.files),s=n[0].webkitRelativePath.split("/",1)[0],a=new t(s,!1);return n.forEach((e=>{const r=e.webkitRelativePath.split("/");r.shift();const n=r.pop();r.reduce(((e,i)=>(e._entries[i]||(e._entries[i]=new t(i,!1)),e._entries[i])),a)._entries[n]=new i(e.name,e,!1)})),new r(a)},getFileHandlesFromInput:async function(e){const{FileHandle:t}=await Promise.resolve().then((function(){return te})),{FileSystemFileHandle:i}=await Promise.resolve().then((function(){return P}));return Array.from(e.files).map((e=>new i(new t(e.name,e,!1))))}});const{GONE:m,MOD_ERR:f}=u,b=Symbol("adapter");class g extends w{[b];constructor(e){super(e),this[b]=e}async getDirectoryHandle(e,t={}){if(""===e)throw new TypeError("Name can't be an empty string.");if("."===e||".."===e||e.includes("/"))throw new TypeError("Name contains invalid characters.");t.create=!!t.create;const i=await this[b].getDirectoryHandle(e,t);return new g(i)}async*entries(){const{FileSystemFileHandle:e}=await Promise.resolve().then((function(){return P}));for await(const[t,i]of this[b].entries())yield[i.name,"file"===i.kind?new e(i):new g(i)]}async*getEntries(){const{FileSystemFileHandle:e}=await Promise.resolve().then((function(){return P}));console.warn("deprecated, use .entries() instead");for await(let t of this[b].entries())yield"file"===t.kind?new e(t):new g(t)}async getFileHandle(e,t={}){const{FileSystemFileHandle:i}=await Promise.resolve().then((function(){return P}));if(""===e)throw new TypeError("Name can't be an empty string.");if("."===e||".."===e||e.includes("/"))throw new TypeError("Name contains invalid characters.");t.create=!!t.create;return new i(await this[b].getFileHandle(e,t))}async removeEntry(e,t={}){if(""===e)throw new TypeError("Name can't be an empty string.");if("."===e||".."===e||e.includes("/"))throw new TypeError("Name contains invalid characters.");return t.recursive=!!t.recursive,this[b].removeEntry(e,t)}async resolve(e){if(await e.isSameEntry(this))return[];const t=[{handle:this,path:[]}];for(;t.length;){let{handle:i,path:r}=t.pop();for await(const n of i.values()){if(await n.isSameEntry(e))return[...r,n.name];"directory"===n.kind&&t.push({handle:n,path:[...r,n.name]})}}return null}async*keys(){for await(const[e]of this[b].entries())yield e}async*values(){for await(const[e,t]of this)yield t}[Symbol.asyncIterator](){return this.entries()}}if(Object.defineProperty(g.prototype,Symbol.toStringTag,{value:"FileSystemDirectoryHandle",writable:!1,enumerable:!1,configurable:!0}),Object.defineProperties(g.prototype,{getDirectoryHandle:{enumerable:!0},entries:{enumerable:!0},getFileHandle:{enumerable:!0},removeEntry:{enumerable:!0}}),globalThis.FileSystemDirectoryHandle){const e=globalThis.FileSystemDirectoryHandle.prototype;e.resolve=async function(e){if(await e.isSameEntry(this))return[];const t=[{handle:this,path:[]}];for(;t.length;){let{handle:i,path:r}=t.pop();for await(const n of i.values()){if(await n.isSameEntry(e))return[...r,n.name];"directory"===n.kind&&t.push({handle:n,path:[...r,n.name]})}}return null};const t=e.entries;e.entries=async function*(){await async function(e){const t=await navigator.storage.getDirectory();if(null===await t.resolve(e))throw new DOMException(...m)}(this),yield*t.call(this)},e[Symbol.asyncIterator]=async function*(){yield*this.entries()};const i=e.removeEntry;e.removeEntry=async function(e,r={}){return i.call(this,e,r).catch((async e=>{if(e instanceof DOMException&&"UnknownError"===e.name&&!r.recursive){if(!(await t.call(this).next()).done)throw new DOMException(...f)}throw e}))}}var F=Object.freeze({__proto__:null,default:g,FileSystemDirectoryHandle:g});const{INVALID:_,SYNTAX:S,GONE:E}=u,v=Symbol("adapter");class H extends w{[v];constructor(e){super(e),this[v]=e}async createWritable(e={}){return new d(await this[v].createWritable(e))}async getFile(){return this[v].getFile()}}if(Object.defineProperty(H.prototype,Symbol.toStringTag,{value:"FileSystemFileHandle",writable:!1,enumerable:!1,configurable:!0}),Object.defineProperties(H.prototype,{createWritable:{enumerable:!0},getFile:{enumerable:!0}}),globalThis.FileSystemFileHandle&&!globalThis.FileSystemFileHandle.prototype.createWritable){const e=new WeakMap;let t;const i=()=>{let e,t;onmessage=async i=>{const r=i.ports[0],n=i.data;switch(n.type){case"open":const i=n.name;let r=await navigator.storage.getDirectory();for(const e of n.path)r=await r.getDirectoryHandle(e);e=await r.getFileHandle(i),t=await e.createSyncAccessHandle();break;case"write":t.write(n.data,{at:n.position}),t.flush();break;case"truncate":t.truncate(n.size);break;case"abort":case"close":t.close()}r.postMessage(0)}};globalThis.FileSystemFileHandle.prototype.createWritable=async function(r){if(!t){const e=`(${i.toString()})()`,r=new Blob([e],{type:"text/javascript"});t=URL.createObjectURL(r)}const n=new Worker(t,{type:"module"});let s=0;const a=new TextEncoder;let o=await this.getFile().then((e=>e.size));const l=e=>new Promise(((t,i)=>{const r=new MessageChannel;r.port1.onmessage=e=>{e.data instanceof Error?i(e.data):t(e.data),r.port1.close(),r.port2.close(),r.port1.onmessage=null},n.postMessage(e,[r.port2])})),c=await navigator.storage.getDirectory(),h=await e.get(this),w=await c.resolve(h);if(null===w)throw new DOMException(...E);await l({type:"open",path:w,name:this.name}),!1===r?.keepExistingData&&(await l({type:"truncate",size:0}),o=0);return new d({start:e=>{},async write(e){if("write"===(e=e?.constructor===Object?{...e}:{type:"write",data:e,position:s}).type){if(!("data"in e))throw await l({type:"close"}),new DOMException(...S("write requires a data argument"));if(e.position??=s,"string"==typeof e.data)e.data=a.encode(e.data);else if(e.data instanceof ArrayBuffer)e.data=new Uint8Array(e.data);else if(e.data instanceof Uint8Array||!ArrayBuffer.isView(e.data)){if(!(e.data instanceof Uint8Array)){const t=await new Response(e.data).arrayBuffer();e.data=new Uint8Array(t)}}else e.data=new Uint8Array(e.data.buffer,e.data.byteOffset,e.data.byteLength);Number.isInteger(e.position)&&e.position>=0&&(s=e.position),s+=e.data.byteLength,o+=e.data.byteLength}else{if("seek"===e.type){if(Number.isInteger(e.position)&&e.position>=0){if(o=0))throw await l({type:"close"}),new DOMException(...S("truncate requires a size argument"));o=e.size,s>o&&(s=o)}}await l(e)},async close(){await l({type:"close"}),n.terminate()},async abort(e){await l({type:"abort",reason:e}),n.terminate()}})};const r=FileSystemDirectoryHandle.prototype.getFileHandle;FileSystemDirectoryHandle.prototype.getFileHandle=async function(...t){const i=await r.call(this,...t);return e.set(i,this),i}}var P=Object.freeze({__proto__:null,default:H,FileSystemFileHandle:H});const{WritableStream:T,TransformStream:D,DOMException:k,Blob:O}=l,{GONE:x}=u,z=/constructor/i.test(window.HTMLElement);class M{constructor(e){e.onmessage=e=>this._onMessage(e.data),this._port=e,this._resetReady()}start(e){return this._controller=e,this._readyPromise}write(e){const t={type:0,chunk:e};return this._port.postMessage(t,[e.buffer]),this._resetReady(),this._readyPromise}close(){this._port.postMessage({type:2}),this._port.close()}abort(e){this._port.postMessage({type:1,reason:e}),this._port.close()}_onMessage(e){0===e.type&&this._resolveReady(),1===e.type&&this._onError(e.reason)}_onError(e){this._controller.error(e),this._rejectReady(e),this._port.close()}_resetReady(){this._readyPromise=new Promise(((e,t)=>{this._readyResolve=e,this._readyReject=t})),this._readyPending=!0}_resolveReady(){this._readyResolve(),this._readyPending=!1}_rejectReady(e){this._readyPending||this._resetReady(),this._readyPromise.catch((()=>{})),this._readyReject(e),this._readyPending=!1}}class I{constructor(e){const t=new MessageChannel;this.readablePort=t.port1,this.writable=new e(new M(t.port2))}}var R=Object.freeze({__proto__:null,FileHandle:class{constructor(e="unkown"){this.name=e,this.kind="file"}async getFile(){throw new k(...x)}async isSameEntry(e){return this===e}async createWritable(e={}){const t=await(navigator.serviceWorker?.getRegistration()),i=document.createElement("a"),r=new D,n=r.writable;if(i.download=this.name,z||!t){let e=[];r.readable.pipeTo(new T({write(t){e.push(new O([t]))},close(){const t=new O(e,{type:"application/octet-stream; charset=utf-8"});e=[],i.href=URL.createObjectURL(t),i.click(),setTimeout((()=>URL.revokeObjectURL(i.href)),1e4)}}))}else{const{writable:i,readablePort:n}=new I(T),s=encodeURIComponent(this.name).replace(/['()]/g,escape).replace(/\*/g,"%2A"),a={"content-disposition":"attachment; filename*=UTF-8''"+s,"content-type":"application/octet-stream; charset=utf-8",...e.size?{"content-length":e.size}:{}},o=setTimeout((()=>t.active.postMessage(0)),1e4);r.readable.pipeThrough(new D({transform(e,t){if(e instanceof Uint8Array)return t.enqueue(e);const i=new Response(e).body.getReader(),r=e=>i.read().then((e=>e.done?0:r(t.enqueue(e.value))));return r()}})).pipeTo(i).finally((()=>{clearInterval(o)})),t.active.postMessage({url:t.scope+s,headers:a,readablePort:n},[n]);const l=document.createElement("iframe");l.hidden=!0,l.src=t.scope+s,document.body.appendChild(l)}return n.getWriter()}}});const{DISALLOWED:j}=u;class A{constructor(e,t){this.writer=e,this.fileEntry=t}async write(e){if("object"==typeof e)if("write"===e.type){if(Number.isInteger(e.position)&&e.position>=0&&(this.writer.seek(e.position),this.writer.position!==e.position&&(await new Promise(((t,i)=>{this.writer.onwriteend=t,this.writer.onerror=i,this.writer.truncate(e.position)})),this.writer.seek(e.position))),!("data"in e))throw new DOMException("Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. write requires a data argument","SyntaxError");e=e.data}else{if("seek"===e.type){if(Number.isInteger(e.position)&&e.position>=0){if(this.writer.seek(e.position),this.writer.position!==e.position)throw new DOMException("seeking position failed","InvalidStateError");return}throw new DOMException("Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. seek requires a position argument","SyntaxError")}if("truncate"===e.type)return new Promise((t=>{if(!(Number.isInteger(e.size)&&e.size>=0))throw new DOMException("Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. truncate requires a size argument","SyntaxError");this.writer.onwriteend=e=>t(),this.writer.truncate(e.size)}))}await new Promise(((t,i)=>{this.writer.onwriteend=t,this.writer.onerror=i,this.writer.write(new Blob([e]))}))}close(){return new Promise(this.fileEntry.file.bind(this.fileEntry))}}class N{constructor(e,t=!0){this.file=e,this.kind="file",this.writable=t,this.readable=!0}get name(){return this.file.name}isSameEntry(e){return this.file.toURL()===e.file.toURL()}getFile(){return new Promise(this.file.file.bind(this.file))}createWritable(e){if(!this.writable)throw new DOMException(...j);return new Promise(((t,i)=>this.file.createWriter((i=>{!1===e.keepExistingData?(i.onwriteend=e=>t(new A(i,this.file)),i.truncate(0)):t(new A(i,this.file))}),i)))}}class W{constructor(e,t=!0){this.dir=e,this.writable=t,this.readable=!0,this.kind="directory",this.name=e.name}isSameEntry(e){return this.dir.fullPath===e.dir.fullPath}async*entries(){const e=this.dir.createReader(),t=await new Promise(e.readEntries.bind(e));for(const e of t)yield[e.name,e.isFile?new N(e,this.writable):new W(e,this.writable)]}getDirectoryHandle(e,t){return new Promise(((i,r)=>{this.dir.getDirectory(e,t,(e=>{i(new W(e))}),r)}))}getFileHandle(e,t){return new Promise(((i,r)=>this.dir.getFile(e,t,(e=>i(new N(e))),r)))}async removeEntry(e,t){const i=await this.getDirectoryHandle(e,{create:!1}).catch((t=>"TypeMismatchError"===t.name?this.getFileHandle(e,{create:!1}):t));if(i instanceof Error)throw i;return new Promise(((e,r)=>{i instanceof W?t.recursive?i.dir.removeRecursively((()=>e()),r):i.dir.remove((()=>e()),r):i.file&&i.file.remove((()=>e()),r)}))}}var L=Object.freeze({__proto__:null,FileHandle:N,FolderHandle:W,default:(e={})=>new Promise(((t,i)=>window.webkitRequestFileSystem(e._persistent,0,(e=>t(new W(e.root))),i)))});const{File:U,Blob:q,DOMException:B}=l,{INVALID:C,GONE:G,MISMATCH:Y,MOD_ERR:V,SYNTAX:$,SECURITY:X,DISALLOWED:J}=u;class K{constructor(e,t){this.fileHandle=e,this.file=t,this.size=t.size,this.position=0}write(e){let t=this.file;if("object"==typeof e)if("write"===e.type){if(Number.isInteger(e.position)&&e.position>=0&&(this.position=e.position,this.size=0){if(this.size=0)return t=e.sizet.size&&(this.position=t.size),void(this.file=t);throw new B(...$("truncate requires a size argument"))}}e=new q([e]);let i=this.file;const r=i.slice(0,this.position),n=i.slice(this.position+e.size);let s=this.position-r.size;s<0&&(s=0),i=new U([r,new Uint8Array(s),e,n],i.name),this.size=i.size,this.position+=e.size,this.file=i}close(){if(this.fileHandle._deleted)throw new B(...G);this.fileHandle._file=this.file,this.file=this.position=this.size=null,this.fileHandle.onclose&&this.fileHandle.onclose(this.fileHandle)}}class Q{constructor(e="",t=new U([],e),i=!0){this._file=t,this.name=e,this.kind="file",this._deleted=!1,this.writable=i,this.readable=!0}async getFile(){if(this._deleted)throw new B(...G);return this._file}async createWritable(e){if(!this.writable)throw new B(...J);if(this._deleted)throw new B(...G);const t=e.keepExistingData?await this.getFile():new U([],this.name);return new K(this,t)}async isSameEntry(e){return this===e}async _destroy(){this._deleted=!0,this._file=null}}class Z{constructor(e,t=!0){this.name=e,this.kind="directory",this._deleted=!1,this._entries={},this.writable=t,this.readable=!0}async*entries(){if(this._deleted)throw new B(...G);yield*Object.entries(this._entries)}async isSameEntry(e){return this===e}async getDirectoryHandle(e,t){if(this._deleted)throw new B(...G);const i=this._entries[e];if(i){if(i instanceof Q)throw new B(...Y);return i}if(t.create)return this._entries[e]=new Z(e);throw new B(...G)}async getFileHandle(e,t){const i=this._entries[e],r=i instanceof Q;if(i&&r)return i;if(i&&!r)throw new B(...Y);if(!i&&!t.create)throw new B(...G);return!i&&t.create?this._entries[e]=new Q(e):void 0}async removeEntry(e,t){const i=this._entries[e];if(!i)throw new B(...G);await i._destroy(t.recursive),delete this._entries[e]}async _destroy(e){for(let t of Object.values(this._entries)){if(!e)throw new B(...V);await t._destroy(e)}this._entries={},this._deleted=!0}}const ee=new Z("");var te=Object.freeze({__proto__:null,Sink:K,FileHandle:Q,FolderHandle:Z,default:()=>ee});export{g as FileSystemDirectoryHandle,H as FileSystemFileHandle,w as FileSystemHandle,d as FileSystemWritableFileStream,o as getOriginPrivateDirectory,t as showDirectoryPicker,n as showOpenFilePicker,a as showSaveFilePicker}; ================================================ FILE: public/apps/nfsadapter/util.js ================================================ export const errors = { INVALID: ['seeking position failed.', 'InvalidStateError'], GONE: ['A requested file or directory could not be found at the time an operation was processed.', 'NotFoundError'], MISMATCH: ['The path supplied exists, but was not an entry of requested type.', 'TypeMismatchError'], MOD_ERR: ['The object can not be modified in this way.', 'InvalidModificationError'], SYNTAX: m => [`Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. ${m}`, 'SyntaxError'], SECURITY: ['It was determined that certain files are unsafe for access within a Web application, or that too many calls are being made on file resources.', 'SecurityError'], DISALLOWED: ['The request is not allowed by the user agent or the platform in the current context.', 'NotAllowedError'] } export const config = { writable: globalThis.WritableStream } export async function fromDataTransfer (entries) { console.warn('deprecated fromDataTransfer - use `dt.items[0].getAsFileSystemHandle()` instead') const [memory, sandbox, fs] = await Promise.all([ import('/public/apps/nfsadapter/adapters/memory.js'), import('/public/apps/nfsadapter/adapters/sandbox.js'), import('/public/apps/nfsadapter/FileSystemDirectoryHandle.js') ]) const folder = new memory.FolderHandle('', false) folder._entries = entries.map(entry => entry.isFile ? new sandbox.FileHandle(entry, false) : new sandbox.FolderHandle(entry, false) ) return new fs.FileSystemDirectoryHandle(folder) } export async function getDirHandlesFromInput (input) { const { FolderHandle, FileHandle } = await import('/public/apps/nfsadapter/adapters/memory.js') const { FileSystemDirectoryHandle } = await import('/public/apps/nfsadapter/FileSystemDirectoryHandle.js') const files = Array.from(input.files) const rootName = files[0].webkitRelativePath.split('/', 1)[0] const root = new FolderHandle(rootName, false) files.forEach(file => { const path = file.webkitRelativePath.split('/') path.shift() const name = path.pop() const dir = path.reduce((dir, path) => { if (!dir._entries[path]) dir._entries[path] = new FolderHandle(path, false) return dir._entries[path] }, root) dir._entries[name] = new FileHandle(file.name, file, false) }) return new FileSystemDirectoryHandle(root) } export async function getFileHandlesFromInput (input) { const { FileHandle } = await import('/public/apps/nfsadapter/adapters/memory.js') const { FileSystemFileHandle } = await import('/public/apps/nfsadapter/FileSystemFileHandle.js') return Array.from(input.files).map(file => new FileSystemFileHandle(new FileHandle(file.name, file, false)) ) } ================================================ FILE: public/apps/settings.tapp/accounts/index.html ================================================ Accounts

Account Manager

================================================ FILE: public/apps/settings.tapp/accounts/index.js ================================================ const tb = parent.window.tb; const currentAccountsEl = document.querySelector(".current-accounts"); const getAccounts = async () => { const entries = await tb.fs.promises.readdir("/home/"); const accounts = await Promise.all( entries.map(async entry => { try { const account = JSON.parse(await tb.fs.promises.readFile(`/home/${entry}/user.json`, "utf8")); return { name: entry, id: account["id"], username: account["username"], perm: account["perm"], pfp: account["pfp"], }; } catch (e) { return null; } }), ); return accounts.filter(account => account !== null); }; const deleteAccount = async id => { const sudoUsers = JSON.parse(await tb.fs.promises.readFile("/system/etc/terbium/sudousers.json", "utf8")); let sudoWithPassword = null; for (const sudoUser of sudoUsers) { const sudoUserData = JSON.parse(await tb.fs.promises.readFile(`/home/${sudoUser}/user.json`, "utf8")); if (sudoUserData.password !== false) { sudoWithPassword = sudoUser; break; } } if (!sudoUsers.includes(sessionStorage.getItem("currAcc"))) { if (!sudoWithPassword) { tb.system.users.remove(id); document.getElementById(id).remove(); return; } tb.dialog.Permissions({ title: "Permission Denied", message: "You do not have permission to delete accounts, would you like to request permission from sudo?", onOk: async () => { tb.dialog.Auth({ title: "Request Permission", defaultUsername: sudoUsers[0], onOk: async (username, password) => { const pass = await tb.crypto(password); if (pass === JSON.parse(await tb.fs.promises.readFile(`/home/${sudoUsers[0]}/user.json`, "utf8")).password) { tb.system.users.remove(id); document.getElementById(id).remove(); } else { tb.dialog.Alert({ title: "Incorrect Password", message: "The password you entered is incorrect.", }); } }, }); }, }); } else { const pw = JSON.parse(await tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, "utf8")).password; if (pw === false) { await tb.system.users.remove(id); document.getElementById(id).remove(); } else { await tb.dialog.Auth({ title: "Authenticate to Delete Account", defaultUsername: sessionStorage.getItem("currAcc"), onOk: async (username, password) => { const pass = await tb.crypto(password); if (pass === pw) { await tb.system.users.remove(id); document.getElementById(id).remove(); } else { tb.dialog.Alert({ title: "Incorrect Password", message: "The password you entered is incorrect.", }); } }, }); } } }; const changePerm = async () => { const data = JSON.parse(await tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, "utf8")); if (data["password"] === false) { await tb.dialog.Select({ title: "Enter the permission level you wish to set (Ex: Admin, User, Group, Public)", options: [ { text: "Admin", value: "admin", }, { text: "User", value: "user", }, { text: "Group", value: "group", }, { text: "Public", value: "public", }, ], onOk: async perm => { if (perm === data["perm"]) return; data["perm"] = perm; permEl.innerHTML = perm.charAt(0).toUpperCase() + perm.slice(1); await tb.fs.promises.writeFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, JSON.stringify(data)); }, }); } else { await tb.dialog.Auth({ sudo: true, title: "Authenticate to change your permissions", defaultUsername: sessionStorage.getItem("currAcc"), onOk: async (username, password) => { const pass = await tb.crypto(password); if (pass === data["password"]) { await tb.dialog.Select({ title: "Enter the permission level you wish to set (Ex: Admin, User, Group, Public)", options: [ { text: "Admin", value: "admin", }, { text: "User", value: "user", }, { text: "Group", value: "group", }, { text: "Public", value: "public", }, ], onOk: async perm => { if (perm === data["perm"]) return; data["perm"] = perm; permEl.innerHTML = perm.charAt(0).toUpperCase() + perm.slice(1); await tb.fs.promises.writeFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, JSON.stringify(data)); }, }); } else { throw new Error("Incorrect Password"); } }, }); } }; const changePfp = async id => { const data = JSON.parse(await tb.fs.promises.readFile(`/home/${id.id}/user.json`, "utf8")); const pfpInp = document.createElement("input"); pfpInp.type = "file"; pfpInp.accept = "image/*"; pfpInp.click(); pfpInp.onchange = async e => { if (e.target.files.length !== 0) { const file = e.target.files[0]; const reader = new FileReader(); reader.onload = async e => { await tb.dialog.Cropper({ title: "Crop Profile Picture", img: e.target.result, onOk: async img => { data["pfp"] = img; await tb.fs.promises.writeFile(`/home/${id.id}/user.json`, JSON.stringify(data)); parent.window.dispatchEvent(new Event("accUpd")); renderAccounts(); }, }); }; reader.readAsDataURL(file); } }; }; const renderAccounts = async () => { const accounts = await getAccounts(); currentAccountsEl.innerHTML = accounts .map( account => `
${account.perm.charAt(0).toUpperCase() + account.perm.slice(1)}
`, ) .join(""); }; renderAccounts(); const createAccount = async () => { const askNewAccountDetails = async () => { 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); } }, }); }, }); }, }); }; 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 => { await tb.dialog.Select({ title: "Do you want to set a profile picture?", options: [ { text: "Yes", value: "yes", }, { text: "No", value: "no", }, ], onOk: async perm => { if (perm === "yes") { const pfpInp = document.createElement("input"); pfpInp.type = "file"; pfpInp.accept = "image/*"; pfpInp.click(); pfpInp.onchange = async e => { if (e.target.files.length === 0) { const randomColorStr = ["blue", "green", "orange", "pink", "purple", "red", "yellow"][Math.floor(Math.random() * 7)]; data["pfp"] = `/assets/img/default - ${randomColorStr}.png`; data["perm"] = "user"; await tb.system.users.add(data); renderAccounts(); } else { const file = e.target.files[0]; const reader = new FileReader(); reader.onload = async e => { await tb.dialog.Cropper({ title: "Crop Profile Picture", img: e.target.result, onOk: async img => { data["pfp"] = img; data["perm"] = "user"; await tb.system.users.add(data); renderAccounts(); }, }); }; reader.readAsDataURL(file); } }; } else { const randomColorStr = ["blue", "green", "orange", "pink", "purple", "red", "yellow"][Math.floor(Math.random() * 7)]; data["pfp"] = `/assets/img/default - ${randomColorStr}.png`; data["perm"] = "user"; await tb.system.users.add(data); renderAccounts(); } }, }); }; const sudoUsers = JSON.parse(await tb.fs.promises.readFile("/system/etc/terbium/sudousers.json", "utf8")); let sudoWithPassword = null; for (const sudoUser of sudoUsers) { const sudoUserData = JSON.parse(await tb.fs.promises.readFile(`/home/${sudoUser}/user.json`, "utf8")); if (sudoUserData.password !== false) { sudoWithPassword = sudoUser; break; } } if (!sudoUsers.includes(sessionStorage.getItem("currAcc"))) { if (!sudoWithPassword) { askNewAccountDetails(); return; } tb.dialog.Permissions({ title: "Permission Denied", message: "You do not have permission to create accounts, would you like to request permission from sudo?", onOk: async () => { tb.dialog.Auth({ title: "Request Permission", defaultUsername: sudoWithPassword, onOk: async (username, password) => { const pass = await tb.crypto(password); if (pass === JSON.parse(await tb.fs.promises.readFile(`/home/${sudoWithPassword}/user.json`, "utf8")).password) { askNewAccountDetails(); } else { tb.dialog.Alert({ title: "Incorrect Password", message: "The password you entered is incorrect.", }); } }, }); }, }); } else { const user = JSON.parse(await tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, "utf8")); if (user["password"] === false) { askNewAccountDetails(); } else { await tb.dialog.Auth({ title: "Authenticate to Create Account", defaultUsername: sessionStorage.getItem("currAcc"), onOk: async (username, password) => { const pass = await tb.crypto(password); if (pass === JSON.parse(await tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, "utf8")).password) { askNewAccountDetails(); } else { tb.dialog.Alert({ title: "Incorrect Password", message: "The password you entered is incorrect.", }); } }, }); } } renderAccounts(); }; ================================================ FILE: public/apps/settings.tapp/index.css ================================================ @font-face { font-family: Inter; src: url(/fonts/Inter.ttf); } * { font-family: Inter; } html, body { height: 100%; margin: 0; font-family: Inter; position: relative; overflow: hidden; } body { display: flex; color: #ffffff; } h1, h2, h3, h4, h5, h6 { margin: 0; } /* big screen */ @media screen and (max-width: 1920px) { .wallpaper-option { width: 200px; height: 112px; } } /* small screen */ @media screen and (max-width: 646px) { .cat-option { padding-right: 0px; } .cat-option .text { position: absolute; opacity: 0; } .cat-option .icon:hover { background-color: #ffffff28; } .cat-option .icon svg { width: 24px; height: auto; } .category-search { width: 100px; } .wallpapers { width: 400px; } .wallpaper-option { width: 130px; height: 73px; } } .category-search { display: flex; align-items: center; gap: 10px; padding: 8px 10px; background-color: #ffffff20; font-weight: 700; font-family: Inter; font-size: 16px; line-height: 16px; color: #dedede; border: none; border-radius: 6px; transition: 150ms ease-in-out; } .category-search:focus { outline: none; } .cat-option .text { font-size: 16px; font-weight: 800; line-height: 16px; transition-property: color; transition-duration: 150ms; transition-timing-function: ease-in-out; pointer-events: none; } .cat-option:hover { color: #ffffff98; } .cat-option { color: #ffffff52; } .cat-option:hover { color: #ffffff; } .cat-option { position: relative; display: flex; gap: 8px; align-items: center; height: min-content; cursor: var(--cursor-pointer); } .cat-option.selected { color: #ffffff !important; } .cat-option .icon { display: flex; justify-content: center; align-items: center; padding: 6px; border-radius: 8px; transition-property: color, background-color; transition-duration: 150ms; transition-timing-function: ease-in-out; pointer-events: none; } .cat-option .icon svg { pointer-events: none; } .cat-tooltip { background-color: #ffffff10; border: 1px solid #ffffff28; border-radius: 6px; font-size: 14px; font-weight: 600; font-family: Inter; position: absolute; left: calc(36px + 8px); backdrop-filter: blur(100px) contrast(0.8) brightness(0.8); z-index: 99; pointer-events: none; padding: 4px 8px; opacity: 1; transition: 150ms ease-in-out; } .cat-tooltip.hidden { opacity: 0; left: calc(36px); } .sidebar { position: relative; display: flex; /* background-color: #ffffff24; */ border-radius: 8px; } .settings-category::-webkit-scrollbar { width: 8px; height: 8px; } .settings-category::-webkit-scrollbar-thumb { background-color: #ffffff28; border-radius: 8px; } .settings-category::-webkit-scrollbar-track { background-color: #ffffff10; border-radius: 8px; } .settings-category { scrollbar-color: #ffffff28 #ffffff10; scrollbar-width: thin; } .option-container { gap: 6px; display: flex; flex-direction: column; } .wallpapers { display: flex; flex-wrap: wrap; gap: 6px; } .wallpaper-container { position: relative; } .wallpaper-option { border-radius: 6px; cursor: var(--cursor-pointer); opacity: 0.7; transition: 150ms ease-in-out; } .wallpaper-option:hover { opacity: 1; } .delete-wallpaper { position: absolute; top: 6px; right: 6px; width: 20px; height: 20px; padding: 4px; background-color: #ffffff; border-radius: 50%; display: flex; justify-content: center; align-items: center; transition: 150ms ease-in-out; cursor: var(--cursor-pointer); } .buttons-flex { display: flex; } #output div span:nth-child(2) { word-spacing: 0.25em; letter-spacing: 0.25em; white-space: pre; } #wispSrvs .wisp-card:nth-child(odd) { background-color: #ffffff28; } .wisp-card { display: flex; justify-content: space-between; gap: 6px; width: calc(100% - 16px); padding: 6px; padding-left: 10px; border-radius: 6px; } .wisp-card .net { display: flex; gap: 6px; align-items: center; } .wisp-card .net-info { display: flex; flex-direction: column; } .wisp-card .net-info .info-text { font-size: 14px; font-weight: 650; font-family: Inter; } .wisp-card .net-info .info-text:nth-child(odd) { user-select: none; } .wisp-card .net-info .info-text:nth-child(even) { color: #ffffff93; font-weight: normal; } .wisp-card .latency { font-size: 14px; font-weight: 800; font-family: Inter; color: #ffffff93; user-select: none; } .wisp-card .connection-info { display: flex; 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); } ================================================ FILE: public/apps/settings.tapp/index.html ================================================ Settings

Wallpaper

Wallpaper Fill Mode

Cover
Cover
Contain
Stretch

Accent

Battery

Show Percentage

Time

24 hour clock

No
No
Yes

Show seconds

No
No
Yes

Weather

Temperature Unit

Celsius
Fahrenheit
Celsius
Kelvin

Enable Window Optimizations

Improves rendering performance for smoother window interactions (drag, resize). Uses GPU acceleration and frame throttling.

Show FPS Counter

Display real-time frames per second in the information island.

💡 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%

Proxy

Ultraviolet
Ultraviolet
Scramjet

Wisp Server

Transport Type

Default (Epoxy)
Default (Epoxy)
Libcurl
Anura BCC

Account

@
Cords:

Import/Export Settings

Import TBS File

Enable Eruda

Invalidate Cache

Export FileSystem (tab may freeze momentarily)

================================================ FILE: public/apps/settings.tapp/index.js ================================================ const Filer = window.Filer; const tb = parent.window.tb; const tb_window = tb.window; const tb_desktop = tb.desktop; const tb_preferences = tb.desktop.preferences; const tb_island = tb.window.island; const tb_context_menu = tb.context_menu; const tb_dialog = tb.dialog; const tb_wallpaper = parent.window.tb.desktop.wallpaper; const parent_body = parent.document.body; setInterval(() => { if (parent_body.getAttribute("theme")) document.body.setAttribute("theme", parent_body.getAttribute("theme")); }); const cat_options = document.querySelectorAll(".cat-option"); cat_options.forEach(option => { function mouseleave() { let tooltip = option.querySelector(".cat-tooltip"); tooltip.classList.add("hidden"); option.removeEventListener("mouseleave", mouseleave); option.addEventListener("mouseover", mouseover); } function mouseover() { setTimeout(() => { if (option.matches(":hover")) { if (option.offsetWidth === 36) { let tooltip = option.querySelector(".cat-tooltip"); tooltip.classList.remove("hidden"); document.querySelectorAll(".cat-tooltip").forEach(tooltip => { if (tooltip !== option.querySelector(".cat-tooltip")) tooltip.classList.add("hidden"); }); option.addEventListener("mouseleave", mouseleave); } } }, 1000); } option.addEventListener("click", e => { const cat = option.getAttribute("data-category"); const current_cat = document.querySelector('.settings-category[data-visible="true"]').getAttribute("category"); if (cat === current_cat) return; document.querySelectorAll(".settings-category").forEach(category => { category.dataset.visible = "false"; category.classList.add("opacity-0", "pointer-events-none", "translate-y-6"); }); document.querySelectorAll(".cat-option").forEach(opt => opt.classList.remove("selected")); const view = document.querySelector(`.settings-category[category="${cat}"]`); if (view === null) return; view.dataset.visible = "true"; view.classList.remove("opacity-0", "pointer-events-none", "translate-y-6"); option.classList.add("selected"); }); option.addEventListener("mouseover", mouseover); }); const wallpaper_options = document.querySelectorAll(".wallpaper-option"); wallpaper_options.forEach(option => { option.addEventListener("click", async e => { const parent_origin = parent.parent.window.location.origin; const wallpaper = option.src.toString().split(parent_origin)[1]; const color = option.getAttribute("color-type"); let data = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8")); data["wallpaper"] = wallpaper; tb_wallpaper.set(wallpaper); const fillMode = parent.window.tb.desktop.wallpaper.fillMode(); if (fillMode === null) parent.window.tb.desktop.wallpaper.cover(); if (color !== parent.window.tb.desktop.preferences.theme()) { // parent.window.tb.desktop.preferences.setTheme(`${color`) document.body.setAttribute("theme", color); } }); }); window.parent.tb.fs.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8", (err, data) => { if (err) return console.log(err); data = JSON.parse(data); const fillMode = data["wallpaperMode"]; const showSeconds = data["times"]["showSeconds"]; const twentyFourHour = data["times"]["format"]; let fillModeCapitalized = fillMode.charAt(0).toUpperCase() + fillMode.slice(1); document.querySelector(`[action-for="wallpaper-fill"]`).querySelector(".select-title .text").innerText = fillModeCapitalized; document.querySelector(`[action-for="proxy"]`).querySelector(".select-title .text").innerText = data["proxy"]; 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"; try { const wallpaperVal = data["wallpaper"]; const isb64 = val => { if (!val || typeof val !== "string") return false; if (val.startsWith("data:")) return true; const stripped = val.replace(/\s+/g, ""); return /^[A-Za-z0-9+/=]+$/.test(stripped) && stripped.length > 200; }; if (isb64(wallpaperVal)) { const wallpaperContainer = document.querySelector(".wallpapers"); if (wallpaperContainer) { const container = document.createElement("div"); container.classList.add("wallpaper-container"); container.style.position = "relative"; const img = document.createElement("img"); img.classList.add("wallpaper-option"); img.src = wallpaperVal.startsWith("data:") ? wallpaperVal : `data:image/png;base64,${wallpaperVal}`; img.alt = "Synced Wallpaper"; img.style.objectFit = "cover"; img.addEventListener("click", async () => { tb_wallpaper.set(img.src); }); const label = document.createElement("div"); label.innerText = "Synced Wallpaper"; label.style.position = "absolute"; label.style.bottom = "6px"; label.style.left = "6px"; label.style.background = "rgba(0,0,0,0.45)"; label.style.padding = "2px 6px"; label.style.borderRadius = "6px"; label.style.color = "#ffffff"; label.style.fontSize = "12px"; container.appendChild(img); container.appendChild(label); wallpaperContainer.prepend(container); } } } catch (e) { console.warn("Unable to show synced wallpaper label:", e); } }); window.parent.tb.fs.readFile("/system/etc/terbium/settings.json", "utf8", (err, data) => { if (err) return console.log(err); data = JSON.parse(data); const cords = data["location"]; document.querySelector(`.cords`).innerText = `${cords}`; const tempunit = data["weather"]["unit"]; document.querySelector(`[action-for="temperature-unit"]`).querySelector(".select-title .text").innerText = tempunit; }); const customWallpaper = () => { window.parent.tb.dialog.Select({ title: "Where do you want to load the wallpaper from?", options: [ { text: "System Storage", value: "sys", }, { text: "Terbium File System", value: "fs", }, { text: "Internet Url", value: "url", }, ], onOk: async perm => { switch (perm) { case "sys": const input = document.createElement("input"); input.type = "file"; input.setAttribute("accept", "image/*"); input.click(); input.addEventListener("change", async e => { const file = input.files[0]; const buffer = await file.arrayBuffer(); const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = async () => { const imgdata = reader.result; const path = "/system/etc/terbium/wallpapers/" + file.name; tb_wallpaper.set(path); const img_container = document.createElement("div"); img_container.classList.add("wallpaper-container"); const wimg = document.createElement("img"); wimg.src = imgdata; wimg.classList.add("wallpaper-option"); const delete_button = document.createElement("img"); 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.parent.tb.user.username()}/settings.json`, "utf8")); if (data["wallpaper"] === path) { tb_wallpaper.set("/assets/wallpapers/1.png"); } await window.parent.tb.fs.promises.unlink(path); img_container.remove(); }); wimg.addEventListener("click", async e => { tb_wallpaper.set(path); }); 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); document.querySelector(".custom-wallpaper").remove(); document.querySelector(".wallpapers").append(img_container); appendCustomWallpaper(); }; }); break; 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, "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()); document.querySelector(".wallpapers").innerHTML = ` `; const wallpaper_options = document.querySelectorAll(".wallpaper-option"); wallpaper_options.forEach(option => { option.addEventListener("click", async e => { const parent_origin = parent.parent.window.location.origin; const wallpaper = option.src.toString().split(parent_origin)[1]; const color = option.getAttribute("color-type"); let data = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8")); data["wallpaper"] = wallpaper; tb_wallpaper.set(wallpaper); const fillMode = parent.window.tb.desktop.wallpaper.fillMode(); if (fillMode === null) parent.window.tb.desktop.wallpaper.cover(); if (color !== parent.window.tb.desktop.preferences.theme()) { // parent.window.tb.desktop.preferences.setTheme(`${color`) document.body.setAttribute("theme", color); } }); }); getWallpapers(); }, }); break; case "url": tb.dialog.Message({ title: "Enter a URL of the wallpaper", onOk: async value => { await window.parent.tb.system.download(value, `/system/etc/terbium/wallpapers/${value.split("/").pop()}`); tb.desktop.wallpaper.set("/system/etc/terbium/wallpapers/" + value.split("/").pop()); document.querySelector(".wallpapers").innerHTML = ` `; const wallpaper_options = document.querySelectorAll(".wallpaper-option"); wallpaper_options.forEach(option => { option.addEventListener("click", async e => { const parent_origin = parent.parent.window.location.origin; const wallpaper = option.src.toString().split(parent_origin)[1]; const color = option.getAttribute("color-type"); let data = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8")); data["wallpaper"] = wallpaper; tb_wallpaper.set(wallpaper); const fillMode = parent.window.tb.desktop.wallpaper.fillMode(); if (fillMode === null) parent.window.tb.desktop.wallpaper.cover(); if (color !== parent.window.tb.desktop.preferences.theme()) { // parent.window.tb.desktop.preferences.setTheme(`${color`) document.body.setAttribute("theme", color); } }); }); getWallpapers(); }, }); break; } }, }); }; const appendCustomWallpaper = () => { if (document.querySelector(".custom-wallpaper")) { console.log(document.querySelector(".custom-wallpaper")); } const newButton = ` `; const wallpaperContainer = document.querySelector(".wallpapers"); wallpaperContainer.insertAdjacentHTML("beforeend", newButton); const customWallpaperBtn = document.querySelector(".custom-wallpaper"); customWallpaperBtn.addEventListener("click", e => { customWallpaper(); }); }; async function getWispSrvs() { const fileExists = await window.parent.tb.fs.promises .stat("//apps/system/settings.tapp/wisp-servers.json") .then(() => true) .catch(() => false); if (!fileExists) { await window.parent.tb.fs.promises.mkdir("//apps/settings.tapp/", { recursive: true }); const stockDat = [ { id: `${location.protocol.replace("http", "ws")}//${location.hostname}:${location.port}/wisp/`, name: "Backend" }, { id: "wss://wisp.terbiumon.top/wisp/", name: "TB Wisp Instance" }, ]; await window.parent.tb.fs.promises.writeFile("//apps/system/settings.tapp/wisp-servers.json", JSON.stringify(stockDat)); } const main = document.getElementById("wispSrvs"); window.parent.window.dispatchEvent(new Event("update-wispsrvs")); const makeCard = async (name, id) => { let settings = await window.parent.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8"); let settdata = JSON.parse(settings); const card = document.createElement("div"); card.classList.add("flex", "justify-between", "w-full", "p-1.5", "rounded-lg", "duration-150", `srv-${id.replace(/\s/g, "-")}`); if (name === settdata.wispServer) { card.classList.add("bg-[#4acd609c]"); } else { card.classList.add("bg-[#ffffff18]", "hover:bg-[#ffffff38]", "cursor-pointer"); } const html = `

${id}

${name}

Pinging...

`; card.innerHTML = html; setTimeout(async () => { const res = await ping(name); document.querySelector(`[latency-${id.replace(/\s/g, "-")}]`).innerHTML = res.latency + "ms"; }, 1750); card.addEventListener("click", async () => { settdata.wispServer = name; const updSet = JSON.stringify(settdata, null, 2); await window.parent.tb.fs.promises.writeFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, updSet); window.parent.tb.proxy.updateSWs(); window.parent.window.dispatchEvent(new Event("update-wispsrvs")); }); main.appendChild(card); }; const data = JSON.parse(await window.parent.tb.fs.promises.readFile("//apps/system/settings.tapp/wisp-servers.json")); data.forEach(item => { makeCard(item.id, item.name); }); const ping = id => { return new Promise(resolve => { const websocket = new WebSocket(id); const startTime = Date.now(); const onOpen = () => { const latency = Date.now() - startTime; websocket.close(); resolve({ status: "OK", latency }); }; const onMessage = () => { const latency = Date.now() - startTime; websocket.close(); resolve({ status: "OK", latency }); }; const onError = () => { websocket.close(); resolve({ status: "Fail", latency: "N/A" }); }; websocket.addEventListener("open", onOpen); websocket.addEventListener("message", onMessage); websocket.addEventListener("error", onError); setTimeout(() => { websocket.close(); resolve({ status: "Fail", latency: "N/A" }); }, 5000); }); }; const addWispbtn = document.getElementById("addWisp"); addWispbtn.addEventListener("click", () => { window.parent.tb.dialog.Message({ title: "Enter a name for the Wisp server", onOk: async val => { sessionStorage.setItem("wispSrv", val); window.parent.tb.dialog.Message({ title: "Enter the socket URL for the Wisp server", onOk: async val => { const ent = { id: val, name: sessionStorage.getItem("wispSrv") }; let data = JSON.parse(await window.parent.tb.fs.promises.readFile("//apps/system/settings.tapp/wisp-servers.json")); data.push(ent); window.parent.tb.fs.promises.writeFile("//apps/system/settings.tapp/wisp-servers.json", JSON.stringify(data)); makeCard(val, sessionStorage.getItem("wispSrv")); }, }); }, }); }); const rmWispbtn = document.getElementById("rmWisp"); rmWispbtn.addEventListener("click", async () => { let data = JSON.parse(await window.parent.tb.fs.promises.readFile("//apps/system/settings.tapp/wisp-servers.json")); const servers = data.map(item => ({ text: item.name, value: item.name, })); window.parent.tb.dialog.Select({ title: "Select the Wisp server to remove", options: servers, onOk: async selectedName => { data = data.filter(item => item.name !== selectedName); await window.parent.tb.fs.promises.writeFile("//apps/system/settings.tapp/wisp-servers.json", JSON.stringify(data)); document.querySelector(`.srv-${selectedName.replace(/\s/g, "-")}`).remove(); window.parent.window.dispatchEvent(new Event("update-wispsrvs")); }, }); }); } getWispSrvs(); async function updateTransport(transport) { 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.parent.tb.user.username()}/settings.json`, JSON.stringify(st), "utf8"); } const accentPreview = document.querySelector(".accent-preview"); 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.parent.tb.user.username()}/settings.json`, "utf8")); settings["accent"] = defaultAccent; 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.parent.tb.user.username()}/settings.json`, "utf8")); var accentColor = settings["accent"]; const defaultAccent = "#32ae62"; if (accentColor !== defaultAccent) { accentPreview.classList.add("group", "cursor-pointer"); accentPreview.addEventListener("mousedown", accentMousedown); } if (accentColor) { accentPreview.style.setProperty("--accent", accentColor); } else { accentPreview.style.setProperty("--accent", "#32ae62"); } }; getAccent(); const custom_accent = document.querySelector(".custom-accent"); custom_accent.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["accent"] = color; 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.parent.tb.user.username()}/settings.json`, "utf8")); settings["accent"] = color; 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")); }); }); const getWallpapers = async () => { const files = await window.parent.tb.fs.promises.readdir("/system/etc/terbium/wallpapers"); for (const file of files) { const path = "/system/etc/terbium/wallpapers/" + file; const data = URL.createObjectURL(new Blob([await window.parent.tb.fs.promises.readFile(path, "utf8")])); const img_container = document.createElement("div"); img_container.classList.add("wallpaper-container"); const img = document.createElement("img"); img.src = `/fs/${path}`; img.classList.add("wallpaper-option"); const delete_button = document.createElement("img"); delete_button.src = "/fs/apps/system/settings.tapp/delete.svg"; delete_button.classList.add("delete-wallpaper"); delete_button.addEventListener("click", e => { window.parent.tb.fs.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8", (err, data) => { if (err) return console.log(err); data = JSON.parse(data); if (data["wallpaper"] === path) { tb_wallpaper.set("/assets/wallpapers/1.png"); } }); window.parent.tb.fs.unlink(path, err => { if (err) return console.log(err); img_container.remove(); }); }); img_container.append(img); img_container.append(delete_button); document.querySelector(".wallpapers").append(img_container); img.addEventListener("click", e => { tb_wallpaper.set(path); }); } appendCustomWallpaper(); }; getWallpapers(); const pfpEl = document.querySelector(".pfp"); window.parent.tb.fs.readFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, "utf8", (err, data) => { if (err) return console.log(err); data = JSON.parse(data); pfpEl.src = data["pfp"]; }); pfpEl.addEventListener("click", async e => { 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 = () => { tb.dialog.Cropper({ 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; await window.parent.tb.fs.promises.writeFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, JSON.stringify(uSettings)); window.parent.dispatchEvent(new Event("accUpd")); }, }); }; reader.readAsDataURL(file); }; uploader.click(); }); const usernameEl = document.querySelector(".username"); usernameEl.addEventListener("input", async e => { usernameEl.addEventListener("blur", async () => { if (usernameEl.value !== JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, "utf8"))["username"]) { await window.parent.tb.system.users.renameUser(sessionStorage.getItem("currAcc"), usernameEl.value); } }); }); const permEl = document.querySelector(".perm"); permEl.addEventListener("click", async () => { const data = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, "utf8")); if (data["password"] === false) { await tb.dialog.Select({ title: "Enter the permission level you wish to set (Ex: Admin, User, Group, Public)", options: [ { text: "Admin", value: "admin", }, { text: "User", value: "user", }, { text: "Group", value: "group", }, { text: "Public", value: "public", }, ], onOk: async perm => { if (perm === data["perm"]) return; data["perm"] = perm; permEl.innerHTML = perm.charAt(0).toUpperCase() + perm.slice(1); await window.parent.tb.fs.promises.writeFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, JSON.stringify(data)); }, }); } else { await tb.dialog.Auth({ sudo: true, title: "Authenticate to change your permissions", defaultUsername: sessionStorage.getItem("currAcc"), onOk: async (_username, password) => { const pass = await tb.crypto(password); if (pass === data["password"]) { await tb.dialog.Select({ title: "Enter the permission level you wish to set (Ex: Admin, User, Group, Public)", options: [ { text: "Admin", value: "admin", }, { text: "User", value: "user", }, { text: "Group", value: "group", }, { text: "Public", value: "public", }, ], onOk: async perm => { if (perm === data["perm"]) return; data["perm"] = perm; permEl.innerHTML = perm.charAt(0).toUpperCase() + perm.slice(1); await window.parent.tb.fs.promises.writeFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, JSON.stringify(data)); }, }); } else { throw new Error("Incorrect Password"); } }, }); } }); 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"; const currAcc = sessionStorage.getItem("currAcc"); sessionStorage.setItem("currAcc", res.data.user.name); await window.parent.tb.tauth.sync.retreive(); sessionStorage.setItem("currAcc", currAcc); const userinfo = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, "utf8")); userinfo["password"] = await window.parent.tb.crypto(res.data.user.password); userinfo["perm"] = "admin"; await window.parent.tb.fs.promises.writeFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, JSON.stringify(userinfo)); await window.parent.tb.system.users.renameUser(sessionStorage.getItem("currAcc"), res.data.user.name); break; case "local": window.parent.tb.dialog.Select({ title: "Save current settings to cloud before converting to local?", options: [ { text: "Yes", value: "yes", }, { text: "No", value: "no", }, ], onOk: async val => { if (val === "yes") { await window.parent.tb.tauth.sync.upload(); } else { return; } }, }); 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); usernameEl.value = data["username"]; permEl.innerHTML = data["perm"].charAt(0).toUpperCase() + data["perm"].slice(1); }); const hostnameEl = document.querySelector(".hostname"); hostnameEl.addEventListener("input", async e => { hostnameEl.addEventListener("blur", async () => { if (hostnameEl.value !== JSON.parse(await window.parent.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8"))["host-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["host-name"] = hostnameEl.value; await window.parent.tb.fs.promises.writeFile("/system/etc/terbium/settings.json", JSON.stringify(data)); }); } }); }); window.parent.tb.fs.readFile("/system/etc/terbium/settings.json", "utf8", (err, data) => { if (err) return console.log(err); data = JSON.parse(data); hostnameEl.value = data["host-name"]; }); const cords = document.querySelector(".cords"); const saveCity = document.querySelector(".save-city"); saveCity.addEventListener("click", e => { window.parent.tb.fs.readFile("/system/etc/terbium/settings.json", "utf8", (err, data) => { if (err) return console.log(err); data = JSON.parse(data); if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( function (position) { const latitude = position.coords.latitude; const longitude = position.coords.longitude; console.log(`${latitude},${longitude}`); data["location"] = `${latitude},${longitude}`; window.parent.dispatchEvent(new Event("updWeather")); window.parent.tb.fs.writeFile("/system/etc/terbium/settings.json", JSON.stringify(data), err => { if (err) return console.log(err); }); }, function (error) { console.error(`Error Occured: ${error.code}`); }, { enableHighAccuracy: true, timeout: 5000, maximumAge: 0, }, ); } }); }); const accountsButton = document.querySelector(".accounts"); accountsButton.addEventListener("mousedown", e => { tb_window.create({ title: "Accounts", src: "/fs/apps/system/settings.tapp/accounts/index.html", icon: "/fs/apps/system/settings.tapp/accounts/icon.svg", size: { width: 400, height: 500, }, }); }); const batteryPercentage = document.querySelector(".battery-percentage"); (async () => { 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; const checkIcon = batteryPercentage.querySelector(".checkIcon"); checkIcon.classList.remove("opacity-0", "scale-85"); } else { realCheckbox.checked = false; const checkIcon = batteryPercentage.querySelector(".checkIcon"); checkIcon.classList.add("opacity-0", "scale-85"); } })(); batteryPercentage.addEventListener("mousedown", async e => { 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"); if (realCheckbox.checked) { checkIcon.classList.remove("opacity-0", "scale-85"); tb.battery.showPercentage(); } else { checkIcon.classList.add("opacity-0", "scale-85"); tb.battery.hidePercentage(); } data["battery-percent"] = realCheckbox.checked; 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.parent.tb.battery.canUse(); if (!battery) { document.querySelector(".battery").remove(); } }; getBat(); const showCords = document.querySelector(".showCords"); showCords.addEventListener("mousedown", async e => { showCords.classList.add("opacity-0", "pointer-events-none"); cords.classList.remove("opacity-0"); const mouseup = () => { showCords.classList.remove("opacity-0", "pointer-events-none"); cords.classList.add("opacity-0"); document.removeEventListener("mouseup", mouseup); }; document.addEventListener("mouseup", mouseup); }); async function exportSettings() { 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); let a = document.createElement("a"); a.href = url; a.download = "settings.json"; 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 initWinAccent = async () => { const defaultAccent = "#ffffff"; try { let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.parent.tb.user.username()}/settings.json`, "utf8")); const current = settings.window && settings.window.winAccent ? settings.window.winAccent : defaultAccent; winAccent.style.setProperty("--accent", current); if (current !== defaultAccent) { winAccent.classList.add("group", "cursor-pointer"); const resetHandler = async () => { let s = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.parent.tb.user.username()}/settings.json`, "utf8")); s.window = s.window || {}; s.window.winAccent = defaultAccent; await window.parent.tb.fs.promises.writeFile(`/home/${await window.parent.tb.user.username()}/settings.json`, JSON.stringify(s)); winAccent.style.setProperty("--accent", defaultAccent); winAccent.classList.remove("group", "cursor-pointer"); winAccent.removeEventListener("mousedown", resetHandler); window.parent.dispatchEvent(new Event("upd-accent")); }; winAccent.addEventListener("mousedown", resetHandler); } } catch (err) { winAccent.style.setProperty("--accent", defaultAccent); } }; initWinAccent(); 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"; input.accept = ".tbs"; input.onchange = async () => { let file = input.files[0]; 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.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; } if (tbs_config.wallpaper && tbs_config.wallpaper !== "default" && settings.wallpaper !== "/assets/wallpapers/1.png") { settings.wallpaper = tbs_config.wallpaper; } if (tbs_config.wallpaperFill && tbs_config.wallpaperFill !== "default") { settings.wallpaperMode = tbs_config.wallpaperFill === "contain" ? "cover" : "contain"; } if (tbs_config.shadow === "yes") { settings["system-blur"] = true; } 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(); }; reader.readAsText(file); }; input.click(); } const animationCheckbox = document.querySelector(".eruda-check"); const eruda = () => { const realCheckbox = animationCheckbox.querySelector("input[type='checkbox']"); const checkIcon = animationCheckbox.querySelector(".checkIcon"); const setState = enabled => { realCheckbox.checked = enabled; if (enabled) { checkIcon.classList.remove("opacity-0", "scale-85"); localStorage.setItem("eruda", "true"); } else { checkIcon.classList.add("opacity-0", "scale-85"); localStorage.removeItem("eruda"); } }; setState(localStorage.getItem("eruda") === "true"); animationCheckbox.addEventListener("mousedown", () => { setState(!realCheckbox.checked); }); }; eruda(); const windowOptimizationsCheckbox = document.querySelector(".window-optimizations-check"); const setupWindowOptimizations = async () => { const realCheckbox = windowOptimizationsCheckbox.querySelector("input[type='checkbox']"); const checkIcon = windowOptimizationsCheckbox.querySelector(".checkIcon"); const setState = async (enabled, forceWrite = false) => { realCheckbox.checked = enabled; if (enabled) { checkIcon.classList.remove("opacity-0", "scale-85"); } else { checkIcon.classList.add("opacity-0", "scale-85"); } try { let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); if (forceWrite || settings.windowOptimizations !== enabled) { settings.windowOptimizations = enabled; await window.parent.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(settings, null, 2), "utf8"); } } catch (err) { if (forceWrite) { const base = { windowOptimizations: enabled }; await window.parent.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(base, null, 2), "utf8"); } } }; try { let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); setState(settings.windowOptimizations ?? true, false); } catch (err) { console.error("Failed to load window optimization settings:", err); setState(true, false); } windowOptimizationsCheckbox.addEventListener("mousedown", async () => { await setState(!realCheckbox.checked, true); }); }; setupWindowOptimizations(); const fpsCounterCheckbox = document.querySelector(".fps-counter-check"); const setupFPSCounter = async () => { const realCheckbox = fpsCounterCheckbox.querySelector("input[type='checkbox']"); const checkIcon = fpsCounterCheckbox.querySelector(".checkIcon"); const setState = async (enabled, forceWrite = false) => { realCheckbox.checked = enabled; if (enabled) { checkIcon.classList.remove("opacity-0", "scale-85"); } else { checkIcon.classList.add("opacity-0", "scale-85"); } try { let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); if (forceWrite || settings.showFPS !== enabled) { settings.showFPS = enabled; await window.parent.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(settings, null, 2), "utf8"); } } catch (err) { if (forceWrite) { const base = { showFPS: enabled }; await window.parent.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(base, null, 2), "utf8"); } } window.parent.dispatchEvent(new CustomEvent("settings-changed", { detail: { showFPS: enabled } })); }; try { let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); setState(settings.showFPS ?? false, false); } catch (err) { console.error("Failed to load FPS counter settings:", err); setState(false, false); } fpsCounterCheckbox.addEventListener("mousedown", async () => { await setState(!realCheckbox.checked, true); }); }; setupFPSCounter(); const realCheckbox = animationCheckbox.querySelector("input[type='checkbox']"); realCheckbox.checked = !realCheckbox.checked; const checkIcon = animationCheckbox.querySelector(".checkIcon"); if (realCheckbox.checked) { checkIcon.classList.remove("opacity-0", "scale-85"); } else { checkIcon.classList.add("opacity-0", "scale-85"); } ================================================ FILE: public/apps/settings.tapp/index.json ================================================ { "name": "Settings", "version": "1.0.0", "config": { "title": "Settings", "src": "/fs/apps/system/settings.tapp/index.html", "icon": "/fs/apps/system/settings.tapp/icon.svg", "single": true } } ================================================ FILE: public/apps/settings.tapp/island.js ================================================ const appisland = window.parent.document.querySelector(".app_island").clientHeight + 12; tb_island.addControl({ text: "File", appname: "Settings", id: "settings_file", click: () => { tb.contextmenu.create({ x: 6, y: appisland, iframe: false, options: [ { text: "Import Settings", click: () => { const input = document.createElement("input"); input.type = "file"; input.accept = ".json"; input.onchange = async () => { let file = input.files[0]; let reader = new FileReader(); reader.onload = async () => { let settings = JSON.parse(reader.result); await window.parent.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(settings), "utf8"); }; reader.readAsText(file); }; input.click(); }, }, { text: "Export Settings", click: async () => { let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); let data = JSON.stringify(settings); let blob = new Blob([data], { type: "application/json" }); let url = URL.createObjectURL(blob); let a = document.createElement("a"); a.href = url; a.download = "settings.json"; a.click(); }, }, ], }); }, }); tb_island.addControl({ text: "View", appname: "Settings", id: "settings_view", click: () => { tb.contextmenu.create({ x: 6, y: appisland, iframe: false, options: [ { text: "Appearance", click: () => { document.querySelector(`[data-category="appearance"]`).click(); }, }, { text: "Window", click: () => { document.querySelector(`[data-category="window"]`).click(); }, }, { text: "Networking", click: () => { document.querySelector(`[data-category="networking"]`).click(); }, }, { text: "Other", click: () => { document.querySelector(`[data-category="other"]`).click(); }, }, ], }); }, }); tb_island.addControl({ text: "Help", appname: "Settings", id: "help", click: () => { window.open("https://github.com/TerbiumOS/web-v2/blob/main/docs/README.md", "_blank"); }, }); ================================================ FILE: public/apps/settings.tapp/message.js ================================================ window.addEventListener("message", function (event) { var data; if (typeof event.data === "object") { try { data = event.data; } catch (e) { console.warn(e); } } else { try { data = JSON.parse(event.data); } catch { console.warn("No Message"); } } if (data && document.querySelector(`[data-category="${data.page}"]`)) { let button = document.querySelector(`[data-category="${data.page}"]`); button.click(); } }); ================================================ FILE: public/apps/settings.tapp/radio.css ================================================ body[theme="light"] button.radio { color: #ffffff88; } body[theme="dark"] button.radio { color: #ffffff88; } button.radio { display: flex; gap: 6px; background-color: transparent; border: none; font-size: 16px; font-family: Inter; font-weight: 700; line-height: 16px; cursor: var(--cursor-pointer); transition: 150ms ease-in-out; } body[theme="light"] button.radio span.radio { background-color: #ffffff78; } button.radio span.radio { display: flex; justify-content: center; align-items: center; width: 16px; height: 16px; border-radius: 50%; transition: 150ms ease-in-out; } button.radio.selected { color: #ffffff !important; } button.radio.selected span.radio { background-color: #ffffff !important; } button.radio.selected span.radio span.radio-check { background-color: #4e8bff; } button.radio span.radio span.radio-check { width: 10px; height: 10px; border-radius: 50%; background-color: #00000050; transition: 150ms ease-in-out; } ================================================ FILE: public/apps/settings.tapp/select.css ================================================ .select { position: relative; display: flex; flex-direction: column; gap: 6px; width: min-content; } .select .select-title { background-color: #ffffff10; box-shadow: 0px 0px 6px 0px #00000052, inset 0 0 0 0.5px #ffffff38; } .select .select-title:hover { background-color: #ffffff36; } .select .select-title { display: flex; align-items: center; gap: 20px; font-size: 18px; font-weight: 800; line-height: 18px; padding: 6px 6px 6px 10px; color: #ffffff; border-radius: 6px; backdrop-filter: blur(100px); cursor: var(--cursor-pointer); transition: 150ms ease-in-out; } .select .select-title .text { width: max-content; } .select .select-title .icon { width: 24px; height: auto; } .select .options { background-color: #ffffff10; box-shadow: 0px 0px 6px 0px #00000052, inset 0 0 0 0.5px #ffffff38; } .select .options { position: absolute; top: calc(100% + 6px); z-index: 9; left: 0; width: 100%; max-height: 160px; display: flex; flex-direction: column; border-radius: 6px; overflow: auto; transition: 150ms ease-in-out; } .select .options::before { content: "\00A0"; display: block; position: absolute; top: 1px; bottom: 1px; left: 1px; right: 1px; z-index: -1; border-radius: 4px; backdrop-filter: blur(14px); } .select .options .option { padding: 10px 10px; cursor: var(--cursor-pointer); font-weight: 650; transition: 150ms ease-in-out; } .select .options .option:hover { background-color: #ffffff36; } .select .options:not(.open) { opacity: 0; pointer-events: none; height: 0px; } ================================================ FILE: public/apps/settings.tapp/select.js ================================================ const selects = document.querySelectorAll(".select"); selects.forEach(select => { const intiator = select.querySelector(".select-title"); const select_options = select.querySelector(".options"); intiator.addEventListener("click", e => { document.querySelectorAll(".options").forEach(option => { if (option !== select_options) { option.classList.remove("open"); } }); select_options.classList.toggle("open"); const options = select_options.querySelectorAll(".option"); options.forEach(option => { option.addEventListener("click", () => { intiator.querySelector(".text").innerHTML = option.getAttribute("value"); select_options.classList.remove("open"); if (select.getAttribute("action") === "fs") { if (select.getAttribute("action-for") === "wallpaper-fill") { switch (option.getAttribute("value").toLowerCase()) { case "cover": tb.desktop.wallpaper.cover(); break; case "contain": tb.desktop.wallpaper.contain(); break; case "stretch": tb.desktop.wallpaper.stretch(); break; } } else if (select.getAttribute("action-for") === "proxy") { switch (option.getAttribute("value").toLowerCase()) { case "Ultraviolet": tb.proxy.set("Ultraviolet"); break; case "Scramjet": tb.proxy.set("Scramjet"); break; } } else if (select.getAttribute("action-for") === "show-seconds") { switch (option.getAttribute("value").toLowerCase()) { case "no": 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["times"]["showSeconds"] = false; window.tb.fs.writeFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, JSON.stringify(settings)); }); break; case "yes": 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["times"]["showSeconds"] = true; window.tb.fs.writeFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, JSON.stringify(settings)); }); break; } } else if (select.getAttribute("action-for") === "24h-12h") { switch (option.getAttribute("value").toLowerCase()) { case "no": 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["times"]["format"] = "12h"; window.tb.fs.writeFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, JSON.stringify(settings)); }); break; case "yes": 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["times"]["format"] = "24h"; window.tb.fs.writeFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, JSON.stringify(settings)); }); break; } } else if (select.getAttribute("action-for") === "location-state") { window.tb.fs.readFile("/system/etc/terbium/settings.json", "utf8", (err, data) => { if (err) return console.log(err); let settings = JSON.parse(data); settings["location"]["state"] = option.getAttribute("value"); window.tb.fs.writeFile("/system/etc/terbium/settings.json", JSON.stringify(settings)); }); } else if (select.getAttribute("action-for") === "temperature-unit") { window.tb.fs.readFile("/system/etc/terbium/settings.json", "utf8", (err, data) => { if (err) return console.log(err); let settings = JSON.parse(data); settings["weather"]["unit"] = option.getAttribute("value"); 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")); }); } } }); }); }); }); ================================================ FILE: public/apps/task manager.tapp/index.css ================================================ @font-face { font-family: Inter; src: url(/fonts/Inter.ttf); } h1 { font-family: Inter; font-weight: 700; } h4 { margin-top: 4px; margin-bottom: 4px; } .image-container { width: 100px; } .centered-image { max-width: 100%; max-height: 100%; } a { color: #5088ff; text-decoration: none; } a:hover { text-decoration: underline; } .opt { width: 100%; height: 100%; margin-top: 35px; } .opt.disabled { display: none; } .topnav { display: flex; flex-direction: row; gap: 5px; } .topnav .sec { margin-top: 5px; background-color: #ffffff24; border-radius: 8px; display: flex; flex-direction: row; justify-content: center; padding: 6px 16px; } .opt .topbr { /*display: flex; position: relative; flex-direction: row;*/ width: 100%; } .opt .topbr h2 { color: #c0bdc1; font-size: 24px; } .opt .topbr h1 { font-size: 32px; } .opt .topbr .m { display: flex; flex-direction: row; align-self: flex-start; justify-content: flex-start; text-align: start; } .opt .topbr .sub { display: flex; flex-direction: row; align-self: flex-end; justify-content: flex-end; text-align: end; margin-top: -50px; gap: 50px; margin-right: 25px; } .opt .apps { display: flex; flex-direction: column; } .opt .apps .apl { display: flex; gap: 25px; align-items: center; width: 100%; } .opt .apps .apl .end-text { margin-left: auto; display: flex; gap: 25px; } .opt .apps .apl .end-text .btn { margin-right: 10px; } .opt .apps .apl img { width: 25px; height: 25px; } ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-thumb { background-color: #ffffff28; border-radius: 8px; } ::-webkit-scrollbar-track { background-color: #ffffff10; border-radius: 8px; } .navbtn span { backdrop-filter: blur(100px) contrast(0.8) brightness(0.8); z-index: 9999; background-color: #ffffff28; } ================================================ FILE: public/apps/task manager.tapp/index.html ================================================ Task Manager
Name Memory PID State
Loading Tasks (This will take a few seconds)

System Information (estimated)

Startup Processes

Name Scope Command Installed By Enabled
Loading startup processes...
================================================ FILE: public/apps/task manager.tapp/index.js ================================================ const navbuttons = document.querySelectorAll(".navbtn"); navbuttons.forEach(btn => { const tooltip = btn.querySelector("span"); let btnWidth = btn.getBoundingClientRect().width; tooltip.classList.add(`top-[${btnWidth + 4}px]`); btn.addEventListener( "mouseover", () => { setTimeout(() => { if (btn.matches(":hover")) { tooltip.classList.remove("opacity-0"); tooltip.classList.remove(`top-[${btnWidth + 4}px]`); tooltip.classList.add(`top-[${btnWidth + 14}px]`); btn.addEventListener("mouseleave", () => { tooltip.classList.add("opacity-0"); tooltip.classList.remove(`top-[${btnWidth + 14}px]`); tooltip.classList.add(`top-[${btnWidth + 4}px]`); }); } }, 1000); }, "once", ); }); document.addEventListener("DOMContentLoaded", () => { getTasks(); setInterval(getTasks, 2500); }); function loadPane(val) { const sys = document.getElementById("sysinf"); const main = document.getElementById("appl"); const startup = document.getElementById("startup"); if (val === "sys") { sys.classList.remove("opacity-0", "pointer-events-none"); main.classList.add("opacity-0", "pointer-events-none"); startup.classList.add("opacity-0", "pointer-events-none"); getSpecs(); } else if (val === "startup") { startup.classList.remove("opacity-0", "pointer-events-none"); main.classList.add("opacity-0", "pointer-events-none"); sys.classList.add("opacity-0", "pointer-events-none"); getStartups(); } else { main.classList.remove("opacity-0", "pointer-events-none"); sys.classList.add("opacity-0", "pointer-events-none"); startup.classList.add("opacity-0", "pointer-events-none"); } } function getSpecs() { const cputxt = document.getElementById("cpu"); const memtxt = document.getElementById("ram"); const ssdtxt = document.getElementById("ssd"); const gputxt = document.getElementById("gpu"); let mem = navigator.deviceMemory ? navigator.deviceMemory + "GB" + " of ram" : "Not Available"; let canvas = document.createElement("canvas"); let gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); if (!gl) { console.error("%cGPU%c: Information not available", `color: ${accent}`, "color: #b6b6b6"); return; } let gpuName; let dbgRenderInfo = gl.getExtension("WEBGL_debug_renderer_info"); if (dbgRenderInfo) { let rndr = gl.getParameter(dbgRenderInfo.UNMASKED_RENDERER_WEBGL); let regex = /ANGLE \(.+?,\s*(.+?) \(/; let match = rndr.match(regex); gpuName = match ? match[1] : "Not Available"; } navigator.storage.estimate().then(estimate => { const totalSize = estimate.quota; const usedSize = estimate.usage; const usedPercentage = (usedSize / totalSize) * 100; let formattedUsedSize, formattedTotalSize; if (usedSize >= 1024 * 1024 * 1024) { formattedUsedSize = `${(usedSize / (1024 * 1024 * 1024)).toFixed(2)} GB`; } else { formattedUsedSize = `${(usedSize / (1024 * 1024)).toFixed(2)} MB`; } if (totalSize >= 1024 * 1024 * 1024) { formattedTotalSize = `${(totalSize / (1024 * 1024 * 1024)).toFixed(2)} GB`; } else { formattedTotalSize = `${Math.round((totalSize / (1024 * 1024)).toFixed(2))} MB`; } ssdtxt.textContent = `${formattedUsedSize} of ${formattedTotalSize}`; }); let cpuCors = navigator.hardwareConcurrency; cputxt.textContent = `${cpuCors} Logical Cores (${Math.floor(cpuCors / 2)} Cores ${cpuCors} threads)`; let memoryUsed = window.tman_info && window.tman_info.bytes ? window.tman_info.bytes : 0; let formattedMemoryUsed; if (memoryUsed >= 1024 * 1024 * 1024) { formattedMemoryUsed = `${(memoryUsed / (1024 * 1024 * 1024)).toFixed(2)} GB`; } else { formattedMemoryUsed = `${(memoryUsed / (1024 * 1024)).toFixed(2)} MB`; } memtxt.textContent = `${formattedMemoryUsed} of ${mem}`; gputxt.textContent = gpuName; } async function getTasks() { let mem; if ("measureUserAgentSpecificMemory" in window.parent.performance) { mem = await window.parent.performance.measureUserAgentSpecificMemory(); } else { mem = { bytes: 0, breakdown: [] }; } window.tman_info = mem; const windows = window.parent.tb.process.list(); let main = document.querySelector("tbody"); let existingEntries = main.querySelectorAll("tr"); let currentWinIds = Array.from(existingEntries).map(entry => entry.getAttribute("win-id")); const currentIdsSet = new Set(currentWinIds); Object.values(windows).forEach(win => { const winID = win.id; if (currentIdsSet.has(winID)) { currentIdsSet.delete(winID); return; } const sysRegex = /^Terbium (Alexa Desktop Experience|Service Worker|Node\.js Runtime)$/; const tr = document.createElement("tr"); tr.classList.add("hover:bg-[#ffffff18]", "duration-150", "ease-in-out", "px-2.5"); tr.setAttribute("win-id", winID); const thName = document.createElement("th"); thName.textContent = typeof win.name === "string" ? win.name : win.name.text; thName.classList.add("text-left", "py-2.5", "pl-3.5", "pr-[100px]"); const tdMemory = document.createElement("td"); let memEntry = null; if (mem && Array.isArray(mem.breakdown)) { memEntry = mem.breakdown.find(entry => entry.attribution.some(attr => { return attr.container && attr.container.src === win.src; }), ); } if (memEntry && typeof memEntry.bytes === "number") { tdMemory.textContent = `${(memEntry.bytes / (1024 * 1024)).toFixed(2)} MB`; } else if (sysRegex.test(win.name)) { tdMemory.textContent = "System Process"; } else { tdMemory.textContent = "N/A"; } const tdPID = document.createElement("td"); tdPID.textContent = win.pid; const tdState = document.createElement("td"); const stateText = document.createElement("span"); if (win.pid === window.parent.tb.window.getId()) { stateText.textContent = "Active"; } else if (sysRegex.test(win.name)) { stateText.textContent = "Active"; } else { stateText.textContent = "Idle"; } tdState.appendChild(stateText); const tdActions = document.createElement("td"); const btnEnd = document.createElement("button"); btnEnd.innerHTML = ``; btnEnd.onclick = () => { window.parent.tb.process.kill(win.pid); }; tr.appendChild(thName); tr.appendChild(tdMemory); tr.appendChild(tdPID); tr.appendChild(tdState); tr.appendChild(tdActions); tdActions.appendChild(btnEnd); main.appendChild(tr); }); existingEntries.forEach(entry => { const winID = entry.getAttribute("win-id"); if (!Object.values(windows).some(win => win.id === winID)) { main.removeChild(entry); } }); } async function getStartups() { const tbody = document.getElementById("startupTbody"); if (!tbody) return; tbody.innerHTML = "Loading startup processes..."; let data; try { data = await window.parent.tb.system.startup.list(); } catch (err) { console.error("Failed to load startup list:", err); tbody.innerHTML = "Failed to load startup processes"; return; } const scopeFilterEl = document.getElementById("startupScopeSelect"); const scopeFilter = scopeFilterEl ? scopeFilterEl.value : "all"; const entries = []; for (const scope of Object.keys(data || {})) { const procs = data[scope] || {}; for (const name of Object.keys(procs)) { const e = procs[name]; entries.push({ name, scope, start: e.start, installedby: e.installedby || "-", enabled: !!e.enabled }); } } if (entries.length === 0) { tbody.innerHTML = "No Startup processes found"; return; } const filtered = entries.filter(ent => (scopeFilter === "all" ? true : ent.scope === scopeFilter)); if (filtered.length === 0) { tbody.innerHTML = "No Startup processes found for selected scope"; return; } tbody.innerHTML = ""; for (const ent of filtered) { const tr = document.createElement("tr"); tr.classList.add("hover:bg-[#ffffff18]", "duration-150", "ease-in-out", "px-2.5"); const thName = document.createElement("th"); thName.textContent = ent.name; thName.classList.add("text-left", "py-2.5", "pl-3.5", "pr-[100px]"); const tdScope = document.createElement("td"); tdScope.textContent = ent.scope; tdScope.classList.add("px-3.5"); const tdCmd = document.createElement("td"); tdCmd.classList.add("px-3.5", "text-sm", "text-[#ffffffb3]"); tdCmd.textContent = ent.start; const tdInstalled = document.createElement("td"); tdInstalled.classList.add("px-3.5"); tdInstalled.textContent = ent.installedby; const tdEnabled = document.createElement("td"); const enabledSpan = document.createElement("span"); enabledSpan.textContent = ent.enabled ? "Yes" : "No"; tdEnabled.appendChild(enabledSpan); const tdActions = document.createElement("td"); const btnToggle = document.createElement("button"); btnToggle.classList.add("mr-2", "w-max", "py-1", "px-2", "rounded-md", "bg-[#ffffff10]"); btnToggle.textContent = ent.enabled ? "Disable" : "Enable"; btnToggle.onclick = async () => { try { if (ent.enabled) { await window.parent.tb.system.startup.disable(ent.name, ent.scope === "System" ? "System" : "User"); } else { await window.parent.tb.system.startup.enable(ent.name, ent.scope === "System" ? "System" : "User"); } await getStartups(); } catch (err) { console.error(err); } }; const btnRemove = document.createElement("button"); btnRemove.classList.add("w-max", "py-1", "px-2", "rounded-md", "bg-[#ff000018]"); btnRemove.textContent = "Remove"; btnRemove.onclick = async () => { window.parent.tb.dialog.Select({ title: `Remove startup entry '${ent.name}'?`, options: [ { text: "Yes", value: "yes" }, { text: "No", value: "no" }, ], onOk: async choice => { if (choice === "yes") { try { await window.parent.tb.system.startup.removeProc(ent.name, ent.scope === "System" ? "System" : "User"); await getStartups(); } catch (err) { console.error(err); } } }, }); }; tdActions.appendChild(btnToggle); tdActions.appendChild(btnRemove); tr.appendChild(thName); tr.appendChild(tdScope); tr.appendChild(tdCmd); tr.appendChild(tdInstalled); tr.appendChild(tdEnabled); tr.appendChild(tdActions); document.getElementById("startupTbody").appendChild(tr); } } const refreshBtn = document.getElementById("refreshStartup"); if (refreshBtn) refreshBtn.addEventListener("click", getStartups); const scopeSelect = document.getElementById("startupScopeSelect"); if (scopeSelect) scopeSelect.addEventListener("change", getStartups); const addBtn = document.getElementById("addStartupBtn"); if (addBtn) { addBtn.addEventListener("click", () => { window.parent.tb.dialog.Message({ title: "Enter a name for the startup entry", onOk: name => { if (!name) return; window.parent.tb.dialog.Select({ title: "Select scope", options: [ { text: "System", value: "System" }, { text: "User", value: "User" }, ], onOk: scope => { window.parent.tb.dialog.Message({ title: "Enter the start command (optional)", onOk: async cmd => { try { await window.parent.tb.system.startup.addProc(name, scope, cmd || undefined); getStartups(); } catch (err) { console.error(err); } }, }); }, }); }, }); }); } ================================================ FILE: public/apps/task manager.tapp/index.json ================================================ { "name": "Task Manager", "config": { "title": "Task Manager", "icon": "/fs/apps/system/task manager.tapp/icon.svg", "src": "/fs/apps/system/task manager.tapp/index.html" } } ================================================ FILE: public/apps/terminal.tapp/index.html ================================================ terminal
================================================ FILE: public/apps/terminal.tapp/index.js ================================================ import parser from "https://unpkg.com/yargs-parser@22.0.0/browser.js"; import http from "https://cdn.jsdelivr.net/npm/isomorphic-git@latest/http/web/index.js"; import git from "https://cdn.jsdelivr.net/npm/isomorphic-git@latest/+esm"; import { Terminal } from "https://cdn.jsdelivr.net/npm/@xterm/xterm@latest/+esm"; /** * @typedef {import("yargs-parser").Arguments} argv */ /** * @typedef {function(string, argv)} commandHandler */ /** * @typedef {Object} appInfo * @property {string} name The name of the app * @property {string} description The description of the app * @property {string} usage How to use the app from the CLI */ // This is just to resove the terbium system api's const tb = window.tb || window.parent.tb || {}; window.http = http; window.gitfetch = git; /** * Converts a hex color to an RGB string * @param {string} hex The hex color to convert * @returns {{r: number, g: number, b: number} | null} The RGB object for use in accent, or null if invalid */ function htorgb(hex) { hex = hex.replace("#", ""); if (hex.length === 3) { hex = hex .split("") .map(h => h + h) .join(""); } if (hex.length !== 6) return null; const bigint = parseInt(hex, 16); return { r: (bigint >> 16) & 255, g: (bigint >> 8) & 255, b: bigint & 255, }; } /** * Command that has been captured from the start of the other command prompt ending and after the newline carriage */ let accCommand = ""; /** * Cursor position within the current command (for left/right arrow navigation) */ let cursorPos = 0; tb.setCommandProcessing = status => { sessions.forEach(s => { try { s.isProcessingCommands = status; s.localEcho = status; } catch (e) {} }); }; /** * Last few commands that have been executed */ let commandHistory = []; let historyIndex = -1; let path = `/home/${sessionStorage.getItem("currAcc")}/`; const HISTORY_LIMIT = 1000; const HISTORY_FILE = ".bash_history"; class TerminalSession { constructor(name = "Terbium TSH") { this.id = `s-${Date.now()}-${Math.floor(Math.random() * 1000)}`; this.name = name; this.container = document.createElement("div"); this.container.className = "term-session"; this.container.style.width = "100%"; this.container.style.height = "100%"; this.container.style.display = "none"; document.getElementById("term").appendChild(this.container); this.term = new Terminal({ theme: { background: "#000000", cursor: "#ffffff", selection: "#444444" }, cursorBlink: true, allowTransparency: true, rightClickSelectsWord: true }); this.term.open(this.container); this.accCommand = ""; this.cursorPos = 0; this.isProcessingCommands = true; this.localEcho = true; this.commandHistory = []; this.historyIndex = 0; this.path = `/home/${sessionStorage.getItem("currAcc")}/`; this._bindEvents(); this.loadHistory(); this.writePowerline(); this.focus(); } _bindEvents() { this.term.element.addEventListener("contextmenu", e => e.preventDefault()); this.term.attachCustomKeyEventHandler(event => { if (event.ctrlKey && event.key === "c") { if (this.term.hasSelection()) { navigator.clipboard.writeText(this.term.getSelection()).catch(console.error); event.preventDefault(); return false; } } if (event.ctrlKey && event.key === "v") { navigator.clipboard .readText() .then(text => { for (const c of text) this.handleChar(c); }) .catch(console.error); event.preventDefault(); return false; } if (event.altKey && event.key === "t") { event.preventDefault(); createSession("Terbium TSH"); return false; } if (event.altKey && event.key === "w") { event.preventDefault(); closeSession(this.id); return false; } if (event.altKey && event.key === "Tab") { event.preventDefault(); switchSessionNext(); return false; } return true; }); this.term.onData(async char => { if (!this.isProcessingCommands) { const dataHandler = this.term._core._inputHandler; if (dataHandler && dataHandler.onData) dataHandler.onData(char); return; } if (char.length > 1 && !char.startsWith("\x1b")) { for (const c of char) await this.handleChar(c); return; } await this.handleChar(char); }); this.term.onLineFeed(() => { this.accCommand = ""; this.cursorPos = 0; this.historyIndex = this.commandHistory.length; }); } async handleChar(char) { if (char === "\x1b[A") { if (this.historyIndex > 0 && this.commandHistory.length > 0) { this.term.write("\r\x1b[K"); await this.writePowerline(); this.historyIndex--; this.accCommand = this.commandHistory[this.historyIndex]; this.cursorPos = this.accCommand.length; this.term.write(this.accCommand); } return; } if (char === "\x1b[B") { this.term.write("\r\x1b[K"); await this.writePowerline(); if (this.historyIndex < this.commandHistory.length - 1) { this.historyIndex++; this.accCommand = this.commandHistory[this.historyIndex]; this.cursorPos = this.accCommand.length; this.term.write(this.accCommand); } else { this.historyIndex = this.commandHistory.length; this.accCommand = ""; this.cursorPos = 0; } return; } if (char === "\x1b[D") { if (this.cursorPos > 0) { this.cursorPos--; this.term.write("\x1b[D"); } return; } if (char === "\x1b[C") { if (this.cursorPos < this.accCommand.length) { this.cursorPos++; this.term.write("\x1b[C"); } return; } if (char === "\x1b[H" || char === "\x1b[1~") { const moveLeft = this.cursorPos; if (moveLeft > 0) { this.term.write(`\x1b[${moveLeft}D`); this.cursorPos = 0; } return; } if (char === "\x1b[F" || char === "\x1b[4~") { const moveRight = this.accCommand.length - this.cursorPos; if (moveRight > 0) { this.term.write(`\x1b[${moveRight}C`); this.cursorPos = this.accCommand.length; } return; } if (char === "\x1b[3~") { if (this.cursorPos < this.accCommand.length) { this.accCommand = this.accCommand.slice(0, this.cursorPos) + this.accCommand.slice(this.cursorPos + 1); const remaining = this.accCommand.slice(this.cursorPos); this.term.write(remaining + " "); this.term.write(`\x1b[${remaining.length + 1}D`); } return; } if (char === "\x7f") { if (this.cursorPos > 0) { this.accCommand = this.accCommand.slice(0, this.cursorPos - 1) + this.accCommand.slice(this.cursorPos); this.cursorPos--; this.term.write("\b"); const remaining = this.accCommand.slice(this.cursorPos); this.term.write(remaining + " "); this.term.write(`\x1b[${remaining.length + 1}D`); } return; } if (char === "\r") { this.term.writeln(""); const input = this.accCommand.trim(); if (input.length > 0) { await this.saveToHistory(input); const [cmd, ...rawArgs] = input.split(" "); const argv = parser(rawArgs); argv._raw = rawArgs.join(" "); await this.handleCommand(cmd, argv); } else { await this.writePowerline(); } this.accCommand = ""; this.cursorPos = 0; return; } if (char >= " " && char <= "~") { // Only echo locally when localEcho is enabled. In passthrough mode the program will echo input itself. this.accCommand = this.accCommand.slice(0, this.cursorPos) + char + this.accCommand.slice(this.cursorPos); this.cursorPos++; if (this.localEcho) { if (this.cursorPos === this.accCommand.length) { this.term.write(char); } else { const remaining = this.accCommand.slice(this.cursorPos - 1); this.term.write(remaining); this.term.write(`\x1b[${remaining.length - 1}D`); } } } } async writePowerline() { const username = await tb.user.username(); const userSettings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${username}/settings.json`, "utf8")); const accent = await htorgb(userSettings.accent); const hostname = JSON.parse(await window.parent.tb.fs.promises.readFile("//system/etc/terbium/settings.json"))["host-name"]; this.term.write(`\x1b[38;2;${accent.r};${accent.g};${accent.b}m${username}@${hostname}\x1b[39m ~ ${this.path}\x1b[0m: `); } async createNewCommandInput() { this.term.write("\r\n"); await this.writePowerline(); this.historyIndex = this.commandHistory.length; } async displayOutput(message, ...styles) { // If output indicates a shell/program exit, restore local echo / command processing try { const txt = String(message || ""); if (/exited with code|shell exited|exit code/gi.test(txt)) { this.exitPassthrough(); } } catch (e) {} if (message.includes("%c")) { const parts = message.split(/(%c)/); let styled = ""; let styleIndex = 0; for (let i = 0; i < parts.length; i++) { if (parts[i] === "%c") { const text = parts[++i] || ""; const style = styles[styleIndex++] || ""; const colorMatch = style.match(/color:\s*(#[0-9a-fA-F]{3,6})/); if (colorMatch) { const rgb = await htorgb(colorMatch[1]); if (rgb) { styled += `\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m${text}\x1b[0m`; } else { styled += text; } } else { styled += text; } } else { styled += parts[i]; } } this.term.writeln(styled); } else { this.term.writeln(message); } } async displayError(message) { this.term.writeln(`\x1b[31mERR: ${message}\x1b[0m`); } async loadHistory() { try { const username = await tb.user.username(); const historyPath = `/home/${username}/${HISTORY_FILE}`; const data = await window.parent.tb.fs.promises.readFile(historyPath, "utf8"); this.commandHistory = data.split("\n").filter(cmd => cmd.trim() !== ""); } catch {} this.historyIndex = this.commandHistory.length; } async saveToHistory(command) { if (!command.trim()) return; this.commandHistory.push(command); if (this.commandHistory.length > HISTORY_LIMIT) this.commandHistory.shift(); this.historyIndex = this.commandHistory.length; try { const username = await tb.user.username(); const historyPath = `/home/${username}/${HISTORY_FILE}`; await window.parent.tb.fs.promises.writeFile(historyPath, this.commandHistory.join("\n")); } catch (error) { console.error("Failed to save history", error); } } async handleCommand(name, args) { const appInfo = await getAppInfo(); if (name === "exit") { closeSession(this.id); return; } if (name === "help") { const commands = appInfo ? appInfo.join(", ") : "No commands available"; this.displayOutput(`Available commands: exit, help, ${commands}`); this.createNewCommandInput(); return; } // If this command is an interactive shell-like command, enter passthrough mode const INTERACTIVE_CMDS = new Set(["node", "python", "bash", "sh", "nodejs", "jsh", "pwsh", "powershell"]); if (INTERACTIVE_CMDS.has(name.toLowerCase())) { this.enterPassthrough(); } const scriptPaths = [`/fs/apps/system/terminal.tapp/scripts/${name.toLowerCase()}.js`, `/apps/terminal.tapp/scripts/${name.toLowerCase()}.js`]; if (appInfo === null) { this.displayOutput("Failed to fetch app info, cannot execute command"); this.createNewCommandInput(); this.exitPassthrough(); return; } if (!appInfo.includes(name)) { this.displayOutput(`Command '${name}' not found! Type 'help' for a list of commands.`); this.createNewCommandInput(); this.exitPassthrough(); return; } let scriptRes; try { scriptRes = await fetch(scriptPaths[0]); } catch { try { scriptRes = await fetch(scriptPaths[1]); } catch (error) { this.displayOutput(`Failed to fetch script: ${error.message}`); this.createNewCommandInput(); this.exitPassthrough(); return; } } try { const script = await scriptRes.text(); const fn = new Function("args", "displayOutput", "createNewCommandInput", "displayError", "term", "path", "terbium", "buffer", "setTabTitle", "exitPassthrough", script); fn(args, this.displayOutput.bind(this), this.createNewCommandInput.bind(this), this.displayError.bind(this), this.term, this.path, window.parent.tb, window.parent.tb.buffer, this.setName.bind(this), this.exitPassthrough.bind(this)); } catch (error) { this.displayOutput(`Failed to execute command '${name}': ${error.message}`); this.createNewCommandInput(); this.exitPassthrough(); return; } } resize() { try { const charWidth = this.term._core._renderService.dimensions.css.cell.width; const charHeight = this.term._core._renderService.dimensions.css.cell.height; const cols = Math.floor(window.innerWidth / charWidth); const rows = Math.floor(window.innerHeight / charHeight); this.term.resize(cols, rows); } catch (e) {} } focus() { try { this.term.focus(); window.term = this.term; } catch (e) {} } enterPassthrough() { this.isProcessingCommands = false; this.localEcho = false; } exitPassthrough() { this.isProcessingCommands = true; this.localEcho = true; // show a new prompt after exiting passthrough try { this.createNewCommandInput(); } catch (e) {} } setName(newName) { this.name = newName; const win = getWinRoot(); if (win) { const tab = win.querySelector(`.term-tab[data-sid="${this.id}"]`); if (tab) { tab.querySelector(".label").textContent = newName; } } } destroy() { try { this.term.dispose(); } catch {} try { this.container.remove(); } catch {} } } const sessions = []; let activeSession = null; // Guard to prevent duplicate rapid-close actions (e.g., a single keypress being handled by two handlers) let _lastCloseTime = 0; function getWinRoot() { try { return window.frameElement?.closest("[pid]"); } catch { return null; } } function addTabToTitle(session) { const win = getWinRoot(); if (!win) return; const tabList = win.querySelector(".term-tab-list"); if (!tabList) return; const btn = document.createElement("button"); btn.className = "term-tab"; btn.dataset.sid = session.id; btn.innerHTML = `${session.name}×`; tabList.appendChild(btn); btn.addEventListener("click", e => { if (e.target && e.target.classList && e.target.classList.contains("close")) { closeSession(session.id); } else { switchSession(session.id); } }); const add = win.querySelector(".term-add"); if (add && !add.dataset.bound) { add.dataset.bound = "1"; add.addEventListener("click", () => createSession("Terbium TSH")); } setActiveTabInTitle(); } function removeTabFromTitle(id) { const win = getWinRoot(); if (!win) return; const tab = win.querySelector(`.term-tab[data-sid="${id}"]`); if (tab) tab.remove(); setActiveTabInTitle(); } function setActiveTabInTitle() { const win = getWinRoot(); if (!win) return; win.querySelectorAll(".term-tab").forEach(t => t.classList.remove("active")); if (!activeSession) return; const sel = win.querySelector(`.term-tab[data-sid="${activeSession.id}"]`); if (sel) sel.classList.add("active"); } function createSession(name = "Terbium TSH") { const isFirst = sessions.length === 0; const s = new TerminalSession(name); sessions.push(s); if (isFirst) { try { s.term.writeln(`TerbiumOS [Version: ${tb.system.version()}]`); s.term.writeln(`Type 'help' for a list of commands.`); } catch (e) { console.error("Failed to display welcome message", e); } } sessions.forEach(se => { se.container.style.display = se === s ? "flex" : "none"; }); activeSession = s; addTabToTitle(s); setActiveTabInTitle(); return s; } function switchSession(id) { const s = sessions.find(x => x.id === id); if (!s) return; sessions.forEach(se => (se.container.style.display = se === s ? "flex" : "none")); activeSession = s; s.focus(); setActiveTabInTitle(); } function switchSessionNext() { if (sessions.length <= 1) return; const idx = sessions.indexOf(activeSession); const next = sessions[(idx + 1) % sessions.length]; switchSession(next.id); } function closeSession(id) { const now = Date.now(); if (now - _lastCloseTime < 250) return; _lastCloseTime = now; const idx = sessions.findIndex(s => s.id === id); if (idx < 0) return; const wasActive = sessions[idx] === activeSession; sessions[idx].destroy(); removeTabFromTitle(id); sessions.splice(idx, 1); if (wasActive) { if (sessions.length) { switchSession(sessions[Math.max(0, idx - 1)].id); } else { activeSession = null; } } if (sessions.length === 0) { window.parent.tb.window.close(); } } document.addEventListener("DOMContentLoaded", () => { createSession("Terbium TSH"); window.addEventListener("resize", () => { sessions.forEach(s => s.resize()); }); }); window.handleCommand = (...args) => (activeSession ? activeSession.handleCommand(...args) : handleCommand(...args)); window.addEventListener("updPath", e => { if (activeSession) activeSession.path = e.detail; }); /** * Resizes the active session terminal to fit the window * @returns {void} */ function resizeTerm() { try { if (activeSession && activeSession.term && activeSession.term._core) { const charWidth = activeSession.term._core._renderService.dimensions.css.cell.width; const charHeight = activeSession.term._core._renderService.dimensions.css.cell.height; const cols = Math.floor(window.innerWidth / charWidth); const rows = Math.floor(window.innerHeight / charHeight); activeSession.term.resize(cols, rows); } } catch (e) { /* ignore */ } } setTimeout(resizeTerm, 50); window.addEventListener("resize", resizeTerm); /** * The command handler, which executes the commands in `scripts/` * @param {string} name The command name * @param {argv} args The command's respective args (from yargs-parser) * @returns {Promise} */ async function handleCommand(name, args) { // Prefer activeSession handling if available if (typeof activeSession !== "undefined" && activeSession) { return activeSession.handleCommand(name, args); } // If no active session, attempt minimal fallback: try to run script but without a term const scriptPaths = [`/fs/apps/system/terminal.tapp/scripts/${name.toLowerCase()}.js`, `/apps/terminal.tapp/scripts/${name.toLowerCase()}.js`]; const appInfo = await getAppInfo(); if (appInfo === null) { console.error("Failed to fetch app info, cannot execute command"); return; } if (!appInfo.includes(name)) { console.error(`Command '${name}' not found!`); return; } let scriptRes; try { scriptRes = await fetch(scriptPaths[0]); } catch { try { scriptRes = await fetch(scriptPaths[1]); } catch (error) { console.error(`Failed to fetch script: ${error.message}`); return; } } try { const script = await scriptRes.text(); const fn = new Function("args", "displayOutput", "createNewCommandInput", "displayError", "term", "path", "terbium", "buffer", script); fn( args, (m, ...s) => console.log(m), () => {}, e => console.error(e), undefined, path, window.parent.tb, window.parent.tb.buffer, ); } catch (error) { console.error(`Failed to execute command '${name}': ${error.message}`); return; } } // Expose a delegating handler so other frames can always call it window.handleCommand = (...args) => { if (typeof activeSession !== "undefined" && activeSession) return activeSession.handleCommand(...args); return handleCommand(...args); }; window.addEventListener("updPath", e => { path = e.detail; }); /** * Fetches the app info from the `info.json` file * @param {boolean} justNames Whether to return just the app names or the full app info * @returns {Promise} The app names or the full app info */ async function getAppInfo(justNames = true) { /** * @type {Response} */ const appInfoResUsr = await fetch(`/fs/apps/user/${await tb.user.username()}/terminal/info.json`); /** * @type {Response} */ const appInfoResSys = await fetch(`/fs/apps/system/terminal.tapp/scripts/info.json`); /** * @type {Response} */ let appInfo; try { let appInfoUsr = await appInfoResUsr.json(); let appInfoSys = await appInfoResSys.json(); if (!Array.isArray(appInfoUsr)) appInfoUsr = appInfoUsr ? [appInfoUsr] : []; if (!Array.isArray(appInfoSys)) appInfoSys = appInfoSys ? [appInfoSys] : []; appInfo = [...appInfoUsr, ...appInfoSys]; } catch (error) { displayError(`Failed to parse one or more info.json files: ${error.message}`); createNewCommandInput(); return null; } if (justNames) return appInfo.map(app => app.name); return appInfo; } /** * Displays a styled message to the terminal * @param {string} message The message to display, can include %c for styling * @param {...string} styles CSS style strings for each %c in the message * @returns {Promise} */ async function displayOutput(message, ...styles) { if (typeof activeSession !== "undefined" && activeSession) return activeSession.displayOutput(message, ...styles); if (message.includes("%c")) console.log(message.replace(/%c/g, "")); else console.log(message); } /** * Writes the powerline prompt to the terminal * @returns {Promise} */ async function writePowerline() { if (typeof activeSession !== "undefined" && activeSession) return activeSession.writePowerline(); // fallback: no-op } /** * Creates new command line with a styled prompt * @returns {Promise} */ async function createNewCommandInput() { if (typeof activeSession !== "undefined" && activeSession) return activeSession.createNewCommandInput(); // fallback: no-op } /** * Logs an error message to terminal * @param {string} message The error message that will be displayed on the output */ function displayError(message) { if (typeof activeSession !== "undefined" && activeSession) return activeSession.displayError(message); console.error(message); } /** * Load the current history from the bash history file * @returns {Promise} */ async function loadHistory() { if (typeof activeSession !== "undefined" && activeSession) return activeSession.loadHistory(); // fallback: no-op } /** * Saves a command to the bash history file * @param {string} command The command to save to history * @returns {Promise} */ async function saveToHistory(command) { if (typeof activeSession !== "undefined" && activeSession) return activeSession.saveToHistory(command); if (!command.trim()) return; commandHistory.push(command); if (commandHistory.length > HISTORY_LIMIT) commandHistory.shift(); historyIndex = commandHistory.length; try { const username = await tb.user.username(); const historyPath = `/home/${username}/${HISTORY_FILE}`; await window.parent.tb.fs.promises.writeFile(historyPath, commandHistory.join("\n")); } catch (error) { console.error("Failed to save history", error); } } ================================================ FILE: public/apps/terminal.tapp/index.json ================================================ { "name": "Terminal", "config": { "title": { "text": "Terminal", "html": "
\n\n
\n
\n
\n\n
\n
\n
" }, "icon": "/fs/apps/system/terminal.tapp/icon.svg", "src": "/fs/apps/system/terminal.tapp/index.html", "size": { "width": 612, "height": 415 } } } ================================================ FILE: public/apps/terminal.tapp/logo.txt ================================================ @@@@@@@@@@@@@@~ B@@@@@@@@#G?. B###&@@@@&####^ #@@@&PPPB@@@G. .. ~@@@@J .. .#@@@P ~&@@@^ ^@@@@? .#@@@@###&@@&7 ^@@@@? .#@@@#555P&@@B7 ^@@@@? .#@@@P G@@@@ ^@@@@? .#@@@&GGG#@@@@Y ^&@@@? B@@@@@@@@&B5~ ================================================ FILE: public/apps/terminal.tapp/scripts/cat.js ================================================ async function cat(args) { if (args._raw.length <= 0) { displayError("cat: missing operand"); createNewCommandInput(); return; } displayOutput(`%cRight now cat only outputs the contents of a file.\n`, "color: #e39d34"); if (path.includes("/mnt/")) { try { const match = path.match(/\/mnt\/([^\/]+)\//); const davName = match ? match[1].toLowerCase() : ""; const text = await tb.vfs.servers.get(davName).connection.promises.readFile(`${path}/${args._raw}`, "utf8"); displayOutput(text); createNewCommandInput(); } catch (e) { displayError(`TNSM cat: ${e.message}`); createNewCommandInput(); return; } } else { tb.sh.cat(`${path}/${args._raw}`, (err, data) => { if (err) { displayError(`cat: ${err.message}`); createNewCommandInput(); } else { displayOutput(data); createNewCommandInput(); } }); } } cat(args); ================================================ FILE: public/apps/terminal.tapp/scripts/cd.js ================================================ function cd(args) { let destination = args._[0]; const raw_destination = destination; if (!destination || destination === "~") { const homePath = `/home/${sessionStorage.getItem("currAcc")}/`; window.dispatchEvent(new CustomEvent("updPath", { detail: homePath })); createNewCommandInput(); return; } if (destination.startsWith("~/")) { destination = destination.replace("~", `/home/${sessionStorage.getItem("currAcc")}`); } let newPath; if (destination.startsWith("/")) { newPath = destination; } else { newPath = path + destination; } const resolvedParts = []; for (const part of newPath.split("/").filter(p => p)) { if (part === "..") { resolvedParts.pop(); } else if (part !== ".") { resolvedParts.push(part); } } let finalPath = "/" + resolvedParts.join("/") + "/"; if (finalPath === "//") { finalPath = "/"; } const checkPath = finalPath.length > 2 ? finalPath.slice(0, -1) : finalPath; window.parent.tb.fs.stat(checkPath, (err, stats) => { if (err) { if (destination.includes("/mnt/") || checkPath.includes("/mnt/")) { window.dispatchEvent( new CustomEvent("updPath", { detail: finalPath, }), ); createNewCommandInput(); } else { displayError(`cd: ${raw_destination}: No such file or directory`); createNewCommandInput(); } } else if (!stats.isDirectory()) { displayError(`cd: ${raw_destination}: Not a directory`); createNewCommandInput(); } else { window.dispatchEvent( new CustomEvent("updPath", { detail: finalPath, }), ); createNewCommandInput(); } }); } cd(args); ================================================ FILE: public/apps/terminal.tapp/scripts/clear.js ================================================ function clear(term) { term.clear(); createNewCommandInput(); } clear(term); ================================================ FILE: public/apps/terminal.tapp/scripts/curl.js ================================================ async function curl(args) { let url = args._raw; if (!url) { displayOutput("Usage: curl "); createNewCommandInput(); return; } let shouldSave = false; if (url.includes("-k")) { shouldSave = true; url = url.replace("-k", "").trim(); } if (shouldSave) { tb.dialog.SaveFile({ title: "Save Script", onOk: async loc => { try { await window.parent.tb.system.download(url, loc); displayOutput(`Saved to ${loc}`); } catch (e) { displayError("Error saving script:", e); return; } createNewCommandInput(); }, }); } else { const response = await window.parent.tb.libcurl.fetch(url); const scriptContent = await response.text(); try { eval(scriptContent); } catch (error) { displayError("Error executing script:", error); } createNewCommandInput(); } } curl(args); ================================================ FILE: public/apps/terminal.tapp/scripts/echo.js ================================================ function echo(args) { displayOutput(args); createNewCommandInput(); } echo(args); ================================================ FILE: public/apps/terminal.tapp/scripts/exit.js ================================================ function exit() { window.parent.tb.window.close(); } exit(); ================================================ FILE: public/apps/terminal.tapp/scripts/git.js ================================================ async function git(args) { let user = await window.parent.tb.user.username(); let currentPath = path; if (currentPath.startsWith("~")) currentPath = currentPath.replace("~", `/home/${window.parent.sessionStorage.getItem("currAcc")}`); let cmds = [ "\ start a working area", "clone: Clone a repository into a new directory", "init: Create an empty Git repository or reinitialize an existing one", "\ work on the current change", "add: Add file contents to the index", "rm: Remove files from the working tree and from the index", "\ examine the history and state", "status: Show the working tree status", "\ grow, mark and tweak your common history", "commit: Record changes to the repository (Make sure to run git add before commiting)", "\ collaborate (Login requires your GitHub Token)", "fetch: Download objects and refs from another repository", "pull: Fetch from and integrate with another repository or a local branch", "push: Update remote refs along with associated objects", ]; try { if (args._raw.includes("clone")) { if (!args._[2]) { path = `/home/${sessionStorage.getItem("currAcc")}/`; } if (path !== "/" && args._[2] === "/") { path = args._[2]; } else if (path !== "/") { path = `${currentPath}/${args._[2]}`; } displayOutput(`Cloning into '${args._[1].split(/(\\|\/)/g).pop()}'...`); const targetDir = args._[2] ?? `${currentPath}/${args._[1].split(/(\\|\/)/g).pop()}`; await window.parent.tb.fs.promises.mkdir(targetDir, { recursive: true }); await gitfetch.clone({ fs: window.parent.tb.fs, http: http, dir: targetDir, corsProxy: "https://cors.isomorphic-git.org", url: args._[1], noCheckout: false, singleBranch: true, depth: 1, onAuth: async () => { return new Promise(async resolve => { await window.parent.tb.dialog.WebAuth({ title: "GitHub Authentication", onOk: async (username, password) => { resolve({ username, password }); }, onCancel: () => { displayError("Authentication was canceled"); createNewCommandInput(); }, }); }); }, onMessage: e => { displayOutput(e); }, }); await gitfetch.setConfig({ fs: window.parent.tb.fs, dir: targetDir, path: "user.name", value: await window.parent.tb.user.username(), }); createNewCommandInput(); } else if (args._raw.includes("init")) { let path = currentPath + args._[1]; if (!args._[1]) { displayError("Error: Target directory must be specified for 'git init'."); createNewCommandInput(); return; } displayOutput(`Initializing empty Git repository in ${path}/.git/...`); await window.parent.tb.fs.promises.mkdir(`${path}/.git`, { recursive: true }); await gitfetch.init({ fs: window.parent.tb.fs, http: http, dir: path, bare: false, defaultBranch: "master", gitdir: `${path}/.git`, }); displayOutput("Initialized empty Git repository."); createNewCommandInput(); } else if (args._raw.includes("checkout")) { if (!args._[1] || !args._[2]) { displayOutput("Usage: git checkout "); createNewCommandInput(); return; } const branchName = args._[1]; const targetDir = currentPath + args._[2]; try { await gitfetch.checkout({ fs: window.parent.tb.fs, dir: targetDir, ref: branchName, onMessage: e => { displayOutput(e); }, }); displayOutput(`Switched branch '${branchName}' in '${targetDir}'`); } catch (error) { displayError(`Error: ${error.message}`); } createNewCommandInput(); } else if (args._raw.includes("add")) { if (!args._[1] || !args._[2]) { displayOutput("Usage: git add "); createNewCommandInput(); return; } const filePath = args._[1]; const targetDir = currentPath + args._[2]; try { await gitfetch.add({ fs: window.parent.tb.fs, dir: targetDir, filepath: filePath, }); displayOutput(`Added '${filePath}' to staging area`); } catch (error) { displayError(`Error: ${error.message}`); } createNewCommandInput(); } else if (args._raw.includes("rm")) { if (!args._[1] || !args._[2]) { displayOutput("Usage: git rm "); createNewCommandInput(); return; } const filePath = args._[1]; const targetDir = currentPath + args._[2]; try { await gitfetch.remove({ fs: window.parent.tb.fs, dir: targetDir, filepath: filePath, }); displayOutput(`Removed '${filePath}' from the working tree and the index`); } catch (error) { displayError(`Error: ${error.message}`); } createNewCommandInput(); } else if (args._raw.includes("status")) { displayError("Command is currently not implemented"); createNewCommandInput(); } else if (args._raw.includes("pull")) { try { const result = await gitfetch.pull({ fs: window.parent.tb.fs, http: http, dir: path, corsProxy: "https://cors.isomorphic-git.org", author: { name: user, email: `${user}@terbiumon.top`, }, onMessage: e => { displayOutput(e); }, }); displayOutput(JSON.stringify(result, null, 2)); } catch (error) { displayError(`Error: ${error.message}`); } createNewCommandInput(); } else if (args._raw.includes("push")) { try { const result = await window.parent.tb.dialog.WebAuth({ title: "GitHub Authentication", onOk: async ({ username, password }) => { try { const gitResult = await gitfetch.push({ fs: window.parent.tb.fs, http: http, dir: path, corsProxy: "https://cors.isomorphic-git.org", remote: "origin", force: false, onMessage: e => { displayOutput(e); }, author: { name: user, email: `${user}@terbiumon.top`, }, onAuth: () => { return { username, password }; }, }); displayOutput(JSON.stringify(gitResult, null, 2)); } catch (error) { displayError(`Error: ${error.message}`); } finally { createNewCommandInput(); } }, onCancel: () => { displayError("GitHub authentication canceled"); createNewCommandInput(); }, }); } catch (error) { displayError(`Error: ${error.message}`); createNewCommandInput(); } } else if (args._raw.includes("fetch")) { if (!args._[1]) { displayOutput("Usage: git fetch "); createNewCommandInput(); return; } const dirName = args._[1]; const targetDir = `${currentPath}/${dirName}/.git`; if (!args._[2]) { displayError("Error: Remote URL must be provided."); createNewCommandInput(); return; } const remoteUrl = args._[2]; try { await gitfetch.fetch({ fs: window.parent.tb.fs, http: http, dir: targetDir, url: remoteUrl, }); displayOutput("Fetch successful."); } catch (error) { displayError(`Error: ${error.message}`); } createNewCommandInput(); } else if (args._raw.includes("commit")) { if (!args._[1]) { displayError("Error: Commit message must be provided."); createNewCommandInput(); return; } const commitMessage = args._[2] || "Blank Commit"; try { await gitfetch.commit({ fs: window.parent.tb.fs, http: http, dir: path, corsProxy: "https://cors.isomorphic-git.org", author: { name: user, email: `${user}@terbiumon.top`, }, message: commitMessage, }); displayOutput("Commit successful."); } catch (error) { displayError(`Error: ${error.message}`); } createNewCommandInput(); } else if (args._raw.includes("gui")) { displayOutput("Opening GitGUI app..."); try { await tb.system.openApp("com.tb.gitgui"); createNewCommandInput(); } catch (err) { displayError(`Error while opening GitGUI: ${err}`); createNewCommandInput(); } } else if (args._raw.includes("version")) { displayOutput(`git version: ${gitfetch.version()}`); createNewCommandInput(); } else { displayOutput("Usage: git [--version] [--help] []"), displayOutput("These are common Git commands used in various situations:"); for (let command of cmds) { if (command.trim() === "") { displayOutput(""); continue; } if (command.startsWith("\ ")) { displayOutput(command.slice(1)); continue; } let [cmd, description] = command.split(": "); displayOutput(` ${cmd.padEnd(15)} ${description}`); } createNewCommandInput(); } } catch (e) { displayError(e); createNewCommandInput(); } } git(args); ================================================ FILE: public/apps/terminal.tapp/scripts/help.js ================================================ async function help(args) { if (args.length > 0) { const scriptName = args[0]; const scriptList = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/terminal/info.json`, "utf8")); const script = scriptList.find(script => script.name === scriptName); if (script) { displayOutput(`${script.name}: ${script.usage ? `${script.usage}` : ""}`); } else { displayError(`help: ${scriptName}: No such script`); } createNewCommandInput(); } else { fetch("./scripts/info.json") .then(response => response.json()) .then(scriptList => { for (let script of scriptList) { displayOutput(`${script.usage.padEnd(30)} ${script.description}`); } createNewCommandInput(); }); } } help(args); ================================================ FILE: public/apps/terminal.tapp/scripts/info.json ================================================ [ { "name": "help", "description": "Display this help message", "usage": "help [command]" }, { "name": "mkdir", "description": "Create a directory", "usage": "mkdir [directory]" }, { "name": "nano", "description": "Simple text editor", "usage": "nano [file]" }, { "name": "touch", "description": "Create a file", "usage": "touch [file]" }, { "name": "rm", "description": "Remove a file or directory", "usage": "rm [file/directory]", "options": { "-r, --recursive": "Remove directories and their contents recursively.", "-f, --force": "Ignore nonexistent files and arguments, never prompt." } }, { "name": "rmdir", "description": "Remove a directory", "usage": "rmdir [directory]" }, { "name": "cd", "description": "Change directory", "usage": "cd [directory]" }, { "name": "ls", "description": "List files in current directory", "usage": "ls" }, { "name": "pwd", "description": "Print the current directory", "usage": "pwd" }, { "name": "pkg", "description": "Package manager for Terbium", "usage": "pkg [package]", "options": { "install": "Install a package", "remove": "Remove a package", "update": "Update a package", "list": "List all packages", "search": "Search for a package" } }, { "name": "git", "description": "Git for Terbium", "usage": "git [command]", "options": { "clone": "Clones a repository", "init": "Initiates a blank repository", "checkout": "Switches the remote branch", "add": "Adds Files to Push", "rm": "Removes Files to Push", "status": "Shows status of Pending Changes (Not implemented yet)", "pull": "Pulls Latest Commit from remote repository", "push": "Pushes Latest Commit to remote repository", "fetch": "Fetches Latest repository Information" } }, { "name": "sysfetch", "description": "Display system information", "usage": "sysfetch" }, { "name": "curl", "description": "Allows you to fetch a script from the internet and run it or save it to the fs", "usage": "curl [script url], -k [path]" }, { "name": "node", "description": "Run JavaScript with Node.js runtime", "usage": "node [options] [script.js] [arguments]", "options": { "-j, --jsh": "Launch the WebContainer JavaScript shell (jsh) instead of Node.js. When this flag is used, no other arguments are passed to the shell.", "-c, --check": "Check the script's syntax without executing it. Exits with an error code if script is invalid.", "-e, --eval string": "Evaluate string as JavaScript.", "-h, --help": "Print command-line options. The output of this option is less detailed than this document.", "-i, --interactive": "Open the REPL even if stdin does not appear to be a terminal.", "-p, --print string": "Identical to -e, but prints the result.", "-r, --require module": "Preload the specified module at startup. Follows require() module resolution rules.", "-v, --version": "Print node's version.", "--abort-on-uncaught-exception": "Aborting instead of exiting causes a core file to be generated for post-mortem analysis using a debugger (such as lldb, gdb, and mdb).", "--cpu-prof": "Start the V8 CPU profiler on start up, and write the CPU profile to disk before exit.", "--diagnostic-dir=directory": "Set the directory to which all diagnostic output files are written. Defaults to current working directory.", "--disable-warning=code-or-type": "Silence specific process warnings by code or type.", "--dns-result-order=order": "Set the default value of order in dns.lookup() and dnsPromises.lookup(). The value could be: ipv4first, ipv6first, verbatim.", "--enable-fips": "Enable FIPS-compliant crypto at startup. (Requires Node.js to be built against FIPS-compatible OpenSSL.)", "--enable-network-family-autoselection": "Enable network family autoselection algorithm.", "--enable-source-maps": "Enable Source Map v3 support for stack traces.", "--experimental-default-type=type": "Set the default module system to use.", "--experimental-import-meta-resolve": "Enable experimental import.meta.resolve() parent URL support.", "--experimental-loader=module": "Use the specified module as a custom loader.", "--experimental-permission": "Enable the experimental Permission Model.", "--experimental-sea-config": "Use this flag to generate a blob that can be injected into the Node.js binary to produce a single executable application.", "--force-context-aware": "Disable loading native addons that are not context-aware.", "--frozen-intrinsics": "Enable experimental frozen intrinsics like Array and Object.", "--heap-prof": "Start the V8 heap profiler on start up, and write the heap profile to disk before exit.", "--icu-data-dir=file": "Specify ICU data load path. (Overrides NODE_ICU_DATA.)", "--input-type=type": "Set the module type for string input via --eval, --print, or STDIN.", "--inspect[=[host:]port]": "Activate inspector on host:port. Default is 127.0.0.1:9229.", "--inspect-brk[=[host:]port]": "Activate inspector on host:port and break at start of user script.", "--inspect-wait[=[host:]port]": "Activate inspector on host:port and wait for debugger to be attached.", "--jitless": "Disable runtime allocation of executable memory.", "--max-http-header-size=size": "Specify the maximum size of HTTP headers in bytes. Defaults to 16KiB.", "--napi-modules": "This option is a no-op. It is kept for compatibility.", "--no-addons": "Disable loading native addons.", "--no-deprecation": "Silence deprecation warnings.", "--no-experimental-fetch": "Disable experimental Fetch API.", "--no-experimental-global-customevent": "Disable exposing CustomEvent on the global scope.", "--no-experimental-global-navigator": "Disable experimental Navigator API.", "--no-experimental-global-webcrypto": "Disable experimental Web Crypto API on the global scope.", "--no-experimental-repl-await": "Disable top-level await keyword support in REPL.", "--no-extra-info-on-fatal-exception": "Hide extra information on fatal exception that causes exit.", "--no-force-async-hooks-checks": "Disable runtime checks for async_hooks.", "--no-network-family-autoselection": "Disable network family autoselection algorithm.", "--no-warnings": "Silence all process warnings (including deprecations).", "--openssl-config=file": "Load an OpenSSL configuration file on startup.", "--openssl-shared-config": "Enable OpenSSL default configuration section, openssl_conf to be read from the OpenSSL configuration file.", "--pending-deprecation": "Emit pending deprecation warnings.", "--preserve-symlinks": "Follow symlinks when resolving modules.", "--preserve-symlinks-main": "Follow symlinks when resolving the main module.", "--prof": "Generate V8 profiler output.", "--redirect-warnings=file": "Write process warnings to the given file instead of printing to stderr.", "--report-compact": "Write reports in a compact format, single-line JSON.", "--report-dir=directory": "Directory where the report is written.", "--report-filename=filename": "Name of the file to which the report will be written.", "--report-on-fatalerror": "Generate a report on fatal errors.", "--report-on-signal": "Generate a report upon receiving a signal.", "--report-signal=signal": "Set the signal upon which a report is generated.", "--report-uncaught-exception": "Generate a report on uncaught exceptions.", "--secure-heap=n": "Initialize an OpenSSL secure heap of n bytes.", "--secure-heap-min=n": "Minimum allocation from the OpenSSL secure heap.", "--test": "Starts the Node.js command line test runner.", "--test-concurrency": "Set the number of test files to run in parallel.", "--test-name-pattern": "Run tests whose name matches the provided pattern.", "--test-reporter": "Set test reporter format.", "--test-reporter-destination": "Set test reporter destination.", "--test-shard": "Configure test suite shard.", "--throw-deprecation": "Throw errors for deprecations.", "--title=title": "Set process.title on startup.", "--tls-cipher-list=list": "Specify an alternative default TLS cipher list.", "--tls-keylog=file": "Log TLS key material to a file.", "--tls-max-v1.2": "Set default maxVersion to 'TLSv1.2'.", "--tls-max-v1.3": "Set default maxVersion to 'TLSv1.3'.", "--tls-min-v1.0": "Set default minVersion to 'TLSv1'.", "--tls-min-v1.1": "Set default minVersion to 'TLSv1.1'.", "--tls-min-v1.2": "Set default minVersion to 'TLSv1.2'.", "--tls-min-v1.3": "Set default minVersion to 'TLSv1.3'.", "--trace-atomics-wait": "Print short summaries of calls to Atomics.wait().", "--trace-deprecation": "Print stack traces for deprecations.", "--trace-event-categories categories": "A comma separated list of categories that should be traced when trace event tracing is enabled.", "--trace-event-file-pattern pattern": "Template string specifying the filepath for the trace event data.", "--trace-events-enabled": "Enable the collection of trace event tracing information.", "--trace-exit": "Print a stack trace whenever an environment is exited proactively.", "--trace-sync-io": "Print a stack trace whenever synchronous I/O is detected after the first turn of the event loop.", "--trace-tls": "Print TLS packet trace information to stderr.", "--trace-uncaught": "Print stack traces for uncaught exceptions.", "--trace-warnings": "Print stack traces for process warnings (including deprecations).", "--track-heap-objects": "Track heap object allocations for heap snapshots.", "--unhandled-rejections=mode": "Define the behavior for unhandled rejections. Mode can be one of: throw, strict, warn, warn-with-error-code, none.", "--use-bundled-ca": "Use bundled Mozilla CA store as supplied by current Node.js version.", "--use-largepages=mode": "Re-map the Node.js static code to large memory pages at startup.", "--use-openssl-ca": "Use OpenSSL's default CA store.", "--watch": "Restart the process when the command is edited.", "--watch-path": "Specify what paths to watch.", "--watch-preserve-output": "Disable clearing the console when watch mode restarts the process.", "--zero-fill-buffers": "Automatically zero-fills all newly allocated Buffer and SlowBuffer instances.", "--allow-child-process": "Allow spawning process when using the permission model.", "--allow-fs-read": "Allow file system read access when using the permission model.", "--allow-fs-write": "Allow file system write access when using the permission model.", "--allow-wasi": "Allow WASI when using the permission model.", "--allow-worker": "Allow creating worker threads when using the permission model." } }, { "name": "ping", "description": "Allows you to ping a site", "usage": "ping [url], -t [pingtime]" }, { "name": "echo", "description": "Print text", "usage": "echo [text]" }, { "name": "cat", "description": "Display the contents of a file", "usage": "cat [file]" }, { "name": "taskkill", "description": "Lets you kill a task", "usage": "taskkill [PID]", "options": { "list": "Lists all Processes" } }, { "name": "pkill", "description": "Lets you kill a task", "usage": "pkill [PID]", "options": { "list": "Lists all Processes" } }, { "name": "clear", "description": "Clear the console", "usage": "clear" }, { "name": "tb", "description": "Command for debugging terbium and interacting directly with it's API's", "usage": "tb [subcmd] [subcmd] ... " }, { "name": "unzip", "description": "unzip a file to a directory", "usage": "unzip [file] [directory]" }, { "name": "exit", "description": "Exit the terminal", "usage": "exit" }, { "name": "ssh", "description": "Connect to a remote server via SSH (uses WebContainer or libcurl.js)", "usage": "ssh [user@]hostname [options]", "options": { "--host": "Remote host to connect to", "--port": "Port number (default: 22)", "--username": "Username for authentication", "--password": "Password for authentication", "-p": "Prompt for password (secure)", "--privateKey": "Private key content for authentication", "-i": "Path to private key file", "--disconnect": "Disconnect current SSH session", "--status": "Show current connection status", "--help, -h": "Show help message" } }, { "name": "ssh-keygen", "description": "Generate, manage and convert SSH keys", "usage": "ssh-keygen [options]", "options": { "-t type": "Specifies the type of key to create.", "-b bits": "Specifies the number of bits in the key.", "-f filename": "Specifies the filename of the key file." } } ] ================================================ FILE: public/apps/terminal.tapp/scripts/info.schema.json ================================================ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "array", "items": { "$ref": "#/definitions/command" }, "definitions": { "command": { "type": "object", "required": ["name", "description", "usage"], "properties": { "name": { "type": "string" }, "description": { "type": "string" }, "usage": { "type": "string" }, "options": { "type": "object", "additionalProperties": { "type": "string" } } }, "additionalProperties": false } } } ================================================ FILE: public/apps/terminal.tapp/scripts/ls.js ================================================ async function ls(args) { if (args._raw === "/mnt/" || path === "/mnt/") { function centerText(text, width) { const pad = Math.max(0, width - text.length); const padLeft = Math.floor(pad / 2); const padRight = pad - padLeft; return " ".repeat(padLeft) + text + " ".repeat(padRight); } const columns = [ { name: "Name", width: 12 }, { name: "URL", width: 32 }, { name: "Mounted", width: 10 }, { name: "Mounted Path", width: 20 }, ]; 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.2.0`, header.length)); displayOutput(header); displayOutput(separator); 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(); } else if ((args._raw.includes("/mnt/") && args._raw !== "/mnt/") || (path.includes("/mnt/") && path !== "/mnt/")) { try { const match = args._raw.match(/\/mnt\/([^\/]+)\//) || path.match(/\/mnt\/([^\/]+)\//); const davName = match ? match[1].toLowerCase() : ""; 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}/`); } else { displayOutput(entry.basename); } } } catch (e) { displayError(`TNSM ls: Dav Drive is not mounted with error: ${e.message}`); createNewCommandInput(); return; } createNewCommandInput(); } else if (args._raw) { try { const entries = await tb.sh.promises.ls(path + args._raw); entries.forEach(entry => { displayOutput(entry.name); }); createNewCommandInput(); } catch { const entries = await tb.sh.promises.ls(args._raw); entries.forEach(entry => { displayOutput(entry.name); }); createNewCommandInput(); } } else { const entries = await tb.sh.promises.ls(path); entries.forEach(entry => { displayOutput(entry.name); }); createNewCommandInput(); } } ls(args); ================================================ FILE: public/apps/terminal.tapp/scripts/mkdir.js ================================================ async function mkdir(args) { if (!args._raw) { displayError("mkdir: Please provide a directory name"); createNewCommandInput(); return; } if (path.includes("/mnt/")) { try { const match = path.match(/\/mnt\/([^\/]+)\//); const davName = match ? match[1].toLowerCase() : ""; const np = path.replace(`/mnt/${davName.toLowerCase()}/`, ""); await tb.vfs.servers.get(davName).connection.promises.mkdir(`${np}/${args._raw}`); createNewCommandInput(); } catch (e) { displayError(`TNSM mkdir: ${e.message}`); createNewCommandInput(); return; } } else { try { await window.parent.tb.fs.promises.mkdir(path + args._raw); createNewCommandInput(); } catch (error) { if (error.code === "ENOENT") { error = "No such file or directory"; } displayError(`mkdir: cannot create directory "${args._raw}': ${error}`); createNewCommandInput(); } } } mkdir(args); ================================================ FILE: public/apps/terminal.tapp/scripts/nano.js ================================================ async function nano(args) { const filename = args._[0]; if (!filename) { displayError("nano: filename required"); createNewCommandInput(); return; } const filepath = path + filename; let content = ""; let isNewFile = false; try { content = await terbium.fs.promises.readFile(filepath, "utf8"); } catch (e) { content = ""; isNewFile = true; } let lines = content.split("\n"); while (lines.length > 0 && lines[lines.length - 1] === "") { lines.pop(); } if (lines.length === 0) { lines = [""]; } let currentLine = 0; let currentCol = 0; let modified = false; let viewStart = 0; let cutBuffer = ""; terbium.setCommandProcessing(false); term.write("\x1b[2J\x1b[H"); drawEditor(); const disposable = term.onData(handleInput); setTabTitle("TSH Nano"); function handleInput(data) { if (data === "\x1b") { return; } if (data === "\r" || data === "\n") { lines.splice(currentLine + 1, 0, lines[currentLine].slice(currentCol)); lines[currentLine] = lines[currentLine].slice(0, currentCol); currentLine++; currentCol = 0; modified = true; drawEditor(); } else if (data === "\x7f") { if (currentCol > 0) { lines[currentLine] = lines[currentLine].slice(0, currentCol - 1) + lines[currentLine].slice(currentCol); currentCol--; modified = true; drawEditor(); } else if (currentLine > 0) { currentCol = lines[currentLine - 1].length; lines[currentLine - 1] += lines[currentLine]; lines.splice(currentLine, 1); currentLine--; modified = true; drawEditor(); } } else if (data === "\x1b[A") { if (currentLine > 0) { currentLine--; currentCol = Math.min(currentCol, lines[currentLine].length); drawEditor(); } } else if (data === "\x1b[B") { if (currentLine < lines.length - 1) { currentLine++; currentCol = Math.min(currentCol, lines[currentLine].length); drawEditor(); } } else if (data === "\x1b[D") { if (currentCol > 0) { currentCol--; drawEditor(); } } else if (data === "\x1b[C") { if (currentCol < lines[currentLine].length) { currentCol++; drawEditor(); } } else if (data === "\x1b[H") { currentCol = 0; drawEditor(); } else if (data === "\x1b[F") { currentCol = lines[currentLine].length; drawEditor(); } else if (data === "\x1b[3~") { if (currentCol < lines[currentLine].length) { lines[currentLine] = lines[currentLine].slice(0, currentCol) + lines[currentLine].slice(currentCol + 1); modified = true; drawEditor(); } else if (currentLine < lines.length - 1) { lines[currentLine] += lines[currentLine + 1]; lines.splice(currentLine + 1, 1); modified = true; drawEditor(); } } else if (data.length === 1 && data >= " " && data <= "~") { lines[currentLine] = lines[currentLine].slice(0, currentCol) + data + lines[currentLine].slice(currentCol); currentCol++; modified = true; drawEditor(); } else if (data === "\x07") { // ^G Help showHelp(); } else if (data === "\x0f") { // ^O Write Out saveFile(); } else if (data === "\x0b") { // ^K Cut cutBuffer = lines[currentLine]; lines.splice(currentLine, 1); if (lines.length === 0) lines = [""]; if (currentLine >= lines.length) currentLine = lines.length - 1; currentCol = Math.min(currentCol, lines[currentLine].length); modified = true; drawEditor(); } else if (data === "\x15") { // ^U Paste if (cutBuffer !== "") { lines.splice(currentLine + 1, 0, cutBuffer); currentLine++; currentCol = 0; modified = true; drawEditor(); } } else if (data === "\x03") { // ^C Cur Pos showPosition(); } else if (data === "\x16") { // ^V Next Page const pageSize = term.rows - 3; currentLine = Math.min(lines.length - 1, currentLine + pageSize); currentCol = Math.min(currentCol, lines[currentLine].length); drawEditor(); } else if (data === "\x19") { // ^Y Prev Page const pageSize = term.rows - 3; currentLine = Math.max(0, currentLine - pageSize); currentCol = Math.min(currentCol, lines[currentLine].length); drawEditor(); } else if (data === "\x18") { // ^X Exit if (modified) { promptSaveOnExit(); } else { exitEditor(); } } } function drawEditor() { term.write("\x1b[2J\x1b[H"); const title = `TSH nano ${modified ? "[ Modified ] " : ""}${filename}`; const centerCol = Math.max(1, Math.floor((term.cols - title.length) / 2)); term.write(`\x1b[1;${centerCol}H\x1b[7m${title}\x1b[0m`); const maxLines = term.rows - 3; viewStart = Math.max(0, Math.min(viewStart, currentLine - maxLines + 1)); viewStart = Math.max(0, Math.min(viewStart, lines.length - maxLines)); for (let i = viewStart; i < Math.min(lines.length, viewStart + maxLines); i++) { const line = lines[i] || ""; const row = i - viewStart + 2; term.write(`\x1b[${row};1H${line}`); term.write(`\x1b[${row};${line.length + 1}H\x1b[K`); } for (let r = Math.min(lines.length, viewStart + maxLines) - viewStart + 2; r <= term.rows - 1; r++) { term.write(`\x1b[${r};1H\x1b[K`); } term.write(`\x1b[${term.rows};1H^G Get Help ^O Write Out ^W Where Is ^K Cut Text ^J Justify ^C Cur Pos ^X Exit`); const cursorRow = currentLine - viewStart + 2; const cursorCol = Math.min(currentCol + 1, (lines[currentLine] || "").length + 1); term.write(`\x1b[${cursorRow};${cursorCol}H`); } async function saveFile() { try { await terbium.fs.promises.writeFile(filepath, lines.join("\n"), "utf8"); modified = false; isNewFile = false; drawEditor(); } catch (e) { term.write(`\x1b[${term.rows};1HError: ${e.message}`); setTimeout(() => drawEditor(), 2000); } } function showHelp() { term.write("\x1b[2J\x1b[H"); term.writeln("Nano Help"); term.writeln(""); term.writeln("^G Get Help ^O Write Out ^W Where Is"); term.writeln("^K Cut Text ^J Justify ^C Cur Pos"); term.writeln("^X Exit ^U Paste ^V Next Page"); term.writeln("^Y Prev Page"); term.writeln(""); term.writeln("Press any key to return to editor"); term.onData(() => { drawEditor(); }); } function showPosition() { const lineNum = currentLine + 1; const colNum = currentCol + 1; const totalLines = lines.length; term.write(`\x1b[${term.rows};1HLine ${lineNum}/${totalLines} Col ${colNum}`); setTimeout(() => drawEditor(), 2000); } function promptSaveOnExit() { term.write("\x1b[2J\x1b[H"); term.writeln(`Save modified buffer (ANSWERING "No" WILL DESTROY CHANGES) ?`); term.writeln(""); term.writeln(" Y Yes"); term.writeln(" N No"); term.writeln(" ^C Cancel"); term.writeln(""); term.write("Save modified buffer? "); let response = ""; const promptDisposable = term.onData(data => { if (data === "\r" || data === "\n") { if (response.toLowerCase() === "y" || response.toLowerCase() === "yes") { saveFile().then(() => exitEditor()); } else if (response.toLowerCase() === "n" || response.toLowerCase() === "no") { exitEditor(); } else { promptSaveOnExit(); } promptDisposable.dispose(); } else if (data === "\x03") { // ^C Cancel drawEditor(); promptDisposable.dispose(); } else if (data === "\x7f") { // Backspace if (response.length > 0) { response = response.slice(0, -1); term.write("\b \b"); } } else if (data.length === 1 && data >= " " && data <= "~") { response += data; term.write(data); } }); } function exitEditor() { setTabTitle("Terbium TSH"); terbium.setCommandProcessing(true); disposable.dispose(); term.write("\x1b[2J\x1b[H"); createNewCommandInput(); } } nano(args); ================================================ FILE: public/apps/terminal.tapp/scripts/node.js ================================================ /** * @typedef {import("yargs-parser").Arguments} argv * @typedef {import("xterm").Terminal} Terminal */ /** * CLI for **Node.js** subsystem * @param {argv} args The arguments to pass into **Node.js** * @param {Terminal} term - **XTERM.js** terminal instance */ async function node(args, term) { const webContainer = tb.node.webContainer; tb.setCommandProcessing(false); term.focus(); // Check for jsh mode const isJshMode = args.j === true || args.jsh === true; let command; let commandArgs; if (tb.node.isReady === false) { displayOutput(`\r\nWebContainer has not booted yet. Please wait a few seconds and try again.`); createNewCommandInput(); tb.setCommandProcessing(true); return; } if (isJshMode) { displayOutput("Starting WebContainer JavaScript shell..."); setTabTitle("JSH: Terminal"); command = "jsh"; commandArgs = []; } else { displayOutput("Starting Node.js..."); setTabTitle("NodeJS"); command = "node"; const { _: positionalArguments, $0: commandName, j: shortJshFlag, jsh: longJshFlag, _raw: rawArgumentString, ...remainingFlags } = args; const positionalArgs = positionalArguments || []; commandArgs = [...positionalArgs]; // Process the POSIX flags for (const [key, value] of Object.entries(remainingFlags)) { if (value === false) continue; if (key.length === 1) { // Handle single letter flags commandArgs.push(`-${key}`); if (value !== true) { commandArgs.push(String(value)); } } else { // Long flags if (value === true) { commandArgs.push(`--${key}`); } else { commandArgs.push(`--${key}`, String(value)); } } } } const shell = await webContainer.spawn(command, commandArgs, { terminal: { cols: term.cols, rows: term.rows, }, }); shell.output.pipeTo( new WritableStream({ write(data) { term.write(data); }, }), ); const writer = shell.input.getWriter(); const inputHandler = term.onData(async data => { await writer.write(data); }); const resizeHandler = () => { shell.resize({ cols: term.cols, rows: term.rows, }); }; window.addEventListener("resize", resizeHandler); // Cleanup const exitCode = await shell.exit; // Cleanup listeners and handlers inputHandler.dispose(); window.removeEventListener("resize", resizeHandler); tb.setCommandProcessing(true); setTabTitle("Terbium TSH"); // Display exit message displayOutput(`\r\nWebContainer shell exited with code ${exitCode}`); // Give the focus back to the terminal createNewCommandInput(); } node(args, term); ================================================ FILE: public/apps/terminal.tapp/scripts/ping.js ================================================ async function ping(args) { const numPings = 5; let url = args._raw; let totalResponseTime = 0; let packetsReceived = 0; if (!url) displayError("No Url was Provided"); if (!url.includes("http://") && !url.includes("https://")) { url = "http://" + url; } for (let i = 0; i < numPings; i++) { const startTime = Date.now(); console.log(url); try { const response = await window.parent.tb.libcurl.fetch(url); console.log(response); if (response.ok) { packetsReceived++; } } catch (error) { displayOutput(`Error Reaching Site: Site turned ${error.response?.status} Error when pinged`); } const endTime = Date.now(); const responseTime = endTime - startTime; totalResponseTime += responseTime; displayOutput(`Ping ${url} - Time: ${responseTime}ms`); } const avgResponseTime = totalResponseTime / numPings; const percentReceived = (packetsReceived / numPings) * 100; const percentLost = 100 - percentReceived; displayOutput(`Pinged ${url} ${numPings} times: ${avgResponseTime.toFixed(2)}ms average, ${percentLost.toFixed(2)}% packet loss, ${percentReceived.toFixed(2)}% packets received`); createNewCommandInput(); } ping(args); ================================================ FILE: public/apps/terminal.tapp/scripts/pkg.js ================================================ async function pkg(args) { let availableCommands = [ "pkg -h: Display help for .", "pkg install : Install an app matching from the repo.", "pkg remove : Uninstall an app matching from the repo.", "pkg update: : Update a package to the latest version if available.", "pkg list: Shows a list of installed apps.", "pkg search : Search for an app matching in the repo.", "pkg repo: Changes the Package Managers Fetch repo (Use -r to remove the repo you added)", ]; let repo = sessionStorage.getItem("pkg-repo") || JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/app store/repos.json`, "utf8"))[0].url; let rType = sessionStorage.getItem("pkg-type") || "terbium"; switch (args._[0]) { case "install": if (args._[1]) { const response = await tb.libcurl.fetch(repo); let repoData = rType === "terbium" ? (await response.json()).apps : (await (await tb.libcurl.fetch(repo.replace("manifest.json", "list.json"))).json()).apps; const packageName = args._[1]; const exactMatch = repoData.find(pkg => pkg.name.toLowerCase() === packageName.toLowerCase()); if (exactMatch) { displayOutput(`Installing ${exactMatch.name}...`); if (exactMatch.requirements) { if (exactMatch.requirements.os && semverCompare(exactMatch.requirements.os, window.parent.tb.system.version()) > 0) { displayError(`This app requires terbium version: ${exactMatch.requirements.os} or later`); createNewCommandInput(); return; } else if (exactMatch.requirements.proxy && exactMatch.requirements.proxy !== (await window.parent.tb.proxy.get())) { displayError(`This app requires ${exactMatch.requirements.proxy} as the default proxy.`); createNewCommandInput(); return; } } const installed = JSON.parse(await window.parent.tb.fs.promises.readFile("/apps/installed.json", "utf8")); let type; if ("pkg-download" in exactMatch) { type = "TAPP"; } else if ("anura-pkg" in exactMatch || rType === "anura") { type = "anura"; } else { type = "web"; } if (installed.some(app => app.name.toLowerCase() === exactMatch.name.toLowerCase())) { displayOutput(`The app "${exactMatch.name}" is already installed.`); displayOutput("Do you want to reinstall it? (y/n)"); term.write("\r\n> "); const onData = async function (input) { const userInput = input.trim().toLowerCase(); if (userInput === "y") { displayOutput(""); displayOutput(`Reinstalling ${exactMatch.name}...`); await tb.launcher.removeApp(exactMatch.name); await installApp(exactMatch, type, installed); displayOutput(`${exactMatch.name} reinstalled successfully!`); createNewCommandInput(); } else { createNewCommandInput(); } disposable.dispose(); }; const disposable = term.onData(onData); return; } else { await installApp(exactMatch, type, installed); displayOutput(`${exactMatch.name} installed successfully!`); createNewCommandInput(); return; } } else { displayOutput(`No package found with the name "${packageName}".`); createNewCommandInput(); return; } } else { displayOutput("Usage: pkg install "); createNewCommandInput(); return; } case "remove": if (args._[1]) { const packageName = args._[1]; let installed = JSON.parse(await window.parent.tb.fs.promises.readFile("/apps/installed.json", "utf8")); const appIndex = installed.findIndex(app => app.name.toLowerCase() === packageName.toLowerCase()); if (appIndex !== -1) { const app = installed[appIndex]; installed.splice(appIndex, 1); await window.parent.tb.fs.promises.writeFile("/apps/installed.json", JSON.stringify(installed, null, 2), "utf8"); displayOutput(`Uninstalling ${app.name}...`); const configPath = app.config; console.log(configPath); if (configPath.endsWith("index.json")) { let webApps = JSON.parse(await window.parent.tb.fs.promises.readFile("/apps/web_apps.json", "utf8")); if (Array.isArray(webApps.apps)) { const waIndex = webApps.apps.findIndex(webApp => webApp.name && webApp.name.toLowerCase() === app.name.toLowerCase()); if (waIndex !== -1) { webApps.apps.splice(waIndex, 1); await window.parent.tb.fs.promises.writeFile("/apps/web_apps.json", JSON.stringify(webApps, null, 2), "utf8"); await tb.launcher.removeApp(app.name); displayOutput(`${app.name} has been uninstalled.`); } } } else if (configPath.endsWith("manifest.json")) { try { await window.parent.tb.fs.promises.unlink(`/system/etc/anura/configs/${app.name}.json`); await tb.sh.rm(configPath.replace("/manifest.json", "/"), { recursive: true }); } catch {} await tb.sh.rm(`/apps/anura/${app.name}`, { recursive: true }); await tb.launcher.removeApp(app.name); delete window.parent.anura.apps[app.package]; displayOutput(`${app.name} has been uninstalled.`); } else if (configPath.endsWith(".tbconfig")) { await tb.sh.rm(configPath.replace("/.tbconfig", "/"), { recursive: true }); await tb.launcher.removeApp(app.name); displayOutput(`${app.name} has been uninstalled.`); } } else { displayOutput(`No installed app found with the name "${packageName}".`); } createNewCommandInput(); } else { displayOutput("Usage: pkg remove "); createNewCommandInput(); } break; case "update": if (args._[1]) { displayOutput("Checking for updates..."); const config = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/system/${args._[1].toLowerCase()}.tapp/.tbconfig`, "utf8")); const response = await tb.libcurl.fetch(repo); let repoData = rType === "terbium" ? (await response.json()).apps : (await (await tb.libcurl.fetch(repo.replace("manifest.json", "list.json"))).json()).apps; const packageName = args._[1]; const exactMatch = repoData.find(pkg => pkg.name.toLowerCase() === packageName.toLowerCase()); if (exactMatch.requirements) { if (exactMatch.requirements.os && semverCompare(exactMatch.requirements.os, window.parent.tb.system.version()) > 0) { displayError(`This app requires terbium version: ${exactMatch.requirements.os} or later`); createNewCommandInput(); return; } else if (exactMatch.requirements.proxy && exactMatch.requirements.proxy !== (await window.parent.tb.proxy.get())) { displayError(`This app requires ${exactMatch.requirements.proxy} as the default proxy.`); createNewCommandInput(); return; } } if (config.version !== exactMatch.version) { displayOutput(`Updating ${exactMatch.name} from version ${config.version} to ${exactMatch.version}...`); await tb.sh.promises.rm(`/apps/system/${args._[1].toLowerCase()}.tapp/`, { recursive: true }); await installApp(exactMatch, "TAPP"); displayOutput(`${exactMatch.name} updated successfully!`); createNewCommandInput(); } } else { displayOutput("Usage: pkg update "); createNewCommandInput(); return; } break; case "list": displayOutput("Installed Packages for this system:"); const installed = JSON.parse(await window.parent.tb.fs.promises.readFile("/apps/installed.json", "utf8")); for (const app of installed) { displayOutput(`${app.name} - ${app.user}`); } displayOutput(""); displayOutput(`${installed.length} are installed.`); createNewCommandInput(); break; case "search": if (args._[1]) { const response = await tb.libcurl.fetch(repo); let repoData = rType === "terbium" ? (await response.json()).apps : (await (await tb.libcurl.fetch(repo.replace("manifest.json", "list.json"))).json()).apps; const searchTerm = args._[1].toLowerCase(); const exactMatch = repoData.find(pkg => pkg.name.toLowerCase() === searchTerm); if (exactMatch) { displayOutput(`Found package: ${exactMatch.name}`); } else { const potentialMatches = repoData.filter(pkg => pkg.name.toLowerCase().includes(searchTerm)); if (potentialMatches.length > 0) { displayOutput(`No exact match found for "${searchTerm}".`); displayOutput("Did you mean? Potential match(es):"); for (const match of potentialMatches) { displayOutput(` - ${match.name}`); } } else { displayOutput("No matching packages found."); } } createNewCommandInput(); } else { displayOutput("Usage: pkg search "); createNewCommandInput(); return; } break; case "repo": switch (args._[1]) { case "r": case "remove": let r = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/app store/repos.json`, "utf8")); r = r.filter(r => r.url !== args._[2]); await window.parent.tb.fs.promises.writeFile(`/apps/user/${sessionStorage.getItem("currAcc")}/app store/repos.json`, JSON.stringify(r)); displayOutput(`Removed ${args._[2]} from the repo list`); createNewCommandInput(); break; case "a": case "add": let newrepo = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/app store/repos.json`, "utf8")); newrepo.push({ url: args._[2] }); await window.parent.tb.fs.promises.writeFile(`/apps/user/${sessionStorage.getItem("currAcc")}/app store/repos.json`, JSON.stringify(newrepo)); displayOutput(`Added ${args._[2]} to the repo list`); createNewCommandInput(); break; case "l": case "list": let repoList = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/app store/repos.json`, "utf8")); displayOutput("Available Repositories:"); repoList.forEach(repo => { displayOutput(` - ${repo.url}`); }); createNewCommandInput(); break; case "s": case "set": repo = args._[2]; try { const response = await fetch(repo); const jsonData = await response.json(); let repoType; if ("repo" in jsonData) { repoType = "terbium"; } else { repoType = "anura"; } sessionStorage.setItem("pkg-repo", repo); sessionStorage.setItem("pkg-type", repoType); displayOutput(`Set repo to ${repo} (type: ${repoType})`); } catch (e) { displayError(`Failed to fetch or detect repo type: ${e.message}`); } createNewCommandInput(); break; default: displayOutput("Usage: pkg repo [repo-url]"); createNewCommandInput(); break; } break; case "help": default: displayOutput(`TPKG v1.4.3 - February 2026`); displayOutput(`Usage: pkg `); displayOutput(" "); displayOutput("All commands:"); for (let command in availableCommands) { command = availableCommands[command]; let [cmd, description] = command.split(": "); displayOutput(` ${cmd.padEnd(40)} ${description}`); } createNewCommandInput(); break; } } async function installApp(app, type) { let repo = sessionStorage.getItem("pkg-repo") || JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/app store/repos.json`, "utf8"))[0].url; switch (type) { case "web": let appPath = `/apps/user/${await window.parent.tb.user.username()}/${app.name}`; let appIndex = { name: app.name, icon: app.icon, description: app.description, authors: app.authors, "pkg-name": app["pkg-name"], version: app.version, images: app.images, wmArgs: app.wmArgs, }; if (!(await dirExists(appPath))) { await window.parent.tb.fs.promises.mkdir(appPath); } await window.parent.tb.fs.promises.writeFile(`${appPath}/index.json`, JSON.stringify(appIndex)); let apps = JSON.parse(await window.parent.tb.fs.promises.readFile("/apps/web_apps.json", "utf8")); apps["apps"].push(app["pkg-name"]); await window.parent.tb.fs.promises.writeFile("/apps/web_apps.json", JSON.stringify(apps)); await window.parent.tb.launcher.addApp({ title: app["wmArgs"]["title"], name: app.name, icon: app.icon, src: app["wmArgs"]["src"], size: { width: app["wmArgs"]["size"]["width"], height: app["wmArgs"]["size"]["height"], }, single: app["wmArgs"]["single"], resizable: app["wmArgs"]["resizable"], controls: app["wmArgs"]["controls"], message: app["wmArgs"]["message"], proxy: app["wmArgs"]["proxy"], snapable: app["wmArgs"]["snapable"], }); try { let apps = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/installed.json`, "utf8")); apps.push({ name: app.name, user: await window.parent.tb.user.username(), config: `/apps/user/${await window.parent.tb.user.username()}/${app.name}/index.json`, }); await window.parent.tb.fs.promises.writeFile(`/apps/installed.json`, JSON.stringify(apps)); } catch { await window.parent.tb.fs.promises.writeFile( `/apps/installed.json`, JSON.stringify([ { name: app.name, user: await window.parent.tb.user.username(), config: `/apps/user/${await window.parent.tb.user.username()}/${app.name}/index.json`, }, ]), ); } break; case "TAPP": const appName = app.name.toLowerCase(); const DLPath = `/apps/${appName}`; const downloadUrl = app["pkg-download"]; try { await tb.system.download(downloadUrl, `${DLPath}.zip`); const targetDirectory = `/apps/system/${appName}.tapp/`; await unzip(`/apps/${appName}.zip`, targetDirectory); const appConf = await window.parent.tb.fs.promises.readFile(`/apps/system/${appName}.tapp/.tbconfig`, "utf8"); const appData = JSON.parse(appConf); await window.parent.tb.launcher.addApp({ title: typeof appData.wmArgs.title === "object" ? { text: appData.wmArgs.title.text, weight: appData.wmArgs.title.weight, html: appData.wmArgs.title.html, } : appData.wmArgs.title, name: appData.title, icon: `/fs/apps/system/${appName}.tapp/${appData.icon}`, src: `/fs/apps/system/${appName}.tapp/${appData.wmArgs.src}`, size: { width: appData.wmArgs.size.width, height: appData.wmArgs.size.height, }, single: appData.wmArgs.single, resizable: appData.wmArgs.resizable, controls: appData.wmArgs.controls, message: appData.wmArgs.message, snapable: appData.wmArgs.snapable, }); await window.parent.tb.fs.promises.unlink(`${DLPath}.zip`); try { let apps = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/installed.json`, "utf8")); apps.push({ name: appName, user: await window.parent.tb.user.username(), config: `/apps/system/${appName}.tapp/.tbconfig`, }); await window.parent.tb.fs.promises.writeFile(`/apps/installed.json`, JSON.stringify(apps)); } catch { await window.parent.tb.fs.promises.writeFile( `/apps/installed.json`, JSON.stringify({ name: appName, user: await window.parent.tb.user.username(), config: `/apps/system/${appName}.tapp/.tbconfig`, }), ); } } catch (e) { displayError(`Failed to install ${appName} with reason: ${e.message}`); return; } break; case "anura": console.log(app); const aName = app.name || app.package; const APath = `/apps/anura/${aName}`; let aDL; if ("anura-pkg" in app) { aDL = app["anura-pkg"]; } else { aDL = `${repo.replace("manifest.json", "")}/apps/${app.package}/${app.data}`; } try { await tb.system.download(aDL, `${APath}.zip`); const targetDirectory = `/apps/anura/${aName}/`; await unzip(`/apps/anura/${aName}.zip`, targetDirectory); const appConf = await window.parent.tb.fs.promises.readFile(`/apps/anura/${aName}/manifest.json`, "utf8"); const appData = JSON.parse(appConf); await window.parent.tb.launcher.addApp({ name: appData.name, title: appData.wininfo.title, icon: `/fs/apps/anura/${app.name}/${appData.icon}`, src: `/fs/apps/anura/${app.name}/${appData.index}`, size: { width: appData.wininfo.width, height: appData.wininfo.height, }, single: appData.wininfo.allowMultipleInstance, }); window.parent.anura.apps[appData.package] = { title: appData.name, icon: appData.icon, id: appData.package, }; await window.parent.tb.fs.promises.unlink(`${APath}.zip`); try { let apps = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/installed.json`, "utf8")); apps.push({ name: appData.name, user: await window.parent.tb.user.username(), config: `/apps/anura/${aName}/manifest.json`, }); await window.parent.tb.fs.promises.writeFile(`/apps/installed.json`, JSON.stringify(apps)); } catch { await window.parent.tb.fs.promises.writeFile( `/apps/installed.json`, JSON.stringify([ { name: appData.name, user: await window.parent.tb.user.username(), config: `/apps/anura/${aName}/manifest.json`, }, ]), ); } } catch (e) { displayError(`Failed to install ${aName} with reason: ${e.message}`); return; } break; } } async function unzip(path, target) { const runUnzip = async () => { const response = await fetch("/fs/" + path); const zipFileContent = await response.arrayBuffer(); if (!(await dirExists(target))) { await window.parent.tb.fs.promises.mkdir(target, { recursive: true }); } const compressedFiles = window.parent.tb.fflate.unzipSync(new Uint8Array(zipFileContent)); for (const [relativePath, content] of Object.entries(compressedFiles)) { const fullPath = `${target}/${relativePath}`; const pathParts = fullPath.split("/"); let currentPath = ""; for (let i = 0; i < pathParts.length; i++) { currentPath += pathParts[i] + "/"; if (i === pathParts.length - 1 && !relativePath.endsWith("/")) { 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)); } catch { displayOutput(`Cant make ${currentPath.slice(0, -1)}`); console.log(`Cant make ${currentPath.slice(0, -1)}`); } } else if (!(await dirExists(currentPath))) { try { console.log(`mkdir ${currentPath}`); displayOutput(`mkdir ${currentPath}`); await window.parent.tb.fs.promises.mkdir(currentPath); } catch { console.log(`Cant make ${currentPath}`); displayOutput(`Cant make ${currentPath}`); } } } if (relativePath.endsWith("/")) { try { console.log(`mkdir fp ${fullPath}`); await window.parent.tb.fs.promises.mkdir(fullPath); } catch { console.log(`Cant make ${fullPath}`); } } } return "Done!"; }; return window.parent.tb.notification.Installing( { message: "Installing package files...", application: "Terminal", iconSrc: "/fs/apps/system/terminal.tapp/icon.svg", }, runUnzip(), { message: "Finished extracting package", application: "Terminal", iconSrc: "/fs/apps/system/terminal.tapp/icon.svg", time: 3200, }, { message: "Failed to extract package", application: "Terminal", iconSrc: "/fs/apps/system/terminal.tapp/icon.svg", time: 2500, }, ); } const dirExists = async path => { return new Promise(resolve => { window.parent.tb.fs.stat(path, (err, stats) => { if (err) { if (err.code === "ENOENT") { resolve(false); } else { console.error(err); resolve(false); } } else { const exists = stats.type === "DIRECTORY"; resolve(exists); } }); }); }; /** * Compares two semantic version strings. * @param {string} a - The first version string. * @param {string} b - The second version string. * @returns {number} - Returns 1 if a > b, -1 if a < b, 0 if they are equal. */ const semverCompare = (a, b) => { const pa = a.split(/[-.]/); const pb = b.split(/[-.]/); for (let i = 0; i < Math.max(pa.length, pb.length); i++) { const na = pa[i] || "0"; const nb = pb[i] || "0"; if (!isNaN(na) && !isNaN(nb)) { if (+na > +nb) return 1; if (+na < +nb) return -1; } else { if (na > nb) return 1; if (na < nb) return -1; } } return 0; }; pkg(args); ================================================ FILE: public/apps/terminal.tapp/scripts/pkill.js ================================================ function pkill(args) { if (args._raw.includes("list")) { const windows = window.parent.tb.process.list(); Object.values(windows).forEach(window => { displayOutput(`${window.name}, ${window.pid}`); }); createNewCommandInput(); } else { try { window.tb.process.kill(args._raw); displayOutput(`Successfully killed task with pid: ${args._raw}`); } catch { displayError("Not task found with that PID"); } createNewCommandInput(); } } pkill(args); ================================================ FILE: public/apps/terminal.tapp/scripts/pwd.js ================================================ function pwd(args) { displayOutput(path); createNewCommandInput(); } pwd(args); ================================================ FILE: public/apps/terminal.tapp/scripts/rm.js ================================================ async function rm(args) { let availableOptions = [ "-f: ignore nonexistent files and arguments, never prompt.", "-r: remove directories and their contents recursively.; optionally you can also use -rf to remove directories and their contents recursively without prompt.", "-v: explain what is being done (not default).", "-d: remove empty directories.; you should use rmdir instead.", ]; if (!args._raw || args._raw.includes("-h")) { displayOutput("Usage: rm [OPTION] [FILE]"); displayOutput("Remove (unlink) the FILE(s)."); displayOutput(" "); displayOutput("Options:"); for (let option of availableOptions) { let nspace = " "; let [opt, desc] = option.split(": "); let optionally = desc.split(";")[1]; desc = desc.replace(";", "").replace(optionally, ""); displayOutput(` ${opt.padEnd(10)} ${desc}`); if (optionally) { displayOutput(` ${nspace.padEnd(10)}${optionally}`); } } createNewCommandInput(); return; } const user = sessionStorage.getItem("currAcc"); const systemDirs = ["/home", `/home/${user}/documents`, `/home/${user}/videos`, `/home/${user}/pictures`, `/home/${user}/music`]; for (let sdir of systemDirs) { if (path === sdir) { displayOutput(`rm: cannot remove "${path}": Is a system directory`); createNewCommandInput(); return; } } let options = { force: false, recursive: false, verbose: false, directory: false, }; if (args._raw.includes("-rf")) { options.force = true; options.recursive = true; } if (args._raw.includes("-r")) { options.recursive = true; } if (args._raw.includes("-f")) { options.force = true; } if (args._raw.includes("-v")) { options.verbose = true; } if (args._raw.includes("-d")) { options.directory = true; } const toDel = `${path}/${args._raw.replace(/^-f|-rf|-r|-v|-d/g, "").trim()}`; console.log(toDel); if (path.includes("/mnt/")) { try { const match = path.match(/\/mnt\/([^\/]+)\//); const davName = match ? match[1].toLowerCase() : ""; const np = path.replace(`/mnt/${davName.toLowerCase()}/`, ""); await tb.vfs.servers.get(davName).connection.promises.unlink(`${np}/${args._raw}`); createNewCommandInput(); } catch (e) { displayError(`TNSM rmdir: ${e.message}`); createNewCommandInput(); return; } } else { window.parent.tb.fs.stat(toDel, (err, stats) => { if (err) return console.log(err); if (stats.isDirectory()) { if (options.force || options.recursive) { tb.sh.rm(toDel, { recursive: options.recursive, force: options.force }, err => { if (err) { displayError(`rm: cannot remove "${toDel}": ${err.message}`); createNewCommandInput(); } else { if (options.verbose) { displayOutput(`removed directory "${toDel}"`); } createNewCommandInput(); } }); } else if (options.directory) { window.parent.tb.fs.rmdir(toDel, err => { if (err) { if (err.code === "ENOTEMPTY") { displayError(`rm: cannot remove "${toDel}": Directory not empty`); displayOutput("Use -r to remove non-empty directories. or -rf to remove non-empty directories without prompt."); } else { displayError(`rm: cannot remove "${toDel}": ${err.message}`); } createNewCommandInput(); } else { if (options.verbose) { displayOutput(`removed directory "${toDel}"`); } createNewCommandInput(); } }); } else { displayError(`rm: cannot remove "${toDel}": Is a directory`); createNewCommandInput(); } } else { tb.sh.rm(toDel, { recursive: options.recursive, force: options.force }, err => { if (err) { displayError(`rm: cannot remove "${toDel}": ${err.message}`); createNewCommandInput(); } else { if (options.verbose) { displayOutput(`removed "${toDel}"`); } createNewCommandInput(); } }); } }); } } rm(args); ================================================ FILE: public/apps/terminal.tapp/scripts/rmdir.js ================================================ async function rmdir(args) { if (args._raw.length <= 0) { displayError("rmdir: missing operand"); createNewCommandInput(); return; } if (path.includes("/mnt/")) { displayError("TNSM rmdir: Removing directories from mounted drives is not supported by the webdav library at this time."); } else { window.parent.tb.sh.rm(`${path}/${args._raw}`, err => { if (err) { displayError(`rmdir: ${err.message}`); createNewCommandInput(); } else { createNewCommandInput(); } }); } } rmdir(args); ================================================ FILE: public/apps/terminal.tapp/scripts/ssh-keygen.js ================================================ function ssh_keygen(args) { if (args.help || args.h) { showHelp(); createNewCommandInput(); return; } const keyType = args.type || args.t || "rsa"; const bits = parseInt(args.bits || args.b) || (keyType === "rsa" ? 2048 : keyType === "ed25519" ? 256 : 2048); const comment = args.comment || args.C || sessionStorage.getItem("currAcc"); const filename = args.file || args.f || `/home/${sessionStorage.getItem("currAcc")}/.ssh/id_${keyType}`; const passphrase = args.passphrase || args.N || ""; if (!["rsa", "ed25519"].includes(keyType)) { displayError(`ssh-keygen: unknown key type ${keyType}`); displayError("Supported types: rsa, ed25519"); createNewCommandInput(); return; } const sshDir = `/home/${sessionStorage.getItem("currAcc")}/.ssh`; if (!window.parent.tb.fs.promises.exists(sshDir)) { window.parent.tb.fs.promises.mkdir(sshDir); } displayOutput(`Generating public/private ${keyType} key pair.`); generateKeyPair(keyType, bits) .then(keyPair => { return Promise.all([formatPrivateKey(keyPair.privateKey, keyType, passphrase), formatPublicKey(keyPair.publicKey, keyType, comment)]); }) .then(([privateKeyPEM, publicKeySSH]) => { window.parent.tb.fs.promises.writeFile(filename, privateKeyPEM); displayOutput(`Your identification has been saved in ${filename}`); const publicFilename = `${filename}.pub`; window.parent.tb.fs.promises.writeFile(publicFilename, publicKeySSH); displayOutput(`Your public key has been saved in ${publicFilename}`); return calculateFingerprint(publicKeySSH); }) .then(fingerprint => { displayOutput(`The key fingerprint is:`); displayOutput(`SHA256:${fingerprint}`); displayOutput(`The key's randomart image is:`); displayOutput(generateRandomArt(fingerprint)); createNewCommandInput(); }) .catch(error => { displayError(`ssh-keygen: ${error.message}`); createNewCommandInput(); }); function showHelp() { displayOutput("usage: ssh-keygen [-t keytype] [-b bits] [-C comment] [-f output_keyfile] [-N new_passphrase]"); displayOutput(""); displayOutput("Options:"); displayOutput(" -t keytype Specifies the type of key to create (rsa, ed25519)"); displayOutput(" -b bits Number of bits in the key (default: 2048 for rsa, 256 for ed25519)"); displayOutput(" -C comment Provides a new comment"); displayOutput(" -f filename Specifies the filename of the key file"); displayOutput(" -N passphrase Provides the new passphrase"); displayOutput(" -h, --help Show this help message"); } async function generateKeyPair(keyType, bits) { if (keyType === "rsa") { return await crypto.subtle.generateKey( { name: "RSASSA-PKCS1-v1_5", modulusLength: bits, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: "SHA-256", }, true, ["sign", "verify"], ); } else if (keyType === "ed25519") { return await crypto.subtle.generateKey( { name: "ECDSA", namedCurve: "P-256", }, true, ["sign", "verify"], ); } } async function formatPrivateKey(privateKey, keyType, passphrase) { const exported = await crypto.subtle.exportKey("pkcs8", privateKey); const pemBody = base64Encode(new Uint8Array(exported)); let pem = "-----BEGIN PRIVATE KEY-----\n"; pem += pemBody.match(/.{1,64}/g).join("\n"); pem += "\n-----END PRIVATE KEY-----\n"; if (passphrase) { pem = "-----BEGIN ENCRYPTED PRIVATE KEY-----\n" + "Note: Passphrase encryption not fully implemented\n" + pemBody.match(/.{1,64}/g).join("\n") + "\n-----END ENCRYPTED PRIVATE KEY-----\n"; } return pem; } async function formatPublicKey(publicKey, keyType, comment) { const exported = await crypto.subtle.exportKey("spki", publicKey); const exportedArray = new Uint8Array(exported); let keyData; let keyTypeStr; if (keyType === "rsa") { keyTypeStr = "ssh-rsa"; keyData = extractRSAPublicKey(exportedArray); } else { keyTypeStr = "ecdsa-sha2-nistp256"; keyData = extractECDSAPublicKey(exportedArray); } const sshKey = encodeSSHPublicKey(keyTypeStr, keyData); return `${keyTypeStr} ${sshKey} ${comment}`; } function extractRSAPublicKey(spki) { let offset = 0; for (let i = 0; i < spki.length - 1; i++) { if (spki[i] === 0x03) { offset = i + 1; if (spki[offset] & 0x80) { const lenBytes = spki[offset] & 0x7f; offset += lenBytes + 1; } else { offset += 1; } offset += 1; break; } } return spki.slice(offset); } function extractECDSAPublicKey(spki) { let offset = spki.length - 65; return spki.slice(offset); } function encodeSSHPublicKey(keyType, keyData) { const keyTypeBytes = stringToBytes(keyType); const parts = []; parts.push(encodeUint32(keyTypeBytes.length)); parts.push(keyTypeBytes); parts.push(encodeUint32(keyData.length)); parts.push(keyData); const totalLength = parts.reduce((sum, part) => sum + part.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const part of parts) { result.set(part, offset); offset += part.length; } return base64Encode(result); } function encodeUint32(value) { const buffer = new ArrayBuffer(4); const view = new DataView(buffer); view.setUint32(0, value, false); return new Uint8Array(buffer); } function stringToBytes(str) { const encoder = new TextEncoder(); return encoder.encode(str); } function base64Encode(buffer) { let binary = ""; const bytes = new Uint8Array(buffer); for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } async function calculateFingerprint(publicKey) { const keyData = publicKey.split(" ")[1]; const decoded = base64Decode(keyData); const hashBuffer = await crypto.subtle.digest("SHA-256", decoded); const hashArray = new Uint8Array(hashBuffer); return base64Encode(hashArray).replace(/=+$/, ""); } function base64Decode(str) { const binary = atob(str); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes; } function generateRandomArt(fingerprint) { const width = 17; const height = 9; const field = Array(height) .fill(0) .map(() => Array(width).fill(0)); let x = Math.floor(width / 2); let y = Math.floor(height / 2); const fpBytes = base64Decode(fingerprint); for (let i = 0; i < fpBytes.length; i++) { const byte = fpBytes[i]; for (let j = 0; j < 4; j++) { const move = (byte >> (j * 2)) & 0x03; const dx = move & 0x01 ? 1 : -1; const dy = move & 0x02 ? 1 : -1; x = Math.max(0, Math.min(width - 1, x + dx)); y = Math.max(0, Math.min(height - 1, y + dy)); field[y][x]++; } } const startX = Math.floor(width / 2); const startY = Math.floor(height / 2); const chars = " .o+=*BOX@%&#/^SE"; let art = "+" + "-".repeat(width) + "+\n"; for (let row = 0; row < height; row++) { let line = "|"; for (let col = 0; col < width; col++) { if (row === startY && col === startX) { line += "S"; } else if (row === y && col === x) { line += "E"; } else { const val = Math.min(field[row][col], chars.length - 1); line += chars[val]; } } line += "|"; art += line + "\n"; } art += "+" + "-".repeat(width) + "+"; return art; } } ssh_keygen(args); ================================================ FILE: public/apps/terminal.tapp/scripts/ssh.js ================================================ const tbSSH = window.tbSSH; if (!tbSSH) { throw new Error("TB-SSH library not loaded!"); } if (tb.node.isReady === false) { tb.setCommandProcessing(true); throw new Error("\r\nWebContainer has not booted yet. Please wait a few seconds and try again."); } const connectionString = args._[0]; const port = args.p || args.port; const identityFile = args.i || args.identity; const verbose = args.v || args.verbose; const proxyUrl = args.proxy || "wss://ssh-proxy.terbiumon.top/"; if (!connectionString) { displayOutput("Usage: ssh [user@]hostname [-p port] [-i identity_file] [-proxy proxy_url] [-v]"); displayOutput("Examples:"); displayOutput(" ssh user@example.com"); displayOutput(" ssh example.com"); displayOutput(" ssh -p 2222 user@example.com"); displayOutput(" ssh -i ~/.ssh/id_rsa user@example.com"); displayOutput(" ssh --proxy ws://localhost:3333 user@example.com"); throw new Error("Invalid Usage or No connection string provided."); } let username, hostname; if (connectionString.includes("@")) { [username, hostname] = connectionString.split("@"); } else { hostname = connectionString; } (async () => { let client = null; try { displayOutput(`Connecting to ${hostname}...`); let usedKey = null; displayOutput(`Using proxy URL: ${proxyUrl}`); const configParser = await tbSSH.loadSSHConfig(); if (configParser) { const hostConfig = configParser.getHost(hostname); if (hostConfig && !username && !port && !identityFile) { displayOutput(`Using SSH config for host: ${hostname}`); if (hostConfig.IdentityFile) { try { const pk = await tbSSH.loadPrivateKey(hostConfig.IdentityFile); if (pk) { usedKey = hostConfig.IdentityFile; displayOutput(`SSH config IdentityFile found: ${hostConfig.IdentityFile} keyLen=${pk.length} startsWithBegin=${pk.trim().startsWith("-----BEGIN")}`); } else { displayOutput(`SSH config IdentityFile not found or unreadable: ${hostConfig.IdentityFile}`); } } catch (e) { displayOutput(`Error reading SSH config IdentityFile ${hostConfig.IdentityFile}: ${e.message}`); } } try { client = await tbSSH.createSSHClientFromConfig(hostname); if (!client) { displayError(`createSSHClientFromConfig returned null or undefined for ${hostname}`); } } catch (err) { displayError(`Error creating SSH client from config for ${hostname}: ${err.message}`); if (verbose) displayError(`Stack: ${err.stack}`); } } } async function getClientInternal() { if (configParser) { const hostConfig = configParser.getHost(hostname); if (hostConfig && !username && !port && !identityFile) { try { const c = await tbSSH.createSSHClientFromConfig(hostname); if (c) return c; displayError(`createSSHClientFromConfig returned null or undefined for ${hostname}`); } catch (e) { displayError(`Error creating SSH client from config for ${hostname}: ${e.message}`); if (verbose) displayError(`Stack: ${e.stack}`); } } } const cfg = { host: hostname, port: port ? parseInt(port) : 22, username: username || sessionStorage.getItem("currAcc") || "root", timeout: 60000, keepaliveInterval: 60000, }; if (identityFile) { const pk = await tbSSH.loadPrivateKey(identityFile); if (pk) { cfg.privateKey = pk; usedKey = identityFile; displayOutput(`Using identity file: ${identityFile}`); } else { displayError(`Failed to load identity file: ${identityFile}`); createNewCommandInput(); return null; } } else { const defaultKeys = ["~/.ssh/id_ed25519", "~/.ssh/id_rsa", "~/.ssh/id_ecdsa", "~/.ssh/id_dsa"]; for (const kp of defaultKeys) { const kd = await tbSSH.loadPrivateKey(kp); if (kd) { cfg.privateKey = kd; usedKey = kp; displayOutput(`Using identity file: ${kp}`); break; } } if (!cfg.privateKey) { const password = await new Promise(resolve => { term.write("Password: "); let pwd = ""; const disposable = term.onData(data => { const char = data; if (char === "\r" || char === "\n") { term.writeln(""); disposable.dispose(); resolve(pwd); } else if (char === "\x7f") { if (pwd.length > 0) { pwd = pwd.slice(0, -1); term.write("\b \b"); } } else if (char >= " " && char <= "~") { pwd += char; term.write("*"); } }); }); cfg.password = password; } } try { if (cfg.privateKey) { try { displayOutput(`Auth: key (source=${usedKey || "inline"}) keyLen=${cfg.privateKey.length} startsWithBegin=${cfg.privateKey.trim().startsWith("-----BEGIN")}`); } catch (e) {} } else if (cfg.password) { displayOutput(`Auth: password (provided)`); } else { displayOutput(`Auth: none`); } } catch (e) {} if (typeof tbSSH.createSSHClientWithProxy === "function") { try { displayOutput(`Attempting WebSocket proxy connection via ${proxyUrl}...`); const wsClient = tbSSH.createSSHClientWithProxy(cfg, proxyUrl, true); await wsClient.connect(); if (wsClient.isConnected()) return wsClient; } catch (e) { displayError(`WebSocket proxy failed: ${e.message}`); if (verbose) displayError(`Stack: ${e.stack}`); } } try { const c = await tbSSH.createSSHClient(cfg); if (c && typeof c.connect === "function") return c; if (c) displayOutput(`createSSHClient returned non-standard object; Client shape: ${Object.getOwnPropertyNames(c).join(", ")}`); } catch (e) { displayError(`Error creating SSH client for ${hostname}: ${e.message}`); if (verbose) displayError(`Stack: ${e.stack}`); } if (typeof tbSSH.connectToSSH === "function") { try { const connected = await tbSSH.connectToSSH(cfg.host, cfg.port, cfg.username, cfg.password); if (connected) return connected; } catch (e) { displayError(`connectToSSH fallback failed: ${e.message}`); if (verbose) displayError(`Stack: ${e.stack}`); } } return null; } client = await getClientInternal(); if (!client) { displayError(`Failed to create SSH client for ${hostname}`); createNewCommandInput(); return; } if (!client || typeof client.connect !== "function") { displayError(`Failed to create SSH client for ${hostname}`); try { displayOutput(`Tried: host=${hostname} port=${port ? parseInt(port) : 22} username=${username || sessionStorage.getItem("currAcc") || "root"} auth=${usedKey ? `key(source=${usedKey})` : "password/none"}`); if (client) { try { displayOutput(`Client shape: ${Object.getOwnPropertyNames(client).join(", ")}`); } catch (e) {} } } catch (e) {} createNewCommandInput(); return; } try { if (typeof client.connect === "function") { if (!(typeof client.isConnected === "function" && client.isConnected())) { await client.connect(); } } else { if (!(typeof client.isConnected === "function" && client.isConnected())) { displayError(`SSH client returned but doesn't expose connect() or isConnected() for ${hostname}`); createNewCommandInput(); return; } } } catch (err) { if (client && typeof client.disconnect === "function") { try { client.disconnect(); } catch (e) {} } displayError(`Failed to connect: ${err.message}`); if (verbose) displayError(`Stack: ${err.stack}`); createNewCommandInput(); return; } displayOutput(`Connected to ${hostname}`); const isWebSocketClient = typeof client.setStream === "function" && typeof client.write === "function" && typeof client.shell !== "function"; if (isWebSocketClient) { if (window.parent.tb?.setCommandProcessing) { window.parent.tb.setCommandProcessing(false); } const stream = { onData: data => { const text = typeof data === "string" ? data : new TextDecoder().decode(data); term.write(text); }, onClose: () => { displayOutput("\r\nConnection closed."); client.disconnect(); if (window.parent.tb?.setCommandProcessing) { window.parent.tb.setCommandProcessing(true); } createNewCommandInput(); }, }; client.setStream(stream); term.onData(data => { client.write(data); }); term.onResize(({ cols, rows }) => { if (typeof client.resize === "function") { client.resize(cols, rows); } }); return; } const terminal = new tbSSH.SSHTerminal(client); await terminal.start(); if (window.parent.tb?.setCommandProcessing) { window.parent.tb.setCommandProcessing(false); } terminal.onData(data => { term.write(data); }); terminal.onClose(() => { displayOutput("\r\nConnection closed."); if (client) client.disconnect(); try { if (typeof inputDisposable !== "undefined" && inputDisposable) inputDisposable.dispose(); } catch (e) {} if (window.parent.tb?.setCommandProcessing) { window.parent.tb.setCommandProcessing(true); } createNewCommandInput(); }); const inputDisposable = term.onData(data => { terminal.write(data); }); } catch (error) { if (client) client.disconnect(); displayError(`Failed to connect: ${error.message}`); if (verbose) { displayError(`Stack: ${error.stack}`); } createNewCommandInput(); } })(); ================================================ FILE: public/apps/terminal.tapp/scripts/sysfetch.js ================================================ async function sysfetch(args, term) { if (args._raw.includes("-v")) { displayOutput("Sysfetch v1.0.0"); createNewCommandInput(); } else { let accent = "#32ae62"; let settings = JSON.parse(await window.parent.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8")); if (settings["accent"]) { accent = settings["accent"]; } displayOutput(" %cSystem Information", "color: #3cc3f0; font-weight: bold; text-decoration: underline;"); displayOutput(`%c@@@@@@@@@@@@@@~ B@@@@@@@@#G?. OS%c: TerbiumOS ${tb.system.version()}`, `color: ${accent}`, "color: #b6b6b6"); displayOutput("%cB###&@@@@&####^ #@@@&PPPB@@@G. Kernel%c: Ayla v1.0.0", `color: ${accent}`, "color: #b6b6b6"); displayOutput("%c ~@@@@J .#@@@P ~&@@@^ DE%c: Alexa", `color: ${accent}`, "color: #b6b6b6"); displayOutput("%c ^@@@@? .#@@@@###&@@&7 %c", `color: ${accent}`, "color: #b6b6b6"); displayOutput("%c ^@@@@? .#@@@#555P&@@B7" + " %cHardware Information (estimated)", `color: ${accent}`, "color: #3cc3f0; font-weight: bold; text-decoration: underline;"); await displayCPUInfo(accent); await displayMemoryInfo(accent); await getStorage(accent); await displayGPUInfo(accent); createNewCommandInput(); } } async function displayCPUInfo(accent) { let cpuCors = navigator.hardwareConcurrency; let cpuInfo = "%c ^@@@@? .#@@@P G@@@@" + " %cCPU%c: " + cpuCors + " Logical Cores " + `(${Math.floor(cpuCors / 2)} Cores ${cpuCors} threads)`; displayOutput(cpuInfo, `color: ${accent}`, `color: ${accent}`, "color: #b6b6b6"); return true; } async function displayMemoryInfo(accent) { let mem = navigator.deviceMemory ? navigator.deviceMemory + "GB" : "Unknown"; let memoryInfo = "%c ^@@@@? .#@@@&GGG#@@@@Y " + " %cMemory%c: " + mem; displayOutput(memoryInfo, `color: ${accent}`, `color: ${accent}`, "color: #b6b6b6"); return true; // Im confused tho cus I dont think memory is causing it but like idk // this is like the process list bug in the tb command where it just wouldnt work in that one specific spot } async function displayGPUInfo(accent) { let canvas = document.createElement("canvas"); let gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); if (!gl) { displayOutput("%cGPU%c: Information not available", `color: ${accent}`, "color: #b6b6b6"); return; } let dbgRenderInfo = gl.getExtension("WEBGL_debug_renderer_info"); if (dbgRenderInfo) { let rndr = gl.getParameter(dbgRenderInfo.UNMASKED_RENDERER_WEBGL); let regex = /ANGLE \(.+?,\s*(.+?) \(/; let match = rndr.match(regex); let gpuName = match ? match[1] : ""; displayOutput(` %cGPU%c: ${gpuName}`, `color: ${accent}`, "color: #b6b6b6"); } else { displayOutput(" %cGPU%c: Information not available", `color: ${accent}`, "color: #b6b6b6"); } return true; } async function getStorage(accent) { const estimate = await navigator.storage.estimate(); const totalSize = estimate.quota; const usedSize = estimate.usage; const usedPercentage = (usedSize / totalSize) * 100; let formattedUsedSize, formattedTotalSize; if (usedSize >= 1024 * 1024 * 1024) { formattedUsedSize = `${(usedSize / (1024 * 1024 * 1024)).toFixed(2)} GB`; } else { formattedUsedSize = `${(usedSize / (1024 * 1024)).toFixed(2)} MB`; } if (totalSize >= 1024 * 1024 * 1024) { formattedTotalSize = `${(totalSize / (1024 * 1024 * 1024)).toFixed(2)} GB`; } else { formattedTotalSize = `${Math.round((totalSize / (1024 * 1024)).toFixed(2))} MB`; } displayOutput("%c ^&@@@? B@@@@@@@@&B5~ " + ` %cStorage%c: ${formattedUsedSize} of ${formattedTotalSize}`, `color: ${accent}`, `color: ${accent}`, "color: #b6b6b6"); return true; } sysfetch(args, term); ================================================ FILE: public/apps/terminal.tapp/scripts/taskkill.js ================================================ function taskkill(args) { if (args._raw.includes("list")) { const windows = tb.process.list(); Object.values(windows).forEach(window => { displayOutput(`${window.name}, ${window.pid}`); }); createNewCommandInput(); } else { try { window.tb.process.kill(args._raw); displayOutput(`Successfully killed task with pid: ${args._raw}`); } catch { displayError("Not task found with that PID"); } createNewCommandInput(); } } taskkill(args); ================================================ FILE: public/apps/terminal.tapp/scripts/tb.js ================================================ const ver = "1.0.0"; var cmdData = { help: { desc: "Shows information about a given subcommand", usage: "tb help ...", args: { subcmd: "The subcommand to look up. I.e. tb help system version", }, }, restart: { desc: "Restarts TerbiumOS", usage: "tb restart ", alias: "reboot", args: { "-f/--force": "Clears the session cache upon reboot and reboots to bootloader", "-s/--skip-prompt": "Skips the confirm reboot prompt", }, }, process: { desc: "Parent command for listing terbium processes.", usage: "tb process [subcmd] ... ", alias: "proc", subcmds: { kill: { desc: "Kill/delete a given process", usage: "tb process kill [pid]", alias: "delete", args: { pid: "The ID of the process to kill/delete", }, }, list: { desc: "[ ! BROKEN ! ] Lists all active processes", usage: "tb process list", }, }, }, system: { desc: "Parent command for details about the terbium system.", usage: "tb system [subcmd] ... ", alias: "sys", subcmds: { version: { desc: "Display the currently installed Terbium version.", usage: "tb system version", alias: "ver", }, exportfs: { desc: "Export the terbium filesystem.", usage: "tb system exportfs", }, restartNode: { desc: "Restarts the NodeJS Container", usage: "tb system restartNode", }, }, }, application: { desc: "Parent command for running/modifying apps.", usage: "tb application [subcmd] ... ", alias: "app", subcmds: { run: { desc: "Runs the app located at the specified package ID", usage: "tb application run [app] ", alias: "open", args: { app: "The name of the app to open. Replace any spaces with an underscore ( _ ). Not case sensitive.", "-l/--legacy": "Toggle if the old `com.tb.appname` format should be used or not.", "-j/--json-file": "Process the app argument as a path to an app's configuration file.", }, }, list: { desc: "Lists the installed applications.", usage: "tb application list ", args: { "-d/--directory": "Shows the directory that the application is located", "-c/--config": "Shows the location of the app's config file", }, }, }, }, network: { desc: "Parent command for interacting with Terbium's networking system", usage: "tb network [subcmd] ... ", alias: "net", subcmds: { proxy: { desc: "Parent command for modifying/reading info about the current proxy.", usage: "tb network proxy [subcmd] ... ", subcmds: { active: { desc: "Prints the active proxy in use by Terbium.", usage: "tb network proxy active", }, set: { desc: "Change the proxy that Terbium will use", usage: "tb network proxy set [proxy]", args: { proxy: "The name of the proxy to switch to. CASE SENSITIVE!!!", }, }, }, }, }, }, node: { desc: "Parent command for interacting with Terbium's NodeJS container", usage: "tb node [subcmd] ... ", subcmds: { restart: { desc: "Restarts the NodeJS container", usage: "tb node restart", }, start: { desc: "Starts the NodeJS container", usage: "tb node start", }, stop: { desc: "Stops the NodeJS container", usage: "tb node stop", }, }, }, }; async function tb(args) { function error(err) { displayError(`${err}\n`); createNewCommandInput(); } function help(args) { function resolveCommand(args) { let current = cmdData; for (let i = 1; i < args.length; i++) { const input = args[i]; const scope = current.subcmds || current; const match = Object.entries(scope).find(([key, val]) => key === input || val.alias === input); if (!match) { displayOutput(`Unknown command or alias: ${input}`); return null; } current = match[1]; } if (args.length === 1) { current.v = true; } return current; } function formatData(info) { if (typeof info.v === "undefined") { displayOutput(`Description: ${info.desc}\n`); displayOutput(`USAGE: ${info.usage}`); if (info.alias) displayOutput(`ALIAS(ES): ${info.alias}\n`); if (info.subcmds) { displayOutput("SUBCOMMANDS:"); const subkeys = Object.keys(info.subcmds); for (let i = 0; i < subkeys.length; i++) { displayOutput(`${`${subkeys[i]} ${info.subcmds[subkeys[i]].alias ? `(alias: ${info.subcmds[subkeys[i]].alias})` : ""}`.padEnd(40)}${info.subcmds[subkeys[i]].desc}\n`); } } else if (info.args) { displayOutput("ARGUMENTS:"); const subkeys = Object.keys(info.args); for (let i = 0; i < subkeys.length; i++) { displayOutput(`${`${subkeys[i]}`.padEnd(40)}${info.args[subkeys[i]]}\n`); } } } else { delete info.v; displayOutput('Any commands listed as "parent" commands have subcommands. Use `tb help ` to view it\'s commands.'); displayOutput("List of available commands:\n"); const cmdKeys = Object.keys(cmdData); for (let i = 0; i < cmdKeys.length; i++) { displayOutput(`${`${cmdKeys[i]} ${cmdData[cmdKeys[i]].alias ? `(alias: ${cmdData[cmdKeys[i]].alias})` : ""}`.padEnd(40)}${cmdData[cmdKeys[i]].desc}\n`); } } } const data = resolveCommand(args); if (data != null) { formatData(data); } displayOutput(`Terbium System CLI v${ver}`); createNewCommandInput(); } switch (args._[0]) { case undefined: case null: help(["help"]); break; case "help": help(args._); break; case "restart": case "reboot": { function handleReboot() { if (args.f || args.force) { window.parent.sessionStorage.clear(); window.parent.location.reload(); } else { window.parent.sessionStorage.setItem("logged-in", "false"); window.parent.location.reload(); } } if (args.s || args.skipPrompt) { handleReboot(); } else { await window.parent.tb.dialog.Permissions({ title: "Confirm restart", message: "Are you sure you want to restart Terbium?", onOk: () => { handleReboot(); }, onCancel: () => { error("tb > restart > operation aborted by user"); }, }); } break; } case "lock": window.parent.sessionStorage.setItem("logged-in", false); window.parent.location.reload(); break; case "process": case "proc": switch (args._[1]) { case undefined: case null: error("tb > process > expected an argument at pos 3, got nothing"); break; case "kill": case "delete": if (args._[2] === undefined) { error("tb > process > kill > expected an argument at pos 4, got nothing"); } else { window.parent.tb.process.kill(args._[2]); } createNewCommandInput(); break; case "list": { /* const proclist = window.parent.tb.process.list(); const proclistIDs = Object.keys(proclist); for (let i = 0; i < proclist.length; i++) { displayOutput(`"${typeof proclist[i].name === "string" ? proclist[i].name : proclist[i].name.text}" [PID: ${proclistIDs[i]}]`); } createNewCommandInput(); */ error("tb > process > list > This command is currently broken. If you need to see a list of process ID's, please use task manager for now"); break; } default: error(`tb > process > unknown subcommand: ${args._[1]}`); } break; case "sys": case "system": switch (args._[1]) { case undefined: case null: error("tb > system > expected an argument at pos 3, got nothing"); break; case "ver": case "version": displayOutput(`TerbiumOS version ${window.parent.tb.system.version()}`); createNewCommandInput(); break; case "exportfs": window.parent.tb.setCommandProcessing(true); displayOutput("! WARNING !"); displayOutput("Using this command may cause the tab to freeze momentarily."); displayOutput("DO NOT close this tab until the file finishes downloading."); await window.parent.tb.system.exportfs(); window.parent.tb.setCommandProcessing(false); displayOutput("Success!"); createNewCommandInput(); break; default: error(`tb > system > unknown subcommand: ${args._[1]}`); } break; case "application": case "app": switch (args._[1]) { case undefined: case null: error("tb > application > expected an argument at pos 3, got nothing"); break; case "run": case "open": { if (args._[2] === undefined) { error("tb > application > run > expected an argument at pos 4, got nothing"); } else { try { if (args.l || args.legacy) { if (args._[3]) { await window.parent.tb.system.openApp(args._[2], { rest: args._[3] }); createNewCommandInput(); } else { await window.parent.tb.system.openApp(args._[2]); createNewCommandInput(); } } else { // biome-ignore lint/correctness/noInnerDeclarations: variable isnt needed at the root, keep it at the current scope var resolvedAppConfigFile = ""; if (args.j || args.jsonFile) resolvedAppConfigFile = args._[2]; else { const trueApp = args._[2].split("_").join(" "); const apps = JSON.parse(await window.parent.tb.fs.promises.readFile("/apps/installed.json", "utf8")); const app = apps.find(obj => obj.name.toLowerCase() === trueApp.toLowerCase()); if (app === undefined) resolvedAppConfigFile = undefined; else resolvedAppConfigFile = app.config; } if (resolvedAppConfigFile === undefined) error("tb > application > run > could not find that app"); else { const appConfig = JSON.parse(await window.parent.tb.fs.promises.readFile(resolvedAppConfigFile)).config; window.parent.tb.window.create(appConfig); createNewCommandInput(); } } } catch (e) { error(`tb > application > run > failed to open app: ${e.message}`); } } break; } case "list": { const apps = JSON.parse(await window.parent.tb.fs.promises.readFile("/apps/installed.json", "utf8")); for (const app of apps) { displayOutput(`"${app.name}"${args.d || args.directory ? ` (Directory: ${app.config.replace("index.json", "")})` : ""}${args.c || args.config ? ` (Configuration: ${app.config})` : ""}`); } createNewCommandInput(); break; } default: error(`tb > application > unknown subcommand: ${args._[1]}`); } break; case "network": case "net": switch (args._[1]) { case "proxy": switch (args._[2]) { case "active": displayOutput(`Active proxy: ${await window.parent.tb.proxy.get()}`); createNewCommandInput(); break; case "set": if (typeof args._[3] !== "undefined") { displayOutput((await window.parent.tb.proxy.set(args._[3])) ? `Successfully set the active proxy to ${args._[3]}` : `Could not set the active proxy to ${args._[3]}.`); createNewCommandInput(); } else { error("tb > network > proxy > set > expected an argument at pos 5, got nothing"); } break; default: error(`tb > network > proxy > unknown subcommand: ${args._[2]}`); break; } break; default: error(`tb > network > unknown subcommand: ${args._[1]}`); break; } break; case "node": switch (args._[1]) { case "restart": window.parent.tb.setCommandProcessing(true); displayOutput("Restarting NodeJS container..."); try { await window.parent.tb.node.stop(); displayOutput("container stopped successfullty. starting..."); await window.parent.tb.node.start(); displayOutput("Container restarted."); window.parent.tb.setCommandProcessing(false); createNewCommandInput(); } catch (e) { error("tb > node > restart > Could not restart the NodeJS container."); } window.parent.tb.setCommandProcessing(false); createNewCommandInput(); break; case "start": window.parent.tb.setCommandProcessing(true); if (window.parent.tb.node.isReady) { displayOutput("NodeJS container is already running."); } else { displayOutput("Starting NodeJS container..."); try { window.parent.tb.node.start(); displayOutput("Successfully started the NodeJS container."); } catch (_) { error("tb > node > start > An error occured while starting the NodeJS container."); } } window.parent.tb.setCommandProcessing(false); createNewCommandInput(); break; case "stop": window.parent.tb.setCommandProcessing(true); if (window.parent.tb.node.isReady) { displayOutput("Stopping NodeJS container..."); try { window.parent.tb.node.start(); displayOutput("Successfully stopped the NodeJS container."); } catch (_) { error("tb > node > stop > An error occured while stopping the NodeJS container."); } } else { displayOutput("NodeJS container is already stopped."); } window.parent.tb.setCommandProcessing(false); createNewCommandInput(); break; default: error(`tb > network > unknown subcommand: ${args._[1]}`); break; } break; default: error(`tb > unknown subcommand: ${args._[0]}`); break; } } tb(args, term); ================================================ FILE: public/apps/terminal.tapp/scripts/touch.js ================================================ async function touch(args) { if (args._raw.length <= 0) { displayError("touch: missing operand"); createNewCommandInput(); return; } if (path.includes("/mnt/")) { try { const match = path.match(/\/mnt\/([^\/]+)\//); const davName = match ? match[1].toLowerCase() : ""; const np = path.replace(`/mnt/${davName.toLowerCase()}/`, ""); await tb.vfs.servers.get(davName).connection.promises.writeFile(`${np}/${args._raw}`, "", "utf8"); createNewCommandInput(); } catch (e) { displayError(`TNSM touch: ${e.message}`); createNewCommandInput(); return; } } else { tb.sh.touch(`${path}/${args._raw}`, err => { if (err) { displayError(`touch: ${err.message}`); createNewCommandInput(); } else { createNewCommandInput(); } }); } } touch(args); ================================================ FILE: public/apps/terminal.tapp/scripts/unzip.js ================================================ async function uzip(path, target) { const response = await fetch("/fs/" + path); const zipFileContent = await response.arrayBuffer(); if (!(await dirExists(target))) { await window.parent.tb.fs.promises.mkdir(target, { recursive: true }); } const compressedFiles = window.parent.tb.fflate.unzipSync(new Uint8Array(zipFileContent)); for (const [relativePath, content] of Object.entries(compressedFiles)) { const fullPath = `${target}/${relativePath}`; const pathParts = fullPath.split("/"); let currentPath = ""; for (let i = 0; i < pathParts.length; i++) { currentPath += pathParts[i] + "/"; if (i === pathParts.length - 1 && !relativePath.endsWith("/")) { 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), window.parent.tb.buffer.from(content), "arraybuffer"); } catch { displayOutput(`Cant make ${currentPath.slice(0, -1)}`); console.log(`Cant make ${currentPath.slice(0, -1)}`); } } else if (!(await dirExists(currentPath))) { try { console.log(`mkdir ${currentPath}`); displayOutput(`mkdir ${currentPath}`); await window.parent.tb.fs.promises.mkdir(currentPath); } catch { console.log(`Cant make ${currentPath}`); displayOutput(`Cant make ${currentPath}`); } } } if (relativePath.endsWith("/")) { try { console.log(`mkdir fp ${fullPath}`); await window.parent.tb.fs.promises.mkdir(fullPath); } catch { console.log(`Cant make ${fullPath}`); } } } return "Done!"; } const dirExists = async path => { return new Promise(resolve => { window.parent.tb.fs.stat(path, (err, stats) => { if (err) { if (err.code === "ENOENT") { resolve(false); } else { console.error(err); resolve(false); } } else { const exists = stats.type === "DIRECTORY"; resolve(exists); } }); }); }; async function unzip(args) { if (!args._raw || args._raw.length < 2) { displayOutput("Usage: unzip "); createNewCommandInput(); return; } else if (path.includes("/mnt/")) { displayError("TNSM unzip: Unzipping files from mounted drives is not supported yet."); createNewCommandInput(); return; } try { await uzip(`${path}/${args._[0]}`, `${path}/${args._[1]}`); displayOutput(`Successfully unzipped ${args._[0]} to ${args._[1]}`); } catch (e) { displayError(`Error unzipping file: ${e.message}`); } createNewCommandInput(); return; } unzip(args); ================================================ FILE: public/apps/terminal.tapp/ssh-util.js ================================================ (()=>{"use strict";var e={};e.d=(n,t)=>{for(var r in t)e.o(t,r)&&!e.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:t[r]})},e.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n);var n={};function t(e,n,t,r,o,s,i){try{var c=e[s](i),a=c.value}catch(e){t(e);return}c.done?n(a):Promise.resolve(a).then(r,o)}function r(e){return function(){var n=this,r=arguments;return new Promise(function(o,s){var i=e.apply(n,r);function c(e){t(i,o,s,c,a,"next",e)}function a(e){t(i,o,s,c,a,"throw",e)}c(void 0)})}}e.d(n,{default:()=>w});function o(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function s(e,n){var t,r,o,s={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]},i=Object.create(("function"==typeof Iterator?Iterator:Object).prototype),c=Object.defineProperty;return c(i,"next",{value:a(0)}),c(i,"throw",{value:a(1)}),c(i,"return",{value:a(2)}),"function"==typeof Symbol&&c(i,Symbol.iterator,{value:function(){return this}}),i;function a(c){return function(a){var l=[c,a];if(t)throw TypeError("Generator is already executing.");for(;i&&(i=0,l[0]&&(s=0)),s;)try{if(t=1,r&&(o=2&l[0]?r.return:l[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,l[1])).done)return o;switch(r=0,o&&(l=[2&l[0],o.value]),l[0]){case 0:case 1:o=l;break;case 4:return s.label++,{value:l[1],done:!1};case 5:s.label++,r=l[1],l=[0];continue;case 7:l=s.ops.pop(),s.trys.pop();continue;default:if(!(o=(o=s.trys).length>0&&o[o.length-1])&&(6===l[0]||2===l[0])){s=0;continue}if(3===l[0]&&(!o||l[1]>o[0]&&l[1]=l)throw console.error("[SSH] All retry attempts failed"),u;this._didRetry=!0,this.config.timeout,this.config.timeout=Math.max(2*this.config.timeout,12e4),console.log("[SSH] Retrying connection with increased timeout:",this.config.timeout);try{this.sshProcess&&"function"==typeof this.sshProcess.kill&&this.sshProcess.kill()}catch(e){console.warn("[SSH] Error killing previous process:",e)}return[4,this.webContainer.spawn("node",[o])];case 15:return f=s.sent(),this.sshProcess=f,h=f.output.getReader(),this.readOutput(h),[3,16];case 16:return[3,11];case 17:return this.connected=!0,this.authenticated=!0,[2]}})}).call(this)}},{key:"ensureSSH2Installed",value:function(){return r(function(){var e,n,t;return s(this,function(r){switch(r.label){case 0:return r.trys.push([0,7,,11]),console.log("[SSH] Checking for existing package.json..."),[4,this.webContainer.fs.readFile("/package.json","utf-8")];case 1:if(console.log("[SSH] Found package.json:",t=JSON.parse(r.sent())),!(!(null==(e=t.dependencies)?void 0:e.ssh2)||!(null==(n=t.dependencies)?void 0:n.sshpk)))return[3,5];return console.log("[SSH] Adding ssh2 and sshpk to dependencies if missing..."),t.dependencies=t.dependencies||{},t.dependencies.ssh2=t.dependencies.ssh2||"^1.15.0",t.dependencies.sshpk=t.dependencies.sshpk||"^1.16.1",[4,this.webContainer.fs.writeFile("/package.json",JSON.stringify(t,null,2))];case 2:return r.sent(),console.log("[SSH] Running npm install..."),[4,this.webContainer.spawn("npm",["install"])];case 3:return[4,r.sent().exit];case 4:return console.log("[SSH] npm install completed with exit code:",r.sent()),[3,6];case 5:console.log("[SSH] ssh2 and sshpk already installed"),r.label=6;case 6:return[3,11];case 7:return r.sent(),console.log("[SSH] No package.json found or error, creating new one..."),[4,this.webContainer.fs.writeFile("/package.json",JSON.stringify({name:"ssh-client",dependencies:{ssh2:"^1.15.0"}},null,2))];case 8:return r.sent(),console.log("[SSH] Running npm install for new package.json..."),[4,this.webContainer.spawn("npm",["install"])];case 9:return[4,r.sent().exit];case 10:return console.log("[SSH] npm install completed with exit code:",r.sent()),[3,11];case 11:return[2]}})}).call(this)}},{key:"generateSSHScript",value:function(){var e=this.config,n=e.host,t=e.port,r=e.username,o=e.password,s=e.privateKey,i=e.passphrase,c=[];c.push("host: ".concat(JSON.stringify(n))),c.push("port: ".concat(t)),c.push("username: ".concat(JSON.stringify(r))),o&&c.push("password: ".concat(JSON.stringify(o))),s&&c.push("privateKey: ".concat(JSON.stringify(s))),i&&c.push("passphrase: ".concat(JSON.stringify(i))),c.push("readyTimeout: ".concat(this.config.timeout)),c.push("keepaliveInterval: ".concat(this.config.keepaliveInterval)),c.push("algorithms: {\n kex: [\n 'diffie-hellman-group14-sha1',\n 'diffie-hellman-group-exchange-sha256',\n 'ecdh-sha2-nistp256',\n 'ecdh-sha2-nistp384',\n 'ecdh-sha2-nistp521',\n 'curve25519-sha256',\n 'curve25519-sha256@libssh.org',\n 'diffie-hellman-group1-sha1',\n 'diffie-hellman-group-exchange-sha1'\n ],\n cipher: [\n 'aes128-ctr','aes192-ctr','aes256-ctr','aes128-gcm','aes128-gcm@openssh.com','aes256-gcm','aes256-gcm@openssh.com','aes128-cbc','aes192-cbc','aes256-cbc','3des-cbc'\n ],\n hmac: ['hmac-sha2-256','hmac-sha2-512','hmac-sha1'],\n serverHostKey: ['ssh-rsa','rsa-sha2-512','rsa-sha2-256','ecdsa-sha2-nistp256','ecdsa-sha2-nistp384','ecdsa-sha2-nistp521','ssh-dss']\n }"),c.push("debug: (msg) => console.log('[ssh2-debug]', msg)"),c.push("hostVerifier: (hash) => { console.log('[ssh2] hostVerifier:', hash); return true; }");var a="{\n ".concat(c.join(",\n "),"\n }");return"\nconsole.log('[SSH Script] Starting...');\nimport { Client } from 'ssh2';\nimport net from 'net';\nconsole.log('[SSH Script] ssh2 module loaded');\n\nconst conn = new Client();\nlet shellStream = null;\n\nconsole.log('[SSH Script] Setting up event handlers...');\n\nconn.on('ready', () => {\n console.log('[SSH Script] Connection ready event fired');\n console.log('___SSH_CONNECTED___');\n\n conn.shell((err, stream) => {\n if (err) {\n console.error('[SSH Script] Shell error:', err.message);\n console.error('___SSH_ERROR___', err.message);\n process.exit(1);\n }\n\n console.log('[SSH Script] Shell stream established');\n shellStream = stream;\n\n stream.on('data', (data) => {\n process.stdout.write(data);\n });\n\n stream.on('close', () => {\n console.log('___SSH_CLOSED___');\n conn.end();\n });\n\n stream.stderr.on('data', (data) => {\n process.stderr.write(data);\n });\n\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (data) => {\n if (shellStream) {\n shellStream.write(data);\n }\n });\n });\n});\n\nconn.on('error', (err) => {\n console.error('[SSH Script] Connection error:', err);\n console.error('___SSH_ERROR___', err.message);\n // dump helpful debug info\n try { console.error('[SSH Script] conn properties:', JSON.stringify({ localAddr: conn.localAddress, localPort: conn.localPort })); } catch(e) {}\n process.exit(1);\n});\n\nconn.on('banner', (msg) => {\n console.log('[SSH Script] Server banner:', msg);\n});\n\nconn.on('close', () => {\n console.log('___SSH_CLOSED___');\n});\n\nconsole.log('[SSH Script] Preparing connection config...');\nconst config = ".concat(a,";\n\n// expose the client identification (ident) to mimic OpenSSH for compatibility\nif (!config.ident) {\n config.ident = 'SSH-2.0-OpenSSH_8.9p1';\n}\n\n// If OPENSSH private key format is present, try converting to PEM (sshpk must be installed in the container)\ntry {\n if (config.privateKey && typeof config.privateKey === 'string' && config.privateKey.includes('-----BEGIN OPENSSH PRIVATE KEY-----')) {\n try {\n const { default: sshpk } = await import('sshpk');\n const keyObj = sshpk.parsePrivateKey(config.privateKey, 'openssh');\n config.privateKey = keyObj.toString('pem');\n console.log('[SSH Script] Converted OPENSSH private key to PEM format');\n } catch (e) {\n console.log('[SSH Script] Key conversion failed:', e && e.message);\n }\n }\n} catch(e) { console.log('[SSH Script] Key conversion check error:', e && e.message); }\n\nconsole.log('[SSH Script] Connecting with config:', JSON.stringify({...config, password: config.password ? '***' : undefined, privateKey: config.privateKey ? '***' : undefined, algorithms: config.algorithms}, null, 2));\n\n// TCP banner check to verify server sends SSH identification\nasync function checkServerBanner(host, port, timeoutMs = 10000) {\n return new Promise((resolve) => {\n const sock = net.createConnection(port, host, () => {});\n sock.setEncoding('utf8');\n let buffer = '';\n let done = false;\n sock.on('data', (chunk) => {\n buffer += chunk;\n if (buffer.indexOf('\\n') !== -1 && !done) {\n done = true;\n const line = buffer.split(/\\r?\\n/)[0];\n console.log('[SSH Script] Server banner (raw):', line);\n sock.destroy();\n resolve(line);\n }\n });\n sock.on('error', (err) => {\n if (!done) { done = true; console.log('[SSH Script] Server banner check error:', err.message); resolve(null); }\n });\n setTimeout(() => {\n if (!done) { done = true; console.log('[SSH Script] Server banner check timeout'); try { sock.destroy(); } catch(e) {} resolve(null); }\n }, timeoutMs);\n });\n}\n\ntry {\n const banner = await checkServerBanner(config.host, config.port, 10000);\n if (!banner) {\n console.log('[SSH Script] No server banner received before connect; proceeding anyway');\n }\n\n conn.connect(config);\n console.log('[SSH Script] connect() called, waiting for events...');\n} catch (e) {\n console.error('[SSH Script] Top-level error during connect flow:', e);\n console.error('___SSH_ERROR___', e && e.message ? e.message : String(e));\n process.exit(1);\n}\n")}},{key:"readOutput",value:function(e){return r(function(){var n,t;return s(this,function(r){switch(r.label){case 0:console.log("[SSH] Starting output reader..."),r.label=1;case 1:r.trys.push([1,5,,6]),t=function(){var t,r,o,i,c,a;return s(this,function(s){switch(s.label){case 0:return[4,e.read()];case 1:if(r=(t=s.sent()).done,o=t.value,r)return console.log("[SSH] Output stream ended"),[2,"break"];if(i=void 0,c=void 0,"string"==typeof o)c=o,i=new TextEncoder().encode(o),console.log("[SSH] Received string output:",c.substring(0,200));else{var l;if(null!=(l=Uint8Array)&&"u">typeof Symbol&&l[Symbol.hasInstance]?!l[Symbol.hasInstance](o):!(o instanceof l))return console.error("[SSH] Unexpected value type:",void 0===o?"undefined":o&&"u">typeof Symbol&&o.constructor===Symbol?"symbol":typeof o),[2,"continue"];i=o,console.log("[SSH] Received bytes output:",(c=new TextDecoder().decode(o)).substring(0,200))}if(c.includes("___SSH_CONNECTED___"))return[2,"continue"];if(c.includes("___SSH_CLOSED___"))return n.handleClose(),[2,"continue"];if(c.includes("___SSH_ERROR___"))return console.error("SSH Error:",null==(a=c.split("___SSH_ERROR___")[1])?void 0:a.trim()),[2,"continue"];return n.dataCallbacks.forEach(function(e){return e(i)}),[2]}})},r.label=2;case 2:return n=this,[5,function(e){var n="function"==typeof Symbol&&Symbol.iterator,t=n&&e[n],r=0;if(t)return t.call(e);if(e&&"number"==typeof e.length)return{next:function(){return e&&r>=e.length&&(e=void 0),{value:e&&e[r++],done:!e}}};throw TypeError(n?"Object is not iterable.":"Symbol.iterator is not defined.")}(t())];case 3:if("break"===r.sent())return[3,4];return[3,2];case 4:return[3,6];case 5:return console.error("Error reading SSH output:",r.sent()),this.handleClose(),[3,6];case 6:return[2]}})}).call(this)}},{key:"waitForConnection",value:function(){return r(function(){var e;return s(this,function(n){return e=this,[2,new Promise(function(n,t){console.log("[SSH] Setting up connection timeout:",e.config.timeout+"ms");var r=setTimeout(function(){console.error("[SSH] Connection timeout! No SSH_CONNECTED marker received within",e.config.timeout+"ms"),console.error("[SSH] Buffer contents:",o),t(Error("SSH connection timeout"))},e.config.timeout),o="",s=function(t){var i=new TextDecoder().decode(t);if(o+=i,console.log("[SSH] Checking for connection marker in chunk:",i.substring(0,100)),o.includes("___SSH_CONNECTED___")){console.log("[SSH] Connection marker found!"),clearTimeout(r);var c=e.dataCallbacks.indexOf(s);c>-1&&e.dataCallbacks.splice(c,1),n()}};console.log("[SSH] Registered connection check callback"),e.dataCallbacks.push(s)})]})}).call(this)}},{key:"handleClose",value:function(){this.connected=!1,this.authenticated=!1,this.closeCallbacks.forEach(function(e){return e()})}},{key:"exec",value:function(e){return r(function(){var n,t,r,o,i,c,a,l,u,f,h;return s(this,function(s){switch(s.label){case 0:if(!this.authenticated)throw Error("Not authenticated");return n="\nconst { Client } = require('ssh2');\nconst conn = new Client();\n\nconn.on('ready', () => {\n conn.exec(".concat(JSON.stringify(e),", (err, stream) => {\n if (err) {\n console.error('ERROR:', err.message);\n process.exit(1);\n }\n \n let stdout = '';\n let stderr = '';\n \n stream.on('data', (data) => {\n stdout += data.toString();\n });\n \n stream.stderr.on('data', (data) => {\n stderr += data.toString();\n });\n \n stream.on('close', (code) => {\n console.log('___STDOUT___' + stdout + '___END_STDOUT___');\n console.log('___STDERR___' + stderr + '___END_STDERR___');\n console.log('___EXIT_CODE___' + code + '___END_EXIT_CODE___');\n conn.end();\n });\n });\n});\n\nconn.connect(").concat(JSON.stringify({host:this.config.host,port:this.config.port,username:this.config.username,password:this.config.password,privateKey:this.config.privateKey,passphrase:this.config.passphrase}),");\n"),t="ssh-exec.js",[4,this.webContainer.fs.writeFile(t,n)];case 1:return s.sent(),[4,this.webContainer.spawn("node",[t])];case 2:r=s.sent(),o="",i=r.output.getReader(),s.label=3;case 3:return[4,i.read()];case 4:if(a=(c=s.sent()).done,l=c.value,a)return[3,5];return o+=new TextDecoder().decode(l),[3,3];case 5:return[4,r.exit];case 6:return s.sent(),u=o.match(RegExp("___STDOUT___(.*?)___END_STDOUT___","s")),f=o.match(RegExp("___STDERR___(.*?)___END_STDERR___","s")),h=o.match(/___EXIT_CODE___(\d+)___END_EXIT_CODE___/),[2,{stdout:u?u[1]:"",stderr:f?f[1]:"",exitCode:h?parseInt(h[1]):0}]}})}).call(this)}},{key:"shell",value:function(){return r(function(){var e;return s(this,function(n){if(e=this,!this.authenticated)throw Error("Not authenticated");return[2,{write:function(n){if(e.sshProcess)try{var t="string"==typeof n?new TextEncoder().encode(n):n,r=e.sshProcess.input.getWriter();r.write(t),r.releaseLock()}catch(e){console.error("Error writing to SSH:",e)}},onData:function(n){e.dataCallbacks.push(n)},onClose:function(n){e.closeCallbacks.push(n)},close:function(){e.sshProcess&&e.sshProcess.kill()}}]})}).call(this)}},{key:"sftp",value:function(){return r(function(){return s(this,function(e){throw Error("SFTP not yet implemented in WebContainer version")})})()}},{key:"disconnect",value:function(){if(this.sshProcess)try{this.sshProcess.kill()}catch(e){console.error("Error killing SSH process:",e)}this.connected=!1,this.authenticated=!1,this.connectionPromise=null}},{key:"isConnected",value:function(){return this.connected}},{key:"isAuthenticated",value:function(){return this.authenticated}}],function(e,n){for(var t=0;t1&&void 0!==arguments[1]?arguments[1]:"ws://localhost:3333",r=arguments.length>2&&void 0!==arguments[2]&&arguments[2];if(!(this instanceof n))throw TypeError("Cannot call a class as a function");a(this,"config",void 0),a(this,"ws",null),a(this,"stream",null),a(this,"connected",!1),a(this,"proxyUrl",void 0),a(this,"useLibcurl",!1),this.config=function(e){for(var n=1;n0&&o[o.length-1])&&(6===l[0]||2===l[0])){s=0;continue}if(3===l[0]&&(!o||l[1]>o[0]&&l[1]typeof window&&window.parent.tb.libcurl.WebSocket?(console.log("[SSH WS] Using libcurl.js WebSocket"),e.ws=new window.parent.tb.libcurl.WebSocket(o)):(console.log("[SSH WS] Using native WebSocket"),e.ws=new WebSocket(o)),e.ws.binaryType="arraybuffer";var s=setTimeout(function(){e.connected||t(Error("WebSocket connection timeout"))},e.config.timeout);e.ws.onopen=function(){console.log("[SSH WS] WebSocket connected to proxy"),clearTimeout(s);var n,t=JSON.stringify({type:"ssh-auth",username:e.config.username,password:e.config.password,privateKey:e.config.privateKey,passphrase:e.config.passphrase});null==(n=e.ws)||n.send(t)},e.ws.onmessage=function(r){if("string"==typeof r.data)try{var o=JSON.parse(r.data);"connected"===o.type?(console.log("[SSH WS] SSH connection established"),e.connected=!0,n()):"error"===o.type&&(console.error("[SSH WS] SSH error:",o.message),t(Error(o.message)))}catch(e){console.error("[SSH WS] Failed to parse message:",e)}else if(e.stream&&e.stream.onData){var s=new Uint8Array(r.data);e.stream.onData(s)}},e.ws.onerror=function(e){console.error("[SSH WS] WebSocket error:",e),t(Error("WebSocket connection failed"))},e.ws.onclose=function(){console.log("[SSH WS] WebSocket closed"),e.connected=!1,e.stream&&e.stream.onClose&&e.stream.onClose()}}catch(e){t(e)}})]})},function(){var n=this,t=arguments;return new Promise(function(r,o){var s=e.apply(n,t);function i(e){c(s,r,o,i,a,"next",e)}function a(e){c(s,r,o,i,a,"throw",e)}i(void 0)})}).call(this)}},{key:"write",value:function(e){this.ws&&this.ws.readyState===WebSocket.OPEN?"string"==typeof e?this.ws.send(e):this.ws.send(e.buffer):console.error("[SSH WS] Cannot write - WebSocket not open")}},{key:"resize",value:function(e,n){if(this.ws&&this.ws.readyState===WebSocket.OPEN){var t=JSON.stringify({type:"resize",cols:e,rows:n});this.ws.send(t)}}},{key:"disconnect",value:function(){console.log("[SSH WS] Disconnecting..."),this.connected=!1,this.ws&&(this.ws.close(),this.ws=null)}},{key:"getStream",value:function(){return this.stream}},{key:"setStream",value:function(e){this.stream=e}},{key:"isConnected",value:function(){var e;return this.connected&&(null==(e=this.ws)?void 0:e.readyState)===WebSocket.OPEN}}],function(e,n){for(var t=0;t0&&o[o.length-1])&&(6===l[0]||2===l[0])){s=0;continue}if(3===l[0]&&(!o||l[1]>o[0]&&l[1]typeof window&&(b().catch(console.error),console.log("TB-SSH initialized (WebContainer mode)")),window.tbSSH=n.default})(); //# sourceMappingURL=main.js.map ================================================ FILE: public/apps/terminal.tapp/terminal.css ================================================ @font-face { font-family: Inter; src: url(/fonts/Inter.ttf); } body { font-family: Inter; margin: 0; padding: 0; color: #b6b6b6; display: flex; flex-direction: column; height: 100vh; background-color: #000000b9; } #term { padding: 0 2.5px 2.5px 2.5px; flex-grow: 1; min-height: 0; display: flex; flex-direction: column; } .term-session { flex: 1 1 auto; min-height: 0; display: flex; position: relative; overflow: hidden; } .xterm { flex: 1 1 auto; height: 100%; width: 100%; } #output { min-height: 200px; overflow-y: auto; font-family: monospace; flex-grow: 1; scrollbar-color: #ffffff1f transparent; scrollbar-width: thin; } #output::-webkit-scrollbar { width: 8px; } #output::-webkit-scrollbar-track { background-color: transparent; } #output::-webkit-scrollbar-thumb { background-color: #ffffff1f; border-radius: 4px; } #output::-webkit-scrollbar-thumb:hover { background-color: #ffffff3c; } #output::-webkit-scrollbar-thumb:active { background-color: #ffffff5c; } #output div { white-space: pre-wrap; } .user-input { display: flex; align-items: center; } #input { border: none; outline: none; background-color: transparent; color: #ffffff; padding: 0; font-family: monospace; cursor: default; margin-left: -2px; } .error-text { color: #e64343; } .xterm-helpers { position: absolute; opacity: 0; } ================================================ FILE: public/apps/terminal.tapp/terminal_com.js ================================================ const tb = parent.window.tb; const tb_island = tb.window.island; const tb_window = tb.window; const tb_context_menu = tb.context_menu; const tb_dialog = tb.dialog; tb_island.addControl({ text: "Help", appname: "Terminal", id: "terminal-help", click: async () => { term.write("help"); await window.handleCommand("help"); }, }); ================================================ FILE: public/apps/text editor.tapp/index.css ================================================ @font-face { font-family: Inter; src: url(/fonts/Inter.ttf); } html, body { margin: 0; padding: 0; } body { display: flex; font-family: Inter; color: #ffffff; scrollbar-width: thin; scrollbar-color: #ffffff44 transparent; overflow: hidden; } ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background-color: #ffffff44; border-radius: 20px; border: transparent; } ================================================ FILE: public/apps/text editor.tapp/index.html ================================================ Text Editor ================================================ FILE: public/apps/text editor.tapp/index.js ================================================ import * as webdav from "/fs/apps/system/files.tapp/webdav.js"; window.webdav = webdav; function openFile(data) { const textarea = document.querySelector("textarea"); textarea.value = data; updateLineNumbers(); } async function updateLineNumbers() { const textarea = document.querySelector("textarea"); //const lines = textarea.value.split("\n"); //const lineNumbers = document.querySelector(".lines"); const obj = await hljs.highlightAuto(document.querySelector("textarea").value); console.log(obj.language); /* lineNumbers.innerHTML = ""; lines.forEach((line, i) => { console.log(i); const span = document.createElement("span"); const linesStyles = [ "leading-tight", "font-extrabold", "cursor-pointer" ] span.innerText = i + 1; span.classList.add(...linesStyles); lineNumbers.appendChild(span); }) document.body.style.setProperty("--lines", lines.length); document.body.style.setProperty("--lines-width", lineNumbers.offsetWidth + "px"); */ } window.addEventListener("contextmenu", e => { e.preventDefault(); return false; }); // window.addEventListener("load", () => { // updateLineNumbers(); // }) window.addEventListener("message", async function load(e) { let data; try { data = JSON.parse(e.data); } catch (err) { data = e.data; } if (data && data.type === "process" && data.path) { if (!data.path.includes("http")) { let file = await window.parent.tb.fs.promises.readFile(data.path, "utf8"); if (typeof file === "object") file = JSON.stringify(file); document.body.setAttribute("path", data.path); openFile(file); // updateLineNumbers(); } else { try { const davInstances = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/davs.json`, "utf8")); const davUrl = data.path.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, }); let filePath; if (data.path.startsWith("http")) { const match = data.path.match(/^https?:\/\/[^\/]+\/dav\/([^\/]+\/)?(.+)$/); filePath = match ? "/" + match[2] : data.path; } else { filePath = data.path.replace(davUrl, "/"); } const response = await client.getFileContents(filePath); document.body.setAttribute("path", data.path); document.body.setAttribute("isDav", "true"); const decoder = new TextDecoder("utf-8"); const text = decoder.decode(response); openFile(text); } catch (err) { window.tb.dialog.Alert({ title: "Failed to read dav file", message: err, }); } } } window.removeEventListener("message", load); }); function updateScroll(type, e) { const textarea = document.querySelector("textarea"); if (type === "key") { const scrollAmount = e === "ArrowUp" ? -20 : 20; textarea.scrollTop += scrollAmount; } else if (type === "mouse") { const scrollAmount = e.deltaY; textarea.scrollTop += scrollAmount; } } const textarea = document.querySelector("textarea"); // hljs.highlightElement(textarea); textarea.addEventListener("keydown", async e => { if ((e.ctrlKey || e.metaKey) && e.key === "s") { e.preventDefault(); const textarea = document.querySelector("textarea"); let ext; const highlightResult = await hljs.highlightAuto(textarea.value); if (highlightResult.language) { ext = highlightResult.language; } else if (highlightResult._top?.aliases) { ext = highlightResult._top.aliases[0]; } else { ext = ".txt"; } const path = document.body.getAttribute("path"); if (path && path !== "undefined") { if (document.body.getAttribute("isDav") === "true") { try { const davInstances = JSON.parse(await window.parent.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/davs.json`, "utf8")); const davUrl = path.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, }); let filePath; if (path.startsWith("http")) { const match = path.match(/^https?:\/\/[^\/]+\/dav\/([^\/]+\/)?(.+)$/); filePath = match ? "/" + match[2] : path; } else { filePath = path.replace(davUrl, "/"); } console.log(filePath); client.putFileContents(filePath, textarea.value); } catch (err) { window.tb.dialog.Alert({ title: "Failed to save dav file", message: err.message, }); } } else { window.parent.tb.fs.promises.writeFile(path, textarea.value); } } else { await tb.dialog.SaveFile({ title: "Save Text File", filename: `untitled.${ext}`, onOk: async txt => { window.parent.tb.fs.writeFile(`${txt}`, textarea.value, err => { if (err) return alert(err); }); }, }); } } // else if(e.key === "ArrowUp" || e.key === "ArrowDown") { // updateScroll("key", e.key); // } }); // textarea.addEventListener("input", () => { // updateLineNumbers(); // updateScroll("key", "ArrowUp"); // }); // window.addEventListener("wheel", (e) => { // updateScroll("mouse", e); // }); ================================================ FILE: public/apps/text editor.tapp/index.json ================================================ { "name": "Text Editor", "config": { "title": "Text Editor", "icon": "/fs/apps/system/text editor.tapp/icon.svg", "src": "/fs/apps/system/text editor.tapp/index.html" } } ================================================ FILE: public/apps/text editor.tapp/text.com.js ================================================ const tb = parent.window.tb; const tb_island = tb.window.island; const tb_window = tb.window; const tb_context_menu = tb.context_menu; const tb_dialog = tb.dialog; const appisland = window.parent.document.querySelector(".app_island").clientHeight + 12; tb_island.addControl({ text: "File", appname: "Text Editor", id: "text-file", click: () => { tb.contextmenu.create({ x: 112, y: appisland, iframe: false, options: [ { text: "Open", click: async () => { const textarea = document.querySelector("textarea"); await tb.dialog.FileBrowser({ title: "Open Text File", filename: "untitled.txt", onOk: async file => { document.body.setAttribute("path", file); textarea.value = await tb.vfs.whatFS(file).promises.readFile(file, "utf8"); }, }); }, }, { text: "Save", click: async () => { const textarea = document.querySelector("textarea"); if (document.body.getAttribute("path") && document.body.getAttribute("path") !== "undefined") { tb.fs.promises.writeFile(document.body.getAttribute("path"), textarea.value); } else { await tb.dialog.SaveFile({ title: "Save Text File", filename: "untitled.txt", onOk: async txt => { tb.vfs.whatFS(txt).writeFile(`${txt}`, textarea.value, err => { if (err) return alert(err); }); }, }); } }, }, ], }); }, }); tb_island.addControl({ text: "Computer", appname: "Text Editor", id: "text-computer", click: () => { tb.contextmenu.create({ x: 156, y: appisland, iframe: false, options: [ { text: "Open", click: async () => { const file = document.createElement("input"); file.type = "file"; file.onchange = async e => { let blob = e.target.files[0]; const fileReader = new FileReader(); fileReader.readAsText(blob); fileReader.onload = () => { openFile(fileReader.result); }; }; file.click(); }, }, { text: "Save", click: async () => { const textarea = document.querySelector("textarea"); const file = new Blob([textarea.value], { type: "text/plain" }); const url = URL.createObjectURL(file); const a = document.createElement("a"); a.href = url; a.download = "text.txt"; a.click(); }, }, ], }); }, }); ================================================ FILE: public/assets/fs.ui/fs.css ================================================ @import 'tailwindcss'; @custom-variant dark (&:where(.dark, .dark *)); @media (prefers-color-scheme: light) { :root { --cursor-normal: url("/cursors/light/normal.svg") 6 0, default; --cursor-pointer: url("/cursors/light/pointer.svg") 6 0, pointer; --cursor-text: url("/cursors/light/text.svg") 10 0, text; --cursor-crosshair: url("/cursors/light/crosshair.svg") 0 0, crosshair; --cursor-wait: url("/cursors/light/wait.svg") 0 0, wait; --cursor-se-resize: url("/cursors/light/resize-l.svg") 0 0, se-resize; --cursor-sw-resize: url("/cursors/light/resize-r.svg") 0 0, sw-resize; --cursor-ne-resize: url("/cursors/light/resize-r.svg") 0 0, ne-resize; --cursor-n-resize: url("/cursors/light/resize-v.svg") 0 0, n-resize; --cursor-s-resize: url("/cursors/light/resize-v.svg") 0 0, s-resize; --cursor-e-resize: url("/cursors/light/resize-h.svg") 0 0, e-resize; --cursor-w-resize: url("/cursors/light/resize-h.svg") 0 0, w-resize; } body { background-color: #ffffff; color: #000000c8; cursor: var(--cursor-normal); } } @media (prefers-color-scheme: dark) { :root { --cursor-normal: url("/cursors/dark/normal.svg") 6 0, default; --cursor-pointer: url("/cursors/dark/pointer.svg") 6 0, pointer; --cursor-text: url("/cursors/dark/text.svg") 10 0, text; --cursor-crosshair: url("/cursors/dark/crosshair.svg") 0 0, crosshair; --cursor-wait: url("/cursors/dark/wait.svg") 0 0, wait; --cursor-nw-resize: url("/cursors/dark/resize-l.svg") 0 0, nw-resize; --cursor-se-resize: url("/cursors/dark/resize-l.svg") 0 0, se-resize; --cursor-sw-resize: url("/cursors/dark/resize-r.svg") 0 0, sw-resize; --cursor-ne-resize: url("/cursors/dark/resize-r.svg") 0 0, ne-resize; --cursor-n-resize: url("/cursors/dark/resize-v.svg") 0 0, n-resize; --cursor-s-resize: url("/cursors/dark/resize-v.svg") 0 0, s-resize; --cursor-e-resize: url("/cursors/dark/resize-h.svg") 0 0, e-resize; --cursor-w-resize: url("/cursors/dark/resize-h.svg") 0 0, w-resize; } body { background-color: #0e0e0e; color: #ffffffde; cursor: var(--cursor-normal); } } @font-face { font-family: Inter; src: url("/fonts/Inter.ttf") format("truetype"); } html, body { height: 100%; margin: 0; padding: 0; font-family: Inter; } body { display: flex; flex-direction: column; } ::-webkit-scrollbar { width: 8px; height: 100%; } ::-webkit-scrollbar-thumb { background-color: #ffffff28; border-radius: 8px; } ::-webkit-scrollbar-track { background-color: #ffffff10; border-radius: 8px; } ================================================ FILE: public/assets/fs.ui/fs.js ================================================ const breadcrumbsEl = document.querySelector('.breadcrumbs'); const navBackEl = document.querySelector('.nav-back'); const navHomeEl = document.querySelector('.nav-home'); const url = new URL(window.location.href); var breadcrumbs = [] var currentPath = url.pathname.replace(/\/+/g, '/'); var currentDir = currentPath.split('/').pop(); if (currentPath.endsWith('/')) { currentPath = currentPath.slice(0, -1); } if (currentPath !== '/fs') { breadcrumbs = currentPath.split('/').slice(2); const fsIndex = breadcrumbs.indexOf('fs'); if (fsIndex !== -1) { breadcrumbs.splice(fsIndex, 1); } } const updateBreadcrumbs = () => { breadcrumbsEl.innerHTML = ''; if(currentPath === '/fs') { const rootHolderEl = document.createElement('span'); rootHolderEl.classList = 'flex leading-none text-2xl text-[#ffffffff]'; rootHolderEl.textContent = 'root'; breadcrumbsEl.appendChild(rootHolderEl); return } breadcrumbs.forEach((crumb, index) => { if(index > 12) return; if(index > 0) { const separatorEl = document.createElement('span'); separatorEl.innerHTML = ` ` separatorEl.classList = 'flex leading-none justify-center items-center dark:text-[#ffffff68] text-[#00000088]'; breadcrumbsEl.appendChild(separatorEl); } const crumbEl = document.createElement('div'); crumbEl.textContent = crumb; if (index !== breadcrumbs.length - 1) { crumbEl.classList = 'flex leading-none p-1.5 text-lg dark:text-[#ffffffd5] dark:hover:text-[#fffffff8] dark:bg-[#ffffff18] dark:inset-shadow-[0_0_0_0.5px_#ffffff38] text-[#00000098] hover:text-[#000000] bg-[#00000025] inset-shadow-[0_0_0_1px_#00000068] rounded-md duration-150 cursor-(--cursor-pointer)'; crumbEl.onmousedown = (e) => { e.preventDefault(); if (e.button === 0) { const newPath = breadcrumbs.slice(0, index + 1).join('/'); window.location.href = `/fs/${newPath}`; } } } else { crumbEl.classList = 'flex leading-none p-1.5 text-lg dark:text-[#ffffffd5] dark:bg-[#ffffff18] dark:inset-shadow-[0_0_0_0.5px_#ffffff38] text-[#00000098] bg-[#00000025] inset-shadow-[0_0_0_1px_#00000068] rounded-md duration-150'; } crumbEl.classList.add("select-none"); breadcrumbsEl.appendChild(crumbEl); }); } updateBreadcrumbs(); navBackEl.onmousedown = (e) => { e.preventDefault(); if(currentPath === '/fs') return; if (e.button === 0) { if(breadcrumbs.length > 0) { breadcrumbs.pop(); const newPath = breadcrumbs.join('/'); window.location.href = `/fs/${newPath}`; } else { window.location.href = '/fs/'; } } } navHomeEl.onmousedown = (e) => { e.preventDefault(); if(currentPath === '/fs') return; if (e.button === 0) { window.location.href = '/fs/'; } } ================================================ FILE: public/assets/libs/comlink.min.umd.js ================================================ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Comlink={})}(this,(function(e){"use strict"; /** * @license * Copyright 2019 Google LLC * SPDX-License-Identifier: Apache-2.0 */const t=Symbol("Comlink.proxy"),n=Symbol("Comlink.endpoint"),r=Symbol("Comlink.releaseProxy"),a=Symbol("Comlink.finalizer"),o=Symbol("Comlink.thrown"),s=e=>"object"==typeof e&&null!==e||"function"==typeof e,i=new Map([["proxy",{canHandle:e=>s(e)&&e[t],serialize(e){const{port1:t,port2:n}=new MessageChannel;return c(e,t),[n,[n]]},deserialize:e=>(e.start(),l(e))}],["throw",{canHandle:e=>s(e)&&o in e,serialize({value:e}){let t;return t=e instanceof Error?{isError:!0,value:{message:e.message,name:e.name,stack:e.stack}}:{isError:!1,value:e},[t,[]]},deserialize(e){if(e.isError)throw Object.assign(new Error(e.value.message),e.value);throw e.value}}]]);function c(e,t=globalThis,n=["*"]){t.addEventListener("message",(function r(s){if(!s||!s.data)return;if(!function(e,t){for(const n of e){if(t===n||"*"===n)return!0;if(n instanceof RegExp&&n.test(t))return!0}return!1}(n,s.origin))return void console.warn(`Invalid origin '${s.origin}' for comlink proxy`);const{id:i,type:l,path:p}=Object.assign({path:[]},s.data),f=(s.data.argumentList||[]).map(w);let d;try{const t=p.slice(0,-1).reduce(((e,t)=>e[t]),e),n=p.reduce(((e,t)=>e[t]),e);switch(l){case"GET":d=n;break;case"SET":t[p.slice(-1)[0]]=w(s.data.value),d=!0;break;case"APPLY":d=n.apply(t,f);break;case"CONSTRUCT":d=b(new n(...f));break;case"ENDPOINT":{const{port1:t,port2:n}=new MessageChannel;c(e,n),d=E(t,[t])}break;case"RELEASE":d=void 0;break;default:return}}catch(e){d={value:e,[o]:0}}Promise.resolve(d).catch((e=>({value:e,[o]:0}))).then((n=>{const[o,s]=v(n);t.postMessage(Object.assign(Object.assign({},o),{id:i}),s),"RELEASE"===l&&(t.removeEventListener("message",r),u(t),a in e&&"function"==typeof e[a]&&e[a]())})).catch((e=>{const[n,r]=v({value:new TypeError("Unserializable return value"),[o]:0});t.postMessage(Object.assign(Object.assign({},n),{id:i}),r)}))})),t.start&&t.start()}function u(e){(function(e){return"MessagePort"===e.constructor.name})(e)&&e.close()}function l(e,t){const n=new Map;return e.addEventListener("message",(function(e){const{data:t}=e;if(!t||!t.id)return;const r=n.get(t.id);if(r)try{r(t)}finally{n.delete(t.id)}})),m(e,n,[],t)}function p(e){if(e)throw new Error("Proxy has been released and is not useable")}function f(e){return k(e,new Map,{type:"RELEASE"}).then((()=>{u(e)}))}const d=new WeakMap,g="FinalizationRegistry"in globalThis&&new FinalizationRegistry((e=>{const t=(d.get(e)||0)-1;d.set(e,t),0===t&&f(e)}));function m(e,t,a=[],o=function(){}){let s=!1;const i=new Proxy(o,{get(n,o){if(p(s),o===r)return()=>{!function(e){g&&g.unregister(e)}(i),f(e),t.clear(),s=!0};if("then"===o){if(0===a.length)return{then:()=>i};const n=k(e,t,{type:"GET",path:a.map((e=>e.toString()))}).then(w);return n.then.bind(n)}return m(e,t,[...a,o])},set(n,r,o){p(s);const[i,c]=v(o);return k(e,t,{type:"SET",path:[...a,r].map((e=>e.toString())),value:i},c).then(w)},apply(r,o,i){p(s);const c=a[a.length-1];if(c===n)return k(e,t,{type:"ENDPOINT"}).then(w);if("bind"===c)return m(e,t,a.slice(0,-1));const[u,l]=y(i);return k(e,t,{type:"APPLY",path:a.map((e=>e.toString())),argumentList:u},l).then(w)},construct(n,r){p(s);const[o,i]=y(r);return k(e,t,{type:"CONSTRUCT",path:a.map((e=>e.toString())),argumentList:o},i).then(w)}});return function(e,t){const n=(d.get(t)||0)+1;d.set(t,n),g&&g.register(e,t,e)}(i,e),i}function y(e){const t=e.map(v);return[t.map((e=>e[0])),(n=t.map((e=>e[1])),Array.prototype.concat.apply([],n))];var n}const h=new WeakMap;function E(e,t){return h.set(e,t),e}function b(e){return Object.assign(e,{[t]:!0})}function v(e){for(const[t,n]of i)if(n.canHandle(e)){const[r,a]=n.serialize(e);return[{type:"HANDLER",name:t,value:r},a]}return[{type:"RAW",value:e},h.get(e)||[]]}function w(e){switch(e.type){case"HANDLER":return i.get(e.name).deserialize(e.value);case"RAW":return e.value}}function k(e,t,n,r){return new Promise((a=>{const o=new Array(4).fill(0).map((()=>Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString(16))).join("-");t.set(o,a),e.start&&e.start(),e.postMessage(Object.assign({id:o},n),r)}))}e.createEndpoint=n,e.expose=c,e.finalizer=a,e.proxy=b,e.proxyMarker=t,e.releaseProxy=r,e.transfer=E,e.transferHandlers=i,e.windowEndpoint=function(e,t=globalThis,n="*"){return{postMessage:(t,r)=>e.postMessage(t,n,r),addEventListener:t.addEventListener.bind(t),removeEventListener:t.removeEventListener.bind(t)}},e.wrap=l})); //# sourceMappingURL=comlink.min.umd.js.map ================================================ FILE: public/assets/libs/idb-keyval.js ================================================ function _slicedToArray(t,n){return _arrayWithHoles(t)||_iterableToArrayLimit(t,n)||_unsupportedIterableToArray(t,n)||_nonIterableRest()}function _nonIterableRest(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function _unsupportedIterableToArray(t,n){if(t){if("string"==typeof t)return _arrayLikeToArray(t,n);var r=Object.prototype.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?_arrayLikeToArray(t,n):void 0}}function _arrayLikeToArray(t,n){(null==n||n>t.length)&&(n=t.length);for(var r=0,e=new Array(n);r0&&void 0!==arguments[0]?arguments[0]:o();return t("readwrite",(function(t){return t.clear(),n(t.transaction)}))},t.createStore=r,t.del=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:o();return r("readwrite",(function(r){return r.delete(t),n(r.transaction)}))},t.delMany=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:o();return r("readwrite",(function(r){return t.forEach((function(t){return r.delete(t)})),n(r.transaction)}))},t.entries=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:o();return t("readonly",(function(r){if(r.getAll&&r.getAllKeys)return Promise.all([n(r.getAllKeys()),n(r.getAll())]).then((function(t){var n=_slicedToArray(t,2),r=n[0],e=n[1];return r.map((function(t,n){return[t,e[n]]}))}));var e=[];return t("readonly",(function(t){return u(t,(function(t){return e.push([t.key,t.value])})).then((function(){return e}))}))}))},t.get=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:o();return r("readonly",(function(r){return n(r.get(t))}))},t.getMany=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:o();return r("readonly",(function(r){return Promise.all(t.map((function(t){return n(r.get(t))})))}))},t.keys=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:o();return t("readonly",(function(t){if(t.getAllKeys)return n(t.getAllKeys());var r=[];return u(t,(function(t){return r.push(t.key)})).then((function(){return r}))}))},t.promisifyRequest=n,t.set=function(t,r){var e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:o();return e("readwrite",(function(e){return e.put(r,t),n(e.transaction)}))},t.setMany=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:o();return r("readwrite",(function(r){return t.forEach((function(t){return r.put(t[1],t[0])})),n(r.transaction)}))},t.update=function(t,r){var e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:o();return e("readwrite",(function(e){return new Promise((function(o,u){e.get(t).onsuccess=function(){try{e.put(r(this.result),t),o(n(e.transaction))}catch(t){u(t)}}}))}))},t.values=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:o();return t("readonly",(function(t){if(t.getAll)return n(t.getAll());var r=[];return u(t,(function(t){return r.push(t.value)})).then((function(){return r}))}))},Object.defineProperty(t,"__esModule",{value:!0})})); ================================================ FILE: public/assets/libs/mime.iife.js ================================================ var mime = (function (exports) { 'use strict'; const types$1 = { 'application/prs.cww': ['cww'], 'application/prs.xsf+xml': ['xsf'], 'application/vnd.1000minds.decision-model+xml': ['1km'], 'application/vnd.3gpp.pic-bw-large': ['plb'], 'application/vnd.3gpp.pic-bw-small': ['psb'], 'application/vnd.3gpp.pic-bw-var': ['pvb'], 'application/vnd.3gpp2.tcap': ['tcap'], 'application/vnd.3m.post-it-notes': ['pwn'], 'application/vnd.accpac.simply.aso': ['aso'], 'application/vnd.accpac.simply.imp': ['imp'], 'application/vnd.acucobol': ['acu'], 'application/vnd.acucorp': ['atc', 'acutc'], 'application/vnd.adobe.air-application-installer-package+zip': ['air'], 'application/vnd.adobe.formscentral.fcdt': ['fcdt'], 'application/vnd.adobe.fxp': ['fxp', 'fxpl'], 'application/vnd.adobe.xdp+xml': ['xdp'], 'application/vnd.adobe.xfdf': ['*xfdf'], 'application/vnd.age': ['age'], 'application/vnd.ahead.space': ['ahead'], 'application/vnd.airzip.filesecure.azf': ['azf'], 'application/vnd.airzip.filesecure.azs': ['azs'], 'application/vnd.amazon.ebook': ['azw'], 'application/vnd.americandynamics.acc': ['acc'], 'application/vnd.amiga.ami': ['ami'], 'application/vnd.android.package-archive': ['apk'], 'application/vnd.anser-web-certificate-issue-initiation': ['cii'], 'application/vnd.anser-web-funds-transfer-initiation': ['fti'], 'application/vnd.antix.game-component': ['atx'], 'application/vnd.apple.installer+xml': ['mpkg'], 'application/vnd.apple.keynote': ['key'], 'application/vnd.apple.mpegurl': ['m3u8'], 'application/vnd.apple.numbers': ['numbers'], 'application/vnd.apple.pages': ['pages'], 'application/vnd.apple.pkpass': ['pkpass'], 'application/vnd.aristanetworks.swi': ['swi'], 'application/vnd.astraea-software.iota': ['iota'], 'application/vnd.audiograph': ['aep'], 'application/vnd.balsamiq.bmml+xml': ['bmml'], 'application/vnd.blueice.multipass': ['mpm'], 'application/vnd.bmi': ['bmi'], 'application/vnd.businessobjects': ['rep'], 'application/vnd.chemdraw+xml': ['cdxml'], 'application/vnd.chipnuts.karaoke-mmd': ['mmd'], 'application/vnd.cinderella': ['cdy'], 'application/vnd.citationstyles.style+xml': ['csl'], 'application/vnd.claymore': ['cla'], 'application/vnd.cloanto.rp9': ['rp9'], 'application/vnd.clonk.c4group': ['c4g', 'c4d', 'c4f', 'c4p', 'c4u'], 'application/vnd.cluetrust.cartomobile-config': ['c11amc'], 'application/vnd.cluetrust.cartomobile-config-pkg': ['c11amz'], 'application/vnd.commonspace': ['csp'], 'application/vnd.contact.cmsg': ['cdbcmsg'], 'application/vnd.cosmocaller': ['cmc'], 'application/vnd.crick.clicker': ['clkx'], 'application/vnd.crick.clicker.keyboard': ['clkk'], 'application/vnd.crick.clicker.palette': ['clkp'], 'application/vnd.crick.clicker.template': ['clkt'], 'application/vnd.crick.clicker.wordbank': ['clkw'], 'application/vnd.criticaltools.wbs+xml': ['wbs'], 'application/vnd.ctc-posml': ['pml'], 'application/vnd.cups-ppd': ['ppd'], 'application/vnd.curl.car': ['car'], 'application/vnd.curl.pcurl': ['pcurl'], 'application/vnd.dart': ['dart'], 'application/vnd.data-vision.rdz': ['rdz'], 'application/vnd.dbf': ['dbf'], 'application/vnd.dece.data': ['uvf', 'uvvf', 'uvd', 'uvvd'], 'application/vnd.dece.ttml+xml': ['uvt', 'uvvt'], 'application/vnd.dece.unspecified': ['uvx', 'uvvx'], 'application/vnd.dece.zip': ['uvz', 'uvvz'], 'application/vnd.denovo.fcselayout-link': ['fe_launch'], 'application/vnd.dna': ['dna'], 'application/vnd.dolby.mlp': ['mlp'], 'application/vnd.dpgraph': ['dpg'], 'application/vnd.dreamfactory': ['dfac'], 'application/vnd.ds-keypoint': ['kpxx'], 'application/vnd.dvb.ait': ['ait'], 'application/vnd.dvb.service': ['svc'], 'application/vnd.dynageo': ['geo'], 'application/vnd.ecowin.chart': ['mag'], 'application/vnd.enliven': ['nml'], 'application/vnd.epson.esf': ['esf'], 'application/vnd.epson.msf': ['msf'], 'application/vnd.epson.quickanime': ['qam'], 'application/vnd.epson.salt': ['slt'], 'application/vnd.epson.ssf': ['ssf'], 'application/vnd.eszigno3+xml': ['es3', 'et3'], 'application/vnd.ezpix-album': ['ez2'], 'application/vnd.ezpix-package': ['ez3'], 'application/vnd.fdf': ['*fdf'], 'application/vnd.fdsn.mseed': ['mseed'], 'application/vnd.fdsn.seed': ['seed', 'dataless'], 'application/vnd.flographit': ['gph'], 'application/vnd.fluxtime.clip': ['ftc'], 'application/vnd.framemaker': ['fm', 'frame', 'maker', 'book'], 'application/vnd.frogans.fnc': ['fnc'], 'application/vnd.frogans.ltf': ['ltf'], 'application/vnd.fsc.weblaunch': ['fsc'], 'application/vnd.fujitsu.oasys': ['oas'], 'application/vnd.fujitsu.oasys2': ['oa2'], 'application/vnd.fujitsu.oasys3': ['oa3'], 'application/vnd.fujitsu.oasysgp': ['fg5'], 'application/vnd.fujitsu.oasysprs': ['bh2'], 'application/vnd.fujixerox.ddd': ['ddd'], 'application/vnd.fujixerox.docuworks': ['xdw'], 'application/vnd.fujixerox.docuworks.binder': ['xbd'], 'application/vnd.fuzzysheet': ['fzs'], 'application/vnd.genomatix.tuxedo': ['txd'], 'application/vnd.geogebra.file': ['ggb'], 'application/vnd.geogebra.slides': ['ggs'], 'application/vnd.geogebra.tool': ['ggt'], 'application/vnd.geometry-explorer': ['gex', 'gre'], 'application/vnd.geonext': ['gxt'], 'application/vnd.geoplan': ['g2w'], 'application/vnd.geospace': ['g3w'], 'application/vnd.gmx': ['gmx'], 'application/vnd.google-apps.document': ['gdoc'], 'application/vnd.google-apps.presentation': ['gslides'], 'application/vnd.google-apps.spreadsheet': ['gsheet'], 'application/vnd.google-earth.kml+xml': ['kml'], 'application/vnd.google-earth.kmz': ['kmz'], 'application/vnd.gov.sk.xmldatacontainer+xml': ['xdcf'], 'application/vnd.grafeq': ['gqf', 'gqs'], 'application/vnd.groove-account': ['gac'], 'application/vnd.groove-help': ['ghf'], 'application/vnd.groove-identity-message': ['gim'], 'application/vnd.groove-injector': ['grv'], 'application/vnd.groove-tool-message': ['gtm'], 'application/vnd.groove-tool-template': ['tpl'], 'application/vnd.groove-vcard': ['vcg'], 'application/vnd.hal+xml': ['hal'], 'application/vnd.handheld-entertainment+xml': ['zmm'], 'application/vnd.hbci': ['hbci'], 'application/vnd.hhe.lesson-player': ['les'], 'application/vnd.hp-hpgl': ['hpgl'], 'application/vnd.hp-hpid': ['hpid'], 'application/vnd.hp-hps': ['hps'], 'application/vnd.hp-jlyt': ['jlt'], 'application/vnd.hp-pcl': ['pcl'], 'application/vnd.hp-pclxl': ['pclxl'], 'application/vnd.hydrostatix.sof-data': ['sfd-hdstx'], 'application/vnd.ibm.minipay': ['mpy'], 'application/vnd.ibm.modcap': ['afp', 'listafp', 'list3820'], 'application/vnd.ibm.rights-management': ['irm'], 'application/vnd.ibm.secure-container': ['sc'], 'application/vnd.iccprofile': ['icc', 'icm'], 'application/vnd.igloader': ['igl'], 'application/vnd.immervision-ivp': ['ivp'], 'application/vnd.immervision-ivu': ['ivu'], 'application/vnd.insors.igm': ['igm'], 'application/vnd.intercon.formnet': ['xpw', 'xpx'], 'application/vnd.intergeo': ['i2g'], 'application/vnd.intu.qbo': ['qbo'], 'application/vnd.intu.qfx': ['qfx'], 'application/vnd.ipunplugged.rcprofile': ['rcprofile'], 'application/vnd.irepository.package+xml': ['irp'], 'application/vnd.is-xpr': ['xpr'], 'application/vnd.isac.fcs': ['fcs'], 'application/vnd.jam': ['jam'], 'application/vnd.jcp.javame.midlet-rms': ['rms'], 'application/vnd.jisp': ['jisp'], 'application/vnd.joost.joda-archive': ['joda'], 'application/vnd.kahootz': ['ktz', 'ktr'], 'application/vnd.kde.karbon': ['karbon'], 'application/vnd.kde.kchart': ['chrt'], 'application/vnd.kde.kformula': ['kfo'], 'application/vnd.kde.kivio': ['flw'], 'application/vnd.kde.kontour': ['kon'], 'application/vnd.kde.kpresenter': ['kpr', 'kpt'], 'application/vnd.kde.kspread': ['ksp'], 'application/vnd.kde.kword': ['kwd', 'kwt'], 'application/vnd.kenameaapp': ['htke'], 'application/vnd.kidspiration': ['kia'], 'application/vnd.kinar': ['kne', 'knp'], 'application/vnd.koan': ['skp', 'skd', 'skt', 'skm'], 'application/vnd.kodak-descriptor': ['sse'], 'application/vnd.las.las+xml': ['lasxml'], 'application/vnd.llamagraphics.life-balance.desktop': ['lbd'], 'application/vnd.llamagraphics.life-balance.exchange+xml': ['lbe'], 'application/vnd.lotus-1-2-3': ['123'], 'application/vnd.lotus-approach': ['apr'], 'application/vnd.lotus-freelance': ['pre'], 'application/vnd.lotus-notes': ['nsf'], 'application/vnd.lotus-organizer': ['org'], 'application/vnd.lotus-screencam': ['scm'], 'application/vnd.lotus-wordpro': ['lwp'], 'application/vnd.macports.portpkg': ['portpkg'], 'application/vnd.mapbox-vector-tile': ['mvt'], 'application/vnd.mcd': ['mcd'], 'application/vnd.medcalcdata': ['mc1'], 'application/vnd.mediastation.cdkey': ['cdkey'], 'application/vnd.mfer': ['mwf'], 'application/vnd.mfmp': ['mfm'], 'application/vnd.micrografx.flo': ['flo'], 'application/vnd.micrografx.igx': ['igx'], 'application/vnd.mif': ['mif'], 'application/vnd.mobius.daf': ['daf'], 'application/vnd.mobius.dis': ['dis'], 'application/vnd.mobius.mbk': ['mbk'], 'application/vnd.mobius.mqy': ['mqy'], 'application/vnd.mobius.msl': ['msl'], 'application/vnd.mobius.plc': ['plc'], 'application/vnd.mobius.txf': ['txf'], 'application/vnd.mophun.application': ['mpn'], 'application/vnd.mophun.certificate': ['mpc'], 'application/vnd.mozilla.xul+xml': ['xul'], 'application/vnd.ms-artgalry': ['cil'], 'application/vnd.ms-cab-compressed': ['cab'], 'application/vnd.ms-excel': ['xls', 'xlm', 'xla', 'xlc', 'xlt', 'xlw'], 'application/vnd.ms-excel.addin.macroenabled.12': ['xlam'], 'application/vnd.ms-excel.sheet.binary.macroenabled.12': ['xlsb'], 'application/vnd.ms-excel.sheet.macroenabled.12': ['xlsm'], 'application/vnd.ms-excel.template.macroenabled.12': ['xltm'], 'application/vnd.ms-fontobject': ['eot'], 'application/vnd.ms-htmlhelp': ['chm'], 'application/vnd.ms-ims': ['ims'], 'application/vnd.ms-lrm': ['lrm'], 'application/vnd.ms-officetheme': ['thmx'], 'application/vnd.ms-outlook': ['msg'], 'application/vnd.ms-pki.seccat': ['cat'], 'application/vnd.ms-pki.stl': ['*stl'], 'application/vnd.ms-powerpoint': ['ppt', 'pps', 'pot'], 'application/vnd.ms-powerpoint.addin.macroenabled.12': ['ppam'], 'application/vnd.ms-powerpoint.presentation.macroenabled.12': ['pptm'], 'application/vnd.ms-powerpoint.slide.macroenabled.12': ['sldm'], 'application/vnd.ms-powerpoint.slideshow.macroenabled.12': ['ppsm'], 'application/vnd.ms-powerpoint.template.macroenabled.12': ['potm'], 'application/vnd.ms-project': ['*mpp', 'mpt'], 'application/vnd.ms-word.document.macroenabled.12': ['docm'], 'application/vnd.ms-word.template.macroenabled.12': ['dotm'], 'application/vnd.ms-works': ['wps', 'wks', 'wcm', 'wdb'], 'application/vnd.ms-wpl': ['wpl'], 'application/vnd.ms-xpsdocument': ['xps'], 'application/vnd.mseq': ['mseq'], 'application/vnd.musician': ['mus'], 'application/vnd.muvee.style': ['msty'], 'application/vnd.mynfc': ['taglet'], 'application/vnd.nato.bindingdataobject+xml': ['bdo'], 'application/vnd.neurolanguage.nlu': ['nlu'], 'application/vnd.nitf': ['ntf', 'nitf'], 'application/vnd.noblenet-directory': ['nnd'], 'application/vnd.noblenet-sealer': ['nns'], 'application/vnd.noblenet-web': ['nnw'], 'application/vnd.nokia.n-gage.ac+xml': ['*ac'], 'application/vnd.nokia.n-gage.data': ['ngdat'], 'application/vnd.nokia.n-gage.symbian.install': ['n-gage'], 'application/vnd.nokia.radio-preset': ['rpst'], 'application/vnd.nokia.radio-presets': ['rpss'], 'application/vnd.novadigm.edm': ['edm'], 'application/vnd.novadigm.edx': ['edx'], 'application/vnd.novadigm.ext': ['ext'], 'application/vnd.oasis.opendocument.chart': ['odc'], 'application/vnd.oasis.opendocument.chart-template': ['otc'], 'application/vnd.oasis.opendocument.database': ['odb'], 'application/vnd.oasis.opendocument.formula': ['odf'], 'application/vnd.oasis.opendocument.formula-template': ['odft'], 'application/vnd.oasis.opendocument.graphics': ['odg'], 'application/vnd.oasis.opendocument.graphics-template': ['otg'], 'application/vnd.oasis.opendocument.image': ['odi'], 'application/vnd.oasis.opendocument.image-template': ['oti'], 'application/vnd.oasis.opendocument.presentation': ['odp'], 'application/vnd.oasis.opendocument.presentation-template': ['otp'], 'application/vnd.oasis.opendocument.spreadsheet': ['ods'], 'application/vnd.oasis.opendocument.spreadsheet-template': ['ots'], 'application/vnd.oasis.opendocument.text': ['odt'], 'application/vnd.oasis.opendocument.text-master': ['odm'], 'application/vnd.oasis.opendocument.text-template': ['ott'], 'application/vnd.oasis.opendocument.text-web': ['oth'], 'application/vnd.olpc-sugar': ['xo'], 'application/vnd.oma.dd2+xml': ['dd2'], 'application/vnd.openblox.game+xml': ['obgx'], 'application/vnd.openofficeorg.extension': ['oxt'], 'application/vnd.openstreetmap.data+xml': ['osm'], 'application/vnd.openxmlformats-officedocument.presentationml.presentation': [ 'pptx', ], 'application/vnd.openxmlformats-officedocument.presentationml.slide': [ 'sldx', ], 'application/vnd.openxmlformats-officedocument.presentationml.slideshow': [ 'ppsx', ], 'application/vnd.openxmlformats-officedocument.presentationml.template': [ 'potx', ], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['xlsx'], 'application/vnd.openxmlformats-officedocument.spreadsheetml.template': [ 'xltx', ], 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': [ 'docx', ], 'application/vnd.openxmlformats-officedocument.wordprocessingml.template': [ 'dotx', ], 'application/vnd.osgeo.mapguide.package': ['mgp'], 'application/vnd.osgi.dp': ['dp'], 'application/vnd.osgi.subsystem': ['esa'], 'application/vnd.palm': ['pdb', 'pqa', 'oprc'], 'application/vnd.pawaafile': ['paw'], 'application/vnd.pg.format': ['str'], 'application/vnd.pg.osasli': ['ei6'], 'application/vnd.picsel': ['efif'], 'application/vnd.pmi.widget': ['wg'], 'application/vnd.pocketlearn': ['plf'], 'application/vnd.powerbuilder6': ['pbd'], 'application/vnd.previewsystems.box': ['box'], 'application/vnd.proteus.magazine': ['mgz'], 'application/vnd.publishare-delta-tree': ['qps'], 'application/vnd.pvi.ptid1': ['ptid'], 'application/vnd.pwg-xhtml-print+xml': ['xhtm'], 'application/vnd.quark.quarkxpress': [ 'qxd', 'qxt', 'qwd', 'qwt', 'qxl', 'qxb', ], 'application/vnd.rar': ['rar'], 'application/vnd.realvnc.bed': ['bed'], 'application/vnd.recordare.musicxml': ['mxl'], 'application/vnd.recordare.musicxml+xml': ['musicxml'], 'application/vnd.rig.cryptonote': ['cryptonote'], 'application/vnd.rim.cod': ['cod'], 'application/vnd.rn-realmedia': ['rm'], 'application/vnd.rn-realmedia-vbr': ['rmvb'], 'application/vnd.route66.link66+xml': ['link66'], 'application/vnd.sailingtracker.track': ['st'], 'application/vnd.seemail': ['see'], 'application/vnd.sema': ['sema'], 'application/vnd.semd': ['semd'], 'application/vnd.semf': ['semf'], 'application/vnd.shana.informed.formdata': ['ifm'], 'application/vnd.shana.informed.formtemplate': ['itp'], 'application/vnd.shana.informed.interchange': ['iif'], 'application/vnd.shana.informed.package': ['ipk'], 'application/vnd.simtech-mindmapper': ['twd', 'twds'], 'application/vnd.smaf': ['mmf'], 'application/vnd.smart.teacher': ['teacher'], 'application/vnd.software602.filler.form+xml': ['fo'], 'application/vnd.solent.sdkm+xml': ['sdkm', 'sdkd'], 'application/vnd.spotfire.dxp': ['dxp'], 'application/vnd.spotfire.sfs': ['sfs'], 'application/vnd.stardivision.calc': ['sdc'], 'application/vnd.stardivision.draw': ['sda'], 'application/vnd.stardivision.impress': ['sdd'], 'application/vnd.stardivision.math': ['smf'], 'application/vnd.stardivision.writer': ['sdw', 'vor'], 'application/vnd.stardivision.writer-global': ['sgl'], 'application/vnd.stepmania.package': ['smzip'], 'application/vnd.stepmania.stepchart': ['sm'], 'application/vnd.sun.wadl+xml': ['wadl'], 'application/vnd.sun.xml.calc': ['sxc'], 'application/vnd.sun.xml.calc.template': ['stc'], 'application/vnd.sun.xml.draw': ['sxd'], 'application/vnd.sun.xml.draw.template': ['std'], 'application/vnd.sun.xml.impress': ['sxi'], 'application/vnd.sun.xml.impress.template': ['sti'], 'application/vnd.sun.xml.math': ['sxm'], 'application/vnd.sun.xml.writer': ['sxw'], 'application/vnd.sun.xml.writer.global': ['sxg'], 'application/vnd.sun.xml.writer.template': ['stw'], 'application/vnd.sus-calendar': ['sus', 'susp'], 'application/vnd.svd': ['svd'], 'application/vnd.symbian.install': ['sis', 'sisx'], 'application/vnd.syncml+xml': ['xsm'], 'application/vnd.syncml.dm+wbxml': ['bdm'], 'application/vnd.syncml.dm+xml': ['xdm'], 'application/vnd.syncml.dmddf+xml': ['ddf'], 'application/vnd.tao.intent-module-archive': ['tao'], 'application/vnd.tcpdump.pcap': ['pcap', 'cap', 'dmp'], 'application/vnd.tmobile-livetv': ['tmo'], 'application/vnd.trid.tpt': ['tpt'], 'application/vnd.triscape.mxs': ['mxs'], 'application/vnd.trueapp': ['tra'], 'application/vnd.ufdl': ['ufd', 'ufdl'], 'application/vnd.uiq.theme': ['utz'], 'application/vnd.umajin': ['umj'], 'application/vnd.unity': ['unityweb'], 'application/vnd.uoml+xml': ['uoml', 'uo'], 'application/vnd.vcx': ['vcx'], 'application/vnd.visio': ['vsd', 'vst', 'vss', 'vsw'], 'application/vnd.visionary': ['vis'], 'application/vnd.vsf': ['vsf'], 'application/vnd.wap.wbxml': ['wbxml'], 'application/vnd.wap.wmlc': ['wmlc'], 'application/vnd.wap.wmlscriptc': ['wmlsc'], 'application/vnd.webturbo': ['wtb'], 'application/vnd.wolfram.player': ['nbp'], 'application/vnd.wordperfect': ['wpd'], 'application/vnd.wqd': ['wqd'], 'application/vnd.wt.stf': ['stf'], 'application/vnd.xara': ['xar'], 'application/vnd.xfdl': ['xfdl'], 'application/vnd.yamaha.hv-dic': ['hvd'], 'application/vnd.yamaha.hv-script': ['hvs'], 'application/vnd.yamaha.hv-voice': ['hvp'], 'application/vnd.yamaha.openscoreformat': ['osf'], 'application/vnd.yamaha.openscoreformat.osfpvg+xml': ['osfpvg'], 'application/vnd.yamaha.smaf-audio': ['saf'], 'application/vnd.yamaha.smaf-phrase': ['spf'], 'application/vnd.yellowriver-custom-menu': ['cmp'], 'application/vnd.zul': ['zir', 'zirz'], 'application/vnd.zzazz.deck+xml': ['zaz'], 'application/x-7z-compressed': ['7z'], 'application/x-abiword': ['abw'], 'application/x-ace-compressed': ['ace'], 'application/x-apple-diskimage': ['*dmg'], 'application/x-arj': ['arj'], 'application/x-authorware-bin': ['aab', 'x32', 'u32', 'vox'], 'application/x-authorware-map': ['aam'], 'application/x-authorware-seg': ['aas'], 'application/x-bcpio': ['bcpio'], 'application/x-bdoc': ['*bdoc'], 'application/x-bittorrent': ['torrent'], 'application/x-blorb': ['blb', 'blorb'], 'application/x-bzip': ['bz'], 'application/x-bzip2': ['bz2', 'boz'], 'application/x-cbr': ['cbr', 'cba', 'cbt', 'cbz', 'cb7'], 'application/x-cdlink': ['vcd'], 'application/x-cfs-compressed': ['cfs'], 'application/x-chat': ['chat'], 'application/x-chess-pgn': ['pgn'], 'application/x-chrome-extension': ['crx'], 'application/x-cocoa': ['cco'], 'application/x-conference': ['nsc'], 'application/x-cpio': ['cpio'], 'application/x-csh': ['csh'], 'application/x-debian-package': ['*deb', 'udeb'], 'application/x-dgc-compressed': ['dgc'], 'application/x-director': [ 'dir', 'dcr', 'dxr', 'cst', 'cct', 'cxt', 'w3d', 'fgd', 'swa', ], 'application/x-doom': ['wad'], 'application/x-dtbncx+xml': ['ncx'], 'application/x-dtbook+xml': ['dtb'], 'application/x-dtbresource+xml': ['res'], 'application/x-dvi': ['dvi'], 'application/x-envoy': ['evy'], 'application/x-eva': ['eva'], 'application/x-font-bdf': ['bdf'], 'application/x-font-ghostscript': ['gsf'], 'application/x-font-linux-psf': ['psf'], 'application/x-font-pcf': ['pcf'], 'application/x-font-snf': ['snf'], 'application/x-font-type1': ['pfa', 'pfb', 'pfm', 'afm'], 'application/x-freearc': ['arc'], 'application/x-futuresplash': ['spl'], 'application/x-gca-compressed': ['gca'], 'application/x-glulx': ['ulx'], 'application/x-gnumeric': ['gnumeric'], 'application/x-gramps-xml': ['gramps'], 'application/x-gtar': ['gtar'], 'application/x-hdf': ['hdf'], 'application/x-httpd-php': ['php'], 'application/x-install-instructions': ['install'], 'application/x-iso9660-image': ['*iso'], 'application/x-iwork-keynote-sffkey': ['*key'], 'application/x-iwork-numbers-sffnumbers': ['*numbers'], 'application/x-iwork-pages-sffpages': ['*pages'], 'application/x-java-archive-diff': ['jardiff'], 'application/x-java-jnlp-file': ['jnlp'], 'application/x-keepass2': ['kdbx'], 'application/x-latex': ['latex'], 'application/x-lua-bytecode': ['luac'], 'application/x-lzh-compressed': ['lzh', 'lha'], 'application/x-makeself': ['run'], 'application/x-mie': ['mie'], 'application/x-mobipocket-ebook': ['*prc', 'mobi'], 'application/x-ms-application': ['application'], 'application/x-ms-shortcut': ['lnk'], 'application/x-ms-wmd': ['wmd'], 'application/x-ms-wmz': ['wmz'], 'application/x-ms-xbap': ['xbap'], 'application/x-msaccess': ['mdb'], 'application/x-msbinder': ['obd'], 'application/x-mscardfile': ['crd'], 'application/x-msclip': ['clp'], 'application/x-msdos-program': ['*exe'], 'application/x-msdownload': ['*exe', '*dll', 'com', 'bat', '*msi'], 'application/x-msmediaview': ['mvb', 'm13', 'm14'], 'application/x-msmetafile': ['*wmf', '*wmz', '*emf', 'emz'], 'application/x-msmoney': ['mny'], 'application/x-mspublisher': ['pub'], 'application/x-msschedule': ['scd'], 'application/x-msterminal': ['trm'], 'application/x-mswrite': ['wri'], 'application/x-netcdf': ['nc', 'cdf'], 'application/x-ns-proxy-autoconfig': ['pac'], 'application/x-nzb': ['nzb'], 'application/x-perl': ['pl', 'pm'], 'application/x-pilot': ['*prc', '*pdb'], 'application/x-pkcs12': ['p12', 'pfx'], 'application/x-pkcs7-certificates': ['p7b', 'spc'], 'application/x-pkcs7-certreqresp': ['p7r'], 'application/x-rar-compressed': ['*rar'], 'application/x-redhat-package-manager': ['rpm'], 'application/x-research-info-systems': ['ris'], 'application/x-sea': ['sea'], 'application/x-sh': ['sh'], 'application/x-shar': ['shar'], 'application/x-shockwave-flash': ['swf'], 'application/x-silverlight-app': ['xap'], 'application/x-sql': ['*sql'], 'application/x-stuffit': ['sit'], 'application/x-stuffitx': ['sitx'], 'application/x-subrip': ['srt'], 'application/x-sv4cpio': ['sv4cpio'], 'application/x-sv4crc': ['sv4crc'], 'application/x-t3vm-image': ['t3'], 'application/x-tads': ['gam'], 'application/x-tar': ['tar'], 'application/x-tcl': ['tcl', 'tk'], 'application/x-tex': ['tex'], 'application/x-tex-tfm': ['tfm'], 'application/x-texinfo': ['texinfo', 'texi'], 'application/x-tgif': ['*obj'], 'application/x-ustar': ['ustar'], 'application/x-virtualbox-hdd': ['hdd'], 'application/x-virtualbox-ova': ['ova'], 'application/x-virtualbox-ovf': ['ovf'], 'application/x-virtualbox-vbox': ['vbox'], 'application/x-virtualbox-vbox-extpack': ['vbox-extpack'], 'application/x-virtualbox-vdi': ['vdi'], 'application/x-virtualbox-vhd': ['vhd'], 'application/x-virtualbox-vmdk': ['vmdk'], 'application/x-wais-source': ['src'], 'application/x-web-app-manifest+json': ['webapp'], 'application/x-x509-ca-cert': ['der', 'crt', 'pem'], 'application/x-xfig': ['fig'], 'application/x-xliff+xml': ['*xlf'], 'application/x-xpinstall': ['xpi'], 'application/x-xz': ['xz'], 'application/x-zmachine': ['z1', 'z2', 'z3', 'z4', 'z5', 'z6', 'z7', 'z8'], 'audio/vnd.dece.audio': ['uva', 'uvva'], 'audio/vnd.digital-winds': ['eol'], 'audio/vnd.dra': ['dra'], 'audio/vnd.dts': ['dts'], 'audio/vnd.dts.hd': ['dtshd'], 'audio/vnd.lucent.voice': ['lvp'], 'audio/vnd.ms-playready.media.pya': ['pya'], 'audio/vnd.nuera.ecelp4800': ['ecelp4800'], 'audio/vnd.nuera.ecelp7470': ['ecelp7470'], 'audio/vnd.nuera.ecelp9600': ['ecelp9600'], 'audio/vnd.rip': ['rip'], 'audio/x-aac': ['*aac'], 'audio/x-aiff': ['aif', 'aiff', 'aifc'], 'audio/x-caf': ['caf'], 'audio/x-flac': ['flac'], 'audio/x-m4a': ['*m4a'], 'audio/x-matroska': ['mka'], 'audio/x-mpegurl': ['m3u'], 'audio/x-ms-wax': ['wax'], 'audio/x-ms-wma': ['wma'], 'audio/x-pn-realaudio': ['ram', 'ra'], 'audio/x-pn-realaudio-plugin': ['rmp'], 'audio/x-realaudio': ['*ra'], 'audio/x-wav': ['*wav'], 'chemical/x-cdx': ['cdx'], 'chemical/x-cif': ['cif'], 'chemical/x-cmdf': ['cmdf'], 'chemical/x-cml': ['cml'], 'chemical/x-csml': ['csml'], 'chemical/x-xyz': ['xyz'], 'image/prs.btif': ['btif', 'btf'], 'image/prs.pti': ['pti'], 'image/vnd.adobe.photoshop': ['psd'], 'image/vnd.airzip.accelerator.azv': ['azv'], 'image/vnd.dece.graphic': ['uvi', 'uvvi', 'uvg', 'uvvg'], 'image/vnd.djvu': ['djvu', 'djv'], 'image/vnd.dvb.subtitle': ['*sub'], 'image/vnd.dwg': ['dwg'], 'image/vnd.dxf': ['dxf'], 'image/vnd.fastbidsheet': ['fbs'], 'image/vnd.fpx': ['fpx'], 'image/vnd.fst': ['fst'], 'image/vnd.fujixerox.edmics-mmr': ['mmr'], 'image/vnd.fujixerox.edmics-rlc': ['rlc'], 'image/vnd.microsoft.icon': ['ico'], 'image/vnd.ms-dds': ['dds'], 'image/vnd.ms-modi': ['mdi'], 'image/vnd.ms-photo': ['wdp'], 'image/vnd.net-fpx': ['npx'], 'image/vnd.pco.b16': ['b16'], 'image/vnd.tencent.tap': ['tap'], 'image/vnd.valve.source.texture': ['vtf'], 'image/vnd.wap.wbmp': ['wbmp'], 'image/vnd.xiff': ['xif'], 'image/vnd.zbrush.pcx': ['pcx'], 'image/x-3ds': ['3ds'], 'image/x-cmu-raster': ['ras'], 'image/x-cmx': ['cmx'], 'image/x-freehand': ['fh', 'fhc', 'fh4', 'fh5', 'fh7'], 'image/x-icon': ['*ico'], 'image/x-jng': ['jng'], 'image/x-mrsid-image': ['sid'], 'image/x-ms-bmp': ['*bmp'], 'image/x-pcx': ['*pcx'], 'image/x-pict': ['pic', 'pct'], 'image/x-portable-anymap': ['pnm'], 'image/x-portable-bitmap': ['pbm'], 'image/x-portable-graymap': ['pgm'], 'image/x-portable-pixmap': ['ppm'], 'image/x-rgb': ['rgb'], 'image/x-tga': ['tga'], 'image/x-xbitmap': ['xbm'], 'image/x-xpixmap': ['xpm'], 'image/x-xwindowdump': ['xwd'], 'message/vnd.wfa.wsc': ['wsc'], 'model/vnd.bary': ['bary'], 'model/vnd.cld': ['cld'], 'model/vnd.collada+xml': ['dae'], 'model/vnd.dwf': ['dwf'], 'model/vnd.gdl': ['gdl'], 'model/vnd.gtw': ['gtw'], 'model/vnd.mts': ['*mts'], 'model/vnd.opengex': ['ogex'], 'model/vnd.parasolid.transmit.binary': ['x_b'], 'model/vnd.parasolid.transmit.text': ['x_t'], 'model/vnd.pytha.pyox': ['pyo', 'pyox'], 'model/vnd.sap.vds': ['vds'], 'model/vnd.usda': ['usda'], 'model/vnd.usdz+zip': ['usdz'], 'model/vnd.valve.source.compiled-map': ['bsp'], 'model/vnd.vtu': ['vtu'], 'text/prs.lines.tag': ['dsc'], 'text/vnd.curl': ['curl'], 'text/vnd.curl.dcurl': ['dcurl'], 'text/vnd.curl.mcurl': ['mcurl'], 'text/vnd.curl.scurl': ['scurl'], 'text/vnd.dvb.subtitle': ['sub'], 'text/vnd.familysearch.gedcom': ['ged'], 'text/vnd.fly': ['fly'], 'text/vnd.fmi.flexstor': ['flx'], 'text/vnd.graphviz': ['gv'], 'text/vnd.in3d.3dml': ['3dml'], 'text/vnd.in3d.spot': ['spot'], 'text/vnd.sun.j2me.app-descriptor': ['jad'], 'text/vnd.wap.wml': ['wml'], 'text/vnd.wap.wmlscript': ['wmls'], 'text/x-asm': ['s', 'asm'], 'text/x-c': ['c', 'cc', 'cxx', 'cpp', 'h', 'hh', 'dic'], 'text/x-component': ['htc'], 'text/x-fortran': ['f', 'for', 'f77', 'f90'], 'text/x-handlebars-template': ['hbs'], 'text/x-java-source': ['java'], 'text/x-lua': ['lua'], 'text/x-markdown': ['mkd'], 'text/x-nfo': ['nfo'], 'text/x-opml': ['opml'], 'text/x-org': ['*org'], 'text/x-pascal': ['p', 'pas'], 'text/x-processing': ['pde'], 'text/x-sass': ['sass'], 'text/x-scss': ['scss'], 'text/x-setext': ['etx'], 'text/x-sfv': ['sfv'], 'text/x-suse-ymp': ['ymp'], 'text/x-uuencode': ['uu'], 'text/x-vcalendar': ['vcs'], 'text/x-vcard': ['vcf'], 'video/vnd.dece.hd': ['uvh', 'uvvh'], 'video/vnd.dece.mobile': ['uvm', 'uvvm'], 'video/vnd.dece.pd': ['uvp', 'uvvp'], 'video/vnd.dece.sd': ['uvs', 'uvvs'], 'video/vnd.dece.video': ['uvv', 'uvvv'], 'video/vnd.dvb.file': ['dvb'], 'video/vnd.fvt': ['fvt'], 'video/vnd.mpegurl': ['mxu', 'm4u'], 'video/vnd.ms-playready.media.pyv': ['pyv'], 'video/vnd.uvvu.mp4': ['uvu', 'uvvu'], 'video/vnd.vivo': ['viv'], 'video/x-f4v': ['f4v'], 'video/x-fli': ['fli'], 'video/x-flv': ['flv'], 'video/x-m4v': ['m4v'], 'video/x-matroska': ['mkv', 'mk3d', 'mks'], 'video/x-mng': ['mng'], 'video/x-ms-asf': ['asf', 'asx'], 'video/x-ms-vob': ['vob'], 'video/x-ms-wm': ['wm'], 'video/x-ms-wmv': ['wmv'], 'video/x-ms-wmx': ['wmx'], 'video/x-ms-wvx': ['wvx'], 'video/x-msvideo': ['avi'], 'video/x-sgi-movie': ['movie'], 'video/x-smv': ['smv'], 'x-conference/x-cooltalk': ['ice'], }; Object.freeze(types$1); const types = { 'application/andrew-inset': ['ez'], 'application/appinstaller': ['appinstaller'], 'application/applixware': ['aw'], 'application/appx': ['appx'], 'application/appxbundle': ['appxbundle'], 'application/atom+xml': ['atom'], 'application/atomcat+xml': ['atomcat'], 'application/atomdeleted+xml': ['atomdeleted'], 'application/atomsvc+xml': ['atomsvc'], 'application/atsc-dwd+xml': ['dwd'], 'application/atsc-held+xml': ['held'], 'application/atsc-rsat+xml': ['rsat'], 'application/automationml-aml+xml': ['aml'], 'application/automationml-amlx+zip': ['amlx'], 'application/bdoc': ['bdoc'], 'application/calendar+xml': ['xcs'], 'application/ccxml+xml': ['ccxml'], 'application/cdfx+xml': ['cdfx'], 'application/cdmi-capability': ['cdmia'], 'application/cdmi-container': ['cdmic'], 'application/cdmi-domain': ['cdmid'], 'application/cdmi-object': ['cdmio'], 'application/cdmi-queue': ['cdmiq'], 'application/cpl+xml': ['cpl'], 'application/cu-seeme': ['cu'], 'application/cwl': ['cwl'], 'application/dash+xml': ['mpd'], 'application/dash-patch+xml': ['mpp'], 'application/davmount+xml': ['davmount'], 'application/docbook+xml': ['dbk'], 'application/dssc+der': ['dssc'], 'application/dssc+xml': ['xdssc'], 'application/ecmascript': ['ecma'], 'application/emma+xml': ['emma'], 'application/emotionml+xml': ['emotionml'], 'application/epub+zip': ['epub'], 'application/exi': ['exi'], 'application/express': ['exp'], 'application/fdf': ['fdf'], 'application/fdt+xml': ['fdt'], 'application/font-tdpfr': ['pfr'], 'application/geo+json': ['geojson'], 'application/gml+xml': ['gml'], 'application/gpx+xml': ['gpx'], 'application/gxf': ['gxf'], 'application/gzip': ['gz'], 'application/hjson': ['hjson'], 'application/hyperstudio': ['stk'], 'application/inkml+xml': ['ink', 'inkml'], 'application/ipfix': ['ipfix'], 'application/its+xml': ['its'], 'application/java-archive': ['jar', 'war', 'ear'], 'application/java-serialized-object': ['ser'], 'application/java-vm': ['class'], 'application/javascript': ['*js'], 'application/json': ['json', 'map'], 'application/json5': ['json5'], 'application/jsonml+json': ['jsonml'], 'application/ld+json': ['jsonld'], 'application/lgr+xml': ['lgr'], 'application/lost+xml': ['lostxml'], 'application/mac-binhex40': ['hqx'], 'application/mac-compactpro': ['cpt'], 'application/mads+xml': ['mads'], 'application/manifest+json': ['webmanifest'], 'application/marc': ['mrc'], 'application/marcxml+xml': ['mrcx'], 'application/mathematica': ['ma', 'nb', 'mb'], 'application/mathml+xml': ['mathml'], 'application/mbox': ['mbox'], 'application/media-policy-dataset+xml': ['mpf'], 'application/mediaservercontrol+xml': ['mscml'], 'application/metalink+xml': ['metalink'], 'application/metalink4+xml': ['meta4'], 'application/mets+xml': ['mets'], 'application/mmt-aei+xml': ['maei'], 'application/mmt-usd+xml': ['musd'], 'application/mods+xml': ['mods'], 'application/mp21': ['m21', 'mp21'], 'application/mp4': ['*mp4', '*mpg4', 'mp4s', 'm4p'], 'application/msix': ['msix'], 'application/msixbundle': ['msixbundle'], 'application/msword': ['doc', 'dot'], 'application/mxf': ['mxf'], 'application/n-quads': ['nq'], 'application/n-triples': ['nt'], 'application/node': ['cjs'], 'application/octet-stream': [ 'bin', 'dms', 'lrf', 'mar', 'so', 'dist', 'distz', 'pkg', 'bpk', 'dump', 'elc', 'deploy', 'exe', 'dll', 'deb', 'dmg', 'iso', 'img', 'msi', 'msp', 'msm', 'buffer', ], 'application/oda': ['oda'], 'application/oebps-package+xml': ['opf'], 'application/ogg': ['ogx'], 'application/omdoc+xml': ['omdoc'], 'application/onenote': ['onetoc', 'onetoc2', 'onetmp', 'onepkg'], 'application/oxps': ['oxps'], 'application/p2p-overlay+xml': ['relo'], 'application/patch-ops-error+xml': ['xer'], 'application/pdf': ['pdf'], 'application/pgp-encrypted': ['pgp'], 'application/pgp-keys': ['asc'], 'application/pgp-signature': ['sig', '*asc'], 'application/pics-rules': ['prf'], 'application/pkcs10': ['p10'], 'application/pkcs7-mime': ['p7m', 'p7c'], 'application/pkcs7-signature': ['p7s'], 'application/pkcs8': ['p8'], 'application/pkix-attr-cert': ['ac'], 'application/pkix-cert': ['cer'], 'application/pkix-crl': ['crl'], 'application/pkix-pkipath': ['pkipath'], 'application/pkixcmp': ['pki'], 'application/pls+xml': ['pls'], 'application/postscript': ['ai', 'eps', 'ps'], 'application/provenance+xml': ['provx'], 'application/pskc+xml': ['pskcxml'], 'application/raml+yaml': ['raml'], 'application/rdf+xml': ['rdf', 'owl'], 'application/reginfo+xml': ['rif'], 'application/relax-ng-compact-syntax': ['rnc'], 'application/resource-lists+xml': ['rl'], 'application/resource-lists-diff+xml': ['rld'], 'application/rls-services+xml': ['rs'], 'application/route-apd+xml': ['rapd'], 'application/route-s-tsid+xml': ['sls'], 'application/route-usd+xml': ['rusd'], 'application/rpki-ghostbusters': ['gbr'], 'application/rpki-manifest': ['mft'], 'application/rpki-roa': ['roa'], 'application/rsd+xml': ['rsd'], 'application/rss+xml': ['rss'], 'application/rtf': ['rtf'], 'application/sbml+xml': ['sbml'], 'application/scvp-cv-request': ['scq'], 'application/scvp-cv-response': ['scs'], 'application/scvp-vp-request': ['spq'], 'application/scvp-vp-response': ['spp'], 'application/sdp': ['sdp'], 'application/senml+xml': ['senmlx'], 'application/sensml+xml': ['sensmlx'], 'application/set-payment-initiation': ['setpay'], 'application/set-registration-initiation': ['setreg'], 'application/shf+xml': ['shf'], 'application/sieve': ['siv', 'sieve'], 'application/smil+xml': ['smi', 'smil'], 'application/sparql-query': ['rq'], 'application/sparql-results+xml': ['srx'], 'application/sql': ['sql'], 'application/srgs': ['gram'], 'application/srgs+xml': ['grxml'], 'application/sru+xml': ['sru'], 'application/ssdl+xml': ['ssdl'], 'application/ssml+xml': ['ssml'], 'application/swid+xml': ['swidtag'], 'application/tei+xml': ['tei', 'teicorpus'], 'application/thraud+xml': ['tfi'], 'application/timestamped-data': ['tsd'], 'application/toml': ['toml'], 'application/trig': ['trig'], 'application/ttml+xml': ['ttml'], 'application/ubjson': ['ubj'], 'application/urc-ressheet+xml': ['rsheet'], 'application/urc-targetdesc+xml': ['td'], 'application/voicexml+xml': ['vxml'], 'application/wasm': ['wasm'], 'application/watcherinfo+xml': ['wif'], 'application/widget': ['wgt'], 'application/winhlp': ['hlp'], 'application/wsdl+xml': ['wsdl'], 'application/wspolicy+xml': ['wspolicy'], 'application/xaml+xml': ['xaml'], 'application/xcap-att+xml': ['xav'], 'application/xcap-caps+xml': ['xca'], 'application/xcap-diff+xml': ['xdf'], 'application/xcap-el+xml': ['xel'], 'application/xcap-ns+xml': ['xns'], 'application/xenc+xml': ['xenc'], 'application/xfdf': ['xfdf'], 'application/xhtml+xml': ['xhtml', 'xht'], 'application/xliff+xml': ['xlf'], 'application/xml': ['xml', 'xsl', 'xsd', 'rng'], 'application/xml-dtd': ['dtd'], 'application/xop+xml': ['xop'], 'application/xproc+xml': ['xpl'], 'application/xslt+xml': ['*xsl', 'xslt'], 'application/xspf+xml': ['xspf'], 'application/xv+xml': ['mxml', 'xhvml', 'xvml', 'xvm'], 'application/yang': ['yang'], 'application/yin+xml': ['yin'], 'application/zip': ['zip'], 'audio/3gpp': ['*3gpp'], 'audio/aac': ['adts', 'aac'], 'audio/adpcm': ['adp'], 'audio/amr': ['amr'], 'audio/basic': ['au', 'snd'], 'audio/midi': ['mid', 'midi', 'kar', 'rmi'], 'audio/mobile-xmf': ['mxmf'], 'audio/mp3': ['*mp3'], 'audio/mp4': ['m4a', 'mp4a'], 'audio/mpeg': ['mpga', 'mp2', 'mp2a', 'mp3', 'm2a', 'm3a'], 'audio/ogg': ['oga', 'ogg', 'spx', 'opus'], 'audio/s3m': ['s3m'], 'audio/silk': ['sil'], 'audio/wav': ['wav'], 'audio/wave': ['*wav'], 'audio/webm': ['weba'], 'audio/xm': ['xm'], 'font/collection': ['ttc'], 'font/otf': ['otf'], 'font/ttf': ['ttf'], 'font/woff': ['woff'], 'font/woff2': ['woff2'], 'image/aces': ['exr'], 'image/apng': ['apng'], 'image/avci': ['avci'], 'image/avcs': ['avcs'], 'image/avif': ['avif'], 'image/bmp': ['bmp', 'dib'], 'image/cgm': ['cgm'], 'image/dicom-rle': ['drle'], 'image/dpx': ['dpx'], 'image/emf': ['emf'], 'image/fits': ['fits'], 'image/g3fax': ['g3'], 'image/gif': ['gif'], 'image/heic': ['heic'], 'image/heic-sequence': ['heics'], 'image/heif': ['heif'], 'image/heif-sequence': ['heifs'], 'image/hej2k': ['hej2'], 'image/hsj2': ['hsj2'], 'image/ief': ['ief'], 'image/jls': ['jls'], 'image/jp2': ['jp2', 'jpg2'], 'image/jpeg': ['jpeg', 'jpg', 'jpe'], 'image/jph': ['jph'], 'image/jphc': ['jhc'], 'image/jpm': ['jpm', 'jpgm'], 'image/jpx': ['jpx', 'jpf'], 'image/jxl': ['jxl'], 'image/jxr': ['jxr'], 'image/jxra': ['jxra'], 'image/jxrs': ['jxrs'], 'image/jxs': ['jxs'], 'image/jxsc': ['jxsc'], 'image/jxsi': ['jxsi'], 'image/jxss': ['jxss'], 'image/ktx': ['ktx'], 'image/ktx2': ['ktx2'], 'image/png': ['png'], 'image/sgi': ['sgi'], 'image/svg+xml': ['svg', 'svgz'], 'image/t38': ['t38'], 'image/tiff': ['tif', 'tiff'], 'image/tiff-fx': ['tfx'], 'image/webp': ['webp'], 'image/wmf': ['wmf'], 'message/disposition-notification': ['disposition-notification'], 'message/global': ['u8msg'], 'message/global-delivery-status': ['u8dsn'], 'message/global-disposition-notification': ['u8mdn'], 'message/global-headers': ['u8hdr'], 'message/rfc822': ['eml', 'mime'], 'model/3mf': ['3mf'], 'model/gltf+json': ['gltf'], 'model/gltf-binary': ['glb'], 'model/iges': ['igs', 'iges'], 'model/jt': ['jt'], 'model/mesh': ['msh', 'mesh', 'silo'], 'model/mtl': ['mtl'], 'model/obj': ['obj'], 'model/prc': ['prc'], 'model/step+xml': ['stpx'], 'model/step+zip': ['stpz'], 'model/step-xml+zip': ['stpxz'], 'model/stl': ['stl'], 'model/u3d': ['u3d'], 'model/vrml': ['wrl', 'vrml'], 'model/x3d+binary': ['*x3db', 'x3dbz'], 'model/x3d+fastinfoset': ['x3db'], 'model/x3d+vrml': ['*x3dv', 'x3dvz'], 'model/x3d+xml': ['x3d', 'x3dz'], 'model/x3d-vrml': ['x3dv'], 'text/cache-manifest': ['appcache', 'manifest'], 'text/calendar': ['ics', 'ifb'], 'text/coffeescript': ['coffee', 'litcoffee'], 'text/css': ['css'], 'text/csv': ['csv'], 'text/html': ['html', 'htm', 'shtml'], 'text/jade': ['jade'], 'text/javascript': ['js', 'mjs'], 'text/jsx': ['jsx'], 'text/less': ['less'], 'text/markdown': ['md', 'markdown'], 'text/mathml': ['mml'], 'text/mdx': ['mdx'], 'text/n3': ['n3'], 'text/plain': ['txt', 'text', 'conf', 'def', 'list', 'log', 'in', 'ini'], 'text/richtext': ['rtx'], 'text/rtf': ['*rtf'], 'text/sgml': ['sgml', 'sgm'], 'text/shex': ['shex'], 'text/slim': ['slim', 'slm'], 'text/spdx': ['spdx'], 'text/stylus': ['stylus', 'styl'], 'text/tab-separated-values': ['tsv'], 'text/troff': ['t', 'tr', 'roff', 'man', 'me', 'ms'], 'text/turtle': ['ttl'], 'text/uri-list': ['uri', 'uris', 'urls'], 'text/vcard': ['vcard'], 'text/vtt': ['vtt'], 'text/wgsl': ['wgsl'], 'text/xml': ['*xml'], 'text/yaml': ['yaml', 'yml'], 'video/3gpp': ['3gp', '3gpp'], 'video/3gpp2': ['3g2'], 'video/h261': ['h261'], 'video/h263': ['h263'], 'video/h264': ['h264'], 'video/iso.segment': ['m4s'], 'video/jpeg': ['jpgv'], 'video/jpm': ['*jpm', '*jpgm'], 'video/mj2': ['mj2', 'mjp2'], 'video/mp2t': ['ts', 'm2t', 'm2ts', 'mts'], 'video/mp4': ['mp4', 'mp4v', 'mpg4'], 'video/mpeg': ['mpeg', 'mpg', 'mpe', 'm1v', 'm2v'], 'video/ogg': ['ogv'], 'video/quicktime': ['qt', 'mov'], 'video/webm': ['webm'], }; Object.freeze(types); var __classPrivateFieldGet = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _Mime_extensionToType, _Mime_typeToExtension, _Mime_typeToExtensions; class Mime { constructor(...args) { _Mime_extensionToType.set(this, new Map()); _Mime_typeToExtension.set(this, new Map()); _Mime_typeToExtensions.set(this, new Map()); for (const arg of args) { this.define(arg); } } define(typeMap, force = false) { for (let [type, extensions] of Object.entries(typeMap)) { type = type.toLowerCase(); extensions = extensions.map((ext) => ext.toLowerCase()); if (!__classPrivateFieldGet(this, _Mime_typeToExtensions, "f").has(type)) { __classPrivateFieldGet(this, _Mime_typeToExtensions, "f").set(type, new Set()); } const allExtensions = __classPrivateFieldGet(this, _Mime_typeToExtensions, "f").get(type); let first = true; for (let extension of extensions) { const starred = extension.startsWith('*'); extension = starred ? extension.slice(1) : extension; allExtensions?.add(extension); if (first) { __classPrivateFieldGet(this, _Mime_typeToExtension, "f").set(type, extension); } first = false; if (starred) continue; const currentType = __classPrivateFieldGet(this, _Mime_extensionToType, "f").get(extension); if (currentType && currentType != type && !force) { throw new Error(`"${type} -> ${extension}" conflicts with "${currentType} -> ${extension}". Pass \`force=true\` to override this definition.`); } __classPrivateFieldGet(this, _Mime_extensionToType, "f").set(extension, type); } } return this; } getType(path) { if (typeof path !== 'string') return null; const last = path.replace(/^.*[/\\]/, '').toLowerCase(); const ext = last.replace(/^.*\./, '').toLowerCase(); const hasPath = last.length < path.length; const hasDot = ext.length < last.length - 1; if (!hasDot && hasPath) return null; return __classPrivateFieldGet(this, _Mime_extensionToType, "f").get(ext) ?? null; } getExtension(type) { if (typeof type !== 'string') return null; type = type?.split?.(';')[0]; return ((type && __classPrivateFieldGet(this, _Mime_typeToExtension, "f").get(type.trim().toLowerCase())) ?? null); } getAllExtensions(type) { if (typeof type !== 'string') return null; return __classPrivateFieldGet(this, _Mime_typeToExtensions, "f").get(type.toLowerCase()) ?? null; } _freeze() { this.define = () => { throw new Error('define() not allowed for built-in Mime objects. See https://github.com/broofa/mime/blob/main/README.md#custom-mime-instances'); }; Object.freeze(this); for (const extensions of __classPrivateFieldGet(this, _Mime_typeToExtensions, "f").values()) { Object.freeze(extensions); } return this; } _getTestState() { return { types: __classPrivateFieldGet(this, _Mime_extensionToType, "f"), extensions: __classPrivateFieldGet(this, _Mime_typeToExtension, "f"), }; } } _Mime_extensionToType = new WeakMap(), _Mime_typeToExtension = new WeakMap(), _Mime_typeToExtensions = new WeakMap(); var index = new Mime(types, types$1)._freeze(); exports.Mime = Mime; exports.default = index; Object.defineProperty(exports, '__esModule', { value: true }); return exports; })({}); ================================================ FILE: public/assets/libs/workbox/workbox-background-sync.dev.js ================================================ this.workbox = this.workbox || {}; this.workbox.backgroundSync = (function (exports, WorkboxError_mjs, logger_mjs, assert_mjs, getFriendlyURL_mjs, DBWrapper_mjs) { 'use strict'; try { self['workbox:background-sync:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const DB_VERSION = 3; const DB_NAME = 'workbox-background-sync'; const OBJECT_STORE_NAME = 'requests'; const INDEXED_PROP = 'queueName'; /** * A class to manage storing requests from a Queue in IndexedbDB, * indexed by their queue name for easier access. * * @private */ class QueueStore { /** * Associates this instance with a Queue instance, so entries added can be * identified by their queue name. * * @param {string} queueName * @private */ constructor(queueName) { this._queueName = queueName; this._db = new DBWrapper_mjs.DBWrapper(DB_NAME, DB_VERSION, { onupgradeneeded: evt => this._upgradeDb(evt) }); } /** * Append an entry last in the queue. * * @param {Object} entry * @param {Object} entry.requestData * @param {number} [entry.timestamp] * @param {Object} [entry.metadata] */ async pushEntry(entry) { { assert_mjs.assert.isType(entry, 'object', { moduleName: 'workbox-background-sync', className: 'QueueStore', funcName: 'pushEntry', paramName: 'entry' }); assert_mjs.assert.isType(entry.requestData, 'object', { moduleName: 'workbox-background-sync', className: 'QueueStore', funcName: 'pushEntry', paramName: 'entry.requestData' }); } // Don't specify an ID since one is automatically generated. delete entry.id; entry.queueName = this._queueName; await this._db.add(OBJECT_STORE_NAME, entry); } /** * Preppend an entry first in the queue. * * @param {Object} entry * @param {Object} entry.requestData * @param {number} [entry.timestamp] * @param {Object} [entry.metadata] */ async unshiftEntry(entry) { { assert_mjs.assert.isType(entry, 'object', { moduleName: 'workbox-background-sync', className: 'QueueStore', funcName: 'unshiftEntry', paramName: 'entry' }); assert_mjs.assert.isType(entry.requestData, 'object', { moduleName: 'workbox-background-sync', className: 'QueueStore', funcName: 'unshiftEntry', paramName: 'entry.requestData' }); } const [firstEntry] = await this._db.getAllMatching(OBJECT_STORE_NAME, { count: 1 }); if (firstEntry) { // Pick an ID one less than the lowest ID in the object store. entry.id = firstEntry.id - 1; } else { delete entry.id; } entry.queueName = this._queueName; await this._db.add(OBJECT_STORE_NAME, entry); } /** * Removes and returns the last entry in the queue matching the `queueName`. * * @return {Promise} */ async popEntry() { return this._removeEntry({ direction: 'prev' }); } /** * Removes and returns the first entry in the queue matching the `queueName`. * * @return {Promise} */ async shiftEntry() { return this._removeEntry({ direction: 'next' }); } /** * Removes and returns the first or last entry in the queue (based on the * `direction` argument) matching the `queueName`. * * @return {Promise} */ async _removeEntry({ direction }) { const [entry] = await this._db.getAllMatching(OBJECT_STORE_NAME, { direction, index: INDEXED_PROP, query: IDBKeyRange.only(this._queueName), count: 1 }); if (entry) { await this._db.delete(OBJECT_STORE_NAME, entry.id); // Dont' expose the ID or queueName; delete entry.id; delete entry.queueName; return entry; } } /** * Upgrades the database given an `upgradeneeded` event. * * @param {Event} event */ _upgradeDb(event) { const db = event.target.result; if (event.oldVersion > 0 && event.oldVersion < DB_VERSION) { db.deleteObjectStore(OBJECT_STORE_NAME); } const objStore = db.createObjectStore(OBJECT_STORE_NAME, { autoIncrement: true, keyPath: 'id' }); objStore.createIndex(INDEXED_PROP, INDEXED_PROP, { unique: false }); } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const serializableProperties = ['method', 'referrer', 'referrerPolicy', 'mode', 'credentials', 'cache', 'redirect', 'integrity', 'keepalive']; /** * A class to make it easier to serialize and de-serialize requests so they * can be stored in IndexedDB. * * @private */ class StorableRequest { /** * Converts a Request object to a plain object that can be structured * cloned or JSON-stringified. * * @param {Request} request * @return {Promise} * * @private */ static async fromRequest(request) { const requestData = { url: request.url, headers: {} }; // Set the body if present. if (request.method !== 'GET') { // Use ArrayBuffer to support non-text request bodies. // NOTE: we can't use Blobs becuse Safari doesn't support storing // Blobs in IndexedDB in some cases: // https://github.com/dfahlander/Dexie.js/issues/618#issuecomment-398348457 requestData.body = await request.clone().arrayBuffer(); } // Convert the headers from an iterable to an object. for (const [key, value] of request.headers.entries()) { requestData.headers[key] = value; } // Add all other serializable request properties for (const prop of serializableProperties) { if (request[prop] !== undefined) { requestData[prop] = request[prop]; } } return new StorableRequest(requestData); } /** * Accepts an object of request data that can be used to construct a * `Request` but can also be stored in IndexedDB. * * @param {Object} requestData An object of request data that includes the * `url` plus any relevant properties of * [requestInit]{@link https://fetch.spec.whatwg.org/#requestinit}. * @private */ constructor(requestData) { { assert_mjs.assert.isType(requestData, 'object', { moduleName: 'workbox-background-sync', className: 'StorableRequest', funcName: 'constructor', paramName: 'requestData' }); assert_mjs.assert.isType(requestData.url, 'string', { moduleName: 'workbox-background-sync', className: 'StorableRequest', funcName: 'constructor', paramName: 'requestData.url' }); } this._requestData = requestData; } /** * Returns a deep clone of the instances `_requestData` object. * * @return {Object} * * @private */ toObject() { const requestData = Object.assign({}, this._requestData); requestData.headers = Object.assign({}, this._requestData.headers); if (requestData.body) { requestData.body = requestData.body.slice(0); } return requestData; } /** * Converts this instance to a Request. * * @return {Request} * * @private */ toRequest() { return new Request(this._requestData.url, this._requestData); } /** * Creates and returns a deep clone of the instance. * * @return {StorableRequest} * * @private */ clone() { return new StorableRequest(this.toObject()); } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const TAG_PREFIX = 'workbox-background-sync'; const MAX_RETENTION_TIME = 60 * 24 * 7; // 7 days in minutes const queueNames = new Set(); /** * A class to manage storing failed requests in IndexedDB and retrying them * later. All parts of the storing and replaying process are observable via * callbacks. * * @memberof workbox.backgroundSync */ class Queue { /** * Creates an instance of Queue with the given options * * @param {string} name The unique name for this queue. This name must be * unique as it's used to register sync events and store requests * in IndexedDB specific to this instance. An error will be thrown if * a duplicate name is detected. * @param {Object} [options] * @param {Function} [options.onSync] A function that gets invoked whenever * the 'sync' event fires. The function is invoked with an object * containing the `queue` property (referencing this instance), and you * can use the callback to customize the replay behavior of the queue. * When not set the `replayRequests()` method is called. * Note: if the replay fails after a sync event, make sure you throw an * error, so the browser knows to retry the sync event later. * @param {number} [options.maxRetentionTime=7 days] The amount of time (in * minutes) a request may be retried. After this amount of time has * passed, the request will be deleted from the queue. */ constructor(name, { onSync, maxRetentionTime } = {}) { // Ensure the store name is not already being used if (queueNames.has(name)) { throw new WorkboxError_mjs.WorkboxError('duplicate-queue-name', { name }); } else { queueNames.add(name); } this._name = name; this._onSync = onSync || this.replayRequests; this._maxRetentionTime = maxRetentionTime || MAX_RETENTION_TIME; this._queueStore = new QueueStore(this._name); this._addSyncListener(); } /** * @return {string} */ get name() { return this._name; } /** * Stores the passed request in IndexedDB (with its timestamp and any * metadata) at the end of the queue. * * @param {Object} entry * @param {Request} entry.request The request to store in the queue. * @param {Object} [entry.metadata] Any metadata you want associated with the * stored request. When requests are replayed you'll have access to this * metadata object in case you need to modify the request beforehand. * @param {number} [entry.timestamp] The timestamp (Epoch time in * milliseconds) when the request was first added to the queue. This is * used along with `maxRetentionTime` to remove outdated requests. In * general you don't need to set this value, as it's automatically set * for you (defaulting to `Date.now()`), but you can update it if you * don't want particular requests to expire. */ async pushRequest(entry) { { assert_mjs.assert.isType(entry, 'object', { moduleName: 'workbox-background-sync', className: 'Queue', funcName: 'pushRequest', paramName: 'entry' }); assert_mjs.assert.isInstance(entry.request, Request, { moduleName: 'workbox-background-sync', className: 'Queue', funcName: 'pushRequest', paramName: 'entry.request' }); } await this._addRequest(entry, 'push'); } /** * Stores the passed request in IndexedDB (with its timestamp and any * metadata) at the beginning of the queue. * * @param {Object} entry * @param {Request} entry.request The request to store in the queue. * @param {Object} [entry.metadata] Any metadata you want associated with the * stored request. When requests are replayed you'll have access to this * metadata object in case you need to modify the request beforehand. * @param {number} [entry.timestamp] The timestamp (Epoch time in * milliseconds) when the request was first added to the queue. This is * used along with `maxRetentionTime` to remove outdated requests. In * general you don't need to set this value, as it's automatically set * for you (defaulting to `Date.now()`), but you can update it if you * don't want particular requests to expire. */ async unshiftRequest(entry) { { assert_mjs.assert.isType(entry, 'object', { moduleName: 'workbox-background-sync', className: 'Queue', funcName: 'unshiftRequest', paramName: 'entry' }); assert_mjs.assert.isInstance(entry.request, Request, { moduleName: 'workbox-background-sync', className: 'Queue', funcName: 'unshiftRequest', paramName: 'entry.request' }); } await this._addRequest(entry, 'unshift'); } /** * Removes and returns the last request in the queue (along with its * timestamp and any metadata). The returned object takes the form: * `{request, timestamp, metadata}`. * * @return {Promise} */ async popRequest() { return this._removeRequest('pop'); } /** * Removes and returns the first request in the queue (along with its * timestamp and any metadata). The returned object takes the form: * `{request, timestamp, metadata}`. * * @return {Promise} */ async shiftRequest() { return this._removeRequest('shift'); } /** * Adds the entry to the QueueStore and registers for a sync event. * * @param {Object} entry * @param {Request} entry.request * @param {Object} [entry.metadata] * @param {number} [entry.timestamp=Date.now()] * @param {string} operation ('push' or 'unshift') */ async _addRequest({ request, metadata, timestamp = Date.now() }, operation) { const storableRequest = await StorableRequest.fromRequest(request.clone()); const entry = { requestData: storableRequest.toObject(), timestamp }; // Only include metadata if it's present. if (metadata) { entry.metadata = metadata; } await this._queueStore[`${operation}Entry`](entry); { logger_mjs.logger.log(`Request for '${getFriendlyURL_mjs.getFriendlyURL(request.url)}' has ` + `been added to background sync queue '${this._name}'.`); } // Don't register for a sync if we're in the middle of a sync. Instead, // we wait until the sync is complete and call register if // `this._requestsAddedDuringSync` is true. if (this._syncInProgress) { this._requestsAddedDuringSync = true; } else { await this.registerSync(); } } /** * Removes and returns the first or last (depending on `operation`) entry * form the QueueStore that's not older than the `maxRetentionTime`. * * @param {string} operation ('pop' or 'shift') * @return {Object|undefined} */ async _removeRequest(operation) { const now = Date.now(); const entry = await this._queueStore[`${operation}Entry`](); if (entry) { // Ignore requests older than maxRetentionTime. Call this function // recursively until an unexpired request is found. const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000; if (now - entry.timestamp > maxRetentionTimeInMs) { return this._removeRequest(operation); } entry.request = new StorableRequest(entry.requestData).toRequest(); delete entry.requestData; return entry; } } /** * Loops through each request in the queue and attempts to re-fetch it. * If any request fails to re-fetch, it's put back in the same position in * the queue (which registers a retry for the next sync event). */ async replayRequests() { let entry; while (entry = await this.shiftRequest()) { try { await fetch(entry.request); { logger_mjs.logger.log(`Request for '${getFriendlyURL_mjs.getFriendlyURL(entry.request.url)}'` + `has been replayed in queue '${this._name}'`); } } catch (error) { await this.unshiftRequest(entry); { logger_mjs.logger.log(`Request for '${getFriendlyURL_mjs.getFriendlyURL(entry.request.url)}'` + `failed to replay, putting it back in queue '${this._name}'`); } throw new WorkboxError_mjs.WorkboxError('queue-replay-failed', { name: this._name }); } } { logger_mjs.logger.log(`All requests in queue '${this.name}' have successfully ` + `replayed; the queue is now empty!`); } } /** * Registers a sync event with a tag unique to this instance. */ async registerSync() { if ('sync' in registration) { try { await registration.sync.register(`${TAG_PREFIX}:${this._name}`); } catch (err) { // This means the registration failed for some reason, possibly due to // the user disabling it. { logger_mjs.logger.warn(`Unable to register sync event for '${this._name}'.`, err); } } } } /** * In sync-supporting browsers, this adds a listener for the sync event. * In non-sync-supporting browsers, this will retry the queue on service * worker startup. * * @private */ _addSyncListener() { if ('sync' in registration) { self.addEventListener('sync', event => { if (event.tag === `${TAG_PREFIX}:${this._name}`) { { logger_mjs.logger.log(`Background sync for tag '${event.tag}'` + `has been received`); } const syncComplete = async () => { this._syncInProgress = true; let syncError; try { await this._onSync({ queue: this }); } catch (error) { syncError = error; // Rethrow the error. Note: the logic in the finally clause // will run before this gets rethrown. throw syncError; } finally { // New items may have been added to the queue during the sync, // so we need to register for a new sync if that's happened... // Unless there was an error during the sync, in which // case the browser will automatically retry later, as long // as `event.lastChance` is not true. if (this._requestsAddedDuringSync && !(syncError && !event.lastChance)) { await this.registerSync(); } this._syncInProgress = false; this._requestsAddedDuringSync = false; } }; event.waitUntil(syncComplete()); } }); } else { { logger_mjs.logger.log(`Background sync replaying without background sync event`); } // If the browser doesn't support background sync, retry // every time the service worker starts up as a fallback. this._onSync({ queue: this }); } } /** * Returns the set of queue names. This is primarily used to reset the list * of queue names in tests. * * @return {Set} * * @private */ static get _queueNames() { return queueNames; } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * A class implementing the `fetchDidFail` lifecycle callback. This makes it * easier to add failed requests to a background sync Queue. * * @memberof workbox.backgroundSync */ class Plugin { /** * @param {...*} queueArgs Args to forward to the composed Queue instance. * See the [Queue]{@link workbox.backgroundSync.Queue} documentation for * parameter details. */ constructor(...queueArgs) { this._queue = new Queue(...queueArgs); this.fetchDidFail = this.fetchDidFail.bind(this); } /** * @param {Object} options * @param {Request} options.request * @private */ async fetchDidFail({ request }) { await this._queue.pushRequest({ request }); } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ exports.Queue = Queue; exports.Plugin = Plugin; return exports; }({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private)); //# sourceMappingURL=workbox-background-sync.dev.js.map ================================================ FILE: public/assets/libs/workbox/workbox-background-sync.prod.js ================================================ this.workbox=this.workbox||{},this.workbox.backgroundSync=function(t,e,s){"use strict";try{self["workbox:background-sync:4.1.1"]&&_()}catch(t){}const i=3,n="workbox-background-sync",a="requests",r="queueName";class c{constructor(t){this.t=t,this.s=new s.DBWrapper(n,i,{onupgradeneeded:t=>this.i(t)})}async pushEntry(t){delete t.id,t.queueName=this.t,await this.s.add(a,t)}async unshiftEntry(t){const[e]=await this.s.getAllMatching(a,{count:1});e?t.id=e.id-1:delete t.id,t.queueName=this.t,await this.s.add(a,t)}async popEntry(){return this.h({direction:"prev"})}async shiftEntry(){return this.h({direction:"next"})}async h({direction:t}){const[e]=await this.s.getAllMatching(a,{direction:t,index:r,query:IDBKeyRange.only(this.t),count:1});if(e)return await this.s.delete(a,e.id),delete e.id,delete e.queueName,e}i(t){const e=t.target.result;t.oldVersion>0&&t.oldVersioni?this.R(t):(s.request=new o(s.requestData).toRequest(),delete s.requestData,s)}}async replayRequests(){let t;for(;t=await this.shiftRequest();)try{await fetch(t.request)}catch(s){throw await this.unshiftRequest(t),new e.WorkboxError("queue-replay-failed",{name:this.u})}}async registerSync(){if("sync"in registration)try{await registration.sync.register(`${u}:${this.u}`)}catch(t){}}p(){"sync"in registration?self.addEventListener("sync",t=>{if(t.tag===`${u}:${this.u}`){const e=async()=>{let e;this.k=!0;try{await this.l({queue:this})}catch(t){throw e=t}finally{!this._||e&&!t.lastChance||await this.registerSync(),this.k=!1,this._=!1}};t.waitUntil(e())}}):this.l({queue:this})}static get D(){return w}}return t.Queue=d,t.Plugin=class{constructor(...t){this.$=new d(...t),this.fetchDidFail=this.fetchDidFail.bind(this)}async fetchDidFail({request:t}){await this.$.pushRequest({request:t})}},t}({},workbox.core._private,workbox.core._private); //# sourceMappingURL=workbox-background-sync.prod.js.map ================================================ FILE: public/assets/libs/workbox/workbox-broadcast-update.dev.js ================================================ this.workbox = this.workbox || {}; this.workbox.broadcastUpdate = (function (exports, assert_mjs, getFriendlyURL_mjs, logger_mjs, Deferred_mjs, WorkboxError_mjs) { 'use strict'; try { self['workbox:broadcast-update:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Given two `Response's`, compares several header values to see if they are * the same or not. * * @param {Response} firstResponse * @param {Response} secondResponse * @param {Array} headersToCheck * @return {boolean} * * @memberof workbox.broadcastUpdate * @private */ const responsesAreSame = (firstResponse, secondResponse, headersToCheck) => { { if (!(firstResponse instanceof Response && secondResponse instanceof Response)) { throw new WorkboxError_mjs.WorkboxError('invalid-responses-are-same-args'); } } const atLeastOneHeaderAvailable = headersToCheck.some(header => { return firstResponse.headers.has(header) && secondResponse.headers.has(header); }); if (!atLeastOneHeaderAvailable) { { logger_mjs.logger.warn(`Unable to determine where the response has been updated ` + `because none of the headers that would be checked are present.`); logger_mjs.logger.debug(`Attempting to compare the following: `, firstResponse, secondResponse, headersToCheck); } // Just return true, indicating the that responses are the same, since we // can't determine otherwise. return true; } return headersToCheck.every(header => { const headerStateComparison = firstResponse.headers.has(header) === secondResponse.headers.has(header); const headerValueComparison = firstResponse.headers.get(header) === secondResponse.headers.get(header); return headerStateComparison && headerValueComparison; }); }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const CACHE_UPDATED_MESSAGE_TYPE = 'CACHE_UPDATED'; const CACHE_UPDATED_MESSAGE_META = 'workbox-broadcast-update'; const DEFAULT_BROADCAST_CHANNEL_NAME = 'workbox'; const DEFAULT_DEFER_NOTIFICATION_TIMEOUT = 10000; const DEFAULT_HEADERS_TO_CHECK = ['content-length', 'etag', 'last-modified']; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * You would not normally call this method directly; it's called automatically * by an instance of the {@link BroadcastCacheUpdate} class. It's exposed here * for the benefit of developers who would rather not use the full * `BroadcastCacheUpdate` implementation. * * Calling this will dispatch a message on the provided * {@link https://developers.google.com/web/updates/2016/09/broadcastchannel|Broadcast Channel} * to notify interested subscribers about a change to a cached resource. * * The message that's posted has a formation inspired by the * [Flux standard action](https://github.com/acdlite/flux-standard-action#introduction) * format like so: * * ``` * { * type: 'CACHE_UPDATED', * meta: 'workbox-broadcast-update', * payload: { * cacheName: 'the-cache-name', * updatedURL: 'https://example.com/' * } * } * ``` * * (Usage of [Flux](https://facebook.github.io/flux/) itself is not at * all required.) * * @param {Object} options * @param {string} options.cacheName The name of the cache in which the updated * `Response` was stored. * @param {string} options.url The URL associated with the updated `Response`. * @param {BroadcastChannel} [options.channel] The `BroadcastChannel` to use. * If no channel is set or the browser doesn't support the BroadcastChannel * api, then an attempt will be made to `postMessage` each window client. * * @memberof workbox.broadcastUpdate */ const broadcastUpdate = async ({ channel, cacheName, url }) => { { assert_mjs.assert.isType(cacheName, 'string', { moduleName: 'workbox-broadcast-update', className: '~', funcName: 'broadcastUpdate', paramName: 'cacheName' }); assert_mjs.assert.isType(url, 'string', { moduleName: 'workbox-broadcast-update', className: '~', funcName: 'broadcastUpdate', paramName: 'url' }); } const data = { type: CACHE_UPDATED_MESSAGE_TYPE, meta: CACHE_UPDATED_MESSAGE_META, payload: { cacheName: cacheName, updatedURL: url } }; if (channel) { channel.postMessage(data); } else { const windows = await clients.matchAll({ type: 'window' }); for (const win of windows) { win.postMessage(data); } } }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Uses the [Broadcast Channel API]{@link https://developers.google.com/web/updates/2016/09/broadcastchannel} * to notify interested parties when a cached response has been updated. * In browsers that do not support the Broadcast Channel API, the instance * falls back to sending the update via `postMessage()` to all window clients. * * For efficiency's sake, the underlying response bodies are not compared; * only specific response headers are checked. * * @memberof workbox.broadcastUpdate */ class BroadcastCacheUpdate { /** * Construct a BroadcastCacheUpdate instance with a specific `channelName` to * broadcast messages on * * @param {Object} options * @param {Array} * [options.headersToCheck=['content-length', 'etag', 'last-modified']] * A list of headers that will be used to determine whether the responses * differ. * @param {string} [options.channelName='workbox'] The name that will be used *. when creating the `BroadcastChannel`, which defaults to 'workbox' (the * channel name used by the `workbox-window` package). * @param {string} [options.deferNoticationTimeout=10000] The amount of time * to wait for a ready message from the window on navigation requests * before sending the update. */ constructor({ headersToCheck, channelName, deferNoticationTimeout } = {}) { this._headersToCheck = headersToCheck || DEFAULT_HEADERS_TO_CHECK; this._channelName = channelName || DEFAULT_BROADCAST_CHANNEL_NAME; this._deferNoticationTimeout = deferNoticationTimeout || DEFAULT_DEFER_NOTIFICATION_TIMEOUT; { assert_mjs.assert.isType(this._channelName, 'string', { moduleName: 'workbox-broadcast-update', className: 'BroadcastCacheUpdate', funcName: 'constructor', paramName: 'channelName' }); assert_mjs.assert.isArray(this._headersToCheck, { moduleName: 'workbox-broadcast-update', className: 'BroadcastCacheUpdate', funcName: 'constructor', paramName: 'headersToCheck' }); } this._initWindowReadyDeferreds(); } /** * Compare two [Responses](https://developer.mozilla.org/en-US/docs/Web/API/Response) * and send a message via the * {@link https://developers.google.com/web/updates/2016/09/broadcastchannel|Broadcast Channel API} * if they differ. * * Neither of the Responses can be {@link http://stackoverflow.com/questions/39109789|opaque}. * * @param {Object} options * @param {Response} options.oldResponse Cached response to compare. * @param {Response} options.newResponse Possibly updated response to compare. * @param {string} options.url The URL of the request. * @param {string} options.cacheName Name of the cache the responses belong * to. This is included in the broadcast message. * @param {Event} [options.event] event An optional event that triggered * this possible cache update. * @return {Promise} Resolves once the update is sent. */ notifyIfUpdated({ oldResponse, newResponse, url, cacheName, event }) { if (!responsesAreSame(oldResponse, newResponse, this._headersToCheck)) { { logger_mjs.logger.log(`Newer response found (and cached) for:`, url); } const sendUpdate = async () => { // In the case of a navigation request, the requesting page will likely // not have loaded its JavaScript in time to recevied the update // notification, so we defer it until ready (or we timeout waiting). if (event && event.request && event.request.mode === 'navigate') { { logger_mjs.logger.debug(`Original request was a navigation request, ` + `waiting for a ready message from the window`, event.request); } await this._windowReadyOrTimeout(event); } await broadcastUpdate({ channel: this._getChannel(), cacheName, url }); }; // Send the update and ensure the SW stays alive until it's sent. const done = sendUpdate(); if (event) { try { event.waitUntil(done); } catch (error) { { logger_mjs.logger.warn(`Unable to ensure service worker stays alive ` + `when broadcasting cache update for ` + `${getFriendlyURL_mjs.getFriendlyURL(event.request.url)}'.`); } } } return done; } } /** * @return {BroadcastChannel|undefined} The BroadcastChannel instance used for * broadcasting updates, or undefined if the browser doesn't support the * Broadcast Channel API. * * @private */ _getChannel() { if ('BroadcastChannel' in self && !this._channel) { this._channel = new BroadcastChannel(this._channelName); } return this._channel; } /** * Waits for a message from the window indicating that it's capable of * receiving broadcasts. By default, this will only wait for the amount of * time specified via the `deferNoticationTimeout` option. * * @param {Event} event The navigation fetch event. * @return {Promise} * @private */ _windowReadyOrTimeout(event) { if (!this._navigationEventsDeferreds.has(event)) { const deferred = new Deferred_mjs.Deferred(); // Set the deferred on the `_navigationEventsDeferreds` map so it will // be resolved when the next ready message event comes. this._navigationEventsDeferreds.set(event, deferred); // But don't wait too long for the message since it may never come. const timeout = setTimeout(() => { { logger_mjs.logger.debug(`Timed out after ${this._deferNoticationTimeout}` + `ms waiting for message from window`); } deferred.resolve(); }, this._deferNoticationTimeout); // Ensure the timeout is cleared if the deferred promise is resolved. deferred.promise.then(() => clearTimeout(timeout)); } return this._navigationEventsDeferreds.get(event).promise; } /** * Creates a mapping between navigation fetch events and deferreds, and adds * a listener for message events from the window. When message events arrive, * all deferreds in the mapping are resolved. * * Note: it would be easier if we could only resolve the deferred of * navigation fetch event whose client ID matched the source ID of the * message event, but currently client IDs are not exposed on navigation * fetch events: https://www.chromestatus.com/feature/4846038800138240 */ _initWindowReadyDeferreds() { // A mapping between navigation events and their deferreds. this._navigationEventsDeferreds = new Map(); // The message listener needs to be added in the initial run of the // service worker, but since we don't actually need to be listening for // messages until the cache updates, we only invoke the callback if set. self.addEventListener('message', event => { if (event.data.type === 'WINDOW_READY' && event.data.meta === 'workbox-window' && this._navigationEventsDeferreds.size > 0) { { logger_mjs.logger.debug(`Received WINDOW_READY event: `, event); } // Resolve any pending deferreds. for (const deferred of this._navigationEventsDeferreds.values()) { deferred.resolve(); } this._navigationEventsDeferreds.clear(); } }); } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * This plugin will automatically broadcast a message whenever a cached response * is updated. * * @memberof workbox.broadcastUpdate */ class Plugin { /** * Construct a BroadcastCacheUpdate instance with the passed options and * calls its `notifyIfUpdated()` method whenever the plugin's * `cacheDidUpdate` callback is invoked. * * @param {Object} options * @param {Array} * [options.headersToCheck=['content-length', 'etag', 'last-modified']] * A list of headers that will be used to determine whether the responses * differ. * @param {string} [options.channelName='workbox'] The name that will be used *. when creating the `BroadcastChannel`, which defaults to 'workbox' (the * channel name used by the `workbox-window` package). * @param {string} [options.deferNoticationTimeout=10000] The amount of time * to wait for a ready message from the window on navigation requests * before sending the update. */ constructor(options) { this._broadcastUpdate = new BroadcastCacheUpdate(options); } /** * A "lifecycle" callback that will be triggered automatically by the * `workbox-sw` and `workbox-runtime-caching` handlers when an entry is * added to a cache. * * @private * @param {Object} options The input object to this function. * @param {string} options.cacheName Name of the cache being updated. * @param {Response} [options.oldResponse] The previous cached value, if any. * @param {Response} options.newResponse The new value in the cache. * @param {Request} options.request The request that triggered the udpate. * @param {Request} [options.event] The event that triggered the update. */ cacheDidUpdate({ cacheName, oldResponse, newResponse, request, event }) { { assert_mjs.assert.isType(cacheName, 'string', { moduleName: 'workbox-broadcast-update', className: 'Plugin', funcName: 'cacheDidUpdate', paramName: 'cacheName' }); assert_mjs.assert.isInstance(newResponse, Response, { moduleName: 'workbox-broadcast-update', className: 'Plugin', funcName: 'cacheDidUpdate', paramName: 'newResponse' }); assert_mjs.assert.isInstance(request, Request, { moduleName: 'workbox-broadcast-update', className: 'Plugin', funcName: 'cacheDidUpdate', paramName: 'request' }); } if (!oldResponse) { // Without a two responses there is nothing to compare. return; } this._broadcastUpdate.notifyIfUpdated({ cacheName, oldResponse, newResponse, event, url: request.url }); } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ exports.BroadcastCacheUpdate = BroadcastCacheUpdate; exports.Plugin = Plugin; exports.broadcastUpdate = broadcastUpdate; exports.responsesAreSame = responsesAreSame; return exports; }({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private)); //# sourceMappingURL=workbox-broadcast-update.dev.js.map ================================================ FILE: public/assets/libs/workbox/workbox-broadcast-update.prod.js ================================================ this.workbox=this.workbox||{},this.workbox.broadcastUpdate=function(e,t){"use strict";try{self["workbox:broadcast-update:4.1.1"]&&_()}catch(e){}const s=(e,t,s)=>{return!s.some(s=>e.headers.has(s)&&t.headers.has(s))||s.every(s=>{const n=e.headers.has(s)===t.headers.has(s),a=e.headers.get(s)===t.headers.get(s);return n&&a})},n="workbox",a=1e4,i=["content-length","etag","last-modified"],o=async({channel:e,cacheName:t,url:s})=>{const n={type:"CACHE_UPDATED",meta:"workbox-broadcast-update",payload:{cacheName:t,updatedURL:s}};if(e)e.postMessage(n);else{const e=await clients.matchAll({type:"window"});for(const t of e)t.postMessage(n)}};class c{constructor({headersToCheck:e,channelName:t,deferNoticationTimeout:s}={}){this.t=e||i,this.s=t||n,this.i=s||a,this.o()}notifyIfUpdated({oldResponse:e,newResponse:t,url:n,cacheName:a,event:i}){if(!s(e,t,this.t)){const e=(async()=>{i&&i.request&&"navigate"===i.request.mode&&await this.h(i),await o({channel:this.l(),cacheName:a,url:n})})();if(i)try{i.waitUntil(e)}catch(e){}return e}}l(){return"BroadcastChannel"in self&&!this.u&&(this.u=new BroadcastChannel(this.s)),this.u}h(e){if(!this.m.has(e)){const s=new t.Deferred;this.m.set(e,s);const n=setTimeout(()=>{s.resolve()},this.i);s.promise.then(()=>clearTimeout(n))}return this.m.get(e).promise}o(){this.m=new Map,self.addEventListener("message",e=>{if("WINDOW_READY"===e.data.type&&"workbox-window"===e.data.meta&&this.m.size>0){for(const e of this.m.values())e.resolve();this.m.clear()}})}}return e.BroadcastCacheUpdate=c,e.Plugin=class{constructor(e){this.p=new c(e)}cacheDidUpdate({cacheName:e,oldResponse:t,newResponse:s,request:n,event:a}){t&&this.p.notifyIfUpdated({cacheName:e,oldResponse:t,newResponse:s,event:a,url:n.url})}},e.broadcastUpdate=o,e.responsesAreSame=s,e}({},workbox.core._private); //# sourceMappingURL=workbox-broadcast-update.prod.js.map ================================================ FILE: public/assets/libs/workbox/workbox-cacheable-response.dev.js ================================================ this.workbox = this.workbox || {}; this.workbox.cacheableResponse = (function (exports, WorkboxError_mjs, assert_mjs, getFriendlyURL_mjs, logger_mjs) { 'use strict'; try { self['workbox:cacheable-response:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * This class allows you to set up rules determining what * status codes and/or headers need to be present in order for a * [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) * to be considered cacheable. * * @memberof workbox.cacheableResponse */ class CacheableResponse { /** * To construct a new CacheableResponse instance you must provide at least * one of the `config` properties. * * If both `statuses` and `headers` are specified, then both conditions must * be met for the `Response` to be considered cacheable. * * @param {Object} config * @param {Array} [config.statuses] One or more status codes that a * `Response` can have and be considered cacheable. * @param {Object} [config.headers] A mapping of header names * and expected values that a `Response` can have and be considered cacheable. * If multiple headers are provided, only one needs to be present. */ constructor(config = {}) { { if (!(config.statuses || config.headers)) { throw new WorkboxError_mjs.WorkboxError('statuses-or-headers-required', { moduleName: 'workbox-cacheable-response', className: 'CacheableResponse', funcName: 'constructor' }); } if (config.statuses) { assert_mjs.assert.isArray(config.statuses, { moduleName: 'workbox-cacheable-response', className: 'CacheableResponse', funcName: 'constructor', paramName: 'config.statuses' }); } if (config.headers) { assert_mjs.assert.isType(config.headers, 'object', { moduleName: 'workbox-cacheable-response', className: 'CacheableResponse', funcName: 'constructor', paramName: 'config.headers' }); } } this._statuses = config.statuses; this._headers = config.headers; } /** * Checks a response to see whether it's cacheable or not, based on this * object's configuration. * * @param {Response} response The response whose cacheability is being * checked. * @return {boolean} `true` if the `Response` is cacheable, and `false` * otherwise. */ isResponseCacheable(response) { { assert_mjs.assert.isInstance(response, Response, { moduleName: 'workbox-cacheable-response', className: 'CacheableResponse', funcName: 'isResponseCacheable', paramName: 'response' }); } let cacheable = true; if (this._statuses) { cacheable = this._statuses.includes(response.status); } if (this._headers && cacheable) { cacheable = Object.keys(this._headers).some(headerName => { return response.headers.get(headerName) === this._headers[headerName]; }); } { if (!cacheable) { logger_mjs.logger.groupCollapsed(`The request for ` + `'${getFriendlyURL_mjs.getFriendlyURL(response.url)}' returned a response that does ` + `not meet the criteria for being cached.`); logger_mjs.logger.groupCollapsed(`View cacheability criteria here.`); logger_mjs.logger.log(`Cacheable statuses: ` + JSON.stringify(this._statuses)); logger_mjs.logger.log(`Cacheable headers: ` + JSON.stringify(this._headers, null, 2)); logger_mjs.logger.groupEnd(); const logFriendlyHeaders = {}; response.headers.forEach((value, key) => { logFriendlyHeaders[key] = value; }); logger_mjs.logger.groupCollapsed(`View response status and headers here.`); logger_mjs.logger.log(`Response status: ` + response.status); logger_mjs.logger.log(`Response headers: ` + JSON.stringify(logFriendlyHeaders, null, 2)); logger_mjs.logger.groupEnd(); logger_mjs.logger.groupCollapsed(`View full response details here.`); logger_mjs.logger.log(response.headers); logger_mjs.logger.log(response); logger_mjs.logger.groupEnd(); logger_mjs.logger.groupEnd(); } } return cacheable; } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * A class implementing the `cacheWillUpdate` lifecycle callback. This makes it * easier to add in cacheability checks to requests made via Workbox's built-in * strategies. * * @memberof workbox.cacheableResponse */ class Plugin { /** * To construct a new cacheable response Plugin instance you must provide at * least one of the `config` properties. * * If both `statuses` and `headers` are specified, then both conditions must * be met for the `Response` to be considered cacheable. * * @param {Object} config * @param {Array} [config.statuses] One or more status codes that a * `Response` can have and be considered cacheable. * @param {Object} [config.headers] A mapping of header names * and expected values that a `Response` can have and be considered cacheable. * If multiple headers are provided, only one needs to be present. */ constructor(config) { this._cacheableResponse = new CacheableResponse(config); } /** * @param {Object} options * @param {Response} options.response * @return {boolean} * @private */ cacheWillUpdate({ response }) { if (this._cacheableResponse.isResponseCacheable(response)) { return response; } return null; } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ exports.CacheableResponse = CacheableResponse; exports.Plugin = Plugin; return exports; }({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private)); //# sourceMappingURL=workbox-cacheable-response.dev.js.map ================================================ FILE: public/assets/libs/workbox/workbox-cacheable-response.prod.js ================================================ this.workbox=this.workbox||{},this.workbox.cacheableResponse=function(t){"use strict";try{self["workbox:cacheable-response:4.1.1"]&&_()}catch(t){}class s{constructor(t={}){this.t=t.statuses,this.s=t.headers}isResponseCacheable(t){let s=!0;return this.t&&(s=this.t.includes(t.status)),this.s&&s&&(s=Object.keys(this.s).some(s=>t.headers.get(s)===this.s[s])),s}}return t.CacheableResponse=s,t.Plugin=class{constructor(t){this.i=new s(t)}cacheWillUpdate({response:t}){return this.i.isResponseCacheable(t)?t:null}},t}({}); //# sourceMappingURL=workbox-cacheable-response.prod.js.map ================================================ FILE: public/assets/libs/workbox/workbox-core.dev.js ================================================ this.workbox = this.workbox || {}; this.workbox.core = (function (exports) { 'use strict'; try { self['workbox:core:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const logger = (() => { let inGroup = false; const methodToColorMap = { debug: `#7f8c8d`, // Gray log: `#2ecc71`, // Green warn: `#f39c12`, // Yellow error: `#c0392b`, // Red groupCollapsed: `#3498db`, // Blue groupEnd: null // No colored prefix on groupEnd }; const print = function (method, args) { if (method === 'groupCollapsed') { // Safari doesn't print all console.groupCollapsed() arguments: // https://bugs.webkit.org/show_bug.cgi?id=182754 if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { console[method](...args); return; } } const styles = [`background: ${methodToColorMap[method]}`, `border-radius: 0.5em`, `color: white`, `font-weight: bold`, `padding: 2px 0.5em`]; // When in a group, the workbox prefix is not displayed. const logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')]; console[method](...logPrefix, ...args); if (method === 'groupCollapsed') { inGroup = true; } if (method === 'groupEnd') { inGroup = false; } }; const api = {}; for (const method of Object.keys(methodToColorMap)) { api[method] = (...args) => { print(method, args); }; } return api; })(); /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const messages = { 'invalid-value': ({ paramName, validValueDescription, value }) => { if (!paramName || !validValueDescription) { throw new Error(`Unexpected input to 'invalid-value' error.`); } return `The '${paramName}' parameter was given a value with an ` + `unexpected value. ${validValueDescription} Received a value of ` + `${JSON.stringify(value)}.`; }, 'not-in-sw': ({ moduleName }) => { if (!moduleName) { throw new Error(`Unexpected input to 'not-in-sw' error.`); } return `The '${moduleName}' must be used in a service worker.`; }, 'not-an-array': ({ moduleName, className, funcName, paramName }) => { if (!moduleName || !className || !funcName || !paramName) { throw new Error(`Unexpected input to 'not-an-array' error.`); } return `The parameter '${paramName}' passed into ` + `'${moduleName}.${className}.${funcName}()' must be an array.`; }, 'incorrect-type': ({ expectedType, paramName, moduleName, className, funcName }) => { if (!expectedType || !paramName || !moduleName || !funcName) { throw new Error(`Unexpected input to 'incorrect-type' error.`); } return `The parameter '${paramName}' passed into ` + `'${moduleName}.${className ? className + '.' : ''}` + `${funcName}()' must be of type ${expectedType}.`; }, 'incorrect-class': ({ expectedClass, paramName, moduleName, className, funcName, isReturnValueProblem }) => { if (!expectedClass || !moduleName || !funcName) { throw new Error(`Unexpected input to 'incorrect-class' error.`); } if (isReturnValueProblem) { return `The return value from ` + `'${moduleName}.${className ? className + '.' : ''}${funcName}()' ` + `must be an instance of class ${expectedClass.name}.`; } return `The parameter '${paramName}' passed into ` + `'${moduleName}.${className ? className + '.' : ''}${funcName}()' ` + `must be an instance of class ${expectedClass.name}.`; }, 'missing-a-method': ({ expectedMethod, paramName, moduleName, className, funcName }) => { if (!expectedMethod || !paramName || !moduleName || !className || !funcName) { throw new Error(`Unexpected input to 'missing-a-method' error.`); } return `${moduleName}.${className}.${funcName}() expected the ` + `'${paramName}' parameter to expose a '${expectedMethod}' method.`; }, 'add-to-cache-list-unexpected-type': ({ entry }) => { return `An unexpected entry was passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' The entry ` + `'${JSON.stringify(entry)}' isn't supported. You must supply an array of ` + `strings with one or more characters, objects with a url property or ` + `Request objects.`; }, 'add-to-cache-list-conflicting-entries': ({ firstEntry, secondEntry }) => { if (!firstEntry || !secondEntry) { throw new Error(`Unexpected input to ` + `'add-to-cache-list-duplicate-entries' error.`); } return `Two of the entries passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + `${firstEntry._entryId} but different revision details. Workbox is ` + `is unable to cache and version the asset correctly. Please remove one ` + `of the entries.`; }, 'plugin-error-request-will-fetch': ({ thrownError }) => { if (!thrownError) { throw new Error(`Unexpected input to ` + `'plugin-error-request-will-fetch', error.`); } return `An error was thrown by a plugins 'requestWillFetch()' method. ` + `The thrown error message was: '${thrownError.message}'.`; }, 'invalid-cache-name': ({ cacheNameId, value }) => { if (!cacheNameId) { throw new Error(`Expected a 'cacheNameId' for error 'invalid-cache-name'`); } return `You must provide a name containing at least one character for ` + `setCacheDeatils({${cacheNameId}: '...'}). Received a value of ` + `'${JSON.stringify(value)}'`; }, 'unregister-route-but-not-found-with-method': ({ method }) => { if (!method) { throw new Error(`Unexpected input to ` + `'unregister-route-but-not-found-with-method' error.`); } return `The route you're trying to unregister was not previously ` + `registered for the method type '${method}'.`; }, 'unregister-route-route-not-registered': () => { return `The route you're trying to unregister was not previously ` + `registered.`; }, 'queue-replay-failed': ({ name }) => { return `Replaying the background sync queue '${name}' failed.`; }, 'duplicate-queue-name': ({ name }) => { return `The Queue name '${name}' is already being used. ` + `All instances of backgroundSync.Queue must be given unique names.`; }, 'expired-test-without-max-age': ({ methodName, paramName }) => { return `The '${methodName}()' method can only be used when the ` + `'${paramName}' is used in the constructor.`; }, 'unsupported-route-type': ({ moduleName, className, funcName, paramName }) => { return `The supplied '${paramName}' parameter was an unsupported type. ` + `Please check the docs for ${moduleName}.${className}.${funcName} for ` + `valid input types.`; }, 'not-array-of-class': ({ value, expectedClass, moduleName, className, funcName, paramName }) => { return `The supplied '${paramName}' parameter must be an array of ` + `'${expectedClass}' objects. Received '${JSON.stringify(value)},'. ` + `Please check the call to ${moduleName}.${className}.${funcName}() ` + `to fix the issue.`; }, 'max-entries-or-age-required': ({ moduleName, className, funcName }) => { return `You must define either config.maxEntries or config.maxAgeSeconds` + `in ${moduleName}.${className}.${funcName}`; }, 'statuses-or-headers-required': ({ moduleName, className, funcName }) => { return `You must define either config.statuses or config.headers` + `in ${moduleName}.${className}.${funcName}`; }, 'invalid-string': ({ moduleName, className, funcName, paramName }) => { if (!paramName || !moduleName || !funcName) { throw new Error(`Unexpected input to 'invalid-string' error.`); } return `When using strings, the '${paramName}' parameter must start with ` + `'http' (for cross-origin matches) or '/' (for same-origin matches). ` + `Please see the docs for ${moduleName}.${funcName}() for ` + `more info.`; }, 'channel-name-required': () => { return `You must provide a channelName to construct a ` + `BroadcastCacheUpdate instance.`; }, 'invalid-responses-are-same-args': () => { return `The arguments passed into responsesAreSame() appear to be ` + `invalid. Please ensure valid Responses are used.`; }, 'expire-custom-caches-only': () => { return `You must provide a 'cacheName' property when using the ` + `expiration plugin with a runtime caching strategy.`; }, 'unit-must-be-bytes': ({ normalizedRangeHeader }) => { if (!normalizedRangeHeader) { throw new Error(`Unexpected input to 'unit-must-be-bytes' error.`); } return `The 'unit' portion of the Range header must be set to 'bytes'. ` + `The Range header provided was "${normalizedRangeHeader}"`; }, 'single-range-only': ({ normalizedRangeHeader }) => { if (!normalizedRangeHeader) { throw new Error(`Unexpected input to 'single-range-only' error.`); } return `Multiple ranges are not supported. Please use a single start ` + `value, and optional end value. The Range header provided was ` + `"${normalizedRangeHeader}"`; }, 'invalid-range-values': ({ normalizedRangeHeader }) => { if (!normalizedRangeHeader) { throw new Error(`Unexpected input to 'invalid-range-values' error.`); } return `The Range header is missing both start and end values. At least ` + `one of those values is needed. The Range header provided was ` + `"${normalizedRangeHeader}"`; }, 'no-range-header': () => { return `No Range header was found in the Request provided.`; }, 'range-not-satisfiable': ({ size, start, end }) => { return `The start (${start}) and end (${end}) values in the Range are ` + `not satisfiable by the cached response, which is ${size} bytes.`; }, 'attempt-to-cache-non-get-request': ({ url, method }) => { return `Unable to cache '${url}' because it is a '${method}' request and ` + `only 'GET' requests can be cached.`; }, 'cache-put-with-no-response': ({ url }) => { return `There was an attempt to cache '${url}' but the response was not ` + `defined.`; }, 'no-response': ({ url, error }) => { let message = `The strategy could not generate a response for '${url}'.`; if (error) { message += ` The underlying error is ${error}.`; } return message; }, 'bad-precaching-response': ({ url, status }) => { return `The precaching request for '${url}' failed with an HTTP ` + `status of ${status}.`; } }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const generatorFunction = (code, ...args) => { const message = messages[code]; if (!message) { throw new Error(`Unable to find message for code '${code}'.`); } return message(...args); }; const messageGenerator = generatorFunction; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Workbox errors should be thrown with this class. * This allows use to ensure the type easily in tests, * helps developers identify errors from workbox * easily and allows use to optimise error * messages correctly. * * @private */ class WorkboxError extends Error { /** * * @param {string} errorCode The error code that * identifies this particular error. * @param {Object=} details Any relevant arguments * that will help developers identify issues should * be added as a key on the context object. */ constructor(errorCode, details) { let message = messageGenerator(errorCode, details); super(message); this.name = errorCode; this.details = details; } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /* * This method returns true if the current context is a service worker. */ const isSWEnv = moduleName => { if (!('ServiceWorkerGlobalScope' in self)) { throw new WorkboxError('not-in-sw', { moduleName }); } }; /* * This method throws if the supplied value is not an array. * The destructed values are required to produce a meaningful error for users. * The destructed and restructured object is so it's clear what is * needed. */ const isArray = (value, { moduleName, className, funcName, paramName }) => { if (!Array.isArray(value)) { throw new WorkboxError('not-an-array', { moduleName, className, funcName, paramName }); } }; const hasMethod = (object, expectedMethod, { moduleName, className, funcName, paramName }) => { const type = typeof object[expectedMethod]; if (type !== 'function') { throw new WorkboxError('missing-a-method', { paramName, expectedMethod, moduleName, className, funcName }); } }; const isType = (object, expectedType, { moduleName, className, funcName, paramName }) => { if (typeof object !== expectedType) { throw new WorkboxError('incorrect-type', { paramName, expectedType, moduleName, className, funcName }); } }; const isInstance = (object, expectedClass, { moduleName, className, funcName, paramName, isReturnValueProblem }) => { if (!(object instanceof expectedClass)) { throw new WorkboxError('incorrect-class', { paramName, expectedClass, moduleName, className, funcName, isReturnValueProblem }); } }; const isOneOf = (value, validValues, { paramName }) => { if (!validValues.includes(value)) { throw new WorkboxError('invalid-value', { paramName, value, validValueDescription: `Valid values are ${JSON.stringify(validValues)}.` }); } }; const isArrayOfClass = (value, expectedClass, { moduleName, className, funcName, paramName }) => { const error = new WorkboxError('not-array-of-class', { value, expectedClass, moduleName, className, funcName, paramName }); if (!Array.isArray(value)) { throw error; } for (let item of value) { if (!(item instanceof expectedClass)) { throw error; } } }; const finalAssertExports = { hasMethod, isArray, isInstance, isOneOf, isSWEnv, isType, isArrayOfClass }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const callbacks = new Set(); /** * Adds a function to the set of callbacks that will be executed when there's * a quota error. * * @param {Function} callback * @memberof workbox.core */ function registerQuotaErrorCallback(callback) { { finalAssertExports.isType(callback, 'function', { moduleName: 'workbox-core', funcName: 'register', paramName: 'callback' }); } callbacks.add(callback); { logger.log('Registered a callback to respond to quota errors.', callback); } } /** * Runs all of the callback functions, one at a time sequentially, in the order * in which they were registered. * * @memberof workbox.core * @private */ async function executeQuotaErrorCallbacks() { { logger.log(`About to run ${callbacks.size} callbacks to clean up caches.`); } for (const callback of callbacks) { await callback(); { logger.log(callback, 'is complete.'); } } { logger.log('Finished running callbacks.'); } } /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * A class that wraps common IndexedDB functionality in a promise-based API. * It exposes all the underlying power and functionality of IndexedDB, but * wraps the most commonly used features in a way that's much simpler to use. * * @private */ class DBWrapper { /** * @param {string} name * @param {number} version * @param {Object=} [callback] * @param {!Function} [callbacks.onupgradeneeded] * @param {!Function} [callbacks.onversionchange] Defaults to * DBWrapper.prototype._onversionchange when not specified. */ constructor(name, version, { onupgradeneeded, onversionchange = this._onversionchange } = {}) { this._name = name; this._version = version; this._onupgradeneeded = onupgradeneeded; this._onversionchange = onversionchange; // If this is null, it means the database isn't open. this._db = null; } /** * Returns the IDBDatabase instance (not normally needed). */ get db() { return this._db; } /** * Opens a connected to an IDBDatabase, invokes any onupgradedneeded * callback, and added an onversionchange callback to the database. * * @return {IDBDatabase} */ async open() { if (this._db) return; this._db = await new Promise((resolve, reject) => { // This flag is flipped to true if the timeout callback runs prior // to the request failing or succeeding. Note: we use a timeout instead // of an onblocked handler since there are cases where onblocked will // never never run. A timeout better handles all possible scenarios: // https://github.com/w3c/IndexedDB/issues/223 let openRequestTimedOut = false; setTimeout(() => { openRequestTimedOut = true; reject(new Error('The open request was blocked and timed out')); }, this.OPEN_TIMEOUT); const openRequest = indexedDB.open(this._name, this._version); openRequest.onerror = () => reject(openRequest.error); openRequest.onupgradeneeded = evt => { if (openRequestTimedOut) { openRequest.transaction.abort(); evt.target.result.close(); } else if (this._onupgradeneeded) { this._onupgradeneeded(evt); } }; openRequest.onsuccess = ({ target }) => { const db = target.result; if (openRequestTimedOut) { db.close(); } else { db.onversionchange = this._onversionchange.bind(this); resolve(db); } }; }); return this; } /** * Polyfills the native `getKey()` method. Note, this is overridden at * runtime if the browser supports the native method. * * @param {string} storeName * @param {*} query * @return {Array} */ async getKey(storeName, query) { return (await this.getAllKeys(storeName, query, 1))[0]; } /** * Polyfills the native `getAll()` method. Note, this is overridden at * runtime if the browser supports the native method. * * @param {string} storeName * @param {*} query * @param {number} count * @return {Array} */ async getAll(storeName, query, count) { return await this.getAllMatching(storeName, { query, count }); } /** * Polyfills the native `getAllKeys()` method. Note, this is overridden at * runtime if the browser supports the native method. * * @param {string} storeName * @param {*} query * @param {number} count * @return {Array} */ async getAllKeys(storeName, query, count) { return (await this.getAllMatching(storeName, { query, count, includeKeys: true })).map(({ key }) => key); } /** * Supports flexible lookup in an object store by specifying an index, * query, direction, and count. This method returns an array of objects * with the signature . * * @param {string} storeName * @param {Object} [opts] * @param {string} [opts.index] The index to use (if specified). * @param {*} [opts.query] * @param {IDBCursorDirection} [opts.direction] * @param {number} [opts.count] The max number of results to return. * @param {boolean} [opts.includeKeys] When true, the structure of the * returned objects is changed from an array of values to an array of * objects in the form {key, primaryKey, value}. * @return {Array} */ async getAllMatching(storeName, { index, query = null, // IE errors if query === `undefined`. direction = 'next', count, includeKeys } = {}) { return await this.transaction([storeName], 'readonly', (txn, done) => { const store = txn.objectStore(storeName); const target = index ? store.index(index) : store; const results = []; target.openCursor(query, direction).onsuccess = ({ target }) => { const cursor = target.result; if (cursor) { const { primaryKey, key, value } = cursor; results.push(includeKeys ? { primaryKey, key, value } : value); if (count && results.length >= count) { done(results); } else { cursor.continue(); } } else { done(results); } }; }); } /** * Accepts a list of stores, a transaction type, and a callback and * performs a transaction. A promise is returned that resolves to whatever * value the callback chooses. The callback holds all the transaction logic * and is invoked with two arguments: * 1. The IDBTransaction object * 2. A `done` function, that's used to resolve the promise when * when the transaction is done, if passed a value, the promise is * resolved to that value. * * @param {Array} storeNames An array of object store names * involved in the transaction. * @param {string} type Can be `readonly` or `readwrite`. * @param {!Function} callback * @return {*} The result of the transaction ran by the callback. */ async transaction(storeNames, type, callback) { await this.open(); return await new Promise((resolve, reject) => { const txn = this._db.transaction(storeNames, type); txn.onabort = ({ target }) => reject(target.error); txn.oncomplete = () => resolve(); callback(txn, value => resolve(value)); }); } /** * Delegates async to a native IDBObjectStore method. * * @param {string} method The method name. * @param {string} storeName The object store name. * @param {string} type Can be `readonly` or `readwrite`. * @param {...*} args The list of args to pass to the native method. * @return {*} The result of the transaction. */ async _call(method, storeName, type, ...args) { const callback = (txn, done) => { txn.objectStore(storeName)[method](...args).onsuccess = ({ target }) => { done(target.result); }; }; return await this.transaction([storeName], type, callback); } /** * The default onversionchange handler, which closes the database so other * connections can open without being blocked. */ _onversionchange() { this.close(); } /** * Closes the connection opened by `DBWrapper.open()`. Generally this method * doesn't need to be called since: * 1. It's usually better to keep a connection open since opening * a new connection is somewhat slow. * 2. Connections are automatically closed when the reference is * garbage collected. * The primary use case for needing to close a connection is when another * reference (typically in another tab) needs to upgrade it and would be * blocked by the current, open connection. */ close() { if (this._db) { this._db.close(); this._db = null; } } } // Exposed to let users modify the default timeout on a per-instance // or global basis. DBWrapper.prototype.OPEN_TIMEOUT = 2000; // Wrap native IDBObjectStore methods according to their mode. const methodsToWrap = { 'readonly': ['get', 'count', 'getKey', 'getAll', 'getAllKeys'], 'readwrite': ['add', 'put', 'clear', 'delete'] }; for (const [mode, methods] of Object.entries(methodsToWrap)) { for (const method of methods) { if (method in IDBObjectStore.prototype) { // Don't use arrow functions here since we're outside of the class. DBWrapper.prototype[method] = async function (storeName, ...args) { return await this._call(method, storeName, mode, ...args); }; } } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Deletes the database. * Note: this is exported separately from the DBWrapper module because most * usages of IndexedDB in workbox dont need deleting, and this way it can be * reused in tests to delete databases without creating DBWrapper instances. * * @param {string} name The database name. * @private */ const deleteDatabase = async name => { await new Promise((resolve, reject) => { const request = indexedDB.deleteDatabase(name); request.onerror = ({ target }) => { reject(target.error); }; request.onblocked = () => { reject(new Error('Delete blocked')); }; request.onsuccess = () => { resolve(); }; }); }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const _cacheNameDetails = { googleAnalytics: 'googleAnalytics', precache: 'precache-v2', prefix: 'workbox', runtime: 'runtime', suffix: self.registration.scope }; const _createCacheName = cacheName => { return [_cacheNameDetails.prefix, cacheName, _cacheNameDetails.suffix].filter(value => value.length > 0).join('-'); }; const cacheNames = { updateDetails: details => { Object.keys(_cacheNameDetails).forEach(key => { if (typeof details[key] !== 'undefined') { _cacheNameDetails[key] = details[key]; } }); }, getGoogleAnalyticsName: userCacheName => { return userCacheName || _createCacheName(_cacheNameDetails.googleAnalytics); }, getPrecacheName: userCacheName => { return userCacheName || _createCacheName(_cacheNameDetails.precache); }, getRuntimeName: userCacheName => { return userCacheName || _createCacheName(_cacheNameDetails.runtime); } }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const pluginEvents = { CACHE_DID_UPDATE: 'cacheDidUpdate', CACHE_WILL_UPDATE: 'cacheWillUpdate', CACHED_RESPONSE_WILL_BE_USED: 'cachedResponseWillBeUsed', FETCH_DID_FAIL: 'fetchDidFail', FETCH_DID_SUCCEED: 'fetchDidSucceed', REQUEST_WILL_FETCH: 'requestWillFetch' }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const pluginUtils = { filter: (plugins, callbackName) => { return plugins.filter(plugin => callbackName in plugin); } }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const getFriendlyURL = url => { const urlObj = new URL(url, location); if (urlObj.origin === location.origin) { return urlObj.pathname; } return urlObj.href; }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Wrapper around cache.put(). * * Will call `cacheDidUpdate` on plugins if the cache was updated, using * `matchOptions` when determining what the old entry is. * * @param {Object} options * @param {string} options.cacheName * @param {Request} options.request * @param {Response} options.response * @param {Event} [options.event] * @param {Array} [options.plugins=[]] * @param {Object} [options.matchOptions] * * @private * @memberof module:workbox-core */ const putWrapper = async ({ cacheName, request, response, event, plugins = [], matchOptions } = {}) => { if (!response) { { logger.error(`Cannot cache non-existent response for ` + `'${getFriendlyURL(request.url)}'.`); } throw new WorkboxError('cache-put-with-no-response', { url: getFriendlyURL(request.url) }); } let responseToCache = await _isResponseSafeToCache({ request, response, event, plugins }); if (!responseToCache) { { logger.debug(`Response '${getFriendlyURL(request.url)}' will not be ` + `cached.`, responseToCache); } return; } { if (responseToCache.method && responseToCache.method !== 'GET') { throw new WorkboxError('attempt-to-cache-non-get-request', { url: getFriendlyURL(request.url), method: responseToCache.method }); } } const cache = await caches.open(cacheName); const updatePlugins = pluginUtils.filter(plugins, pluginEvents.CACHE_DID_UPDATE); let oldResponse = updatePlugins.length > 0 ? await matchWrapper({ cacheName, request, matchOptions }) : null; { logger.debug(`Updating the '${cacheName}' cache with a new Response for ` + `${getFriendlyURL(request.url)}.`); } try { await cache.put(request, responseToCache); } catch (error) { // See https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-QuotaExceededError if (error.name === 'QuotaExceededError') { await executeQuotaErrorCallbacks(); } throw error; } for (let plugin of updatePlugins) { await plugin[pluginEvents.CACHE_DID_UPDATE].call(plugin, { cacheName, request, event, oldResponse, newResponse: responseToCache }); } }; /** * This is a wrapper around cache.match(). * * @param {Object} options * @param {string} options.cacheName Name of the cache to match against. * @param {Request} options.request The Request that will be used to look up * cache entries. * @param {Event} [options.event] The event that propted the action. * @param {Object} [options.matchOptions] Options passed to cache.match(). * @param {Array} [options.plugins=[]] Array of plugins. * @return {Response} A cached response if available. * * @private * @memberof module:workbox-core */ const matchWrapper = async ({ cacheName, request, event, matchOptions, plugins = [] }) => { const cache = await caches.open(cacheName); let cachedResponse = await cache.match(request, matchOptions); { if (cachedResponse) { logger.debug(`Found a cached response in '${cacheName}'.`); } else { logger.debug(`No cached response found in '${cacheName}'.`); } } for (const plugin of plugins) { if (pluginEvents.CACHED_RESPONSE_WILL_BE_USED in plugin) { cachedResponse = await plugin[pluginEvents.CACHED_RESPONSE_WILL_BE_USED].call(plugin, { cacheName, request, event, matchOptions, cachedResponse }); { if (cachedResponse) { finalAssertExports.isInstance(cachedResponse, Response, { moduleName: 'Plugin', funcName: pluginEvents.CACHED_RESPONSE_WILL_BE_USED, isReturnValueProblem: true }); } } } } return cachedResponse; }; /** * This method will call cacheWillUpdate on the available plugins (or use * status === 200) to determine if the Response is safe and valid to cache. * * @param {Object} options * @param {Request} options.request * @param {Response} options.response * @param {Event} [options.event] * @param {Array} [options.plugins=[]] * @return {Promise} * * @private * @memberof module:workbox-core */ const _isResponseSafeToCache = async ({ request, response, event, plugins }) => { let responseToCache = response; let pluginsUsed = false; for (let plugin of plugins) { if (pluginEvents.CACHE_WILL_UPDATE in plugin) { pluginsUsed = true; responseToCache = await plugin[pluginEvents.CACHE_WILL_UPDATE].call(plugin, { request, response: responseToCache, event }); { if (responseToCache) { finalAssertExports.isInstance(responseToCache, Response, { moduleName: 'Plugin', funcName: pluginEvents.CACHE_WILL_UPDATE, isReturnValueProblem: true }); } } if (!responseToCache) { break; } } } if (!pluginsUsed) { { if (!responseToCache.status === 200) { if (responseToCache.status === 0) { logger.warn(`The response for '${request.url}' is an opaque ` + `response. The caching strategy that you're using will not ` + `cache opaque responses by default.`); } else { logger.debug(`The response for '${request.url}' returned ` + `a status code of '${response.status}' and won't be cached as a ` + `result.`); } } } responseToCache = responseToCache.status === 200 ? responseToCache : null; } return responseToCache ? responseToCache : null; }; const cacheWrapper = { put: putWrapper, match: matchWrapper }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Wrapper around the fetch API. * * Will call requestWillFetch on available plugins. * * @param {Object} options * @param {Request|string} options.request * @param {Object} [options.fetchOptions] * @param {Event} [options.event] * @param {Array} [options.plugins=[]] * @return {Promise} * * @private * @memberof module:workbox-core */ const wrappedFetch = async ({ request, fetchOptions, event, plugins = [] }) => { // We *should* be able to call `await event.preloadResponse` even if it's // undefined, but for some reason, doing so leads to errors in our Node unit // tests. To work around that, explicitly check preloadResponse's value first. if (event && event.preloadResponse) { const possiblePreloadResponse = await event.preloadResponse; if (possiblePreloadResponse) { { logger.log(`Using a preloaded navigation response for ` + `'${getFriendlyURL(request.url)}'`); } return possiblePreloadResponse; } } if (typeof request === 'string') { request = new Request(request); } { finalAssertExports.isInstance(request, Request, { paramName: request, expectedClass: 'Request', moduleName: 'workbox-core', className: 'fetchWrapper', funcName: 'wrappedFetch' }); } const failedFetchPlugins = pluginUtils.filter(plugins, pluginEvents.FETCH_DID_FAIL); // If there is a fetchDidFail plugin, we need to save a clone of the // original request before it's either modified by a requestWillFetch // plugin or before the original request's body is consumed via fetch(). const originalRequest = failedFetchPlugins.length > 0 ? request.clone() : null; try { for (let plugin of plugins) { if (pluginEvents.REQUEST_WILL_FETCH in plugin) { request = await plugin[pluginEvents.REQUEST_WILL_FETCH].call(plugin, { request: request.clone(), event }); { if (request) { finalAssertExports.isInstance(request, Request, { moduleName: 'Plugin', funcName: pluginEvents.CACHED_RESPONSE_WILL_BE_USED, isReturnValueProblem: true }); } } } } } catch (err) { throw new WorkboxError('plugin-error-request-will-fetch', { thrownError: err }); } // The request can be altered by plugins with `requestWillFetch` making // the original request (Most likely from a `fetch` event) to be different // to the Request we make. Pass both to `fetchDidFail` to aid debugging. let pluginFilteredRequest = request.clone(); try { let fetchResponse; // See https://github.com/GoogleChrome/workbox/issues/1796 if (request.mode === 'navigate') { fetchResponse = await fetch(request); } else { fetchResponse = await fetch(request, fetchOptions); } { logger.debug(`Network request for ` + `'${getFriendlyURL(request.url)}' returned a response with ` + `status '${fetchResponse.status}'.`); } for (const plugin of plugins) { if (pluginEvents.FETCH_DID_SUCCEED in plugin) { fetchResponse = await plugin[pluginEvents.FETCH_DID_SUCCEED].call(plugin, { event, request: pluginFilteredRequest, response: fetchResponse }); { if (fetchResponse) { finalAssertExports.isInstance(fetchResponse, Response, { moduleName: 'Plugin', funcName: pluginEvents.FETCH_DID_SUCCEED, isReturnValueProblem: true }); } } } } return fetchResponse; } catch (error) { { logger.error(`Network request for ` + `'${getFriendlyURL(request.url)}' threw an error.`, error); } for (const plugin of failedFetchPlugins) { await plugin[pluginEvents.FETCH_DID_FAIL].call(plugin, { error, event, originalRequest: originalRequest.clone(), request: pluginFilteredRequest.clone() }); } throw error; } }; const fetchWrapper = { fetch: wrappedFetch }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ var _private = /*#__PURE__*/Object.freeze({ DBWrapper: DBWrapper, deleteDatabase: deleteDatabase, WorkboxError: WorkboxError, assert: finalAssertExports, cacheNames: cacheNames, cacheWrapper: cacheWrapper, fetchWrapper: fetchWrapper, getFriendlyURL: getFriendlyURL, logger: logger }); /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Claim any currently available clients once the service worker * becomes active. This is normally used in conjunction with `skipWaiting()`. * * @alias workbox.core.clientsClaim */ const clientsClaim = () => { addEventListener('activate', () => clients.claim()); }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Get the current cache names used by Workbox. * * `cacheNames.precache` is used for precached assets, * `cacheNames.googleAnalytics` is used by `workbox-google-analytics` to * store `analytics.js`, and `cacheNames.runtime` is used for everything else. * * @return {Object} An object with `precache`, `runtime`, and * `googleAnalytics` cache names. * * @alias workbox.core.cacheNames */ const cacheNames$1 = { get googleAnalytics() { return cacheNames.getGoogleAnalyticsName(); }, get precache() { return cacheNames.getPrecacheName(); }, get runtime() { return cacheNames.getRuntimeName(); } }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Modifies the default cache names used by the Workbox packages. * Cache names are generated as `--`. * * @param {Object} details * @param {Object} [details.prefix] The string to add to the beginning of * the precache and runtime cache names. * @param {Object} [details.suffix] The string to add to the end of * the precache and runtime cache names. * @param {Object} [details.precache] The cache name to use for precache * caching. * @param {Object} [details.runtime] The cache name to use for runtime caching. * @param {Object} [details.googleAnalytics] The cache name to use for * `workbox-google-analytics` caching. * * @alias workbox.core.setCacheNameDetails */ const setCacheNameDetails = details => { { Object.keys(details).forEach(key => { finalAssertExports.isType(details[key], 'string', { moduleName: 'workbox-core', funcName: 'setCacheNameDetails', paramName: `details.${key}` }); }); if ('precache' in details && details.precache.length === 0) { throw new WorkboxError('invalid-cache-name', { cacheNameId: 'precache', value: details.precache }); } if ('runtime' in details && details.runtime.length === 0) { throw new WorkboxError('invalid-cache-name', { cacheNameId: 'runtime', value: details.runtime }); } if ('googleAnalytics' in details && details.googleAnalytics.length === 0) { throw new WorkboxError('invalid-cache-name', { cacheNameId: 'googleAnalytics', value: details.googleAnalytics }); } } cacheNames.updateDetails(details); }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Force a service worker to become active, instead of waiting. This is * normally used in conjunction with `clientsClaim()`. * * @alias workbox.core.skipWaiting */ const skipWaiting = () => { // We need to explicitly call `self.skipWaiting()` here because we're // shadowing `skipWaiting` with this local function. addEventListener('install', () => self.skipWaiting()); }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ try { self.workbox.v = self.workbox.v || {}; } catch (errer) {} // NOOP exports._private = _private; exports.clientsClaim = clientsClaim; exports.cacheNames = cacheNames$1; exports.registerQuotaErrorCallback = registerQuotaErrorCallback; exports.setCacheNameDetails = setCacheNameDetails; exports.skipWaiting = skipWaiting; return exports; }({})); //# sourceMappingURL=workbox-core.dev.js.map ================================================ FILE: public/assets/libs/workbox/workbox-core.prod.js ================================================ this.workbox=this.workbox||{},this.workbox.core=function(e){"use strict";try{self["workbox:core:4.1.1"]&&_()}catch(e){}const t=(e,...t)=>{let n=e;return t.length>0&&(n+=` :: ${JSON.stringify(t)}`),n};class n extends Error{constructor(e,n){super(t(e,n)),this.name=e,this.details=n}}const s=new Set;class r{constructor(e,t,{onupgradeneeded:n,onversionchange:s=this.t}={}){this.s=e,this.i=t,this.o=n,this.t=s,this.l=null}get db(){return this.l}async open(){if(!this.l)return this.l=await new Promise((e,t)=>{let n=!1;setTimeout(()=>{n=!0,t(new Error("The open request was blocked and timed out"))},this.OPEN_TIMEOUT);const s=indexedDB.open(this.s,this.i);s.onerror=(()=>t(s.error)),s.onupgradeneeded=(e=>{n?(s.transaction.abort(),e.target.result.close()):this.o&&this.o(e)}),s.onsuccess=(({target:t})=>{const s=t.result;n?s.close():(s.onversionchange=this.t.bind(this),e(s))})}),this}async getKey(e,t){return(await this.getAllKeys(e,t,1))[0]}async getAll(e,t,n){return await this.getAllMatching(e,{query:t,count:n})}async getAllKeys(e,t,n){return(await this.getAllMatching(e,{query:t,count:n,includeKeys:!0})).map(({key:e})=>e)}async getAllMatching(e,{index:t,query:n=null,direction:s="next",count:r,includeKeys:a}={}){return await this.transaction([e],"readonly",(i,c)=>{const o=i.objectStore(e),l=t?o.index(t):o,u=[];l.openCursor(n,s).onsuccess=(({target:e})=>{const t=e.result;if(t){const{primaryKey:e,key:n,value:s}=t;u.push(a?{primaryKey:e,key:n,value:s}:s),r&&u.length>=r?c(u):t.continue()}else c(u)})})}async transaction(e,t,n){return await this.open(),await new Promise((s,r)=>{const a=this.l.transaction(e,t);a.onabort=(({target:e})=>r(e.error)),a.oncomplete=(()=>s()),n(a,e=>s(e))})}async u(e,t,n,...s){return await this.transaction([t],n,(n,r)=>{n.objectStore(t)[e](...s).onsuccess=(({target:e})=>{r(e.result)})})}t(){this.close()}close(){this.l&&(this.l.close(),this.l=null)}}r.prototype.OPEN_TIMEOUT=2e3;const a={readonly:["get","count","getKey","getAll","getAllKeys"],readwrite:["add","put","clear","delete"]};for(const[e,t]of Object.entries(a))for(const n of t)n in IDBObjectStore.prototype&&(r.prototype[n]=async function(t,...s){return await this.u(n,t,e,...s)});const i={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:self.registration.scope},c=e=>[i.prefix,e,i.suffix].filter(e=>e.length>0).join("-"),o={updateDetails:e=>{Object.keys(i).forEach(t=>{void 0!==e[t]&&(i[t]=e[t])})},getGoogleAnalyticsName:e=>e||c(i.googleAnalytics),getPrecacheName:e=>e||c(i.precache),getRuntimeName:e=>e||c(i.runtime)},l="cacheDidUpdate",u="cacheWillUpdate",h="cachedResponseWillBeUsed",w="fetchDidFail",f="fetchDidSucceed",p="requestWillFetch",d=(e,t)=>e.filter(e=>t in e),g=e=>{const t=new URL(e,location);return t.origin===location.origin?t.pathname:t.href},y=async({cacheName:e,request:t,event:n,matchOptions:s,plugins:r=[]})=>{const a=await caches.open(e);let i=await a.match(t,s);for(const a of r)h in a&&(i=await a[h].call(a,{cacheName:e,request:t,event:n,matchOptions:s,cachedResponse:i}));return i},m=async({request:e,response:t,event:n,plugins:s})=>{let r=t,a=!1;for(let t of s)if(u in t&&(a=!0,!(r=await t[u].call(t,{request:e,response:r,event:n}))))break;return a||(r=200===r.status?r:null),r||null},v={put:async({cacheName:e,request:t,response:r,event:a,plugins:i=[],matchOptions:c}={})=>{if(!r)throw new n("cache-put-with-no-response",{url:g(t.url)});let o=await m({request:t,response:r,event:a,plugins:i});if(!o)return;const u=await caches.open(e),h=d(i,l);let w=h.length>0?await y({cacheName:e,request:t,matchOptions:c}):null;try{await u.put(t,o)}catch(e){throw"QuotaExceededError"===e.name&&await async function(){for(const e of s)await e()}(),e}for(let n of h)await n[l].call(n,{cacheName:e,request:t,event:a,oldResponse:w,newResponse:o})},match:y},q={fetch:async({request:e,fetchOptions:t,event:s,plugins:r=[]})=>{if(s&&s.preloadResponse){const e=await s.preloadResponse;if(e)return e}"string"==typeof e&&(e=new Request(e));const a=d(r,w),i=a.length>0?e.clone():null;try{for(let t of r)p in t&&(e=await t[p].call(t,{request:e.clone(),event:s}))}catch(e){throw new n("plugin-error-request-will-fetch",{thrownError:e})}let c=e.clone();try{let n;n="navigate"===e.mode?await fetch(e):await fetch(e,t);for(const e of r)f in e&&(n=await e[f].call(e,{event:s,request:c,response:n}));return n}catch(e){for(const t of a)await t[w].call(t,{error:e,event:s,originalRequest:i.clone(),request:c.clone()});throw e}}};var b=Object.freeze({DBWrapper:r,deleteDatabase:async e=>{await new Promise((t,n)=>{const s=indexedDB.deleteDatabase(e);s.onerror=(({target:e})=>{n(e.error)}),s.onblocked=(()=>{n(new Error("Delete blocked"))}),s.onsuccess=(()=>{t()})})},WorkboxError:n,assert:null,cacheNames:o,cacheWrapper:v,fetchWrapper:q,getFriendlyURL:g,logger:null});const x={get googleAnalytics(){return o.getGoogleAnalyticsName()},get precache(){return o.getPrecacheName()},get runtime(){return o.getRuntimeName()}};try{self.workbox.v=self.workbox.v||{}}catch(e){}return e._private=b,e.clientsClaim=(()=>{addEventListener("activate",()=>clients.claim())}),e.cacheNames=x,e.registerQuotaErrorCallback=function(e){s.add(e)},e.setCacheNameDetails=(e=>{o.updateDetails(e)}),e.skipWaiting=(()=>{addEventListener("install",()=>self.skipWaiting())}),e}({}); //# sourceMappingURL=workbox-core.prod.js.map ================================================ FILE: public/assets/libs/workbox/workbox-expiration.dev.js ================================================ this.workbox = this.workbox || {}; this.workbox.expiration = (function (exports, DBWrapper_mjs, deleteDatabase_mjs, WorkboxError_mjs, assert_mjs, logger_mjs, cacheNames_mjs, getFriendlyURL_mjs, registerQuotaErrorCallback_mjs) { 'use strict'; try { self['workbox:expiration:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const DB_NAME = 'workbox-expiration'; const OBJECT_STORE_NAME = 'cache-entries'; const normalizeURL = unNormalizedUrl => { const url = new URL(unNormalizedUrl, location); url.hash = ''; return url.href; }; /** * Returns the timestamp model. * * @private */ class CacheTimestampsModel { /** * * @param {string} cacheName * * @private */ constructor(cacheName) { this._cacheName = cacheName; this._db = new DBWrapper_mjs.DBWrapper(DB_NAME, 1, { onupgradeneeded: event => this._handleUpgrade(event) }); } /** * Should perform an upgrade of indexedDB. * * @param {Event} event * * @private */ _handleUpgrade(event) { const db = event.target.result; // TODO(philipwalton): EdgeHTML doesn't support arrays as a keyPath, so we // have to use the `id` keyPath here and create our own values (a // concatenation of `url + cacheName`) instead of simply using // `keyPath: ['url', 'cacheName']`, which is supported in other browsers. const objStore = db.createObjectStore(OBJECT_STORE_NAME, { keyPath: 'id' }); // TODO(philipwalton): once we don't have to support EdgeHTML, we can // create a single index with the keyPath `['cacheName', 'timestamp']` // instead of doing both these indexes. objStore.createIndex('cacheName', 'cacheName', { unique: false }); objStore.createIndex('timestamp', 'timestamp', { unique: false }); // Previous versions of `workbox-expiration` used `this._cacheName` // as the IDBDatabase name. deleteDatabase_mjs.deleteDatabase(this._cacheName); } /** * @param {string} url * @param {number} timestamp * * @private */ async setTimestamp(url, timestamp) { url = normalizeURL(url); await this._db.put(OBJECT_STORE_NAME, { url, timestamp, cacheName: this._cacheName, // Creating an ID from the URL and cache name won't be necessary once // Edge switches to Chromium and all browsers we support work with // array keyPaths. id: this._getId(url) }); } /** * Returns the timestamp stored for a given URL. * * @param {string} url * @return {number} * * @private */ async getTimestamp(url) { const entry = await this._db.get(OBJECT_STORE_NAME, this._getId(url)); return entry.timestamp; } /** * Iterates through all the entries in the object store (from newest to * oldest) and removes entries once either `maxCount` is reached or the * entry's timestamp is less than `minTimestamp`. * * @param {number} minTimestamp * @param {number} maxCount * * @private */ async expireEntries(minTimestamp, maxCount) { return await this._db.transaction(OBJECT_STORE_NAME, 'readwrite', (txn, done) => { const store = txn.objectStore(OBJECT_STORE_NAME); const entriesDeleted = []; let entriesNotDeletedCount = 0; store.index('timestamp').openCursor(null, 'prev').onsuccess = ({ target }) => { const cursor = target.result; if (cursor) { const result = cursor.value; // TODO(philipwalton): once we can use a multi-key index, we // won't have to check `cacheName` here. if (result.cacheName === this._cacheName) { // Delete an entry if it's older than the max age or // if we already have the max number allowed. if (minTimestamp && result.timestamp < minTimestamp || maxCount && entriesNotDeletedCount >= maxCount) { cursor.delete(); // We only need to return the URL, not the whole entry. entriesDeleted.push(cursor.value.url); } else { entriesNotDeletedCount++; } } cursor.continue(); } else { done(entriesDeleted); } }; }); } /** * Takes a URL and returns an ID that will be unique in the object store. * * @param {string} url * @return {string} */ _getId(url) { // Creating an ID from the URL and cache name won't be necessary once // Edge switches to Chromium and all browsers we support work with // array keyPaths. return this._cacheName + '|' + normalizeURL(url); } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * The `CacheExpiration` class allows you define an expiration and / or * limit on the number of responses stored in a * [`Cache`](https://developer.mozilla.org/en-US/docs/Web/API/Cache). * * @memberof workbox.expiration */ class CacheExpiration { /** * To construct a new CacheExpiration instance you must provide at least * one of the `config` properties. * * @param {string} cacheName Name of the cache to apply restrictions to. * @param {Object} config * @param {number} [config.maxEntries] The maximum number of entries to cache. * Entries used the least will be removed as the maximum is reached. * @param {number} [config.maxAgeSeconds] The maximum age of an entry before * it's treated as stale and removed. */ constructor(cacheName, config = {}) { { assert_mjs.assert.isType(cacheName, 'string', { moduleName: 'workbox-expiration', className: 'CacheExpiration', funcName: 'constructor', paramName: 'cacheName' }); if (!(config.maxEntries || config.maxAgeSeconds)) { throw new WorkboxError_mjs.WorkboxError('max-entries-or-age-required', { moduleName: 'workbox-expiration', className: 'CacheExpiration', funcName: 'constructor' }); } if (config.maxEntries) { assert_mjs.assert.isType(config.maxEntries, 'number', { moduleName: 'workbox-expiration', className: 'CacheExpiration', funcName: 'constructor', paramName: 'config.maxEntries' }); // TODO: Assert is positive } if (config.maxAgeSeconds) { assert_mjs.assert.isType(config.maxAgeSeconds, 'number', { moduleName: 'workbox-expiration', className: 'CacheExpiration', funcName: 'constructor', paramName: 'config.maxAgeSeconds' }); // TODO: Assert is positive } } this._isRunning = false; this._rerunRequested = false; this._maxEntries = config.maxEntries; this._maxAgeSeconds = config.maxAgeSeconds; this._cacheName = cacheName; this._timestampModel = new CacheTimestampsModel(cacheName); } /** * Expires entries for the given cache and given criteria. */ async expireEntries() { if (this._isRunning) { this._rerunRequested = true; return; } this._isRunning = true; const minTimestamp = this._maxAgeSeconds ? Date.now() - this._maxAgeSeconds * 1000 : undefined; const urlsExpired = await this._timestampModel.expireEntries(minTimestamp, this._maxEntries); // Delete URLs from the cache const cache = await caches.open(this._cacheName); for (const url of urlsExpired) { await cache.delete(url); } { if (urlsExpired.length > 0) { logger_mjs.logger.groupCollapsed(`Expired ${urlsExpired.length} ` + `${urlsExpired.length === 1 ? 'entry' : 'entries'} and removed ` + `${urlsExpired.length === 1 ? 'it' : 'them'} from the ` + `'${this._cacheName}' cache.`); logger_mjs.logger.log(`Expired the following ${urlsExpired.length === 1 ? 'URL' : 'URLs'}:`); urlsExpired.forEach(url => logger_mjs.logger.log(` ${url}`)); logger_mjs.logger.groupEnd(); } else { logger_mjs.logger.debug(`Cache expiration ran and found no entries to remove.`); } } this._isRunning = false; if (this._rerunRequested) { this._rerunRequested = false; this.expireEntries(); } } /** * Update the timestamp for the given URL. This ensures the when * removing entries based on maximum entries, most recently used * is accurate or when expiring, the timestamp is up-to-date. * * @param {string} url */ async updateTimestamp(url) { { assert_mjs.assert.isType(url, 'string', { moduleName: 'workbox-expiration', className: 'CacheExpiration', funcName: 'updateTimestamp', paramName: 'url' }); } await this._timestampModel.setTimestamp(url, Date.now()); } /** * Can be used to check if a URL has expired or not before it's used. * * This requires a look up from IndexedDB, so can be slow. * * Note: This method will not remove the cached entry, call * `expireEntries()` to remove indexedDB and Cache entries. * * @param {string} url * @return {boolean} */ async isURLExpired(url) { { if (!this._maxAgeSeconds) { throw new WorkboxError_mjs.WorkboxError(`expired-test-without-max-age`, { methodName: 'isURLExpired', paramName: 'maxAgeSeconds' }); } } const timestamp = await this._timestampModel.getTimestamp(url); const expireOlderThan = Date.now() - this._maxAgeSeconds * 1000; return timestamp < expireOlderThan; } /** * Removes the IndexedDB object store used to keep track of cache expiration * metadata. */ async delete() { // Make sure we don't attempt another rerun if we're called in the middle of // a cache expiration. this._rerunRequested = false; await this._timestampModel.expireEntries(Infinity); // Expires all. } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * This plugin can be used in the Workbox APIs to regularly enforce a * limit on the age and / or the number of cached requests. * * Whenever a cached request is used or updated, this plugin will look * at the used Cache and remove any old or extra requests. * * When using `maxAgeSeconds`, requests may be used *once* after expiring * because the expiration clean up will not have occurred until *after* the * cached request has been used. If the request has a "Date" header, then * a light weight expiration check is performed and the request will not be * used immediately. * * When using `maxEntries`, the last request to be used will be the request * that is removed from the Cache. * * @memberof workbox.expiration */ class Plugin { /** * @param {Object} config * @param {number} [config.maxEntries] The maximum number of entries to cache. * Entries used the least will be removed as the maximum is reached. * @param {number} [config.maxAgeSeconds] The maximum age of an entry before * it's treated as stale and removed. * @param {boolean} [config.purgeOnQuotaError] Whether to opt this cache in to * automatic deletion if the available storage quota has been exceeded. */ constructor(config = {}) { { if (!(config.maxEntries || config.maxAgeSeconds)) { throw new WorkboxError_mjs.WorkboxError('max-entries-or-age-required', { moduleName: 'workbox-expiration', className: 'Plugin', funcName: 'constructor' }); } if (config.maxEntries) { assert_mjs.assert.isType(config.maxEntries, 'number', { moduleName: 'workbox-expiration', className: 'Plugin', funcName: 'constructor', paramName: 'config.maxEntries' }); } if (config.maxAgeSeconds) { assert_mjs.assert.isType(config.maxAgeSeconds, 'number', { moduleName: 'workbox-expiration', className: 'Plugin', funcName: 'constructor', paramName: 'config.maxAgeSeconds' }); } } this._config = config; this._maxAgeSeconds = config.maxAgeSeconds; this._cacheExpirations = new Map(); if (config.purgeOnQuotaError) { registerQuotaErrorCallback_mjs.registerQuotaErrorCallback(() => this.deleteCacheAndMetadata()); } } /** * A simple helper method to return a CacheExpiration instance for a given * cache name. * * @param {string} cacheName * @return {CacheExpiration} * * @private */ _getCacheExpiration(cacheName) { if (cacheName === cacheNames_mjs.cacheNames.getRuntimeName()) { throw new WorkboxError_mjs.WorkboxError('expire-custom-caches-only'); } let cacheExpiration = this._cacheExpirations.get(cacheName); if (!cacheExpiration) { cacheExpiration = new CacheExpiration(cacheName, this._config); this._cacheExpirations.set(cacheName, cacheExpiration); } return cacheExpiration; } /** * A "lifecycle" callback that will be triggered automatically by the * `workbox.strategies` handlers when a `Response` is about to be returned * from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to * the handler. It allows the `Response` to be inspected for freshness and * prevents it from being used if the `Response`'s `Date` header value is * older than the configured `maxAgeSeconds`. * * @param {Object} options * @param {string} options.cacheName Name of the cache the response is in. * @param {Response} options.cachedResponse The `Response` object that's been * read from a cache and whose freshness should be checked. * @return {Response} Either the `cachedResponse`, if it's * fresh, or `null` if the `Response` is older than `maxAgeSeconds`. * * @private */ cachedResponseWillBeUsed({ event, request, cacheName, cachedResponse }) { if (!cachedResponse) { return null; } let isFresh = this._isResponseDateFresh(cachedResponse); // Expire entries to ensure that even if the expiration date has // expired, it'll only be used once. const cacheExpiration = this._getCacheExpiration(cacheName); cacheExpiration.expireEntries(); // Update the metadata for the request URL to the current timestamp, // but don't `await` it as we don't want to block the response. const updateTimestampDone = cacheExpiration.updateTimestamp(request.url); if (event) { try { event.waitUntil(updateTimestampDone); } catch (error) { { logger_mjs.logger.warn(`Unable to ensure service worker stays alive when ` + `updating cache entry for '${getFriendlyURL_mjs.getFriendlyURL(event.request.url)}'.`); } } } return isFresh ? cachedResponse : null; } /** * @param {Response} cachedResponse * @return {boolean} * * @private */ _isResponseDateFresh(cachedResponse) { if (!this._maxAgeSeconds) { // We aren't expiring by age, so return true, it's fresh return true; } // Check if the 'date' header will suffice a quick expiration check. // See https://github.com/GoogleChromeLabs/sw-toolbox/issues/164 for // discussion. const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse); if (dateHeaderTimestamp === null) { // Unable to parse date, so assume it's fresh. return true; } // If we have a valid headerTime, then our response is fresh iff the // headerTime plus maxAgeSeconds is greater than the current time. const now = Date.now(); return dateHeaderTimestamp >= now - this._maxAgeSeconds * 1000; } /** * This method will extract the data header and parse it into a useful * value. * * @param {Response} cachedResponse * @return {number} * * @private */ _getDateHeaderTimestamp(cachedResponse) { if (!cachedResponse.headers.has('date')) { return null; } const dateHeader = cachedResponse.headers.get('date'); const parsedDate = new Date(dateHeader); const headerTime = parsedDate.getTime(); // If the Date header was invalid for some reason, parsedDate.getTime() // will return NaN. if (isNaN(headerTime)) { return null; } return headerTime; } /** * A "lifecycle" callback that will be triggered automatically by the * `workbox.strategies` handlers when an entry is added to a cache. * * @param {Object} options * @param {string} options.cacheName Name of the cache that was updated. * @param {string} options.request The Request for the cached entry. * * @private */ async cacheDidUpdate({ cacheName, request }) { { assert_mjs.assert.isType(cacheName, 'string', { moduleName: 'workbox-expiration', className: 'Plugin', funcName: 'cacheDidUpdate', paramName: 'cacheName' }); assert_mjs.assert.isInstance(request, Request, { moduleName: 'workbox-expiration', className: 'Plugin', funcName: 'cacheDidUpdate', paramName: 'request' }); } const cacheExpiration = this._getCacheExpiration(cacheName); await cacheExpiration.updateTimestamp(request.url); await cacheExpiration.expireEntries(); } /** * This is a helper method that performs two operations: * * - Deletes *all* the underlying Cache instances associated with this plugin * instance, by calling caches.delete() on your behalf. * - Deletes the metadata from IndexedDB used to keep track of expiration * details for each Cache instance. * * When using cache expiration, calling this method is preferable to calling * `caches.delete()` directly, since this will ensure that the IndexedDB * metadata is also cleanly removed and open IndexedDB instances are deleted. * * Note that if you're *not* using cache expiration for a given cache, calling * `caches.delete()` and passing in the cache's name should be sufficient. * There is no Workbox-specific method needed for cleanup in that case. */ async deleteCacheAndMetadata() { // Do this one at a time instead of all at once via `Promise.all()` to // reduce the chance of inconsistency if a promise rejects. for (const [cacheName, cacheExpiration] of this._cacheExpirations) { await caches.delete(cacheName); await cacheExpiration.delete(); } // Reset this._cacheExpirations to its initial state. this._cacheExpirations = new Map(); } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ exports.CacheExpiration = CacheExpiration; exports.Plugin = Plugin; return exports; }({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core)); //# sourceMappingURL=workbox-expiration.dev.js.map ================================================ FILE: public/assets/libs/workbox/workbox-expiration.prod.js ================================================ this.workbox=this.workbox||{},this.workbox.expiration=function(t,e,s,i,a,h){"use strict";try{self["workbox:expiration:4.1.1"]&&_()}catch(t){}const n="workbox-expiration",c="cache-entries",r=t=>{const e=new URL(t,location);return e.hash="",e.href};class o{constructor(t){this.t=t,this.s=new e.DBWrapper(n,1,{onupgradeneeded:t=>this.i(t)})}i(t){const e=t.target.result.createObjectStore(c,{keyPath:"id"});e.createIndex("cacheName","cacheName",{unique:!1}),e.createIndex("timestamp","timestamp",{unique:!1}),s.deleteDatabase(this.t)}async setTimestamp(t,e){t=r(t),await this.s.put(c,{url:t,timestamp:e,cacheName:this.t,id:this.h(t)})}async getTimestamp(t){return(await this.s.get(c,this.h(t))).timestamp}async expireEntries(t,e){return await this.s.transaction(c,"readwrite",(s,i)=>{const a=s.objectStore(c),h=[];let n=0;a.index("timestamp").openCursor(null,"prev").onsuccess=(({target:s})=>{const a=s.result;if(a){const s=a.value;s.cacheName===this.t&&(t&&s.timestamp=e?(a.delete(),h.push(a.value.url)):n++),a.continue()}else i(h)})})}h(t){return this.t+"|"+r(t)}}class u{constructor(t,e={}){this.o=!1,this.u=!1,this.l=e.maxEntries,this.p=e.maxAgeSeconds,this.t=t,this.m=new o(t)}async expireEntries(){if(this.o)return void(this.u=!0);this.o=!0;const t=this.p?Date.now()-1e3*this.p:void 0,e=await this.m.expireEntries(t,this.l),s=await caches.open(this.t);for(const t of e)await s.delete(t);this.o=!1,this.u&&(this.u=!1,this.expireEntries())}async updateTimestamp(t){await this.m.setTimestamp(t,Date.now())}async isURLExpired(t){return await this.m.getTimestamp(t)this.deleteCacheAndMetadata())}k(t){if(t===a.cacheNames.getRuntimeName())throw new i.WorkboxError("expire-custom-caches-only");let e=this.g.get(t);return e||(e=new u(t,this.D),this.g.set(t,e)),e}cachedResponseWillBeUsed({event:t,request:e,cacheName:s,cachedResponse:i}){if(!i)return null;let a=this.N(i);const h=this.k(s);h.expireEntries();const n=h.updateTimestamp(e.url);if(t)try{t.waitUntil(n)}catch(t){}return a?i:null}N(t){if(!this.p)return!0;const e=this._(t);return null===e||e>=Date.now()-1e3*this.p}_(t){if(!t.headers.has("date"))return null;const e=t.headers.get("date"),s=new Date(e).getTime();return isNaN(s)?null:s}async cacheDidUpdate({cacheName:t,request:e}){const s=this.k(t);await s.updateTimestamp(e.url),await s.expireEntries()}async deleteCacheAndMetadata(){for(const[t,e]of this.g)await caches.delete(t),await e.delete();this.g=new Map}},t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private,workbox.core); //# sourceMappingURL=workbox-expiration.prod.js.map ================================================ FILE: public/assets/libs/workbox/workbox-navigation-preload.dev.js ================================================ this.workbox = this.workbox || {}; this.workbox.navigationPreload = (function (exports, logger_mjs) { 'use strict'; try { self['workbox:navigation-preload:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * @return {boolean} Whether or not the current browser supports enabling * navigation preload. * * @memberof workbox.navigationPreload */ function isSupported() { return Boolean(self.registration && self.registration.navigationPreload); } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * If the browser supports Navigation Preload, then this will disable it. * * @memberof workbox.navigationPreload */ function disable() { if (isSupported()) { self.addEventListener('activate', event => { event.waitUntil(self.registration.navigationPreload.disable().then(() => { { logger_mjs.logger.log(`Navigation preload is disabled.`); } })); }); } else { { logger_mjs.logger.log(`Navigation preload is not supported in this browser.`); } } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * If the browser supports Navigation Preload, then this will enable it. * * @param {string} [headerValue] Optionally, allows developers to * [override](https://developers.google.com/web/updates/2017/02/navigation-preload#changing_the_header) * the value of the `Service-Worker-Navigation-Preload` header which will be * sent to the server when making the navigation request. * * @memberof workbox.navigationPreload */ function enable(headerValue) { if (isSupported()) { self.addEventListener('activate', event => { event.waitUntil(self.registration.navigationPreload.enable().then(() => { // Defaults to Service-Worker-Navigation-Preload: true if not set. if (headerValue) { self.registration.navigationPreload.setHeaderValue(headerValue); } { logger_mjs.logger.log(`Navigation preload is enabled.`); } })); }); } else { { logger_mjs.logger.log(`Navigation preload is not supported in this browser.`); } } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ exports.disable = disable; exports.enable = enable; exports.isSupported = isSupported; return exports; }({}, workbox.core._private)); //# sourceMappingURL=workbox-navigation-preload.dev.js.map ================================================ FILE: public/assets/libs/workbox/workbox-navigation-preload.prod.js ================================================ this.workbox=this.workbox||{},this.workbox.navigationPreload=function(t){"use strict";try{self["workbox:navigation-preload:4.1.1"]&&_()}catch(t){}function e(){return Boolean(self.registration&&self.registration.navigationPreload)}return t.disable=function(){e()&&self.addEventListener("activate",t=>{t.waitUntil(self.registration.navigationPreload.disable().then(()=>{}))})},t.enable=function(t){e()&&self.addEventListener("activate",e=>{e.waitUntil(self.registration.navigationPreload.enable().then(()=>{t&&self.registration.navigationPreload.setHeaderValue(t)}))})},t.isSupported=e,t}({}); //# sourceMappingURL=workbox-navigation-preload.prod.js.map ================================================ FILE: public/assets/libs/workbox/workbox-offline-ga.dev.js ================================================ this.workbox = this.workbox || {}; this.workbox.googleAnalytics = (function (exports, Plugin_mjs, cacheNames_mjs, getFriendlyURL_mjs, logger_mjs, Route_mjs, Router_mjs, NetworkFirst_mjs, NetworkOnly_mjs) { 'use strict'; try { self['workbox:google-analytics:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const QUEUE_NAME = 'workbox-google-analytics'; const MAX_RETENTION_TIME = 60 * 48; // Two days in minutes const GOOGLE_ANALYTICS_HOST = 'www.google-analytics.com'; const GTM_HOST = 'www.googletagmanager.com'; const ANALYTICS_JS_PATH = '/analytics.js'; const GTAG_JS_PATH = '/gtag/js'; const GTM_JS_PATH = '/gtm.js'; // endpoints. Most of the time the default path (/collect) is used, but // occasionally an experimental endpoint is used when testing new features, // (e.g. /r/collect or /j/collect) const COLLECT_PATHS_REGEX = /^\/(\w+\/)?collect/; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Creates the requestWillDequeue callback to be used with the background * sync queue plugin. The callback takes the failed request and adds the * `qt` param based on the current time, as well as applies any other * user-defined hit modifications. * * @param {Object} config See workbox.googleAnalytics.initialize. * @return {Function} The requestWillDequeu callback function. * * @private */ const createOnSyncCallback = config => { return async ({ queue }) => { let entry; while (entry = await queue.shiftRequest()) { const { request, timestamp } = entry; const url = new URL(request.url); try { // Measurement protocol requests can set their payload parameters in // either the URL query string (for GET requests) or the POST body. const params = request.method === 'POST' ? new URLSearchParams((await request.clone().text())) : url.searchParams; // Calculate the qt param, accounting for the fact that an existing // qt param may be present and should be updated rather than replaced. const originalHitTime = timestamp - (Number(params.get('qt')) || 0); const queueTime = Date.now() - originalHitTime; // Set the qt param prior to applying hitFilter or parameterOverrides. params.set('qt', queueTime); // Apply `paramterOverrideds`, if set. if (config.parameterOverrides) { for (const param of Object.keys(config.parameterOverrides)) { const value = config.parameterOverrides[param]; params.set(param, value); } } // Apply `hitFilter`, if set. if (typeof config.hitFilter === 'function') { config.hitFilter.call(null, params); } // Retry the fetch. Ignore URL search params from the URL as they're // now in the post body. await fetch(new Request(url.origin + url.pathname, { body: params.toString(), method: 'POST', mode: 'cors', credentials: 'omit', headers: { 'Content-Type': 'text/plain' } })); { logger_mjs.logger.log(`Request for '${getFriendlyURL_mjs.getFriendlyURL(url.href)}'` + `has been replayed`); } } catch (err) { await queue.unshiftRequest(entry); { logger_mjs.logger.log(`Request for '${getFriendlyURL_mjs.getFriendlyURL(url.href)}'` + `failed to replay, putting it back in the queue.`); } throw err; } } { logger_mjs.logger.log(`All Google Analytics request successfully replayed; ` + `the queue is now empty!`); } }; }; /** * Creates GET and POST routes to catch failed Measurement Protocol hits. * * @param {Plugin} queuePlugin * @return {Array} The created routes. * * @private */ const createCollectRoutes = queuePlugin => { const match = ({ url }) => url.hostname === GOOGLE_ANALYTICS_HOST && COLLECT_PATHS_REGEX.test(url.pathname); const handler = new NetworkOnly_mjs.NetworkOnly({ plugins: [queuePlugin] }); return [new Route_mjs.Route(match, handler, 'GET'), new Route_mjs.Route(match, handler, 'POST')]; }; /** * Creates a route with a network first strategy for the analytics.js script. * * @param {string} cacheName * @return {Route} The created route. * * @private */ const createAnalyticsJsRoute = cacheName => { const match = ({ url }) => url.hostname === GOOGLE_ANALYTICS_HOST && url.pathname === ANALYTICS_JS_PATH; const handler = new NetworkFirst_mjs.NetworkFirst({ cacheName }); return new Route_mjs.Route(match, handler, 'GET'); }; /** * Creates a route with a network first strategy for the gtag.js script. * * @param {string} cacheName * @return {Route} The created route. * * @private */ const createGtagJsRoute = cacheName => { const match = ({ url }) => url.hostname === GTM_HOST && url.pathname === GTAG_JS_PATH; const handler = new NetworkFirst_mjs.NetworkFirst({ cacheName }); return new Route_mjs.Route(match, handler, 'GET'); }; /** * Creates a route with a network first strategy for the gtm.js script. * * @param {string} cacheName * @return {Route} The created route. * * @private */ const createGtmJsRoute = cacheName => { const match = ({ url }) => url.hostname === GTM_HOST && url.pathname === GTM_JS_PATH; const handler = new NetworkFirst_mjs.NetworkFirst({ cacheName }); return new Route_mjs.Route(match, handler, 'GET'); }; /** * @param {Object=} [options] * @param {Object} [options.cacheName] The cache name to store and retrieve * analytics.js. Defaults to the cache names provided by `workbox-core`. * @param {Object} [options.parameterOverrides] * [Measurement Protocol parameters](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters), * expressed as key/value pairs, to be added to replayed Google Analytics * requests. This can be used to, e.g., set a custom dimension indicating * that the request was replayed. * @param {Function} [options.hitFilter] A function that allows you to modify * the hit parameters prior to replaying * the hit. The function is invoked with the original hit's URLSearchParams * object as its only argument. * * @memberof workbox.googleAnalytics */ const initialize = (options = {}) => { const cacheName = cacheNames_mjs.cacheNames.getGoogleAnalyticsName(options.cacheName); const queuePlugin = new Plugin_mjs.Plugin(QUEUE_NAME, { maxRetentionTime: MAX_RETENTION_TIME, onSync: createOnSyncCallback(options) }); const routes = [createGtmJsRoute(cacheName), createAnalyticsJsRoute(cacheName), createGtagJsRoute(cacheName), ...createCollectRoutes(queuePlugin)]; const router = new Router_mjs.Router(); for (const route of routes) { router.registerRoute(route); } router.addFetchListener(); }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ exports.initialize = initialize; return exports; }({}, workbox.backgroundSync, workbox.core._private, workbox.core._private, workbox.core._private, workbox.routing, workbox.routing, workbox.strategies, workbox.strategies)); //# sourceMappingURL=workbox-offline-ga.dev.js.map ================================================ FILE: public/assets/libs/workbox/workbox-offline-ga.prod.js ================================================ this.workbox=this.workbox||{},this.workbox.googleAnalytics=function(e,t,o,n,a,c,w){"use strict";try{self["workbox:google-analytics:4.1.1"]&&_()}catch(e){}const r=/^\/(\w+\/)?collect/,s=e=>async({queue:t})=>{let o;for(;o=await t.shiftRequest();){const{request:n,timestamp:a}=o,c=new URL(n.url);try{const w="POST"===n.method?new URLSearchParams(await n.clone().text()):c.searchParams,r=a-(Number(w.get("qt"))||0),s=Date.now()-r;if(w.set("qt",s),e.parameterOverrides)for(const t of Object.keys(e.parameterOverrides)){const o=e.parameterOverrides[t];w.set(t,o)}"function"==typeof e.hitFilter&&e.hitFilter.call(null,w),await fetch(new Request(c.origin+c.pathname,{body:w.toString(),method:"POST",mode:"cors",credentials:"omit",headers:{"Content-Type":"text/plain"}}))}catch(e){throw await t.unshiftRequest(o),e}}},i=e=>{const t=({url:e})=>"www.google-analytics.com"===e.hostname&&r.test(e.pathname),o=new w.NetworkOnly({plugins:[e]});return[new n.Route(t,o,"GET"),new n.Route(t,o,"POST")]},l=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.google-analytics.com"===e.hostname&&"/analytics.js"===e.pathname,t,"GET")},m=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.googletagmanager.com"===e.hostname&&"/gtag/js"===e.pathname,t,"GET")},u=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.googletagmanager.com"===e.hostname&&"/gtm.js"===e.pathname,t,"GET")};return e.initialize=((e={})=>{const n=o.cacheNames.getGoogleAnalyticsName(e.cacheName),c=new t.Plugin("workbox-google-analytics",{maxRetentionTime:2880,onSync:s(e)}),w=[u(n),l(n),m(n),...i(c)],r=new a.Router;for(const e of w)r.registerRoute(e);r.addFetchListener()}),e}({},workbox.backgroundSync,workbox.core._private,workbox.routing,workbox.routing,workbox.strategies,workbox.strategies); //# sourceMappingURL=workbox-offline-ga.prod.js.map ================================================ FILE: public/assets/libs/workbox/workbox-precaching.dev.js ================================================ this.workbox = this.workbox || {}; this.workbox.precaching = (function (exports, assert_mjs, cacheNames_mjs, getFriendlyURL_mjs, logger_mjs, cacheWrapper_mjs, fetchWrapper_mjs, WorkboxError_mjs) { 'use strict'; try { self['workbox:precaching:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const plugins = []; const precachePlugins = { /* * @return {Array} * @private */ get() { return plugins; }, /* * @param {Array} newPlugins * @private */ add(newPlugins) { plugins.push(...newPlugins); } }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Adds plugins to precaching. * * @param {Array} newPlugins * * @alias workbox.precaching.addPlugins */ const addPlugins = newPlugins => { precachePlugins.add(newPlugins); }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * @param {Response} response * @return {Response} * * @private * @memberof module:workbox-precaching */ async function cleanRedirect(response) { const clonedResponse = response.clone(); // Not all browsers support the Response.body stream, so fall back // to reading the entire body into memory as a blob. const bodyPromise = 'body' in clonedResponse ? Promise.resolve(clonedResponse.body) : clonedResponse.blob(); const body = await bodyPromise; // new Response() is happy when passed either a stream or a Blob. return new Response(body, { headers: clonedResponse.headers, status: clonedResponse.status, statusText: clonedResponse.statusText }); } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const REVISION_SEARCH_PARAM = '__WB_REVISION__'; /** * Converts a manifest entry into a versioned URL suitable for precaching. * * @param {Object} entry * @return {string} A URL with versioning info. * * @private * @memberof module:workbox-precaching */ function createCacheKey(entry) { if (!entry) { throw new WorkboxError_mjs.WorkboxError('add-to-cache-list-unexpected-type', { entry }); } // If a precache manifest entry is a string, it's assumed to be a versioned // URL, like '/app.abcd1234.js'. Return as-is. if (typeof entry === 'string') { const urlObject = new URL(entry, location); return { cacheKey: urlObject.href, url: urlObject.href }; } const { revision, url } = entry; if (!url) { throw new WorkboxError_mjs.WorkboxError('add-to-cache-list-unexpected-type', { entry }); } // If there's just a URL and no revision, then it's also assumed to be a // versioned URL. if (!revision) { const urlObject = new URL(url, location); return { cacheKey: urlObject.href, url: urlObject.href }; } // Otherwise, construct a properly versioned URL using the custom Workbox // search parameter along with the revision info. const originalURL = new URL(url, location); const cacheKeyURL = new URL(url, location); cacheKeyURL.searchParams.set(REVISION_SEARCH_PARAM, revision); return { cacheKey: cacheKeyURL.href, url: originalURL.href }; } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const logGroup = (groupTitle, deletedURLs) => { logger_mjs.logger.groupCollapsed(groupTitle); for (const url of deletedURLs) { logger_mjs.logger.log(url); } logger_mjs.logger.groupEnd(); }; /** * @param {Array} deletedURLs * * @private * @memberof module:workbox-precaching */ function printCleanupDetails(deletedURLs) { const deletionCount = deletedURLs.length; if (deletionCount > 0) { logger_mjs.logger.groupCollapsed(`During precaching cleanup, ` + `${deletionCount} cached ` + `request${deletionCount === 1 ? ' was' : 's were'} deleted.`); logGroup('Deleted Cache Requests', deletedURLs); logger_mjs.logger.groupEnd(); } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * @param {string} groupTitle * @param {Array} urls * * @private */ function _nestedGroup(groupTitle, urls) { if (urls.length === 0) { return; } logger_mjs.logger.groupCollapsed(groupTitle); for (const url of urls) { logger_mjs.logger.log(url); } logger_mjs.logger.groupEnd(); } /** * @param {Array} urlsToPrecache * @param {Array} urlsAlreadyPrecached * * @private * @memberof module:workbox-precaching */ function printInstallDetails(urlsToPrecache, urlsAlreadyPrecached) { const precachedCount = urlsToPrecache.length; const alreadyPrecachedCount = urlsAlreadyPrecached.length; if (precachedCount || alreadyPrecachedCount) { let message = `Precaching ${precachedCount} file${precachedCount === 1 ? '' : 's'}.`; if (alreadyPrecachedCount > 0) { message += ` ${alreadyPrecachedCount} ` + `file${alreadyPrecachedCount === 1 ? ' is' : 's are'} already cached.`; } logger_mjs.logger.groupCollapsed(message); _nestedGroup(`View newly precached URLs.`, urlsToPrecache); _nestedGroup(`View previously precached URLs.`, urlsAlreadyPrecached); logger_mjs.logger.groupEnd(); } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Performs efficient precaching of assets. * * @memberof module:workbox-precaching */ class PrecacheController { /** * Create a new PrecacheController. * * @param {string} [cacheName] An optional name for the cache, to override * the default precache name. */ constructor(cacheName) { this._cacheName = cacheNames_mjs.cacheNames.getPrecacheName(cacheName); this._urlsToCacheKeys = new Map(); } /** * This method will add items to the precache list, removing duplicates * and ensuring the information is valid. * * @param { * Array * } entries Array of entries to precache. */ addToCacheList(entries) { { assert_mjs.assert.isArray(entries, { moduleName: 'workbox-precaching', className: 'PrecacheController', funcName: 'addToCacheList', paramName: 'entries' }); } for (const entry of entries) { const { cacheKey, url } = createCacheKey(entry); if (this._urlsToCacheKeys.has(url) && this._urlsToCacheKeys.get(url) !== cacheKey) { throw new WorkboxError_mjs.WorkboxError('add-to-cache-list-conflicting-entries', { firstEntry: this._urlsToCacheKeys.get(url), secondEntry: cacheKey }); } this._urlsToCacheKeys.set(url, cacheKey); } } /** * Precaches new and updated assets. Call this method from the service worker * install event. * * @param {Object} options * @param {Event} [options.event] The install event (if needed). * @param {Array} [options.plugins] Plugins to be used for fetching * and caching during install. * @return {Promise} */ async install({ event, plugins } = {}) { { if (plugins) { assert_mjs.assert.isArray(plugins, { moduleName: 'workbox-precaching', className: 'PrecacheController', funcName: 'install', paramName: 'plugins' }); } } const urlsToPrecache = []; const urlsAlreadyPrecached = []; const cache = await caches.open(this._cacheName); const alreadyCachedRequests = await cache.keys(); const alreadyCachedURLs = new Set(alreadyCachedRequests.map(request => request.url)); for (const cacheKey of this._urlsToCacheKeys.values()) { if (alreadyCachedURLs.has(cacheKey)) { urlsAlreadyPrecached.push(cacheKey); } else { urlsToPrecache.push(cacheKey); } } const precacheRequests = urlsToPrecache.map(url => { return this._addURLToCache({ event, plugins, url }); }); await Promise.all(precacheRequests); { printInstallDetails(urlsToPrecache, urlsAlreadyPrecached); } return { updatedURLs: urlsToPrecache, notUpdatedURLs: urlsAlreadyPrecached }; } /** * Deletes assets that are no longer present in the current precache manifest. * Call this method from the service worker activate event. * * @return {Promise} */ async activate() { const cache = await caches.open(this._cacheName); const currentlyCachedRequests = await cache.keys(); const expectedCacheKeys = new Set(this._urlsToCacheKeys.values()); const deletedURLs = []; for (const request of currentlyCachedRequests) { if (!expectedCacheKeys.has(request.url)) { await cache.delete(request); deletedURLs.push(request.url); } } { printCleanupDetails(deletedURLs); } return { deletedURLs }; } /** * Requests the entry and saves it to the cache if the response is valid. * By default, any response with a status code of less than 400 (including * opaque responses) is considered valid. * * If you need to use custom criteria to determine what's valid and what * isn't, then pass in an item in `options.plugins` that implements the * `cacheWillUpdate()` lifecycle event. * * @private * @param {Object} options * @param {string} options.url The URL to fetch and cache. * @param {Event} [options.event] The install event (if passed). * @param {Array} [options.plugins] An array of plugins to apply to * fetch and caching. */ async _addURLToCache({ url, event, plugins }) { const request = new Request(url, { credentials: 'same-origin' }); let response = await fetchWrapper_mjs.fetchWrapper.fetch({ event, plugins, request }); // Allow developers to override the default logic about what is and isn't // valid by passing in a plugin implementing cacheWillUpdate(), e.g. // a workbox.cacheableResponse.Plugin instance. let cacheWillUpdateCallback; for (const plugin of plugins || []) { if ('cacheWillUpdate' in plugin) { cacheWillUpdateCallback = plugin.cacheWillUpdate.bind(plugin); } } const isValidResponse = cacheWillUpdateCallback ? // Use a callback if provided. It returns a truthy value if valid. cacheWillUpdateCallback({ event, request, response }) : // Otherwise, default to considering any response status under 400 valid. // This includes, by default, considering opaque responses valid. response.status < 400; // Consider this a failure, leading to the `install` handler failing, if // we get back an invalid response. if (!isValidResponse) { throw new WorkboxError_mjs.WorkboxError('bad-precaching-response', { url, status: response.status }); } if (response.redirected) { response = await cleanRedirect(response); } await cacheWrapper_mjs.cacheWrapper.put({ event, plugins, request, response, cacheName: this._cacheName, matchOptions: { ignoreSearch: true } }); } /** * Returns a mapping of a precached URL to the corresponding cache key, taking * into account the revision information for the URL. * * @return {Map} A URL to cache key mapping. */ getURLsToCacheKeys() { return this._urlsToCacheKeys; } /** * Returns a list of all the URLs that have been precached by the current * service worker. * * @return {Array} The precached URLs. */ getCachedURLs() { return [...this._urlsToCacheKeys.keys()]; } /** * Returns the cache key used for storing a given URL. If that URL is * unversioned, like `/index.html', then the cache key will be the original * URL with a search parameter appended to it. * * @param {string} url A URL whose cache key you want to look up. * @return {string} The versioned URL that corresponds to a cache key * for the original URL, or undefined if that URL isn't precached. */ getCacheKeyForURL(url) { const urlObject = new URL(url, location); return this._urlsToCacheKeys.get(urlObject.href); } } /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ let precacheController; /** * @return {PrecacheController} * @private */ const getOrCreatePrecacheController = () => { if (!precacheController) { precacheController = new PrecacheController(); } return precacheController; }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Removes any URL search parameters that should be ignored. * * @param {URL} urlObject The original URL. * @param {Array} ignoreURLParametersMatching RegExps to test against * each search parameter name. Matches mean that the search parameter should be * ignored. * @return {URL} The URL with any ignored search parameters removed. * * @private * @memberof module:workbox-precaching */ function removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching) { // Convert the iterable into an array at the start of the loop to make sure // deletion doesn't mess up iteration. for (const paramName of [...urlObject.searchParams.keys()]) { if (ignoreURLParametersMatching.some(regExp => regExp.test(paramName))) { urlObject.searchParams.delete(paramName); } } return urlObject; } /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Generator function that yields possible variations on the original URL to * check, one at a time. * * @param {string} url * @param {Object} options * * @private * @memberof module:workbox-precaching */ function* generateURLVariations(url, { ignoreURLParametersMatching, directoryIndex, cleanURLs, urlManipulation } = {}) { const urlObject = new URL(url, location); urlObject.hash = ''; yield urlObject.href; const urlWithoutIgnoredParams = removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching); yield urlWithoutIgnoredParams.href; if (directoryIndex && urlWithoutIgnoredParams.pathname.endsWith('/')) { const directoryURL = new URL(urlWithoutIgnoredParams); directoryURL.pathname += directoryIndex; yield directoryURL.href; } if (cleanURLs) { const cleanURL = new URL(urlWithoutIgnoredParams); cleanURL.pathname += '.html'; yield cleanURL.href; } if (urlManipulation) { const additionalURLs = urlManipulation({ url: urlObject }); for (const urlToAttempt of additionalURLs) { yield urlToAttempt.href; } } } /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * This function will take the request URL and manipulate it based on the * configuration options. * * @param {string} url * @param {Object} options * @return {string} Returns the URL in the cache that matches the request, * if possible. * * @private */ const getCacheKeyForURL = (url, options) => { const precacheController = getOrCreatePrecacheController(); const urlsToCacheKeys = precacheController.getURLsToCacheKeys(); for (const possibleURL of generateURLVariations(url, options)) { const possibleCacheKey = urlsToCacheKeys.get(possibleURL); if (possibleCacheKey) { return possibleCacheKey; } } }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ let listenerAdded = false; /** * Add a `fetch` listener to the service worker that will * respond to * [network requests]{@link https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers#Custom_responses_to_requests} * with precached assets. * * Requests for assets that aren't precached, the `FetchEvent` will not be * responded to, allowing the event to fall through to other `fetch` event * listeners. * * @param {Object} options * @param {string} [options.directoryIndex=index.html] The `directoryIndex` will * check cache entries for a URLs ending with '/' to see if there is a hit when * appending the `directoryIndex` value. * @param {Array} [options.ignoreURLParametersMatching=[/^utm_/]] An * array of regex's to remove search params when looking for a cache match. * @param {boolean} [options.cleanURLs=true] The `cleanURLs` option will * check the cache for the URL with a `.html` added to the end of the end. * @param {workbox.precaching~urlManipulation} [options.urlManipulation] * This is a function that should take a URL and return an array of * alternative URL's that should be checked for precache matches. * * @alias workbox.precaching.addRoute */ const addRoute = ({ ignoreURLParametersMatching = [/^utm_/], directoryIndex = 'index.html', cleanURLs = true, urlManipulation = null } = {}) => { if (!listenerAdded) { const cacheName = cacheNames_mjs.cacheNames.getPrecacheName(); addEventListener('fetch', event => { const precachedURL = getCacheKeyForURL(event.request.url, { cleanURLs, directoryIndex, ignoreURLParametersMatching, urlManipulation }); if (!precachedURL) { { logger_mjs.logger.debug(`Precaching did not find a match for ` + getFriendlyURL_mjs.getFriendlyURL(event.request.url)); } return; } let responsePromise = caches.open(cacheName).then(cache => { return cache.match(precachedURL); }).then(cachedResponse => { if (cachedResponse) { return cachedResponse; } // Fall back to the network if we don't have a cached response // (perhaps due to manual cache cleanup). { logger_mjs.logger.warn(`The precached response for ` + `${getFriendlyURL_mjs.getFriendlyURL(precachedURL)} in ${cacheName} was not found. ` + `Falling back to the network instead.`); } return fetch(precachedURL); }); { responsePromise = responsePromise.then(response => { // Workbox is going to handle the route. // print the routing details to the console. logger_mjs.logger.groupCollapsed(`Precaching is responding to: ` + getFriendlyURL_mjs.getFriendlyURL(event.request.url)); logger_mjs.logger.log(`Serving the precached url: ${precachedURL}`); logger_mjs.logger.groupCollapsed(`View request details here.`); logger_mjs.logger.log(event.request); logger_mjs.logger.groupEnd(); logger_mjs.logger.groupCollapsed(`View response details here.`); logger_mjs.logger.log(response); logger_mjs.logger.groupEnd(); logger_mjs.logger.groupEnd(); return response; }); } event.respondWith(responsePromise); }); listenerAdded = true; } }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const SUBSTRING_TO_FIND = '-precache-'; /** * Cleans up incompatible precaches that were created by older versions of * Workbox, by a service worker registered under the current scope. * * This is meant to be called as part of the `activate` event. * * This should be safe to use as long as you don't include `substringToFind` * (defaulting to `-precache-`) in your non-precache cache names. * * @param {string} currentPrecacheName The cache name currently in use for * precaching. This cache won't be deleted. * @param {string} [substringToFind='-precache-'] Cache names which include this * substring will be deleted (excluding `currentPrecacheName`). * @return {Array} A list of all the cache names that were deleted. * * @private * @memberof module:workbox-precaching */ const deleteOutdatedCaches = async (currentPrecacheName, substringToFind = SUBSTRING_TO_FIND) => { const cacheNames = await caches.keys(); const cacheNamesToDelete = cacheNames.filter(cacheName => { return cacheName.includes(substringToFind) && cacheName.includes(self.registration.scope) && cacheName !== currentPrecacheName; }); await Promise.all(cacheNamesToDelete.map(cacheName => caches.delete(cacheName))); return cacheNamesToDelete; }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Adds an `activate` event listener which will clean up incompatible * precaches that were created by older versions of Workbox. * * @alias workbox.precaching.cleanupOutdatedCaches */ const cleanupOutdatedCaches = () => { addEventListener('activate', event => { const cacheName = cacheNames_mjs.cacheNames.getPrecacheName(); event.waitUntil(deleteOutdatedCaches(cacheName).then(cachesDeleted => { { if (cachesDeleted.length > 0) { logger_mjs.logger.log(`The following out-of-date precaches were cleaned up ` + `automatically:`, cachesDeleted); } } })); }); }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Takes in a URL, and returns the corresponding URL that could be used to * lookup the entry in the precache. * * If a relative URL is provided, the location of the service worker file will * be used as the base. * * For precached entries without revision information, the cache key will be the * same as the original URL. * * For precached entries with revision information, the cache key will be the * original URL with the addition of a query parameter used for keeping track of * the revision info. * * @param {string} url The URL whose cache key to look up. * @return {string} The cache key that corresponds to that URL. * * @alias workbox.precaching.getCacheKeyForURL */ const getCacheKeyForURL$1 = url => { const precacheController = getOrCreatePrecacheController(); return precacheController.getCacheKeyForURL(url); }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ let listenersAdded = false; /** * Adds items to the precache list, removing any duplicates and * stores the files in the * ["precache cache"]{@link module:workbox-core.cacheNames} when the service * worker installs. * * This method can be called multiple times. * * Please note: This method **will not** serve any of the cached files for you. * It only precaches files. To respond to a network request you call * [addRoute()]{@link module:workbox-precaching.addRoute}. * * If you have a single array of files to precache, you can just call * [precacheAndRoute()]{@link module:workbox-precaching.precacheAndRoute}. * * @param {Array} entries Array of entries to precache. * * @alias workbox.precaching.precache */ const precache = entries => { const precacheController = getOrCreatePrecacheController(); precacheController.addToCacheList(entries); if (!listenersAdded && entries.length > 0) { const plugins = precachePlugins.get(); self.addEventListener('install', event => { event.waitUntil(precacheController.install({ event, plugins }).catch(error => { { logger_mjs.logger.error(`Service worker installation failed. It will ` + `be retried automatically during the next navigation.`); } // Re-throw the error to ensure installation fails. throw error; })); }); self.addEventListener('activate', event => { event.waitUntil(precacheController.activate({ event, plugins })); }); listenersAdded = true; } }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * This method will add entries to the precache list and add a route to * respond to fetch events. * * This is a convenience method that will call * [precache()]{@link module:workbox-precaching.precache} and * [addRoute()]{@link module:workbox-precaching.addRoute} in a single call. * * @param {Array} entries Array of entries to precache. * @param {Object} options See * [addRoute() options]{@link module:workbox-precaching.addRoute}. * * @alias workbox.precaching.precacheAndRoute */ const precacheAndRoute = (entries, options) => { precache(entries); addRoute(options); }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ { assert_mjs.assert.isSWEnv('workbox-precaching'); } exports.addPlugins = addPlugins; exports.addRoute = addRoute; exports.cleanupOutdatedCaches = cleanupOutdatedCaches; exports.getCacheKeyForURL = getCacheKeyForURL$1; exports.precache = precache; exports.precacheAndRoute = precacheAndRoute; exports.PrecacheController = PrecacheController; return exports; }({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private)); //# sourceMappingURL=workbox-precaching.dev.js.map ================================================ FILE: public/assets/libs/workbox/workbox-precaching.prod.js ================================================ this.workbox=this.workbox||{},this.workbox.precaching=function(t,e,n,s,c){"use strict";try{self["workbox:precaching:4.1.1"]&&_()}catch(t){}const o=[],i={get:()=>o,add(t){o.push(...t)}};const a="__WB_REVISION__";function r(t){if(!t)throw new c.WorkboxError("add-to-cache-list-unexpected-type",{entry:t});if("string"==typeof t){const e=new URL(t,location);return{cacheKey:e.href,url:e.href}}const{revision:e,url:n}=t;if(!n)throw new c.WorkboxError("add-to-cache-list-unexpected-type",{entry:t});if(!e){const t=new URL(n,location);return{cacheKey:t.href,url:t.href}}const s=new URL(n,location),o=new URL(n,location);return o.searchParams.set(a,e),{cacheKey:o.href,url:s.href}}class l{constructor(t){this.t=e.cacheNames.getPrecacheName(t),this.s=new Map}addToCacheList(t){for(const e of t){const{cacheKey:t,url:n}=r(e);if(this.s.has(n)&&this.s.get(n)!==t)throw new c.WorkboxError("add-to-cache-list-conflicting-entries",{firstEntry:this.s.get(n),secondEntry:t});this.s.set(n,t)}}async install({event:t,plugins:e}={}){const n=[],s=[],c=await caches.open(this.t),o=await c.keys(),i=new Set(o.map(t=>t.url));for(const t of this.s.values())i.has(t)?s.push(t):n.push(t);const a=n.map(n=>this.o({event:t,plugins:e,url:n}));return await Promise.all(a),{updatedURLs:n,notUpdatedURLs:s}}async activate(){const t=await caches.open(this.t),e=await t.keys(),n=new Set(this.s.values()),s=[];for(const c of e)n.has(c.url)||(await t.delete(c),s.push(c.url));return{deletedURLs:s}}async o({url:t,event:e,plugins:o}){const i=new Request(t,{credentials:"same-origin"});let a,r=await s.fetchWrapper.fetch({event:e,plugins:o,request:i});for(const t of o||[])"cacheWillUpdate"in t&&(a=t.cacheWillUpdate.bind(t));if(!(a?a({event:e,request:i,response:r}):r.status<400))throw new c.WorkboxError("bad-precaching-response",{url:t,status:r.status});r.redirected&&(r=await async function(t){const e=t.clone(),n="body"in e?Promise.resolve(e.body):e.blob(),s=await n;return new Response(s,{headers:e.headers,status:e.status,statusText:e.statusText})}(r)),await n.cacheWrapper.put({event:e,plugins:o,request:i,response:r,cacheName:this.t,matchOptions:{ignoreSearch:!0}})}getURLsToCacheKeys(){return this.s}getCachedURLs(){return[...this.s.keys()]}getCacheKeyForURL(t){const e=new URL(t,location);return this.s.get(e.href)}}let u;const h=()=>(u||(u=new l),u);const d=(t,e)=>{const n=h().getURLsToCacheKeys();for(const s of function*(t,{ignoreURLParametersMatching:e,directoryIndex:n,cleanURLs:s,urlManipulation:c}={}){const o=new URL(t,location);o.hash="",yield o.href;const i=function(t,e){for(const n of[...t.searchParams.keys()])e.some(t=>t.test(n))&&t.searchParams.delete(n);return t}(o,e);if(yield i.href,n&&i.pathname.endsWith("/")){const t=new URL(i);t.pathname+=n,yield t.href}if(s){const t=new URL(i);t.pathname+=".html",yield t.href}if(c){const t=c({url:o});for(const e of t)yield e.href}}(t,e)){const t=n.get(s);if(t)return t}};let f=!1;const w=({ignoreURLParametersMatching:t=[/^utm_/],directoryIndex:n="index.html",cleanURLs:s=!0,urlManipulation:c=null}={})=>{if(!f){const o=e.cacheNames.getPrecacheName();addEventListener("fetch",e=>{const i=d(e.request.url,{cleanURLs:s,directoryIndex:n,ignoreURLParametersMatching:t,urlManipulation:c});if(!i)return;let a=caches.open(o).then(t=>t.match(i)).then(t=>t||fetch(i));e.respondWith(a)}),f=!0}};let y=!1;const p=t=>{const e=h();if(e.addToCacheList(t),!y&&t.length>0){const t=i.get();self.addEventListener("install",n=>{n.waitUntil(e.install({event:n,plugins:t}).catch(t=>{throw t}))}),self.addEventListener("activate",n=>{n.waitUntil(e.activate({event:n,plugins:t}))}),y=!0}};return t.addPlugins=(t=>{i.add(t)}),t.addRoute=w,t.cleanupOutdatedCaches=(()=>{addEventListener("activate",t=>{const n=e.cacheNames.getPrecacheName();t.waitUntil((async(t,e="-precache-")=>{const n=(await caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==t);return await Promise.all(n.map(t=>caches.delete(t))),n})(n).then(t=>{}))})}),t.getCacheKeyForURL=(t=>{return h().getCacheKeyForURL(t)}),t.precache=p,t.precacheAndRoute=((t,e)=>{p(t),w(e)}),t.PrecacheController=l,t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private); //# sourceMappingURL=workbox-precaching.prod.js.map ================================================ FILE: public/assets/libs/workbox/workbox-range-requests.dev.js ================================================ this.workbox = this.workbox || {}; this.workbox.rangeRequests = (function (exports, WorkboxError_mjs, assert_mjs, logger_mjs) { 'use strict'; try { self['workbox:range-requests:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * @param {Blob} blob A source blob. * @param {number|null} start The offset to use as the start of the * slice. * @param {number|null} end The offset to use as the end of the slice. * @return {Object} An object with `start` and `end` properties, reflecting * the effective boundaries to use given the size of the blob. * * @private */ function calculateEffectiveBoundaries(blob, start, end) { { assert_mjs.assert.isInstance(blob, Blob, { moduleName: 'workbox-range-requests', funcName: 'calculateEffectiveBoundaries', paramName: 'blob' }); } const blobSize = blob.size; if (end > blobSize || start < 0) { throw new WorkboxError_mjs.WorkboxError('range-not-satisfiable', { size: blobSize, end, start }); } let effectiveStart; let effectiveEnd; if (start === null) { effectiveStart = blobSize - end; effectiveEnd = blobSize; } else if (end === null) { effectiveStart = start; effectiveEnd = blobSize; } else { effectiveStart = start; // Range values are inclusive, so add 1 to the value. effectiveEnd = end + 1; } return { start: effectiveStart, end: effectiveEnd }; } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * @param {string} rangeHeader A Range: header value. * @return {Object} An object with `start` and `end` properties, reflecting * the parsed value of the Range: header. If either the `start` or `end` are * omitted, then `null` will be returned. * * @private */ function parseRangeHeader(rangeHeader) { { assert_mjs.assert.isType(rangeHeader, 'string', { moduleName: 'workbox-range-requests', funcName: 'parseRangeHeader', paramName: 'rangeHeader' }); } const normalizedRangeHeader = rangeHeader.trim().toLowerCase(); if (!normalizedRangeHeader.startsWith('bytes=')) { throw new WorkboxError_mjs.WorkboxError('unit-must-be-bytes', { normalizedRangeHeader }); } // Specifying multiple ranges separate by commas is valid syntax, but this // library only attempts to handle a single, contiguous sequence of bytes. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range#Syntax if (normalizedRangeHeader.includes(',')) { throw new WorkboxError_mjs.WorkboxError('single-range-only', { normalizedRangeHeader }); } const rangeParts = /(\d*)-(\d*)/.exec(normalizedRangeHeader); // We need either at least one of the start or end values. if (rangeParts === null || !(rangeParts[1] || rangeParts[2])) { throw new WorkboxError_mjs.WorkboxError('invalid-range-values', { normalizedRangeHeader }); } return { start: rangeParts[1] === '' ? null : Number(rangeParts[1]), end: rangeParts[2] === '' ? null : Number(rangeParts[2]) }; } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Given a `Request` and `Response` objects as input, this will return a * promise for a new `Response`. * * If the original `Response` already contains partial content (i.e. it has * a status of 206), then this assumes it already fulfills the `Range:` * requirements, and will return it as-is. * * @param {Request} request A request, which should contain a Range: * header. * @param {Response} originalResponse A response. * @return {Promise} Either a `206 Partial Content` response, with * the response body set to the slice of content specified by the request's * `Range:` header, or a `416 Range Not Satisfiable` response if the * conditions of the `Range:` header can't be met. * * @memberof workbox.rangeRequests */ async function createPartialResponse(request, originalResponse) { try { { assert_mjs.assert.isInstance(request, Request, { moduleName: 'workbox-range-requests', funcName: 'createPartialResponse', paramName: 'request' }); assert_mjs.assert.isInstance(originalResponse, Response, { moduleName: 'workbox-range-requests', funcName: 'createPartialResponse', paramName: 'originalResponse' }); } if (originalResponse.status === 206) { // If we already have a 206, then just pass it through as-is; // see https://github.com/GoogleChrome/workbox/issues/1720 return originalResponse; } const rangeHeader = request.headers.get('range'); if (!rangeHeader) { throw new WorkboxError_mjs.WorkboxError('no-range-header'); } const boundaries = parseRangeHeader(rangeHeader); const originalBlob = await originalResponse.blob(); const effectiveBoundaries = calculateEffectiveBoundaries(originalBlob, boundaries.start, boundaries.end); const slicedBlob = originalBlob.slice(effectiveBoundaries.start, effectiveBoundaries.end); const slicedBlobSize = slicedBlob.size; const slicedResponse = new Response(slicedBlob, { // Status code 206 is for a Partial Content response. // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206 status: 206, statusText: 'Partial Content', headers: originalResponse.headers }); slicedResponse.headers.set('Content-Length', slicedBlobSize); slicedResponse.headers.set('Content-Range', `bytes ${effectiveBoundaries.start}-${effectiveBoundaries.end - 1}/` + originalBlob.size); return slicedResponse; } catch (error) { { logger_mjs.logger.warn(`Unable to construct a partial response; returning a ` + `416 Range Not Satisfiable response instead.`); logger_mjs.logger.groupCollapsed(`View details here.`); logger_mjs.logger.log(error); logger_mjs.logger.log(request); logger_mjs.logger.log(originalResponse); logger_mjs.logger.groupEnd(); } return new Response('', { status: 416, statusText: 'Range Not Satisfiable' }); } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * The range request plugin makes it easy for a request with a 'Range' header to * be fulfilled by a cached response. * * It does this by intercepting the `cachedResponseWillBeUsed` plugin callback * and returning the appropriate subset of the cached response body. * * @memberof workbox.rangeRequests */ class Plugin { /** * @param {Object} options * @param {Request} options.request The original request, which may or may not * contain a Range: header. * @param {Response} options.cachedResponse The complete cached response. * @return {Promise} If request contains a 'Range' header, then a * new response with status 206 whose body is a subset of `cachedResponse` is * returned. Otherwise, `cachedResponse` is returned as-is. * * @private */ async cachedResponseWillBeUsed({ request, cachedResponse }) { // Only return a sliced response if there's something valid in the cache, // and there's a Range: header in the request. if (cachedResponse && request.headers.has('range')) { return await createPartialResponse(request, cachedResponse); } // If there was no Range: header, or if cachedResponse wasn't valid, just // pass it through as-is. return cachedResponse; } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ exports.createPartialResponse = createPartialResponse; exports.Plugin = Plugin; return exports; }({}, workbox.core._private, workbox.core._private, workbox.core._private)); //# sourceMappingURL=workbox-range-requests.dev.js.map ================================================ FILE: public/assets/libs/workbox/workbox-range-requests.prod.js ================================================ this.workbox=this.workbox||{},this.workbox.rangeRequests=function(e,n){"use strict";try{self["workbox:range-requests:4.1.1"]&&_()}catch(e){}async function t(e,t){try{if(206===t.status)return t;const s=e.headers.get("range");if(!s)throw new n.WorkboxError("no-range-header");const a=function(e){const t=e.trim().toLowerCase();if(!t.startsWith("bytes="))throw new n.WorkboxError("unit-must-be-bytes",{normalizedRangeHeader:t});if(t.includes(","))throw new n.WorkboxError("single-range-only",{normalizedRangeHeader:t});const s=/(\d*)-(\d*)/.exec(t);if(null===s||!s[1]&&!s[2])throw new n.WorkboxError("invalid-range-values",{normalizedRangeHeader:t});return{start:""===s[1]?null:Number(s[1]),end:""===s[2]?null:Number(s[2])}}(s),r=await t.blob(),i=function(e,t,s){const a=e.size;if(s>a||t<0)throw new n.WorkboxError("range-not-satisfiable",{size:a,end:s,start:t});let r,i;return null===t?(r=a-s,i=a):null===s?(r=t,i=a):(r=t,i=s+1),{start:r,end:i}}(r,a.start,a.end),o=r.slice(i.start,i.end),u=o.size,l=new Response(o,{status:206,statusText:"Partial Content",headers:t.headers});return l.headers.set("Content-Length",u),l.headers.set("Content-Range",`bytes ${i.start}-${i.end-1}/`+r.size),l}catch(e){return new Response("",{status:416,statusText:"Range Not Satisfiable"})}}return e.createPartialResponse=t,e.Plugin=class{async cachedResponseWillBeUsed({request:e,cachedResponse:n}){return n&&e.headers.has("range")?await t(e,n):n}},e}({},workbox.core._private); //# sourceMappingURL=workbox-range-requests.prod.js.map ================================================ FILE: public/assets/libs/workbox/workbox-routing.dev.js ================================================ this.workbox = this.workbox || {}; this.workbox.routing = (function (exports, assert_mjs, logger_mjs, cacheNames_mjs, WorkboxError_mjs, getFriendlyURL_mjs) { 'use strict'; try { self['workbox:routing:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * The default HTTP method, 'GET', used when there's no specific method * configured for a route. * * @type {string} * * @private */ const defaultMethod = 'GET'; /** * The list of valid HTTP methods associated with requests that could be routed. * * @type {Array} * * @private */ const validMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT']; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * @param {function()|Object} handler Either a function, or an object with a * 'handle' method. * @return {Object} An object with a handle method. * * @private */ const normalizeHandler = handler => { if (handler && typeof handler === 'object') { { assert_mjs.assert.hasMethod(handler, 'handle', { moduleName: 'workbox-routing', className: 'Route', funcName: 'constructor', paramName: 'handler' }); } return handler; } else { { assert_mjs.assert.isType(handler, 'function', { moduleName: 'workbox-routing', className: 'Route', funcName: 'constructor', paramName: 'handler' }); } return { handle: handler }; } }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * A `Route` consists of a pair of callback functions, "match" and "handler". * The "match" callback determine if a route should be used to "handle" a * request by returning a non-falsy value if it can. The "handler" callback * is called when there is a match and should return a Promise that resolves * to a `Response`. * * @memberof workbox.routing */ class Route { /** * Constructor for Route class. * * @param {workbox.routing.Route~matchCallback} match * A callback function that determines whether the route matches a given * `fetch` event by returning a non-falsy value. * @param {workbox.routing.Route~handlerCallback} handler A callback * function that returns a Promise resolving to a Response. * @param {string} [method='GET'] The HTTP method to match the Route * against. */ constructor(match, handler, method) { { assert_mjs.assert.isType(match, 'function', { moduleName: 'workbox-routing', className: 'Route', funcName: 'constructor', paramName: 'match' }); if (method) { assert_mjs.assert.isOneOf(method, validMethods, { paramName: 'method' }); } } // These values are referenced directly by Router so cannot be // altered by minifification. this.handler = normalizeHandler(handler); this.match = match; this.method = method || defaultMethod; } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * NavigationRoute makes it easy to create a [Route]{@link * workbox.routing.Route} that matches for browser * [navigation requests]{@link https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests}. * * It will only match incoming Requests whose * [`mode`]{@link https://fetch.spec.whatwg.org/#concept-request-mode} * is set to `navigate`. * * You can optionally only apply this route to a subset of navigation requests * by using one or both of the `blacklist` and `whitelist` parameters. * * @memberof workbox.routing * @extends workbox.routing.Route */ class NavigationRoute extends Route { /** * If both `blacklist` and `whiltelist` are provided, the `blacklist` will * take precedence and the request will not match this route. * * The regular expressions in `whitelist` and `blacklist` * are matched against the concatenated * [`pathname`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/pathname} * and [`search`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/search} * portions of the requested URL. * * @param {workbox.routing.Route~handlerCallback} handler A callback * function that returns a Promise resulting in a Response. * @param {Object} options * @param {Array} [options.blacklist] If any of these patterns match, * the route will not handle the request (even if a whitelist RegExp matches). * @param {Array} [options.whitelist=[/./]] If any of these patterns * match the URL's pathname and search parameter, the route will handle the * request (assuming the blacklist doesn't match). */ constructor(handler, { whitelist = [/./], blacklist = [] } = {}) { { assert_mjs.assert.isArrayOfClass(whitelist, RegExp, { moduleName: 'workbox-routing', className: 'NavigationRoute', funcName: 'constructor', paramName: 'options.whitelist' }); assert_mjs.assert.isArrayOfClass(blacklist, RegExp, { moduleName: 'workbox-routing', className: 'NavigationRoute', funcName: 'constructor', paramName: 'options.blacklist' }); } super(options => this._match(options), handler); this._whitelist = whitelist; this._blacklist = blacklist; } /** * Routes match handler. * * @param {Object} options * @param {URL} options.url * @param {Request} options.request * @return {boolean} * * @private */ _match({ url, request }) { if (request.mode !== 'navigate') { return false; } const pathnameAndSearch = url.pathname + url.search; for (const regExp of this._blacklist) { if (regExp.test(pathnameAndSearch)) { { logger_mjs.logger.log(`The navigation route is not being used, since the ` + `URL matches this blacklist pattern: ${regExp}`); } return false; } } if (this._whitelist.some(regExp => regExp.test(pathnameAndSearch))) { { logger_mjs.logger.debug(`The navigation route is being used.`); } return true; } { logger_mjs.logger.log(`The navigation route is not being used, since the URL ` + `being navigated to doesn't match the whitelist.`); } return false; } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * RegExpRoute makes it easy to create a regular expression based * [Route]{@link workbox.routing.Route}. * * For same-origin requests the RegExp only needs to match part of the URL. For * requests against third-party servers, you must define a RegExp that matches * the start of the URL. * * [See the module docs for info.]{@link https://developers.google.com/web/tools/workbox/modules/workbox-routing} * * @memberof workbox.routing * @extends workbox.routing.Route */ class RegExpRoute extends Route { /** * If the regulard expression contains * [capture groups]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#grouping-back-references}, * th ecaptured values will be passed to the * [handler's]{@link workbox.routing.Route~handlerCallback} `params` * argument. * * @param {RegExp} regExp The regular expression to match against URLs. * @param {workbox.routing.Route~handlerCallback} handler A callback * function that returns a Promise resulting in a Response. * @param {string} [method='GET'] The HTTP method to match the Route * against. */ constructor(regExp, handler, method) { { assert_mjs.assert.isInstance(regExp, RegExp, { moduleName: 'workbox-routing', className: 'RegExpRoute', funcName: 'constructor', paramName: 'pattern' }); } const match = ({ url }) => { const result = regExp.exec(url.href); // Return null immediately if there's no match. if (!result) { return null; } // Require that the match start at the first character in the URL string // if it's a cross-origin request. // See https://github.com/GoogleChrome/workbox/issues/281 for the context // behind this behavior. if (url.origin !== location.origin && result.index !== 0) { { logger_mjs.logger.debug(`The regular expression '${regExp}' only partially matched ` + `against the cross-origin URL '${url}'. RegExpRoute's will only ` + `handle cross-origin requests if they match the entire URL.`); } return null; } // If the route matches, but there aren't any capture groups defined, then // this will return [], which is truthy and therefore sufficient to // indicate a match. // If there are capture groups, then it will return their values. return result.slice(1); }; super(match, handler, method); } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * The Router can be used to process a FetchEvent through one or more * [Routes]{@link workbox.routing.Route} responding with a Request if * a matching route exists. * * If no route matches a given a request, the Router will use a "default" * handler if one is defined. * * Should the matching Route throw an error, the Router will use a "catch" * handler if one is defined to gracefully deal with issues and respond with a * Request. * * If a request matches multiple routes, the **earliest** registered route will * be used to respond to the request. * * @memberof workbox.routing */ class Router { /** * Initializes a new Router. */ constructor() { this._routes = new Map(); } /** * @return {Map>} routes A `Map` of HTTP * method name ('GET', etc.) to an array of all the corresponding `Route` * instances that are registered. */ get routes() { return this._routes; } /** * Adds a fetch event listener to respond to events when a route matches * the event's request. */ addFetchListener() { self.addEventListener('fetch', event => { const { request } = event; const responsePromise = this.handleRequest({ request, event }); if (responsePromise) { event.respondWith(responsePromise); } }); } /** * Adds a message event listener for URLs to cache from the window. * This is useful to cache resources loaded on the page prior to when the * service worker started controlling it. * * The format of the message data sent from the window should be as follows. * Where the `urlsToCache` array may consist of URL strings or an array of * URL string + `requestInit` object (the same as you'd pass to `fetch()`). * * ``` * { * type: 'CACHE_URLS', * payload: { * urlsToCache: [ * './script1.js', * './script2.js', * ['./script3.js', {mode: 'no-cors'}], * ], * }, * } * ``` */ addCacheListener() { self.addEventListener('message', async event => { if (event.data && event.data.type === 'CACHE_URLS') { const { payload } = event.data; { logger_mjs.logger.debug(`Caching URLs from the window`, payload.urlsToCache); } const requestPromises = Promise.all(payload.urlsToCache.map(entry => { if (typeof entry === 'string') { entry = [entry]; } const request = new Request(...entry); return this.handleRequest({ request }); })); event.waitUntil(requestPromises); // If a MessageChannel was used, reply to the message on success. if (event.ports) { await requestPromises; event.ports[0].postMessage(true); } } }); } /** * Apply the routing rules to a FetchEvent object to get a Response from an * appropriate Route's handler. * * @param {Object} options * @param {Request} options.request The request to handle (this is usually * from a fetch event, but it does not have to be). * @param {FetchEvent} [options.event] The event that triggered the request, * if applicable. * @return {Promise|undefined} A promise is returned if a * registered route can handle the request. If there is no matching * route and there's no `defaultHandler`, `undefined` is returned. */ handleRequest({ request, event }) { { assert_mjs.assert.isInstance(request, Request, { moduleName: 'workbox-routing', className: 'Router', funcName: 'handleRequest', paramName: 'options.request' }); } const url = new URL(request.url, location); if (!url.protocol.startsWith('http')) { { logger_mjs.logger.debug(`Workbox Router only supports URLs that start with 'http'.`); } return; } let { params, route } = this.findMatchingRoute({ url, request, event }); let handler = route && route.handler; let debugMessages = []; { if (handler) { debugMessages.push([`Found a route to handle this request:`, route]); if (params) { debugMessages.push([`Passing the following params to the route's handler:`, params]); } } } // If we don't have a handler because there was no matching route, then // fall back to defaultHandler if that's defined. if (!handler && this._defaultHandler) { { debugMessages.push(`Failed to find a matching route. Falling ` + `back to the default handler.`); // This is used for debugging in logs in the case of an error. route = '[Default Handler]'; } handler = this._defaultHandler; } if (!handler) { { // No handler so Workbox will do nothing. If logs is set of debug // i.e. verbose, we should print out this information. logger_mjs.logger.debug(`No route found for: ${getFriendlyURL_mjs.getFriendlyURL(url)}`); } return; } { // We have a handler, meaning Workbox is going to handle the route. // print the routing details to the console. logger_mjs.logger.groupCollapsed(`Router is responding to: ${getFriendlyURL_mjs.getFriendlyURL(url)}`); debugMessages.forEach(msg => { if (Array.isArray(msg)) { logger_mjs.logger.log(...msg); } else { logger_mjs.logger.log(msg); } }); // The Request and Response objects contains a great deal of information, // hide it under a group in case developers want to see it. logger_mjs.logger.groupCollapsed(`View request details here.`); logger_mjs.logger.log(request); logger_mjs.logger.groupEnd(); logger_mjs.logger.groupEnd(); } // Wrap in try and catch in case the handle method throws a synchronous // error. It should still callback to the catch handler. let responsePromise; try { responsePromise = handler.handle({ url, request, event, params }); } catch (err) { responsePromise = Promise.reject(err); } if (responsePromise && this._catchHandler) { responsePromise = responsePromise.catch(err => { { // Still include URL here as it will be async from the console group // and may not make sense without the URL logger_mjs.logger.groupCollapsed(`Error thrown when responding to: ` + ` ${getFriendlyURL_mjs.getFriendlyURL(url)}. Falling back to Catch Handler.`); logger_mjs.logger.error(`Error thrown by:`, route); logger_mjs.logger.error(err); logger_mjs.logger.groupEnd(); } return this._catchHandler.handle({ url, event, err }); }); } return responsePromise; } /** * Checks a request and URL (and optionally an event) against the list of * registered routes, and if there's a match, returns the corresponding * route along with any params generated by the match. * * @param {Object} options * @param {URL} options.url * @param {Request} options.request The request to match. * @param {FetchEvent} [options.event] The corresponding event (unless N/A). * @return {Object} An object with `route` and `params` properties. * They are populated if a matching route was found or `undefined` * otherwise. */ findMatchingRoute({ url, request, event }) { { assert_mjs.assert.isInstance(url, URL, { moduleName: 'workbox-routing', className: 'Router', funcName: 'findMatchingRoute', paramName: 'options.url' }); assert_mjs.assert.isInstance(request, Request, { moduleName: 'workbox-routing', className: 'Router', funcName: 'findMatchingRoute', paramName: 'options.request' }); } const routes = this._routes.get(request.method) || []; for (const route of routes) { let params; let matchResult = route.match({ url, request, event }); if (matchResult) { if (Array.isArray(matchResult) && matchResult.length > 0) { // Instead of passing an empty array in as params, use undefined. params = matchResult; } else if (matchResult.constructor === Object && Object.keys(matchResult).length > 0) { // Instead of passing an empty object in as params, use undefined. params = matchResult; } // Return early if have a match. return { route, params }; } } // If no match was found above, return and empty object. return {}; } /** * Define a default `handler` that's called when no routes explicitly * match the incoming request. * * Without a default handler, unmatched requests will go against the * network as if there were no service worker present. * * @param {workbox.routing.Route~handlerCallback} handler A callback * function that returns a Promise resulting in a Response. */ setDefaultHandler(handler) { this._defaultHandler = normalizeHandler(handler); } /** * If a Route throws an error while handling a request, this `handler` * will be called and given a chance to provide a response. * * @param {workbox.routing.Route~handlerCallback} handler A callback * function that returns a Promise resulting in a Response. */ setCatchHandler(handler) { this._catchHandler = normalizeHandler(handler); } /** * Registers a route with the router. * * @param {workbox.routing.Route} route The route to register. */ registerRoute(route) { { assert_mjs.assert.isType(route, 'object', { moduleName: 'workbox-routing', className: 'Router', funcName: 'registerRoute', paramName: 'route' }); assert_mjs.assert.hasMethod(route, 'match', { moduleName: 'workbox-routing', className: 'Router', funcName: 'registerRoute', paramName: 'route' }); assert_mjs.assert.isType(route.handler, 'object', { moduleName: 'workbox-routing', className: 'Router', funcName: 'registerRoute', paramName: 'route' }); assert_mjs.assert.hasMethod(route.handler, 'handle', { moduleName: 'workbox-routing', className: 'Router', funcName: 'registerRoute', paramName: 'route.handler' }); assert_mjs.assert.isType(route.method, 'string', { moduleName: 'workbox-routing', className: 'Router', funcName: 'registerRoute', paramName: 'route.method' }); } if (!this._routes.has(route.method)) { this._routes.set(route.method, []); } // Give precedence to all of the earlier routes by adding this additional // route to the end of the array. this._routes.get(route.method).push(route); } /** * Unregisters a route with the router. * * @param {workbox.routing.Route} route The route to unregister. */ unregisterRoute(route) { if (!this._routes.has(route.method)) { throw new WorkboxError_mjs.WorkboxError('unregister-route-but-not-found-with-method', { method: route.method }); } const routeIndex = this._routes.get(route.method).indexOf(route); if (routeIndex > -1) { this._routes.get(route.method).splice(routeIndex, 1); } else { throw new WorkboxError_mjs.WorkboxError('unregister-route-route-not-registered'); } } } /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ let defaultRouter; /** * Creates a new, singleton Router instance if one does not exist. If one * does already exist, that instance is returned. * * @private * @return {Router} */ const getOrCreateDefaultRouter = () => { if (!defaultRouter) { defaultRouter = new Router(); // The helpers that use the default Router assume these listeners exist. defaultRouter.addFetchListener(); defaultRouter.addCacheListener(); } return defaultRouter; }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Registers a route that will return a precached file for a navigation * request. This is useful for the * [application shell pattern]{@link https://developers.google.com/web/fundamentals/architecture/app-shell}. * * When determining the URL of the precached HTML document, you will likely need * to call `workbox.precaching.getCacheKeyForURL(originalUrl)`, to account for * the fact that Workbox's precaching naming conventions often results in URL * cache keys that contain extra revisioning info. * * This method will generate a * [NavigationRoute]{@link workbox.routing.NavigationRoute} * and call * [Router.registerRoute()]{@link workbox.routing.Router#registerRoute} on a * singleton Router instance. * * @param {string} cachedAssetUrl The cache key to use for the HTML file. * @param {Object} [options] * @param {string} [options.cacheName] Cache name to store and retrieve * requests. Defaults to precache cache name provided by * [workbox-core.cacheNames]{@link workbox.core.cacheNames}. * @param {Array} [options.blacklist=[]] If any of these patterns * match, the route will not handle the request (even if a whitelist entry * matches). * @param {Array} [options.whitelist=[/./]] If any of these patterns * match the URL's pathname and search parameter, the route will handle the * request (assuming the blacklist doesn't match). * @return {workbox.routing.NavigationRoute} Returns the generated * Route. * * @alias workbox.routing.registerNavigationRoute */ const registerNavigationRoute = (cachedAssetUrl, options = {}) => { { assert_mjs.assert.isType(cachedAssetUrl, 'string', { moduleName: 'workbox-routing', funcName: 'registerNavigationRoute', paramName: 'cachedAssetUrl' }); } const cacheName = cacheNames_mjs.cacheNames.getPrecacheName(options.cacheName); const handler = async () => { try { const response = await caches.match(cachedAssetUrl, { cacheName }); if (response) { return response; } // This shouldn't normally happen, but there are edge cases: // https://github.com/GoogleChrome/workbox/issues/1441 throw new Error(`The cache ${cacheName} did not have an entry for ` + `${cachedAssetUrl}.`); } catch (error) { // If there's either a cache miss, or the caches.match() call threw // an exception, then attempt to fulfill the navigation request with // a response from the network rather than leaving the user with a // failed navigation. { logger_mjs.logger.debug(`Unable to respond to navigation request with ` + `cached response. Falling back to network.`, error); } // This might still fail if the browser is offline... return fetch(cachedAssetUrl); } }; const route = new NavigationRoute(handler, { whitelist: options.whitelist, blacklist: options.blacklist }); const defaultRouter = getOrCreateDefaultRouter(); defaultRouter.registerRoute(route); return route; }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Easily register a RegExp, string, or function with a caching * strategy to a singleton Router instance. * * This method will generate a Route for you if needed and * call [Router.registerRoute()]{@link * workbox.routing.Router#registerRoute}. * * @param { * RegExp| * string| * workbox.routing.Route~matchCallback| * workbox.routing.Route * } capture * If the capture param is a `Route`, all other arguments will be ignored. * @param {workbox.routing.Route~handlerCallback} handler A callback * function that returns a Promise resulting in a Response. * @param {string} [method='GET'] The HTTP method to match the Route * against. * @return {workbox.routing.Route} The generated `Route`(Useful for * unregistering). * * @alias workbox.routing.registerRoute */ const registerRoute = (capture, handler, method = 'GET') => { let route; if (typeof capture === 'string') { const captureUrl = new URL(capture, location); { if (!(capture.startsWith('/') || capture.startsWith('http'))) { throw new WorkboxError_mjs.WorkboxError('invalid-string', { moduleName: 'workbox-routing', funcName: 'registerRoute', paramName: 'capture' }); } // We want to check if Express-style wildcards are in the pathname only. // TODO: Remove this log message in v4. const valueToCheck = capture.startsWith('http') ? captureUrl.pathname : capture; // See https://github.com/pillarjs/path-to-regexp#parameters const wildcards = '[*:?+]'; if (valueToCheck.match(new RegExp(`${wildcards}`))) { logger_mjs.logger.debug(`The '$capture' parameter contains an Express-style wildcard ` + `character (${wildcards}). Strings are now always interpreted as ` + `exact matches; use a RegExp for partial or wildcard matches.`); } } const matchCallback = ({ url }) => { { if (url.pathname === captureUrl.pathname && url.origin !== captureUrl.origin) { logger_mjs.logger.debug(`${capture} only partially matches the cross-origin URL ` + `${url}. This route will only handle cross-origin requests ` + `if they match the entire URL.`); } } return url.href === captureUrl.href; }; route = new Route(matchCallback, handler, method); } else if (capture instanceof RegExp) { route = new RegExpRoute(capture, handler, method); } else if (typeof capture === 'function') { route = new Route(capture, handler, method); } else if (capture instanceof Route) { route = capture; } else { throw new WorkboxError_mjs.WorkboxError('unsupported-route-type', { moduleName: 'workbox-routing', funcName: 'registerRoute', paramName: 'capture' }); } const defaultRouter = getOrCreateDefaultRouter(); defaultRouter.registerRoute(route); return route; }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * If a Route throws an error while handling a request, this `handler` * will be called and given a chance to provide a response. * * @param {workbox.routing.Route~handlerCallback} handler A callback * function that returns a Promise resulting in a Response. * * @alias workbox.routing.setCatchHandler */ const setCatchHandler = handler => { const defaultRouter = getOrCreateDefaultRouter(); defaultRouter.setCatchHandler(handler); }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Define a default `handler` that's called when no routes explicitly * match the incoming request. * * Without a default handler, unmatched requests will go against the * network as if there were no service worker present. * * @param {workbox.routing.Route~handlerCallback} handler A callback * function that returns a Promise resulting in a Response. * * @alias workbox.routing.setDefaultHandler */ const setDefaultHandler = handler => { const defaultRouter = getOrCreateDefaultRouter(); defaultRouter.setDefaultHandler(handler); }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ { assert_mjs.assert.isSWEnv('workbox-routing'); } exports.NavigationRoute = NavigationRoute; exports.RegExpRoute = RegExpRoute; exports.registerNavigationRoute = registerNavigationRoute; exports.registerRoute = registerRoute; exports.Route = Route; exports.Router = Router; exports.setCatchHandler = setCatchHandler; exports.setDefaultHandler = setDefaultHandler; return exports; }({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private)); //# sourceMappingURL=workbox-routing.dev.js.map ================================================ FILE: public/assets/libs/workbox/workbox-routing.prod.js ================================================ this.workbox=this.workbox||{},this.workbox.routing=function(t,e,r){"use strict";try{self["workbox:routing:4.1.1"]&&_()}catch(t){}const s="GET",n=t=>t&&"object"==typeof t?t:{handle:t};class o{constructor(t,e,r){this.handler=n(e),this.match=t,this.method=r||s}}class i extends o{constructor(t,{whitelist:e=[/./],blacklist:r=[]}={}){super(t=>this.t(t),t),this.s=e,this.o=r}t({url:t,request:e}){if("navigate"!==e.mode)return!1;const r=t.pathname+t.search;for(const t of this.o)if(t.test(r))return!1;return!!this.s.some(t=>t.test(r))}}class u extends o{constructor(t,e,r){super(({url:e})=>{const r=t.exec(e.href);return r?e.origin!==location.origin&&0!==r.index?null:r.slice(1):null},e,r)}}class c{constructor(){this.i=new Map}get routes(){return this.i}addFetchListener(){self.addEventListener("fetch",t=>{const{request:e}=t,r=this.handleRequest({request:e,event:t});r&&t.respondWith(r)})}addCacheListener(){self.addEventListener("message",async t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,r=Promise.all(e.urlsToCache.map(t=>{"string"==typeof t&&(t=[t]);const e=new Request(...t);return this.handleRequest({request:e})}));t.waitUntil(r),t.ports&&(await r,t.ports[0].postMessage(!0))}})}handleRequest({request:t,event:e}){const r=new URL(t.url,location);if(!r.protocol.startsWith("http"))return;let s,{params:n,route:o}=this.findMatchingRoute({url:r,request:t,event:e}),i=o&&o.handler;if(!i&&this.u&&(i=this.u),i){try{s=i.handle({url:r,request:t,event:e,params:n})}catch(t){s=Promise.reject(t)}return s&&this.h&&(s=s.catch(t=>this.h.handle({url:r,event:e,err:t}))),s}}findMatchingRoute({url:t,request:e,event:r}){const s=this.i.get(e.method)||[];for(const n of s){let s,o=n.match({url:t,request:e,event:r});if(o)return Array.isArray(o)&&o.length>0?s=o:o.constructor===Object&&Object.keys(o).length>0&&(s=o),{route:n,params:s}}return{}}setDefaultHandler(t){this.u=n(t)}setCatchHandler(t){this.h=n(t)}registerRoute(t){this.i.has(t.method)||this.i.set(t.method,[]),this.i.get(t.method).push(t)}unregisterRoute(t){if(!this.i.has(t.method))throw new r.WorkboxError("unregister-route-but-not-found-with-method",{method:t.method});const e=this.i.get(t.method).indexOf(t);if(!(e>-1))throw new r.WorkboxError("unregister-route-route-not-registered");this.i.get(t.method).splice(e,1)}}let a;const h=()=>(a||((a=new c).addFetchListener(),a.addCacheListener()),a);return t.NavigationRoute=i,t.RegExpRoute=u,t.registerNavigationRoute=((t,r={})=>{const s=e.cacheNames.getPrecacheName(r.cacheName),n=new i(async()=>{try{const e=await caches.match(t,{cacheName:s});if(e)return e;throw new Error(`The cache ${s} did not have an entry for `+`${t}.`)}catch(e){return fetch(t)}},{whitelist:r.whitelist,blacklist:r.blacklist});return h().registerRoute(n),n}),t.registerRoute=((t,e,s="GET")=>{let n;if("string"==typeof t){const r=new URL(t,location);n=new o(({url:t})=>t.href===r.href,e,s)}else if(t instanceof RegExp)n=new u(t,e,s);else if("function"==typeof t)n=new o(t,e,s);else{if(!(t instanceof o))throw new r.WorkboxError("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});n=t}return h().registerRoute(n),n}),t.Route=o,t.Router=c,t.setCatchHandler=(t=>{h().setCatchHandler(t)}),t.setDefaultHandler=(t=>{h().setDefaultHandler(t)}),t}({},workbox.core._private,workbox.core._private); //# sourceMappingURL=workbox-routing.prod.js.map ================================================ FILE: public/assets/libs/workbox/workbox-strategies.dev.js ================================================ this.workbox = this.workbox || {}; this.workbox.strategies = (function (exports, logger_mjs, assert_mjs, cacheNames_mjs, cacheWrapper_mjs, fetchWrapper_mjs, getFriendlyURL_mjs, WorkboxError_mjs) { 'use strict'; try { self['workbox:strategies:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const getFriendlyURL = url => { const urlObj = new URL(url, location); if (urlObj.origin === location.origin) { return urlObj.pathname; } return urlObj.href; }; const messages = { strategyStart: (strategyName, request) => `Using ${strategyName} to ` + `respond to '${getFriendlyURL(request.url)}'`, printFinalResponse: response => { if (response) { logger_mjs.logger.groupCollapsed(`View the final response here.`); logger_mjs.logger.log(response); logger_mjs.logger.groupEnd(); } } }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * An implementation of a [cache-first]{@link https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#cache-falling-back-to-network} * request strategy. * * A cache first strategy is useful for assets that have been revisioned, * such as URLs like `/styles/example.a8f5f1.css`, since they * can be cached for long periods of time. * * If the network request fails, and there is no cache match, this will throw * a `WorkboxError` exception. * * @memberof workbox.strategies */ class CacheFirst { /** * @param {Object} options * @param {string} options.cacheName Cache name to store and retrieve * requests. Defaults to cache names provided by * [workbox-core]{@link workbox.core.cacheNames}. * @param {Array} options.plugins [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} * to use in conjunction with this caching strategy. * @param {Object} options.fetchOptions Values passed along to the * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) * of all fetch() requests made by this strategy. * @param {Object} options.matchOptions [`CacheQueryOptions`](https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions) */ constructor(options = {}) { this._cacheName = cacheNames_mjs.cacheNames.getRuntimeName(options.cacheName); this._plugins = options.plugins || []; this._fetchOptions = options.fetchOptions || null; this._matchOptions = options.matchOptions || null; } /** * This method will perform a request strategy and follows an API that * will work with the * [Workbox Router]{@link workbox.routing.Router}. * * @param {Object} options * @param {Request} options.request The request to run this strategy for. * @param {Event} [options.event] The event that triggered the request. * @return {Promise} */ async handle({ event, request }) { return this.makeRequest({ event, request: request || event.request }); } /** * This method can be used to perform a make a standalone request outside the * context of the [Workbox Router]{@link workbox.routing.Router}. * * See "[Advanced Recipes](https://developers.google.com/web/tools/workbox/guides/advanced-recipes#make-requests)" * for more usage information. * * @param {Object} options * @param {Request|string} options.request Either a * [`Request`]{@link https://developer.mozilla.org/en-US/docs/Web/API/Request} * object, or a string URL, corresponding to the request to be made. * @param {FetchEvent} [options.event] If provided, `event.waitUntil()` will be called automatically to extend the service worker's lifetime. * @return {Promise} */ async makeRequest({ event, request }) { const logs = []; if (typeof request === 'string') { request = new Request(request); } { assert_mjs.assert.isInstance(request, Request, { moduleName: 'workbox-strategies', className: 'CacheFirst', funcName: 'makeRequest', paramName: 'request' }); } let response = await cacheWrapper_mjs.cacheWrapper.match({ cacheName: this._cacheName, request, event, matchOptions: this._matchOptions, plugins: this._plugins }); let error; if (!response) { { logs.push(`No response found in the '${this._cacheName}' cache. ` + `Will respond with a network request.`); } try { response = await this._getFromNetwork(request, event); } catch (err) { error = err; } { if (response) { logs.push(`Got response from network.`); } else { logs.push(`Unable to get a response from the network.`); } } } else { { logs.push(`Found a cached response in the '${this._cacheName}' cache.`); } } { logger_mjs.logger.groupCollapsed(messages.strategyStart('CacheFirst', request)); for (let log of logs) { logger_mjs.logger.log(log); } messages.printFinalResponse(response); logger_mjs.logger.groupEnd(); } if (!response) { throw new WorkboxError_mjs.WorkboxError('no-response', { url: request.url, error }); } return response; } /** * Handles the network and cache part of CacheFirst. * * @param {Request} request * @param {FetchEvent} [event] * @return {Promise} * * @private */ async _getFromNetwork(request, event) { const response = await fetchWrapper_mjs.fetchWrapper.fetch({ request, event, fetchOptions: this._fetchOptions, plugins: this._plugins }); // Keep the service worker while we put the request to the cache const responseClone = response.clone(); const cachePutPromise = cacheWrapper_mjs.cacheWrapper.put({ cacheName: this._cacheName, request, response: responseClone, event, plugins: this._plugins }); if (event) { try { event.waitUntil(cachePutPromise); } catch (error) { { logger_mjs.logger.warn(`Unable to ensure service worker stays alive when ` + `updating cache for '${getFriendlyURL_mjs.getFriendlyURL(request.url)}'.`); } } } return response; } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * An implementation of a * [cache-only]{@link https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#cache-only} * request strategy. * * This class is useful if you want to take advantage of any * [Workbox plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins}. * * If there is no cache match, this will throw a `WorkboxError` exception. * * @memberof workbox.strategies */ class CacheOnly { /** * @param {Object} options * @param {string} options.cacheName Cache name to store and retrieve * requests. Defaults to cache names provided by * [workbox-core]{@link workbox.core.cacheNames}. * @param {Array} options.plugins [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} * to use in conjunction with this caching strategy. * @param {Object} options.matchOptions [`CacheQueryOptions`](https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions) */ constructor(options = {}) { this._cacheName = cacheNames_mjs.cacheNames.getRuntimeName(options.cacheName); this._plugins = options.plugins || []; this._matchOptions = options.matchOptions || null; } /** * This method will perform a request strategy and follows an API that * will work with the * [Workbox Router]{@link workbox.routing.Router}. * * @param {Object} options * @param {Request} options.request The request to run this strategy for. * @param {Event} [options.event] The event that triggered the request. * @return {Promise} */ async handle({ event, request }) { return this.makeRequest({ event, request: request || event.request }); } /** * This method can be used to perform a make a standalone request outside the * context of the [Workbox Router]{@link workbox.routing.Router}. * * See "[Advanced Recipes](https://developers.google.com/web/tools/workbox/guides/advanced-recipes#make-requests)" * for more usage information. * * @param {Object} options * @param {Request|string} options.request Either a * [`Request`]{@link https://developer.mozilla.org/en-US/docs/Web/API/Request} * object, or a string URL, corresponding to the request to be made. * @param {FetchEvent} [options.event] If provided, `event.waitUntil()` will * be called automatically to extend the service worker's lifetime. * @return {Promise} */ async makeRequest({ event, request }) { if (typeof request === 'string') { request = new Request(request); } { assert_mjs.assert.isInstance(request, Request, { moduleName: 'workbox-strategies', className: 'CacheOnly', funcName: 'makeRequest', paramName: 'request' }); } const response = await cacheWrapper_mjs.cacheWrapper.match({ cacheName: this._cacheName, request, event, matchOptions: this._matchOptions, plugins: this._plugins }); { logger_mjs.logger.groupCollapsed(messages.strategyStart('CacheOnly', request)); if (response) { logger_mjs.logger.log(`Found a cached response in the '${this._cacheName}'` + ` cache.`); messages.printFinalResponse(response); } else { logger_mjs.logger.log(`No response found in the '${this._cacheName}' cache.`); } logger_mjs.logger.groupEnd(); } if (!response) { throw new WorkboxError_mjs.WorkboxError('no-response', { url: request.url }); } return response; } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const cacheOkAndOpaquePlugin = { /** * Returns a valid response (to allow caching) if the status is 200 (OK) or * 0 (opaque). * * @param {Object} options * @param {Response} options.response * @return {Response|null} * * @private */ cacheWillUpdate: ({ response }) => { if (response.status === 200 || response.status === 0) { return response; } return null; } }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * An implementation of a * [network first]{@link https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#network-falling-back-to-cache} * request strategy. * * By default, this strategy will cache responses with a 200 status code as * well as [opaque responses]{@link https://developers.google.com/web/tools/workbox/guides/handle-third-party-requests}. * Opaque responses are are cross-origin requests where the response doesn't * support [CORS]{@link https://enable-cors.org/}. * * If the network request fails, and there is no cache match, this will throw * a `WorkboxError` exception. * * @memberof workbox.strategies */ class NetworkFirst { /** * @param {Object} options * @param {string} options.cacheName Cache name to store and retrieve * requests. Defaults to cache names provided by * [workbox-core]{@link workbox.core.cacheNames}. * @param {Array} options.plugins [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} * to use in conjunction with this caching strategy. * @param {Object} options.fetchOptions Values passed along to the * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) * of all fetch() requests made by this strategy. * @param {Object} options.matchOptions [`CacheQueryOptions`](https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions) * @param {number} options.networkTimeoutSeconds If set, any network requests * that fail to respond within the timeout will fallback to the cache. * * This option can be used to combat * "[lie-fi]{@link https://developers.google.com/web/fundamentals/performance/poor-connectivity/#lie-fi}" * scenarios. */ constructor(options = {}) { this._cacheName = cacheNames_mjs.cacheNames.getRuntimeName(options.cacheName); if (options.plugins) { let isUsingCacheWillUpdate = options.plugins.some(plugin => !!plugin.cacheWillUpdate); this._plugins = isUsingCacheWillUpdate ? options.plugins : [cacheOkAndOpaquePlugin, ...options.plugins]; } else { // No plugins passed in, use the default plugin. this._plugins = [cacheOkAndOpaquePlugin]; } this._networkTimeoutSeconds = options.networkTimeoutSeconds; { if (this._networkTimeoutSeconds) { assert_mjs.assert.isType(this._networkTimeoutSeconds, 'number', { moduleName: 'workbox-strategies', className: 'NetworkFirst', funcName: 'constructor', paramName: 'networkTimeoutSeconds' }); } } this._fetchOptions = options.fetchOptions || null; this._matchOptions = options.matchOptions || null; } /** * This method will perform a request strategy and follows an API that * will work with the * [Workbox Router]{@link workbox.routing.Router}. * * @param {Object} options * @param {Request} options.request The request to run this strategy for. * @param {Event} [options.event] The event that triggered the request. * @return {Promise} */ async handle({ event, request }) { return this.makeRequest({ event, request: request || event.request }); } /** * This method can be used to perform a make a standalone request outside the * context of the [Workbox Router]{@link workbox.routing.Router}. * * See "[Advanced Recipes](https://developers.google.com/web/tools/workbox/guides/advanced-recipes#make-requests)" * for more usage information. * * @param {Object} options * @param {Request|string} options.request Either a * [`Request`]{@link https://developer.mozilla.org/en-US/docs/Web/API/Request} * object, or a string URL, corresponding to the request to be made. * @param {FetchEvent} [options.event] If provided, `event.waitUntil()` will * be called automatically to extend the service worker's lifetime. * @return {Promise} */ async makeRequest({ event, request }) { const logs = []; if (typeof request === 'string') { request = new Request(request); } { assert_mjs.assert.isInstance(request, Request, { moduleName: 'workbox-strategies', className: 'NetworkFirst', funcName: 'handle', paramName: 'makeRequest' }); } const promises = []; let timeoutId; if (this._networkTimeoutSeconds) { const { id, promise } = this._getTimeoutPromise({ request, event, logs }); timeoutId = id; promises.push(promise); } const networkPromise = this._getNetworkPromise({ timeoutId, request, event, logs }); promises.push(networkPromise); // Promise.race() will resolve as soon as the first promise resolves. let response = await Promise.race(promises); // If Promise.race() resolved with null, it might be due to a network // timeout + a cache miss. If that were to happen, we'd rather wait until // the networkPromise resolves instead of returning null. // Note that it's fine to await an already-resolved promise, so we don't // have to check to see if it's still "in flight". if (!response) { response = await networkPromise; } { logger_mjs.logger.groupCollapsed(messages.strategyStart('NetworkFirst', request)); for (let log of logs) { logger_mjs.logger.log(log); } messages.printFinalResponse(response); logger_mjs.logger.groupEnd(); } if (!response) { throw new WorkboxError_mjs.WorkboxError('no-response', { url: request.url }); } return response; } /** * @param {Object} options * @param {Request} options.request * @param {Array} options.logs A reference to the logs array * @param {Event} [options.event] * @return {Promise} * * @private */ _getTimeoutPromise({ request, logs, event }) { let timeoutId; const timeoutPromise = new Promise(resolve => { const onNetworkTimeout = async () => { { logs.push(`Timing out the network response at ` + `${this._networkTimeoutSeconds} seconds.`); } resolve((await this._respondFromCache({ request, event }))); }; timeoutId = setTimeout(onNetworkTimeout, this._networkTimeoutSeconds * 1000); }); return { promise: timeoutPromise, id: timeoutId }; } /** * @param {Object} options * @param {number|undefined} options.timeoutId * @param {Request} options.request * @param {Array} options.logs A reference to the logs Array. * @param {Event} [options.event] * @return {Promise} * * @private */ async _getNetworkPromise({ timeoutId, request, logs, event }) { let error; let response; try { response = await fetchWrapper_mjs.fetchWrapper.fetch({ request, event, fetchOptions: this._fetchOptions, plugins: this._plugins }); } catch (err) { error = err; } if (timeoutId) { clearTimeout(timeoutId); } { if (response) { logs.push(`Got response from network.`); } else { logs.push(`Unable to get a response from the network. Will respond ` + `with a cached response.`); } } if (error || !response) { response = await this._respondFromCache({ request, event }); { if (response) { logs.push(`Found a cached response in the '${this._cacheName}'` + ` cache.`); } else { logs.push(`No response found in the '${this._cacheName}' cache.`); } } } else { // Keep the service worker alive while we put the request in the cache const responseClone = response.clone(); const cachePut = cacheWrapper_mjs.cacheWrapper.put({ cacheName: this._cacheName, request, response: responseClone, event, plugins: this._plugins }); if (event) { try { // The event has been responded to so we can keep the SW alive to // respond to the request event.waitUntil(cachePut); } catch (err) { { logger_mjs.logger.warn(`Unable to ensure service worker stays alive when ` + `updating cache for '${getFriendlyURL_mjs.getFriendlyURL(request.url)}'.`); } } } } return response; } /** * Used if the network timeouts or fails to make the request. * * @param {Object} options * @param {Request} request The request to match in the cache * @param {Event} [options.event] * @return {Promise} * * @private */ _respondFromCache({ event, request }) { return cacheWrapper_mjs.cacheWrapper.match({ cacheName: this._cacheName, request, event, matchOptions: this._matchOptions, plugins: this._plugins }); } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * An implementation of a * [network-only]{@link https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#network-only} * request strategy. * * This class is useful if you want to take advantage of any * [Workbox plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins}. * * If the network request fails, this will throw a `WorkboxError` exception. * * @memberof workbox.strategies */ class NetworkOnly { /** * @param {Object} options * @param {string} options.cacheName Cache name to store and retrieve * requests. Defaults to cache names provided by * [workbox-core]{@link workbox.core.cacheNames}. * @param {Array} options.plugins [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} * to use in conjunction with this caching strategy. * @param {Object} options.fetchOptions Values passed along to the * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) * of all fetch() requests made by this strategy. */ constructor(options = {}) { this._cacheName = cacheNames_mjs.cacheNames.getRuntimeName(options.cacheName); this._plugins = options.plugins || []; this._fetchOptions = options.fetchOptions || null; } /** * This method will perform a request strategy and follows an API that * will work with the * [Workbox Router]{@link workbox.routing.Router}. * * @param {Object} options * @param {Request} options.request The request to run this strategy for. * @param {Event} [options.event] The event that triggered the request. * @return {Promise} */ async handle({ event, request }) { return this.makeRequest({ event, request: request || event.request }); } /** * This method can be used to perform a make a standalone request outside the * context of the [Workbox Router]{@link workbox.routing.Router}. * * See "[Advanced Recipes](https://developers.google.com/web/tools/workbox/guides/advanced-recipes#make-requests)" * for more usage information. * * @param {Object} options * @param {Request|string} options.request Either a * [`Request`]{@link https://developer.mozilla.org/en-US/docs/Web/API/Request} * object, or a string URL, corresponding to the request to be made. * @param {FetchEvent} [options.event] If provided, `event.waitUntil()` will * be called automatically to extend the service worker's lifetime. * @return {Promise} */ async makeRequest({ event, request }) { if (typeof request === 'string') { request = new Request(request); } { assert_mjs.assert.isInstance(request, Request, { moduleName: 'workbox-strategies', className: 'NetworkOnly', funcName: 'handle', paramName: 'request' }); } let error; let response; try { response = await fetchWrapper_mjs.fetchWrapper.fetch({ request, event, fetchOptions: this._fetchOptions, plugins: this._plugins }); } catch (err) { error = err; } { logger_mjs.logger.groupCollapsed(messages.strategyStart('NetworkOnly', request)); if (response) { logger_mjs.logger.log(`Got response from network.`); } else { logger_mjs.logger.log(`Unable to get a response from the network.`); } messages.printFinalResponse(response); logger_mjs.logger.groupEnd(); } if (!response) { throw new WorkboxError_mjs.WorkboxError('no-response', { url: request.url, error }); } return response; } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * An implementation of a * [stale-while-revalidate]{@link https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#stale-while-revalidate} * request strategy. * * Resources are requested from both the cache and the network in parallel. * The strategy will respond with the cached version if available, otherwise * wait for the network response. The cache is updated with the network response * with each successful request. * * By default, this strategy will cache responses with a 200 status code as * well as [opaque responses]{@link https://developers.google.com/web/tools/workbox/guides/handle-third-party-requests}. * Opaque responses are are cross-origin requests where the response doesn't * support [CORS]{@link https://enable-cors.org/}. * * If the network request fails, and there is no cache match, this will throw * a `WorkboxError` exception. * * @memberof workbox.strategies */ class StaleWhileRevalidate { /** * @param {Object} options * @param {string} options.cacheName Cache name to store and retrieve * requests. Defaults to cache names provided by * [workbox-core]{@link workbox.core.cacheNames}. * @param {Array} options.plugins [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} * to use in conjunction with this caching strategy. * @param {Object} options.fetchOptions Values passed along to the * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) * of all fetch() requests made by this strategy. * @param {Object} options.matchOptions [`CacheQueryOptions`](https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions) */ constructor(options = {}) { this._cacheName = cacheNames_mjs.cacheNames.getRuntimeName(options.cacheName); this._plugins = options.plugins || []; if (options.plugins) { let isUsingCacheWillUpdate = options.plugins.some(plugin => !!plugin.cacheWillUpdate); this._plugins = isUsingCacheWillUpdate ? options.plugins : [cacheOkAndOpaquePlugin, ...options.plugins]; } else { // No plugins passed in, use the default plugin. this._plugins = [cacheOkAndOpaquePlugin]; } this._fetchOptions = options.fetchOptions || null; this._matchOptions = options.matchOptions || null; } /** * This method will perform a request strategy and follows an API that * will work with the * [Workbox Router]{@link workbox.routing.Router}. * * @param {Object} options * @param {Request} options.request The request to run this strategy for. * @param {Event} [options.event] The event that triggered the request. * @return {Promise} */ async handle({ event, request }) { return this.makeRequest({ event, request: request || event.request }); } /** * This method can be used to perform a make a standalone request outside the * context of the [Workbox Router]{@link workbox.routing.Router}. * * See "[Advanced Recipes](https://developers.google.com/web/tools/workbox/guides/advanced-recipes#make-requests)" * for more usage information. * * @param {Object} options * @param {Request|string} options.request Either a * [`Request`]{@link https://developer.mozilla.org/en-US/docs/Web/API/Request} * object, or a string URL, corresponding to the request to be made. * @param {FetchEvent} [options.event] If provided, `event.waitUntil()` will * be called automatically to extend the service worker's lifetime. * @return {Promise} */ async makeRequest({ event, request }) { const logs = []; if (typeof request === 'string') { request = new Request(request); } { assert_mjs.assert.isInstance(request, Request, { moduleName: 'workbox-strategies', className: 'StaleWhileRevalidate', funcName: 'handle', paramName: 'request' }); } const fetchAndCachePromise = this._getFromNetwork({ request, event }); let response = await cacheWrapper_mjs.cacheWrapper.match({ cacheName: this._cacheName, request, event, matchOptions: this._matchOptions, plugins: this._plugins }); let error; if (response) { { logs.push(`Found a cached response in the '${this._cacheName}'` + ` cache. Will update with the network response in the background.`); } if (event) { try { event.waitUntil(fetchAndCachePromise); } catch (error) { { logger_mjs.logger.warn(`Unable to ensure service worker stays alive when ` + `updating cache for '${getFriendlyURL_mjs.getFriendlyURL(request.url)}'.`); } } } } else { { logs.push(`No response found in the '${this._cacheName}' cache. ` + `Will wait for the network response.`); } try { response = await fetchAndCachePromise; } catch (err) { error = err; } } { logger_mjs.logger.groupCollapsed(messages.strategyStart('StaleWhileRevalidate', request)); for (let log of logs) { logger_mjs.logger.log(log); } messages.printFinalResponse(response); logger_mjs.logger.groupEnd(); } if (!response) { throw new WorkboxError_mjs.WorkboxError('no-response', { url: request.url, error }); } return response; } /** * @param {Object} options * @param {Request} options.request * @param {Event} [options.event] * @return {Promise} * * @private */ async _getFromNetwork({ request, event }) { const response = await fetchWrapper_mjs.fetchWrapper.fetch({ request, event, fetchOptions: this._fetchOptions, plugins: this._plugins }); const cachePutPromise = cacheWrapper_mjs.cacheWrapper.put({ cacheName: this._cacheName, request, response: response.clone(), event, plugins: this._plugins }); if (event) { try { event.waitUntil(cachePutPromise); } catch (error) { { logger_mjs.logger.warn(`Unable to ensure service worker stays alive when ` + `updating cache for '${getFriendlyURL_mjs.getFriendlyURL(request.url)}'.`); } } } return response; } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const mapping = { cacheFirst: CacheFirst, cacheOnly: CacheOnly, networkFirst: NetworkFirst, networkOnly: NetworkOnly, staleWhileRevalidate: StaleWhileRevalidate }; const deprecate = strategy => { const StrategyCtr = mapping[strategy]; return options => { { const strategyCtrName = strategy[0].toUpperCase() + strategy.slice(1); logger_mjs.logger.warn(`The 'workbox.strategies.${strategy}()' function has been ` + `deprecated and will be removed in a future version of Workbox.\n` + `Please use 'new workbox.strategies.${strategyCtrName}()' instead.`); } return new StrategyCtr(options); }; }; /** * @function workbox.strategies.cacheFirst * @param {Object} options See the {@link workbox.strategies.CacheFirst} * constructor for more info. * @deprecated since v4.0.0 */ const cacheFirst = deprecate('cacheFirst'); /** * @function workbox.strategies.cacheOnly * @param {Object} options See the {@link workbox.strategies.CacheOnly} * constructor for more info. * @deprecated since v4.0.0 */ const cacheOnly = deprecate('cacheOnly'); /** * @function workbox.strategies.networkFirst * @param {Object} options See the {@link workbox.strategies.NetworkFirst} * constructor for more info. * @deprecated since v4.0.0 */ const networkFirst = deprecate('networkFirst'); /** * @function workbox.strategies.networkOnly * @param {Object} options See the {@link workbox.strategies.NetworkOnly} * constructor for more info. * @deprecated since v4.0.0 */ const networkOnly = deprecate('networkOnly'); /** * @function workbox.strategies.staleWhileRevalidate * @param {Object} options See the * {@link workbox.strategies.StaleWhileRevalidate} constructor for more info. * @deprecated since v4.0.0 */ const staleWhileRevalidate = deprecate('staleWhileRevalidate'); exports.CacheFirst = CacheFirst; exports.CacheOnly = CacheOnly; exports.NetworkFirst = NetworkFirst; exports.NetworkOnly = NetworkOnly; exports.StaleWhileRevalidate = StaleWhileRevalidate; exports.cacheFirst = cacheFirst; exports.cacheOnly = cacheOnly; exports.networkFirst = networkFirst; exports.networkOnly = networkOnly; exports.staleWhileRevalidate = staleWhileRevalidate; return exports; }({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private)); //# sourceMappingURL=workbox-strategies.dev.js.map ================================================ FILE: public/assets/libs/workbox/workbox-strategies.prod.js ================================================ this.workbox=this.workbox||{},this.workbox.strategies=function(e,t,s,n,r){"use strict";try{self["workbox:strategies:4.1.1"]&&_()}catch(e){}class i{constructor(e={}){this.t=t.cacheNames.getRuntimeName(e.cacheName),this.s=e.plugins||[],this.i=e.fetchOptions||null,this.h=e.matchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){"string"==typeof t&&(t=new Request(t));let n,i=await s.cacheWrapper.match({cacheName:this.t,request:t,event:e,matchOptions:this.h,plugins:this.s});if(!i)try{i=await this.u(t,e)}catch(e){n=e}if(!i)throw new r.WorkboxError("no-response",{url:t.url,error:n});return i}async u(e,t){const r=await n.fetchWrapper.fetch({request:e,event:t,fetchOptions:this.i,plugins:this.s}),i=r.clone(),h=s.cacheWrapper.put({cacheName:this.t,request:e,response:i,event:t,plugins:this.s});if(t)try{t.waitUntil(h)}catch(e){}return r}}class h{constructor(e={}){this.t=t.cacheNames.getRuntimeName(e.cacheName),this.s=e.plugins||[],this.h=e.matchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){"string"==typeof t&&(t=new Request(t));const n=await s.cacheWrapper.match({cacheName:this.t,request:t,event:e,matchOptions:this.h,plugins:this.s});if(!n)throw new r.WorkboxError("no-response",{url:t.url});return n}}const u={cacheWillUpdate:({response:e})=>200===e.status||0===e.status?e:null};class a{constructor(e={}){if(this.t=t.cacheNames.getRuntimeName(e.cacheName),e.plugins){let t=e.plugins.some(e=>!!e.cacheWillUpdate);this.s=t?e.plugins:[u,...e.plugins]}else this.s=[u];this.o=e.networkTimeoutSeconds,this.i=e.fetchOptions||null,this.h=e.matchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){const s=[];"string"==typeof t&&(t=new Request(t));const n=[];let i;if(this.o){const{id:r,promise:h}=this.l({request:t,event:e,logs:s});i=r,n.push(h)}const h=this.q({timeoutId:i,request:t,event:e,logs:s});n.push(h);let u=await Promise.race(n);if(u||(u=await h),!u)throw new r.WorkboxError("no-response",{url:t.url});return u}l({request:e,logs:t,event:s}){let n;return{promise:new Promise(t=>{n=setTimeout(async()=>{t(await this.p({request:e,event:s}))},1e3*this.o)}),id:n}}async q({timeoutId:e,request:t,logs:r,event:i}){let h,u;try{u=await n.fetchWrapper.fetch({request:t,event:i,fetchOptions:this.i,plugins:this.s})}catch(e){h=e}if(e&&clearTimeout(e),h||!u)u=await this.p({request:t,event:i});else{const e=u.clone(),n=s.cacheWrapper.put({cacheName:this.t,request:t,response:e,event:i,plugins:this.s});if(i)try{i.waitUntil(n)}catch(e){}}return u}p({event:e,request:t}){return s.cacheWrapper.match({cacheName:this.t,request:t,event:e,matchOptions:this.h,plugins:this.s})}}class c{constructor(e={}){this.t=t.cacheNames.getRuntimeName(e.cacheName),this.s=e.plugins||[],this.i=e.fetchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){let s,i;"string"==typeof t&&(t=new Request(t));try{i=await n.fetchWrapper.fetch({request:t,event:e,fetchOptions:this.i,plugins:this.s})}catch(e){s=e}if(!i)throw new r.WorkboxError("no-response",{url:t.url,error:s});return i}}class o{constructor(e={}){if(this.t=t.cacheNames.getRuntimeName(e.cacheName),this.s=e.plugins||[],e.plugins){let t=e.plugins.some(e=>!!e.cacheWillUpdate);this.s=t?e.plugins:[u,...e.plugins]}else this.s=[u];this.i=e.fetchOptions||null,this.h=e.matchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){"string"==typeof t&&(t=new Request(t));const n=this.u({request:t,event:e});let i,h=await s.cacheWrapper.match({cacheName:this.t,request:t,event:e,matchOptions:this.h,plugins:this.s});if(h){if(e)try{e.waitUntil(n)}catch(i){}}else try{h=await n}catch(e){i=e}if(!h)throw new r.WorkboxError("no-response",{url:t.url,error:i});return h}async u({request:e,event:t}){const r=await n.fetchWrapper.fetch({request:e,event:t,fetchOptions:this.i,plugins:this.s}),i=s.cacheWrapper.put({cacheName:this.t,request:e,response:r.clone(),event:t,plugins:this.s});if(t)try{t.waitUntil(i)}catch(e){}return r}}const l={cacheFirst:i,cacheOnly:h,networkFirst:a,networkOnly:c,staleWhileRevalidate:o},q=e=>{const t=l[e];return e=>new t(e)},w=q("cacheFirst"),p=q("cacheOnly"),v=q("networkFirst"),y=q("networkOnly"),m=q("staleWhileRevalidate");return e.CacheFirst=i,e.CacheOnly=h,e.NetworkFirst=a,e.NetworkOnly=c,e.StaleWhileRevalidate=o,e.cacheFirst=w,e.cacheOnly=p,e.networkFirst=v,e.networkOnly=y,e.staleWhileRevalidate=m,e}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private); //# sourceMappingURL=workbox-strategies.prod.js.map ================================================ FILE: public/assets/libs/workbox/workbox-streams.dev.js ================================================ this.workbox = this.workbox || {}; this.workbox.streams = (function (exports, logger_mjs, assert_mjs) { 'use strict'; try { self['workbox:streams:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Takes either a Response, a ReadableStream, or a * [BodyInit](https://fetch.spec.whatwg.org/#bodyinit) and returns the * ReadableStreamReader object associated with it. * * @param {workbox.streams.StreamSource} source * @return {ReadableStreamReader} * @private */ function _getReaderFromSource(source) { if (source.body && source.body.getReader) { return source.body.getReader(); } if (source.getReader) { return source.getReader(); } // TODO: This should be possible to do by constructing a ReadableStream, but // I can't get it to work. As a hack, construct a new Response, and use the // reader associated with its body. return new Response(source).body.getReader(); } /** * Takes multiple source Promises, each of which could resolve to a Response, a * ReadableStream, or a [BodyInit](https://fetch.spec.whatwg.org/#bodyinit). * * Returns an object exposing a ReadableStream with each individual stream's * data returned in sequence, along with a Promise which signals when the * stream is finished (useful for passing to a FetchEvent's waitUntil()). * * @param {Array>} sourcePromises * @return {Object<{done: Promise, stream: ReadableStream}>} * * @memberof workbox.streams */ function concatenate(sourcePromises) { { assert_mjs.assert.isArray(sourcePromises, { moduleName: 'workbox-streams', funcName: 'concatenate', paramName: 'sourcePromises' }); } const readerPromises = sourcePromises.map(sourcePromise => { return Promise.resolve(sourcePromise).then(source => { return _getReaderFromSource(source); }); }); let fullyStreamedResolve; let fullyStreamedReject; const done = new Promise((resolve, reject) => { fullyStreamedResolve = resolve; fullyStreamedReject = reject; }); let i = 0; const logMessages = []; const stream = new ReadableStream({ pull(controller) { return readerPromises[i].then(reader => reader.read()).then(result => { if (result.done) { { logMessages.push(['Reached the end of source:', sourcePromises[i]]); } i++; if (i >= readerPromises.length) { // Log all the messages in the group at once in a single group. { logger_mjs.logger.groupCollapsed(`Concatenating ${readerPromises.length} sources.`); for (const message of logMessages) { if (Array.isArray(message)) { logger_mjs.logger.log(...message); } else { logger_mjs.logger.log(message); } } logger_mjs.logger.log('Finished reading all sources.'); logger_mjs.logger.groupEnd(); } controller.close(); fullyStreamedResolve(); return; } return this.pull(controller); } else { controller.enqueue(result.value); } }).catch(error => { { logger_mjs.logger.error('An error occurred:', error); } fullyStreamedReject(error); throw error; }); }, cancel() { { logger_mjs.logger.warn('The ReadableStream was cancelled.'); } fullyStreamedResolve(); } }); return { done, stream }; } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * This is a utility method that determines whether the current browser supports * the features required to create streamed responses. Currently, it checks if * [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream) * is available. * * @param {HeadersInit} [headersInit] If there's no `Content-Type` specified, * `'text/html'` will be used by default. * @return {boolean} `true`, if the current browser meets the requirements for * streaming responses, and `false` otherwise. * * @memberof workbox.streams */ function createHeaders(headersInit = {}) { // See https://github.com/GoogleChrome/workbox/issues/1461 const headers = new Headers(headersInit); if (!headers.has('content-type')) { headers.set('content-type', 'text/html'); } return headers; } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Takes multiple source Promises, each of which could resolve to a Response, a * ReadableStream, or a [BodyInit](https://fetch.spec.whatwg.org/#bodyinit), * along with a * [HeadersInit](https://fetch.spec.whatwg.org/#typedefdef-headersinit). * * Returns an object exposing a Response whose body consists of each individual * stream's data returned in sequence, along with a Promise which signals when * the stream is finished (useful for passing to a FetchEvent's waitUntil()). * * @param {Array>} sourcePromises * @param {HeadersInit} [headersInit] If there's no `Content-Type` specified, * `'text/html'` will be used by default. * @return {Object<{done: Promise, response: Response}>} * * @memberof workbox.streams */ function concatenateToResponse(sourcePromises, headersInit) { const { done, stream } = concatenate(sourcePromises); const headers = createHeaders(headersInit); const response = new Response(stream, { headers }); return { done, response }; } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ let cachedIsSupported = undefined; /** * This is a utility method that determines whether the current browser supports * the features required to create streamed responses. Currently, it checks if * [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream) * can be created. * * @return {boolean} `true`, if the current browser meets the requirements for * streaming responses, and `false` otherwise. * * @memberof workbox.streams */ function isSupported() { if (cachedIsSupported === undefined) { // See https://github.com/GoogleChrome/workbox/issues/1473 try { new ReadableStream({ start() {} }); cachedIsSupported = true; } catch (error) { cachedIsSupported = false; } } return cachedIsSupported; } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * A shortcut to create a strategy that could be dropped-in to Workbox's router. * * On browsers that do not support constructing new `ReadableStream`s, this * strategy will automatically wait for all the `sourceFunctions` to complete, * and create a final response that concatenates their values together. * * @param { * Array} sourceFunctions * Each function should return a {@link workbox.streams.StreamSource} (or a * Promise which resolves to one). * @param {HeadersInit} [headersInit] If there's no `Content-Type` specified, * `'text/html'` will be used by default. * @return {workbox.routing.Route~handlerCallback} * * @memberof workbox.streams */ function strategy(sourceFunctions, headersInit) { return async ({ event, url, params }) => { if (isSupported()) { const { done, response } = concatenateToResponse(sourceFunctions.map(fn => fn({ event, url, params })), headersInit); event.waitUntil(done); return response; } { logger_mjs.logger.log(`The current browser doesn't support creating response ` + `streams. Falling back to non-streaming response instead.`); } // Fallback to waiting for everything to finish, and concatenating the // responses. const parts = await Promise.all(sourceFunctions.map(sourceFunction => sourceFunction({ event, url, params })).map(async responsePromise => { const response = await responsePromise; if (response instanceof Response) { return response.blob(); } // Otherwise, assume it's something like a string which can be used // as-is when constructing the final composite blob. return response; })); const headers = createHeaders(headersInit); // Constructing a new Response from a Blob source is well-supported. // So is constructing a new Blob from multiple source Blobs or strings. return new Response(new Blob(parts), { headers }); }; } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ exports.concatenate = concatenate; exports.concatenateToResponse = concatenateToResponse; exports.isSupported = isSupported; exports.strategy = strategy; return exports; }({}, workbox.core._private, workbox.core._private)); //# sourceMappingURL=workbox-streams.dev.js.map ================================================ FILE: public/assets/libs/workbox/workbox-streams.prod.js ================================================ this.workbox=this.workbox||{},this.workbox.streams=function(e){"use strict";try{self["workbox:streams:4.1.1"]&&_()}catch(e){}function n(e){const n=e.map(e=>Promise.resolve(e).then(e=>(function(e){return e.body&&e.body.getReader?e.body.getReader():e.getReader?e.getReader():new Response(e).body.getReader()})(e)));let t,r;const s=new Promise((e,n)=>{t=e,r=n});let o=0;return{done:s,stream:new ReadableStream({pull(e){return n[o].then(e=>e.read()).then(r=>{if(r.done)return++o>=n.length?(e.close(),void t()):this.pull(e);e.enqueue(r.value)}).catch(e=>{throw r(e),e})},cancel(){t()}})}}function t(e={}){const n=new Headers(e);return n.has("content-type")||n.set("content-type","text/html"),n}function r(e,r){const{done:s,stream:o}=n(e),a=t(r);return{done:s,response:new Response(o,{headers:a})}}let s=void 0;function o(){if(void 0===s)try{new ReadableStream({start(){}}),s=!0}catch(e){s=!1}return s}return e.concatenate=n,e.concatenateToResponse=r,e.isSupported=o,e.strategy=function(e,n){return async({event:s,url:a,params:c})=>{if(o()){const{done:t,response:o}=r(e.map(e=>e({event:s,url:a,params:c})),n);return s.waitUntil(t),o}const i=await Promise.all(e.map(e=>e({event:s,url:a,params:c})).map(async e=>{const n=await e;return n instanceof Response?n.blob():n})),u=t(n);return new Response(new Blob(i),{headers:u})}},e}({}); //# sourceMappingURL=workbox-streams.prod.js.map ================================================ FILE: public/assets/libs/workbox/workbox-sw.js ================================================ !function(){"use strict";try{self["workbox:sw:4.1.1"]&&_()}catch(t){}const t="https://storage.googleapis.com/workbox-cdn/releases/4.1.1",e={backgroundSync:"background-sync",broadcastUpdate:"broadcast-update",cacheableResponse:"cacheable-response",core:"core",expiration:"expiration",googleAnalytics:"offline-ga",navigationPreload:"navigation-preload",precaching:"precaching",rangeRequests:"range-requests",routing:"routing",strategies:"strategies",streams:"streams"};self.workbox=new class{constructor(){return this.v={},this.t={debug:"localhost"===self.location.hostname,modulePathPrefix:null,modulePathCb:null},this.s=this.t.debug?"dev":"prod",this.o=!1,new Proxy(this,{get(t,s){if(t[s])return t[s];const o=e[s];return o&&t.loadModule(`workbox-${o}`),t[s]}})}setConfig(t={}){if(this.o)throw new Error("Config must be set before accessing workbox.* modules");Object.assign(this.t,t),this.s=this.t.debug?"dev":"prod"}loadModule(t){const e=this.i(t);try{importScripts(e),this.o=!0}catch(s){throw console.error(`Unable to import module '${t}' from '${e}'.`),s}}i(e){if(this.t.modulePathCb)return this.t.modulePathCb(e,this.t.debug);let s=[t];const o=`${e}.${this.s}.js`,r=this.t.modulePathPrefix;return r&&""===(s=r.split("/"))[s.length-1]&&s.splice(s.length-1,1),s.push(o),s.join("/")}}}(); //# sourceMappingURL=workbox-sw.js.map ================================================ FILE: public/assets/libs/workbox/workbox-window.dev.es5.mjs ================================================ try { self['workbox:window:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Sends a data object to a service worker via `postMessage` and resolves with * a response (if any). * * A response can be set in a message handler in the service worker by * calling `event.ports[0].postMessage(...)`, which will resolve the promise * returned by `messageSW()`. If no response is set, the promise will not * resolve. * * @param {ServiceWorker} sw The service worker to send the message to. * @param {Object} data An object to send to the service worker. * @return {Promise} * * @memberof module:workbox-window */ var messageSW = function messageSW(sw, data) { return new Promise(function (resolve) { var messageChannel = new MessageChannel(); messageChannel.port1.onmessage = function (evt) { return resolve(evt.data); }; sw.postMessage(data, [messageChannel.port2]); }); }; function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } try { self['workbox:core:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * The Deferred class composes Promises in a way that allows for them to be * resolved or rejected from outside the constructor. In most cases promises * should be used directly, but Deferreds can be necessary when the logic to * resolve a promise must be separate. * * @private */ var Deferred = /** * Creates a promise and exposes its resolve and reject functions as methods. */ function Deferred() { var _this = this; this.promise = new Promise(function (resolve, reject) { _this.resolve = resolve; _this.reject = reject; }); }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ var logger = function () { var inGroup = false; var methodToColorMap = { debug: "#7f8c8d", // Gray log: "#2ecc71", // Green warn: "#f39c12", // Yellow error: "#c0392b", // Red groupCollapsed: "#3498db", // Blue groupEnd: null // No colored prefix on groupEnd }; var print = function print(method, args) { var _console2; if (method === 'groupCollapsed') { // Safari doesn't print all console.groupCollapsed() arguments: // https://bugs.webkit.org/show_bug.cgi?id=182754 if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { var _console; (_console = console)[method].apply(_console, args); return; } } var styles = ["background: " + methodToColorMap[method], "border-radius: 0.5em", "color: white", "font-weight: bold", "padding: 2px 0.5em"]; // When in a group, the workbox prefix is not displayed. var logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')]; (_console2 = console)[method].apply(_console2, logPrefix.concat(args)); if (method === 'groupCollapsed') { inGroup = true; } if (method === 'groupEnd') { inGroup = false; } }; var api = {}; var _arr = Object.keys(methodToColorMap); var _loop = function _loop() { var method = _arr[_i]; api[method] = function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } print(method, args); }; }; for (var _i = 0; _i < _arr.length; _i++) { _loop(); } return api; }(); /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * A minimal `EventTarget` shim. * This is necessary because not all browsers support constructable * `EventTarget`, so using a real `EventTarget` will error. * @private */ var EventTargetShim = /*#__PURE__*/ function () { /** * Creates an event listener registry */ function EventTargetShim() { // A registry of event types to listeners. this._eventListenerRegistry = {}; } /** * @param {string} type * @param {Function} listener */ var _proto = EventTargetShim.prototype; _proto.addEventListener = function addEventListener(type, listener) { this._getEventListenersByType(type).add(listener); }; /** * @param {string} type * @param {Function} listener */ _proto.removeEventListener = function removeEventListener(type, listener) { this._getEventListenersByType(type).delete(listener); }; /** * @param {Event} event */ _proto.dispatchEvent = function dispatchEvent(event) { event.target = this; this._getEventListenersByType(event.type).forEach(function (listener) { return listener(event); }); }; /** * Returns a Set of listeners associated with the passed event type. * If no handlers have been registered, an empty Set is returned. * * @param {string} type The event type. * @return {Set} An array of handler functions. */ _proto._getEventListenersByType = function _getEventListenersByType(type) { return this._eventListenerRegistry[type] = this._eventListenerRegistry[type] || new Set(); }; return EventTargetShim; }(); /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Returns true if two URLs have the same `.href` property. The URLS can be * relative, and if they are the current location href is used to resolve URLs. * * @private * @param {string} url1 * @param {string} url2 * @return {boolean} */ var urlsMatch = function urlsMatch(url1, url2) { return new URL(url1, location).href === new URL(url2, location).href; }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * A minimal `Event` subclass shim. * This doesn't *actually* subclass `Event` because not all browsers support * constructable `EventTarget`, and using a real `Event` will error. * @private */ var WorkboxEvent = /** * @param {string} type * @param {Object} props */ function WorkboxEvent(type, props) { Object.assign(this, props, { type: type }); }; function _catch(body, recover) { try { var result = body(); } catch (e) { return recover(e); } if (result && result.then) { return result.then(void 0, recover); } return result; } function _async(f) { return function () { for (var args = [], i = 0; i < arguments.length; i++) { args[i] = arguments[i]; } try { return Promise.resolve(f.apply(this, args)); } catch (e) { return Promise.reject(e); } }; } function _invoke(body, then) { var result = body(); if (result && result.then) { return result.then(then); } return then(result); } function _await(value, then, direct) { if (direct) { return then ? then(value) : value; } if (!value || !value.then) { value = Promise.resolve(value); } return then ? value.then(then) : value; } function _awaitIgnored(value, direct) { if (!direct) { return value && value.then ? value.then(_empty) : Promise.resolve(); } } function _empty() {} // `skipWaiting()` wasn't called. This 200 amount wasn't scientifically // chosen, but it seems to avoid false positives in my testing. var WAITING_TIMEOUT_DURATION = 200; // The amount of time after a registration that we can reasonably conclude // that the registration didn't trigger an update. var REGISTRATION_TIMEOUT_DURATION = 60000; /** * A class to aid in handling service worker registration, updates, and * reacting to service worker lifecycle events. * * @fires [message]{@link module:workbox-window.Workbox#message} * @fires [installed]{@link module:workbox-window.Workbox#installed} * @fires [waiting]{@link module:workbox-window.Workbox#waiting} * @fires [controlling]{@link module:workbox-window.Workbox#controlling} * @fires [activated]{@link module:workbox-window.Workbox#activated} * @fires [redundant]{@link module:workbox-window.Workbox#redundant} * @fires [externalinstalled]{@link module:workbox-window.Workbox#externalinstalled} * @fires [externalwaiting]{@link module:workbox-window.Workbox#externalwaiting} * @fires [externalactivated]{@link module:workbox-window.Workbox#externalactivated} * * @memberof module:workbox-window */ var Workbox = /*#__PURE__*/ function (_EventTargetShim) { _inheritsLoose(Workbox, _EventTargetShim); /** * Creates a new Workbox instance with a script URL and service worker * options. The script URL and options are the same as those used when * calling `navigator.serviceWorker.register(scriptURL, options)`. See: * https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register * * @param {string} scriptURL The service worker script associated with this * instance. * @param {Object} [registerOptions] The service worker options associated * with this instance. */ function Workbox(scriptURL, registerOptions) { var _this; if (registerOptions === void 0) { registerOptions = {}; } _this = _EventTargetShim.call(this) || this; _this._scriptURL = scriptURL; _this._registerOptions = registerOptions; _this._updateFoundCount = 0; // Deferreds we can resolve later. _this._swDeferred = new Deferred(); _this._activeDeferred = new Deferred(); _this._controllingDeferred = new Deferred(); // Bind event handler callbacks. _this._onMessage = _this._onMessage.bind(_assertThisInitialized(_assertThisInitialized(_this))); _this._onStateChange = _this._onStateChange.bind(_assertThisInitialized(_assertThisInitialized(_this))); _this._onUpdateFound = _this._onUpdateFound.bind(_assertThisInitialized(_assertThisInitialized(_this))); _this._onControllerChange = _this._onControllerChange.bind(_assertThisInitialized(_assertThisInitialized(_this))); return _this; } /** * Registers a service worker for this instances script URL and service * worker options. By default this method delays registration until after * the window has loaded. * * @param {Object} [options] * @param {Function} [options.immediate=false] Setting this to true will * register the service worker immediately, even if the window has * not loaded (not recommended). */ var _proto = Workbox.prototype; _proto.register = _async(function (_temp) { var _this2 = this; var _ref = _temp === void 0 ? {} : _temp, _ref$immediate = _ref.immediate, immediate = _ref$immediate === void 0 ? false : _ref$immediate; { if (_this2._registrationTime) { logger.error('Cannot re-register a Workbox instance after it has ' + 'been registered. Create a new instance instead.'); return; } } return _invoke(function () { if (!immediate && document.readyState !== 'complete') { return _awaitIgnored(new Promise(function (res) { return addEventListener('load', res); })); } }, function () { // Set this flag to true if any service worker was controlling the page // at registration time. _this2._isUpdate = Boolean(navigator.serviceWorker.controller); // Before registering, attempt to determine if a SW is already controlling // the page, and if that SW script (and version, if specified) matches this // instance's script. _this2._compatibleControllingSW = _this2._getControllingSWIfCompatible(); return _await(_this2._registerScript(), function (_this2$_registerScrip) { _this2._registration = _this2$_registerScrip; // If we have a compatible controller, store the controller as the "own" // SW, resolve active/controlling deferreds and add necessary listeners. if (_this2._compatibleControllingSW) { _this2._sw = _this2._compatibleControllingSW; _this2._activeDeferred.resolve(_this2._compatibleControllingSW); _this2._controllingDeferred.resolve(_this2._compatibleControllingSW); _this2._reportWindowReady(_this2._compatibleControllingSW); _this2._compatibleControllingSW.addEventListener('statechange', _this2._onStateChange, { once: true }); } // If there's a waiting service worker with a matching URL before the // `updatefound` event fires, it likely means that this site is open // in another tab, or the user refreshed the page (and thus the prevoius // page wasn't fully unloaded before this page started loading). // https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#waiting var waitingSW = _this2._registration.waiting; if (waitingSW && urlsMatch(waitingSW.scriptURL, _this2._scriptURL)) { // Store the waiting SW as the "own" Sw, even if it means overwriting // a compatible controller. _this2._sw = waitingSW; // Run this in the next microtask, so any code that adds an event // listener after awaiting `register()` will get this event. Promise.resolve().then(function () { _this2.dispatchEvent(new WorkboxEvent('waiting', { sw: waitingSW, wasWaitingBeforeRegister: true })); { logger.warn('A service worker was already waiting to activate ' + 'before this script was registered...'); } }); } // If an "own" SW is already set, resolve the deferred. if (_this2._sw) { _this2._swDeferred.resolve(_this2._sw); } { logger.log('Successfully registered service worker.', _this2._scriptURL); if (navigator.serviceWorker.controller) { if (_this2._compatibleControllingSW) { logger.debug('A service worker with the same script URL ' + 'is already controlling this page.'); } else { logger.debug('A service worker with a different script URL is ' + 'currently controlling the page. The browser is now fetching ' + 'the new script now...'); } } var currentPageIsOutOfScope = function currentPageIsOutOfScope() { var scopeURL = new URL(_this2._registerOptions.scope || _this2._scriptURL, document.baseURI); var scopeURLBasePath = new URL('./', scopeURL.href).pathname; return !location.pathname.startsWith(scopeURLBasePath); }; if (currentPageIsOutOfScope()) { logger.warn('The current page is not in scope for the registered ' + 'service worker. Was this a mistake?'); } } _this2._registration.addEventListener('updatefound', _this2._onUpdateFound); navigator.serviceWorker.addEventListener('controllerchange', _this2._onControllerChange, { once: true }); // Add message listeners. if ('BroadcastChannel' in self) { _this2._broadcastChannel = new BroadcastChannel('workbox'); _this2._broadcastChannel.addEventListener('message', _this2._onMessage); } navigator.serviceWorker.addEventListener('message', _this2._onMessage); return _this2._registration; }); }); }); /** * Resolves to the service worker registered by this instance as soon as it * is active. If a service worker was already controlling at registration * time then it will resolve to that if the script URLs (and optionally * script versions) match, otherwise it will wait until an update is found * and activates. * * @return {Promise} */ /** * Resolves with a reference to a service worker that matches the script URL * of this instance, as soon as it's available. * * If, at registration time, there's already an active or waiting service * worker with a matching script URL, it will be used (with the waiting * service worker taking precedence over the active service worker if both * match, since the waiting service worker would have been registered more * recently). * If there's no matching active or waiting service worker at registration * time then the promise will not resolve until an update is found and starts * installing, at which point the installing service worker is used. * * @return {Promise} */ _proto.getSW = _async(function () { var _this3 = this; // If `this._sw` is set, resolve with that as we want `getSW()` to // return the correct (new) service worker if an update is found. return _this3._sw || _this3._swDeferred.promise; }); /** * Sends the passed data object to the service worker registered by this * instance (via [`getSW()`]{@link module:workbox-window.Workbox#getSW}) and resolves * with a response (if any). * * A response can be set in a message handler in the service worker by * calling `event.ports[0].postMessage(...)`, which will resolve the promise * returned by `messageSW()`. If no response is set, the promise will never * resolve. * * @param {Object} data An object to send to the service worker * @return {Promise} */ _proto.messageSW = _async(function (data) { var _this4 = this; return _await(_this4.getSW(), function (sw) { return messageSW(sw, data); }); }); /** * Checks for a service worker already controlling the page and returns * it if its script URL matchs. * * @private * @return {ServiceWorker|undefined} */ _proto._getControllingSWIfCompatible = function _getControllingSWIfCompatible() { var controller = navigator.serviceWorker.controller; if (controller && urlsMatch(controller.scriptURL, this._scriptURL)) { return controller; } }; /** * Registers a service worker for this instances script URL and register * options and tracks the time registration was complete. * * @private */ _proto._registerScript = _async(function () { var _this5 = this; return _catch(function () { return _await(navigator.serviceWorker.register(_this5._scriptURL, _this5._registerOptions), function (reg) { // Keep track of when registration happened, so it can be used in the // `this._onUpdateFound` heuristic. Also use the presence of this // property as a way to see if `.register()` has been called. _this5._registrationTime = performance.now(); return reg; }); }, function (error) { { logger.error(error); } // Re-throw the error. throw error; }); }); /** * Sends a message to the passed service worker that the window is ready. * * @param {ServiceWorker} sw * @private */ _proto._reportWindowReady = function _reportWindowReady(sw) { messageSW(sw, { type: 'WINDOW_READY', meta: 'workbox-window' }); }; /** * @private */ _proto._onUpdateFound = function _onUpdateFound() { var installingSW = this._registration.installing; // If the script URL passed to `navigator.serviceWorker.register()` is // different from the current controlling SW's script URL, we know any // successful registration calls will trigger an `updatefound` event. // But if the registered script URL is the same as the current controlling // SW's script URL, we'll only get an `updatefound` event if the file // changed since it was last registered. This can be a problem if the user // opens up the same page in a different tab, and that page registers // a SW that triggers an update. It's a problem because this page has no // good way of knowing whether the `updatefound` event came from the SW // script it registered or from a registration attempt made by a newer // version of the page running in another tab. // To minimize the possibility of a false positive, we use the logic here: var updateLikelyTriggeredExternally = // Since we enforce only calling `register()` once, and since we don't // add the `updatefound` event listener until the `register()` call, if // `_updateFoundCount` is > 0 then it means this method has already // been called, thus this SW must be external this._updateFoundCount > 0 || // If the script URL of the installing SW is different from this // instance's script URL, we know it's definitely not from our // registration. !urlsMatch(installingSW.scriptURL, this._scriptURL) || // If all of the above are false, then we use a time-based heuristic: // Any `updatefound` event that occurs long after our registration is // assumed to be external. performance.now() > this._registrationTime + REGISTRATION_TIMEOUT_DURATION ? // If any of the above are not true, we assume the update was // triggered by this instance. true : false; if (updateLikelyTriggeredExternally) { this._externalSW = installingSW; this._registration.removeEventListener('updatefound', this._onUpdateFound); } else { // If the update was not triggered externally we know the installing // SW is the one we registered, so we set it. this._sw = installingSW; this._swDeferred.resolve(installingSW); // The `installing` state isn't something we have a dedicated // callback for, but we do log messages for it in development. { if (navigator.serviceWorker.controller) { logger.log('Updated service worker found. Installing now...'); } else { logger.log('Service worker is installing...'); } } } // Increment the `updatefound` count, so future invocations of this // method can be sure they were triggered externally. ++this._updateFoundCount; // Add a `statechange` listener regardless of whether this update was // triggered externally, since we have callbacks for both. installingSW.addEventListener('statechange', this._onStateChange); }; /** * @private * @param {Event} originalEvent */ _proto._onStateChange = function _onStateChange(originalEvent) { var _this6 = this; var sw = originalEvent.target; var state = sw.state; var isExternal = sw === this._externalSW; var eventPrefix = isExternal ? 'external' : ''; var eventProps = { sw: sw, originalEvent: originalEvent }; if (!isExternal && this._isUpdate) { eventProps.isUpdate = true; } this.dispatchEvent(new WorkboxEvent(eventPrefix + state, eventProps)); if (state === 'installed') { // This timeout is used to ignore cases where the service worker calls // `skipWaiting()` in the install event, thus moving it directly in the // activating state. (Since all service workers *must* go through the // waiting phase, the only way to detect `skipWaiting()` called in the // install event is to observe that the time spent in the waiting phase // is very short.) // NOTE: we don't need separate timeouts for the own and external SWs // since they can't go through these phases at the same time. this._waitingTimeout = setTimeout(function () { // Ensure the SW is still waiting (it may now be redundant). if (state === 'installed' && _this6._registration.waiting === sw) { _this6.dispatchEvent(new WorkboxEvent(eventPrefix + 'waiting', eventProps)); { if (isExternal) { logger.warn('An external service worker has installed but is ' + 'waiting for this client to close before activating...'); } else { logger.warn('The service worker has installed but is waiting ' + 'for existing clients to close before activating...'); } } } }, WAITING_TIMEOUT_DURATION); } else if (state === 'activating') { clearTimeout(this._waitingTimeout); if (!isExternal) { this._activeDeferred.resolve(sw); } } { switch (state) { case 'installed': if (isExternal) { logger.warn('An external service worker has installed. ' + 'You may want to suggest users reload this page.'); } else { logger.log('Registered service worker installed.'); } break; case 'activated': if (isExternal) { logger.warn('An external service worker has activated.'); } else { logger.log('Registered service worker activated.'); if (sw !== navigator.serviceWorker.controller) { logger.warn('The registered service worker is active but ' + 'not yet controlling the page. Reload or run ' + '`clients.claim()` in the service worker.'); } } break; case 'redundant': if (sw === this._compatibleControllingSW) { logger.log('Previously controlling service worker now redundant!'); } else if (!isExternal) { logger.log('Registered service worker now redundant!'); } break; } } }; /** * @private * @param {Event} originalEvent */ _proto._onControllerChange = function _onControllerChange(originalEvent) { var sw = this._sw; if (sw === navigator.serviceWorker.controller) { this.dispatchEvent(new WorkboxEvent('controlling', { sw: sw, originalEvent: originalEvent })); { logger.log('Registered service worker now controlling this page.'); } this._controllingDeferred.resolve(sw); } }; /** * @private * @param {Event} originalEvent */ _proto._onMessage = function _onMessage(originalEvent) { var data = originalEvent.data; this.dispatchEvent(new WorkboxEvent('message', { data: data, originalEvent: originalEvent })); }; _createClass(Workbox, [{ key: "active", get: function get() { return this._activeDeferred.promise; } /** * Resolves to the service worker registered by this instance as soon as it * is controlling the page. If a service worker was already controlling at * registration time then it will resolve to that if the script URLs (and * optionally script versions) match, otherwise it will wait until an update * is found and starts controlling the page. * Note: the first time a service worker is installed it will active but * not start controlling the page unless `clients.claim()` is called in the * service worker. * * @return {Promise} */ }, { key: "controlling", get: function get() { return this._controllingDeferred.promise; } }]); return Workbox; }(EventTargetShim); // The jsdoc comments below outline the events this instance may dispatch: /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ export { Workbox, messageSW }; //# sourceMappingURL=workbox-window.dev.es5.mjs.map ================================================ FILE: public/assets/libs/workbox/workbox-window.dev.mjs ================================================ try { self['workbox:window:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Sends a data object to a service worker via `postMessage` and resolves with * a response (if any). * * A response can be set in a message handler in the service worker by * calling `event.ports[0].postMessage(...)`, which will resolve the promise * returned by `messageSW()`. If no response is set, the promise will not * resolve. * * @param {ServiceWorker} sw The service worker to send the message to. * @param {Object} data An object to send to the service worker. * @return {Promise} * * @memberof module:workbox-window */ const messageSW = (sw, data) => { return new Promise(resolve => { let messageChannel = new MessageChannel(); messageChannel.port1.onmessage = evt => resolve(evt.data); sw.postMessage(data, [messageChannel.port2]); }); }; try { self['workbox:core:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * The Deferred class composes Promises in a way that allows for them to be * resolved or rejected from outside the constructor. In most cases promises * should be used directly, but Deferreds can be necessary when the logic to * resolve a promise must be separate. * * @private */ class Deferred { /** * Creates a promise and exposes its resolve and reject functions as methods. */ constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } } /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const logger = (() => { let inGroup = false; const methodToColorMap = { debug: `#7f8c8d`, // Gray log: `#2ecc71`, // Green warn: `#f39c12`, // Yellow error: `#c0392b`, // Red groupCollapsed: `#3498db`, // Blue groupEnd: null // No colored prefix on groupEnd }; const print = function (method, args) { if (method === 'groupCollapsed') { // Safari doesn't print all console.groupCollapsed() arguments: // https://bugs.webkit.org/show_bug.cgi?id=182754 if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { console[method](...args); return; } } const styles = [`background: ${methodToColorMap[method]}`, `border-radius: 0.5em`, `color: white`, `font-weight: bold`, `padding: 2px 0.5em`]; // When in a group, the workbox prefix is not displayed. const logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')]; console[method](...logPrefix, ...args); if (method === 'groupCollapsed') { inGroup = true; } if (method === 'groupEnd') { inGroup = false; } }; const api = {}; for (const method of Object.keys(methodToColorMap)) { api[method] = (...args) => { print(method, args); }; } return api; })(); /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * A minimal `EventTarget` shim. * This is necessary because not all browsers support constructable * `EventTarget`, so using a real `EventTarget` will error. * @private */ class EventTargetShim { /** * Creates an event listener registry */ constructor() { // A registry of event types to listeners. this._eventListenerRegistry = {}; } /** * @param {string} type * @param {Function} listener */ addEventListener(type, listener) { this._getEventListenersByType(type).add(listener); } /** * @param {string} type * @param {Function} listener */ removeEventListener(type, listener) { this._getEventListenersByType(type).delete(listener); } /** * @param {Event} event */ dispatchEvent(event) { event.target = this; this._getEventListenersByType(event.type).forEach(listener => listener(event)); } /** * Returns a Set of listeners associated with the passed event type. * If no handlers have been registered, an empty Set is returned. * * @param {string} type The event type. * @return {Set} An array of handler functions. */ _getEventListenersByType(type) { return this._eventListenerRegistry[type] = this._eventListenerRegistry[type] || new Set(); } } /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Returns true if two URLs have the same `.href` property. The URLS can be * relative, and if they are the current location href is used to resolve URLs. * * @private * @param {string} url1 * @param {string} url2 * @return {boolean} */ const urlsMatch = (url1, url2) => { return new URL(url1, location).href === new URL(url2, location).href; }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * A minimal `Event` subclass shim. * This doesn't *actually* subclass `Event` because not all browsers support * constructable `EventTarget`, and using a real `Event` will error. * @private */ class WorkboxEvent { /** * @param {string} type * @param {Object} props */ constructor(type, props) { Object.assign(this, props, { type }); } } /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ // `skipWaiting()` wasn't called. This 200 amount wasn't scientifically // chosen, but it seems to avoid false positives in my testing. const WAITING_TIMEOUT_DURATION = 200; // The amount of time after a registration that we can reasonably conclude // that the registration didn't trigger an update. const REGISTRATION_TIMEOUT_DURATION = 60000; /** * A class to aid in handling service worker registration, updates, and * reacting to service worker lifecycle events. * * @fires [message]{@link module:workbox-window.Workbox#message} * @fires [installed]{@link module:workbox-window.Workbox#installed} * @fires [waiting]{@link module:workbox-window.Workbox#waiting} * @fires [controlling]{@link module:workbox-window.Workbox#controlling} * @fires [activated]{@link module:workbox-window.Workbox#activated} * @fires [redundant]{@link module:workbox-window.Workbox#redundant} * @fires [externalinstalled]{@link module:workbox-window.Workbox#externalinstalled} * @fires [externalwaiting]{@link module:workbox-window.Workbox#externalwaiting} * @fires [externalactivated]{@link module:workbox-window.Workbox#externalactivated} * * @memberof module:workbox-window */ class Workbox extends EventTargetShim { /** * Creates a new Workbox instance with a script URL and service worker * options. The script URL and options are the same as those used when * calling `navigator.serviceWorker.register(scriptURL, options)`. See: * https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register * * @param {string} scriptURL The service worker script associated with this * instance. * @param {Object} [registerOptions] The service worker options associated * with this instance. */ constructor(scriptURL, registerOptions = {}) { super(); this._scriptURL = scriptURL; this._registerOptions = registerOptions; this._updateFoundCount = 0; // Deferreds we can resolve later. this._swDeferred = new Deferred(); this._activeDeferred = new Deferred(); this._controllingDeferred = new Deferred(); // Bind event handler callbacks. this._onMessage = this._onMessage.bind(this); this._onStateChange = this._onStateChange.bind(this); this._onUpdateFound = this._onUpdateFound.bind(this); this._onControllerChange = this._onControllerChange.bind(this); } /** * Registers a service worker for this instances script URL and service * worker options. By default this method delays registration until after * the window has loaded. * * @param {Object} [options] * @param {Function} [options.immediate=false] Setting this to true will * register the service worker immediately, even if the window has * not loaded (not recommended). */ async register({ immediate = false } = {}) { { if (this._registrationTime) { logger.error('Cannot re-register a Workbox instance after it has ' + 'been registered. Create a new instance instead.'); return; } } if (!immediate && document.readyState !== 'complete') { await new Promise(res => addEventListener('load', res)); } // Set this flag to true if any service worker was controlling the page // at registration time. this._isUpdate = Boolean(navigator.serviceWorker.controller); // Before registering, attempt to determine if a SW is already controlling // the page, and if that SW script (and version, if specified) matches this // instance's script. this._compatibleControllingSW = this._getControllingSWIfCompatible(); this._registration = await this._registerScript(); // If we have a compatible controller, store the controller as the "own" // SW, resolve active/controlling deferreds and add necessary listeners. if (this._compatibleControllingSW) { this._sw = this._compatibleControllingSW; this._activeDeferred.resolve(this._compatibleControllingSW); this._controllingDeferred.resolve(this._compatibleControllingSW); this._reportWindowReady(this._compatibleControllingSW); this._compatibleControllingSW.addEventListener('statechange', this._onStateChange, { once: true }); } // If there's a waiting service worker with a matching URL before the // `updatefound` event fires, it likely means that this site is open // in another tab, or the user refreshed the page (and thus the prevoius // page wasn't fully unloaded before this page started loading). // https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#waiting const waitingSW = this._registration.waiting; if (waitingSW && urlsMatch(waitingSW.scriptURL, this._scriptURL)) { // Store the waiting SW as the "own" Sw, even if it means overwriting // a compatible controller. this._sw = waitingSW; // Run this in the next microtask, so any code that adds an event // listener after awaiting `register()` will get this event. Promise.resolve().then(() => { this.dispatchEvent(new WorkboxEvent('waiting', { sw: waitingSW, wasWaitingBeforeRegister: true })); { logger.warn('A service worker was already waiting to activate ' + 'before this script was registered...'); } }); } // If an "own" SW is already set, resolve the deferred. if (this._sw) { this._swDeferred.resolve(this._sw); } { logger.log('Successfully registered service worker.', this._scriptURL); if (navigator.serviceWorker.controller) { if (this._compatibleControllingSW) { logger.debug('A service worker with the same script URL ' + 'is already controlling this page.'); } else { logger.debug('A service worker with a different script URL is ' + 'currently controlling the page. The browser is now fetching ' + 'the new script now...'); } } const currentPageIsOutOfScope = () => { const scopeURL = new URL(this._registerOptions.scope || this._scriptURL, document.baseURI); const scopeURLBasePath = new URL('./', scopeURL.href).pathname; return !location.pathname.startsWith(scopeURLBasePath); }; if (currentPageIsOutOfScope()) { logger.warn('The current page is not in scope for the registered ' + 'service worker. Was this a mistake?'); } } this._registration.addEventListener('updatefound', this._onUpdateFound); navigator.serviceWorker.addEventListener('controllerchange', this._onControllerChange, { once: true }); // Add message listeners. if ('BroadcastChannel' in self) { this._broadcastChannel = new BroadcastChannel('workbox'); this._broadcastChannel.addEventListener('message', this._onMessage); } navigator.serviceWorker.addEventListener('message', this._onMessage); return this._registration; } /** * Resolves to the service worker registered by this instance as soon as it * is active. If a service worker was already controlling at registration * time then it will resolve to that if the script URLs (and optionally * script versions) match, otherwise it will wait until an update is found * and activates. * * @return {Promise} */ get active() { return this._activeDeferred.promise; } /** * Resolves to the service worker registered by this instance as soon as it * is controlling the page. If a service worker was already controlling at * registration time then it will resolve to that if the script URLs (and * optionally script versions) match, otherwise it will wait until an update * is found and starts controlling the page. * Note: the first time a service worker is installed it will active but * not start controlling the page unless `clients.claim()` is called in the * service worker. * * @return {Promise} */ get controlling() { return this._controllingDeferred.promise; } /** * Resolves with a reference to a service worker that matches the script URL * of this instance, as soon as it's available. * * If, at registration time, there's already an active or waiting service * worker with a matching script URL, it will be used (with the waiting * service worker taking precedence over the active service worker if both * match, since the waiting service worker would have been registered more * recently). * If there's no matching active or waiting service worker at registration * time then the promise will not resolve until an update is found and starts * installing, at which point the installing service worker is used. * * @return {Promise} */ async getSW() { // If `this._sw` is set, resolve with that as we want `getSW()` to // return the correct (new) service worker if an update is found. return this._sw || this._swDeferred.promise; } /** * Sends the passed data object to the service worker registered by this * instance (via [`getSW()`]{@link module:workbox-window.Workbox#getSW}) and resolves * with a response (if any). * * A response can be set in a message handler in the service worker by * calling `event.ports[0].postMessage(...)`, which will resolve the promise * returned by `messageSW()`. If no response is set, the promise will never * resolve. * * @param {Object} data An object to send to the service worker * @return {Promise} */ async messageSW(data) { const sw = await this.getSW(); return messageSW(sw, data); } /** * Checks for a service worker already controlling the page and returns * it if its script URL matchs. * * @private * @return {ServiceWorker|undefined} */ _getControllingSWIfCompatible() { const controller = navigator.serviceWorker.controller; if (controller && urlsMatch(controller.scriptURL, this._scriptURL)) { return controller; } } /** * Registers a service worker for this instances script URL and register * options and tracks the time registration was complete. * * @private */ async _registerScript() { try { const reg = await navigator.serviceWorker.register(this._scriptURL, this._registerOptions); // Keep track of when registration happened, so it can be used in the // `this._onUpdateFound` heuristic. Also use the presence of this // property as a way to see if `.register()` has been called. this._registrationTime = performance.now(); return reg; } catch (error) { { logger.error(error); } // Re-throw the error. throw error; } } /** * Sends a message to the passed service worker that the window is ready. * * @param {ServiceWorker} sw * @private */ _reportWindowReady(sw) { messageSW(sw, { type: 'WINDOW_READY', meta: 'workbox-window' }); } /** * @private */ _onUpdateFound() { const installingSW = this._registration.installing; // If the script URL passed to `navigator.serviceWorker.register()` is // different from the current controlling SW's script URL, we know any // successful registration calls will trigger an `updatefound` event. // But if the registered script URL is the same as the current controlling // SW's script URL, we'll only get an `updatefound` event if the file // changed since it was last registered. This can be a problem if the user // opens up the same page in a different tab, and that page registers // a SW that triggers an update. It's a problem because this page has no // good way of knowing whether the `updatefound` event came from the SW // script it registered or from a registration attempt made by a newer // version of the page running in another tab. // To minimize the possibility of a false positive, we use the logic here: let updateLikelyTriggeredExternally = // Since we enforce only calling `register()` once, and since we don't // add the `updatefound` event listener until the `register()` call, if // `_updateFoundCount` is > 0 then it means this method has already // been called, thus this SW must be external this._updateFoundCount > 0 || // If the script URL of the installing SW is different from this // instance's script URL, we know it's definitely not from our // registration. !urlsMatch(installingSW.scriptURL, this._scriptURL) || // If all of the above are false, then we use a time-based heuristic: // Any `updatefound` event that occurs long after our registration is // assumed to be external. performance.now() > this._registrationTime + REGISTRATION_TIMEOUT_DURATION ? // If any of the above are not true, we assume the update was // triggered by this instance. true : false; if (updateLikelyTriggeredExternally) { this._externalSW = installingSW; this._registration.removeEventListener('updatefound', this._onUpdateFound); } else { // If the update was not triggered externally we know the installing // SW is the one we registered, so we set it. this._sw = installingSW; this._swDeferred.resolve(installingSW); // The `installing` state isn't something we have a dedicated // callback for, but we do log messages for it in development. { if (navigator.serviceWorker.controller) { logger.log('Updated service worker found. Installing now...'); } else { logger.log('Service worker is installing...'); } } } // Increment the `updatefound` count, so future invocations of this // method can be sure they were triggered externally. ++this._updateFoundCount; // Add a `statechange` listener regardless of whether this update was // triggered externally, since we have callbacks for both. installingSW.addEventListener('statechange', this._onStateChange); } /** * @private * @param {Event} originalEvent */ _onStateChange(originalEvent) { const sw = originalEvent.target; const { state } = sw; const isExternal = sw === this._externalSW; const eventPrefix = isExternal ? 'external' : ''; const eventProps = { sw, originalEvent }; if (!isExternal && this._isUpdate) { eventProps.isUpdate = true; } this.dispatchEvent(new WorkboxEvent(eventPrefix + state, eventProps)); if (state === 'installed') { // This timeout is used to ignore cases where the service worker calls // `skipWaiting()` in the install event, thus moving it directly in the // activating state. (Since all service workers *must* go through the // waiting phase, the only way to detect `skipWaiting()` called in the // install event is to observe that the time spent in the waiting phase // is very short.) // NOTE: we don't need separate timeouts for the own and external SWs // since they can't go through these phases at the same time. this._waitingTimeout = setTimeout(() => { // Ensure the SW is still waiting (it may now be redundant). if (state === 'installed' && this._registration.waiting === sw) { this.dispatchEvent(new WorkboxEvent(eventPrefix + 'waiting', eventProps)); { if (isExternal) { logger.warn('An external service worker has installed but is ' + 'waiting for this client to close before activating...'); } else { logger.warn('The service worker has installed but is waiting ' + 'for existing clients to close before activating...'); } } } }, WAITING_TIMEOUT_DURATION); } else if (state === 'activating') { clearTimeout(this._waitingTimeout); if (!isExternal) { this._activeDeferred.resolve(sw); } } { switch (state) { case 'installed': if (isExternal) { logger.warn('An external service worker has installed. ' + 'You may want to suggest users reload this page.'); } else { logger.log('Registered service worker installed.'); } break; case 'activated': if (isExternal) { logger.warn('An external service worker has activated.'); } else { logger.log('Registered service worker activated.'); if (sw !== navigator.serviceWorker.controller) { logger.warn('The registered service worker is active but ' + 'not yet controlling the page. Reload or run ' + '`clients.claim()` in the service worker.'); } } break; case 'redundant': if (sw === this._compatibleControllingSW) { logger.log('Previously controlling service worker now redundant!'); } else if (!isExternal) { logger.log('Registered service worker now redundant!'); } break; } } } /** * @private * @param {Event} originalEvent */ _onControllerChange(originalEvent) { const sw = this._sw; if (sw === navigator.serviceWorker.controller) { this.dispatchEvent(new WorkboxEvent('controlling', { sw, originalEvent })); { logger.log('Registered service worker now controlling this page.'); } this._controllingDeferred.resolve(sw); } } /** * @private * @param {Event} originalEvent */ _onMessage(originalEvent) { const { data } = originalEvent; this.dispatchEvent(new WorkboxEvent('message', { data, originalEvent })); } } // The jsdoc comments below outline the events this instance may dispatch: /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ export { Workbox, messageSW }; //# sourceMappingURL=workbox-window.dev.mjs.map ================================================ FILE: public/assets/libs/workbox/workbox-window.dev.umd.js ================================================ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = global || self, factory(global.workbox = {})); }(this, function (exports) { 'use strict'; try { self['workbox:window:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Sends a data object to a service worker via `postMessage` and resolves with * a response (if any). * * A response can be set in a message handler in the service worker by * calling `event.ports[0].postMessage(...)`, which will resolve the promise * returned by `messageSW()`. If no response is set, the promise will not * resolve. * * @param {ServiceWorker} sw The service worker to send the message to. * @param {Object} data An object to send to the service worker. * @return {Promise} * * @memberof module:workbox-window */ var messageSW = function messageSW(sw, data) { return new Promise(function (resolve) { var messageChannel = new MessageChannel(); messageChannel.port1.onmessage = function (evt) { return resolve(evt.data); }; sw.postMessage(data, [messageChannel.port2]); }); }; function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } try { self['workbox:core:4.1.1'] && _(); } catch (e) {} // eslint-disable-line /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * The Deferred class composes Promises in a way that allows for them to be * resolved or rejected from outside the constructor. In most cases promises * should be used directly, but Deferreds can be necessary when the logic to * resolve a promise must be separate. * * @private */ var Deferred = /** * Creates a promise and exposes its resolve and reject functions as methods. */ function Deferred() { var _this = this; this.promise = new Promise(function (resolve, reject) { _this.resolve = resolve; _this.reject = reject; }); }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ var logger = function () { var inGroup = false; var methodToColorMap = { debug: "#7f8c8d", // Gray log: "#2ecc71", // Green warn: "#f39c12", // Yellow error: "#c0392b", // Red groupCollapsed: "#3498db", // Blue groupEnd: null // No colored prefix on groupEnd }; var print = function print(method, args) { var _console2; if (method === 'groupCollapsed') { // Safari doesn't print all console.groupCollapsed() arguments: // https://bugs.webkit.org/show_bug.cgi?id=182754 if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { var _console; (_console = console)[method].apply(_console, args); return; } } var styles = ["background: " + methodToColorMap[method], "border-radius: 0.5em", "color: white", "font-weight: bold", "padding: 2px 0.5em"]; // When in a group, the workbox prefix is not displayed. var logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')]; (_console2 = console)[method].apply(_console2, logPrefix.concat(args)); if (method === 'groupCollapsed') { inGroup = true; } if (method === 'groupEnd') { inGroup = false; } }; var api = {}; var _arr = Object.keys(methodToColorMap); var _loop = function _loop() { var method = _arr[_i]; api[method] = function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } print(method, args); }; }; for (var _i = 0; _i < _arr.length; _i++) { _loop(); } return api; }(); /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * A minimal `EventTarget` shim. * This is necessary because not all browsers support constructable * `EventTarget`, so using a real `EventTarget` will error. * @private */ var EventTargetShim = /*#__PURE__*/ function () { /** * Creates an event listener registry */ function EventTargetShim() { // A registry of event types to listeners. this._eventListenerRegistry = {}; } /** * @param {string} type * @param {Function} listener */ var _proto = EventTargetShim.prototype; _proto.addEventListener = function addEventListener(type, listener) { this._getEventListenersByType(type).add(listener); }; /** * @param {string} type * @param {Function} listener */ _proto.removeEventListener = function removeEventListener(type, listener) { this._getEventListenersByType(type).delete(listener); }; /** * @param {Event} event */ _proto.dispatchEvent = function dispatchEvent(event) { event.target = this; this._getEventListenersByType(event.type).forEach(function (listener) { return listener(event); }); }; /** * Returns a Set of listeners associated with the passed event type. * If no handlers have been registered, an empty Set is returned. * * @param {string} type The event type. * @return {Set} An array of handler functions. */ _proto._getEventListenersByType = function _getEventListenersByType(type) { return this._eventListenerRegistry[type] = this._eventListenerRegistry[type] || new Set(); }; return EventTargetShim; }(); /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Returns true if two URLs have the same `.href` property. The URLS can be * relative, and if they are the current location href is used to resolve URLs. * * @private * @param {string} url1 * @param {string} url2 * @return {boolean} */ var urlsMatch = function urlsMatch(url1, url2) { return new URL(url1, location).href === new URL(url2, location).href; }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * A minimal `Event` subclass shim. * This doesn't *actually* subclass `Event` because not all browsers support * constructable `EventTarget`, and using a real `Event` will error. * @private */ var WorkboxEvent = /** * @param {string} type * @param {Object} props */ function WorkboxEvent(type, props) { Object.assign(this, props, { type: type }); }; function _catch(body, recover) { try { var result = body(); } catch (e) { return recover(e); } if (result && result.then) { return result.then(void 0, recover); } return result; } function _async(f) { return function () { for (var args = [], i = 0; i < arguments.length; i++) { args[i] = arguments[i]; } try { return Promise.resolve(f.apply(this, args)); } catch (e) { return Promise.reject(e); } }; } function _invoke(body, then) { var result = body(); if (result && result.then) { return result.then(then); } return then(result); } function _await(value, then, direct) { if (direct) { return then ? then(value) : value; } if (!value || !value.then) { value = Promise.resolve(value); } return then ? value.then(then) : value; } function _awaitIgnored(value, direct) { if (!direct) { return value && value.then ? value.then(_empty) : Promise.resolve(); } } function _empty() {} // `skipWaiting()` wasn't called. This 200 amount wasn't scientifically // chosen, but it seems to avoid false positives in my testing. var WAITING_TIMEOUT_DURATION = 200; // The amount of time after a registration that we can reasonably conclude // that the registration didn't trigger an update. var REGISTRATION_TIMEOUT_DURATION = 60000; /** * A class to aid in handling service worker registration, updates, and * reacting to service worker lifecycle events. * * @fires [message]{@link module:workbox-window.Workbox#message} * @fires [installed]{@link module:workbox-window.Workbox#installed} * @fires [waiting]{@link module:workbox-window.Workbox#waiting} * @fires [controlling]{@link module:workbox-window.Workbox#controlling} * @fires [activated]{@link module:workbox-window.Workbox#activated} * @fires [redundant]{@link module:workbox-window.Workbox#redundant} * @fires [externalinstalled]{@link module:workbox-window.Workbox#externalinstalled} * @fires [externalwaiting]{@link module:workbox-window.Workbox#externalwaiting} * @fires [externalactivated]{@link module:workbox-window.Workbox#externalactivated} * * @memberof module:workbox-window */ var Workbox = /*#__PURE__*/ function (_EventTargetShim) { _inheritsLoose(Workbox, _EventTargetShim); /** * Creates a new Workbox instance with a script URL and service worker * options. The script URL and options are the same as those used when * calling `navigator.serviceWorker.register(scriptURL, options)`. See: * https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register * * @param {string} scriptURL The service worker script associated with this * instance. * @param {Object} [registerOptions] The service worker options associated * with this instance. */ function Workbox(scriptURL, registerOptions) { var _this; if (registerOptions === void 0) { registerOptions = {}; } _this = _EventTargetShim.call(this) || this; _this._scriptURL = scriptURL; _this._registerOptions = registerOptions; _this._updateFoundCount = 0; // Deferreds we can resolve later. _this._swDeferred = new Deferred(); _this._activeDeferred = new Deferred(); _this._controllingDeferred = new Deferred(); // Bind event handler callbacks. _this._onMessage = _this._onMessage.bind(_assertThisInitialized(_assertThisInitialized(_this))); _this._onStateChange = _this._onStateChange.bind(_assertThisInitialized(_assertThisInitialized(_this))); _this._onUpdateFound = _this._onUpdateFound.bind(_assertThisInitialized(_assertThisInitialized(_this))); _this._onControllerChange = _this._onControllerChange.bind(_assertThisInitialized(_assertThisInitialized(_this))); return _this; } /** * Registers a service worker for this instances script URL and service * worker options. By default this method delays registration until after * the window has loaded. * * @param {Object} [options] * @param {Function} [options.immediate=false] Setting this to true will * register the service worker immediately, even if the window has * not loaded (not recommended). */ var _proto = Workbox.prototype; _proto.register = _async(function (_temp) { var _this2 = this; var _ref = _temp === void 0 ? {} : _temp, _ref$immediate = _ref.immediate, immediate = _ref$immediate === void 0 ? false : _ref$immediate; { if (_this2._registrationTime) { logger.error('Cannot re-register a Workbox instance after it has ' + 'been registered. Create a new instance instead.'); return; } } return _invoke(function () { if (!immediate && document.readyState !== 'complete') { return _awaitIgnored(new Promise(function (res) { return addEventListener('load', res); })); } }, function () { // Set this flag to true if any service worker was controlling the page // at registration time. _this2._isUpdate = Boolean(navigator.serviceWorker.controller); // Before registering, attempt to determine if a SW is already controlling // the page, and if that SW script (and version, if specified) matches this // instance's script. _this2._compatibleControllingSW = _this2._getControllingSWIfCompatible(); return _await(_this2._registerScript(), function (_this2$_registerScrip) { _this2._registration = _this2$_registerScrip; // If we have a compatible controller, store the controller as the "own" // SW, resolve active/controlling deferreds and add necessary listeners. if (_this2._compatibleControllingSW) { _this2._sw = _this2._compatibleControllingSW; _this2._activeDeferred.resolve(_this2._compatibleControllingSW); _this2._controllingDeferred.resolve(_this2._compatibleControllingSW); _this2._reportWindowReady(_this2._compatibleControllingSW); _this2._compatibleControllingSW.addEventListener('statechange', _this2._onStateChange, { once: true }); } // If there's a waiting service worker with a matching URL before the // `updatefound` event fires, it likely means that this site is open // in another tab, or the user refreshed the page (and thus the prevoius // page wasn't fully unloaded before this page started loading). // https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#waiting var waitingSW = _this2._registration.waiting; if (waitingSW && urlsMatch(waitingSW.scriptURL, _this2._scriptURL)) { // Store the waiting SW as the "own" Sw, even if it means overwriting // a compatible controller. _this2._sw = waitingSW; // Run this in the next microtask, so any code that adds an event // listener after awaiting `register()` will get this event. Promise.resolve().then(function () { _this2.dispatchEvent(new WorkboxEvent('waiting', { sw: waitingSW, wasWaitingBeforeRegister: true })); { logger.warn('A service worker was already waiting to activate ' + 'before this script was registered...'); } }); } // If an "own" SW is already set, resolve the deferred. if (_this2._sw) { _this2._swDeferred.resolve(_this2._sw); } { logger.log('Successfully registered service worker.', _this2._scriptURL); if (navigator.serviceWorker.controller) { if (_this2._compatibleControllingSW) { logger.debug('A service worker with the same script URL ' + 'is already controlling this page.'); } else { logger.debug('A service worker with a different script URL is ' + 'currently controlling the page. The browser is now fetching ' + 'the new script now...'); } } var currentPageIsOutOfScope = function currentPageIsOutOfScope() { var scopeURL = new URL(_this2._registerOptions.scope || _this2._scriptURL, document.baseURI); var scopeURLBasePath = new URL('./', scopeURL.href).pathname; return !location.pathname.startsWith(scopeURLBasePath); }; if (currentPageIsOutOfScope()) { logger.warn('The current page is not in scope for the registered ' + 'service worker. Was this a mistake?'); } } _this2._registration.addEventListener('updatefound', _this2._onUpdateFound); navigator.serviceWorker.addEventListener('controllerchange', _this2._onControllerChange, { once: true }); // Add message listeners. if ('BroadcastChannel' in self) { _this2._broadcastChannel = new BroadcastChannel('workbox'); _this2._broadcastChannel.addEventListener('message', _this2._onMessage); } navigator.serviceWorker.addEventListener('message', _this2._onMessage); return _this2._registration; }); }); }); /** * Resolves to the service worker registered by this instance as soon as it * is active. If a service worker was already controlling at registration * time then it will resolve to that if the script URLs (and optionally * script versions) match, otherwise it will wait until an update is found * and activates. * * @return {Promise} */ /** * Resolves with a reference to a service worker that matches the script URL * of this instance, as soon as it's available. * * If, at registration time, there's already an active or waiting service * worker with a matching script URL, it will be used (with the waiting * service worker taking precedence over the active service worker if both * match, since the waiting service worker would have been registered more * recently). * If there's no matching active or waiting service worker at registration * time then the promise will not resolve until an update is found and starts * installing, at which point the installing service worker is used. * * @return {Promise} */ _proto.getSW = _async(function () { var _this3 = this; // If `this._sw` is set, resolve with that as we want `getSW()` to // return the correct (new) service worker if an update is found. return _this3._sw || _this3._swDeferred.promise; }); /** * Sends the passed data object to the service worker registered by this * instance (via [`getSW()`]{@link module:workbox-window.Workbox#getSW}) and resolves * with a response (if any). * * A response can be set in a message handler in the service worker by * calling `event.ports[0].postMessage(...)`, which will resolve the promise * returned by `messageSW()`. If no response is set, the promise will never * resolve. * * @param {Object} data An object to send to the service worker * @return {Promise} */ _proto.messageSW = _async(function (data) { var _this4 = this; return _await(_this4.getSW(), function (sw) { return messageSW(sw, data); }); }); /** * Checks for a service worker already controlling the page and returns * it if its script URL matchs. * * @private * @return {ServiceWorker|undefined} */ _proto._getControllingSWIfCompatible = function _getControllingSWIfCompatible() { var controller = navigator.serviceWorker.controller; if (controller && urlsMatch(controller.scriptURL, this._scriptURL)) { return controller; } }; /** * Registers a service worker for this instances script URL and register * options and tracks the time registration was complete. * * @private */ _proto._registerScript = _async(function () { var _this5 = this; return _catch(function () { return _await(navigator.serviceWorker.register(_this5._scriptURL, _this5._registerOptions), function (reg) { // Keep track of when registration happened, so it can be used in the // `this._onUpdateFound` heuristic. Also use the presence of this // property as a way to see if `.register()` has been called. _this5._registrationTime = performance.now(); return reg; }); }, function (error) { { logger.error(error); } // Re-throw the error. throw error; }); }); /** * Sends a message to the passed service worker that the window is ready. * * @param {ServiceWorker} sw * @private */ _proto._reportWindowReady = function _reportWindowReady(sw) { messageSW(sw, { type: 'WINDOW_READY', meta: 'workbox-window' }); }; /** * @private */ _proto._onUpdateFound = function _onUpdateFound() { var installingSW = this._registration.installing; // If the script URL passed to `navigator.serviceWorker.register()` is // different from the current controlling SW's script URL, we know any // successful registration calls will trigger an `updatefound` event. // But if the registered script URL is the same as the current controlling // SW's script URL, we'll only get an `updatefound` event if the file // changed since it was last registered. This can be a problem if the user // opens up the same page in a different tab, and that page registers // a SW that triggers an update. It's a problem because this page has no // good way of knowing whether the `updatefound` event came from the SW // script it registered or from a registration attempt made by a newer // version of the page running in another tab. // To minimize the possibility of a false positive, we use the logic here: var updateLikelyTriggeredExternally = // Since we enforce only calling `register()` once, and since we don't // add the `updatefound` event listener until the `register()` call, if // `_updateFoundCount` is > 0 then it means this method has already // been called, thus this SW must be external this._updateFoundCount > 0 || // If the script URL of the installing SW is different from this // instance's script URL, we know it's definitely not from our // registration. !urlsMatch(installingSW.scriptURL, this._scriptURL) || // If all of the above are false, then we use a time-based heuristic: // Any `updatefound` event that occurs long after our registration is // assumed to be external. performance.now() > this._registrationTime + REGISTRATION_TIMEOUT_DURATION ? // If any of the above are not true, we assume the update was // triggered by this instance. true : false; if (updateLikelyTriggeredExternally) { this._externalSW = installingSW; this._registration.removeEventListener('updatefound', this._onUpdateFound); } else { // If the update was not triggered externally we know the installing // SW is the one we registered, so we set it. this._sw = installingSW; this._swDeferred.resolve(installingSW); // The `installing` state isn't something we have a dedicated // callback for, but we do log messages for it in development. { if (navigator.serviceWorker.controller) { logger.log('Updated service worker found. Installing now...'); } else { logger.log('Service worker is installing...'); } } } // Increment the `updatefound` count, so future invocations of this // method can be sure they were triggered externally. ++this._updateFoundCount; // Add a `statechange` listener regardless of whether this update was // triggered externally, since we have callbacks for both. installingSW.addEventListener('statechange', this._onStateChange); }; /** * @private * @param {Event} originalEvent */ _proto._onStateChange = function _onStateChange(originalEvent) { var _this6 = this; var sw = originalEvent.target; var state = sw.state; var isExternal = sw === this._externalSW; var eventPrefix = isExternal ? 'external' : ''; var eventProps = { sw: sw, originalEvent: originalEvent }; if (!isExternal && this._isUpdate) { eventProps.isUpdate = true; } this.dispatchEvent(new WorkboxEvent(eventPrefix + state, eventProps)); if (state === 'installed') { // This timeout is used to ignore cases where the service worker calls // `skipWaiting()` in the install event, thus moving it directly in the // activating state. (Since all service workers *must* go through the // waiting phase, the only way to detect `skipWaiting()` called in the // install event is to observe that the time spent in the waiting phase // is very short.) // NOTE: we don't need separate timeouts for the own and external SWs // since they can't go through these phases at the same time. this._waitingTimeout = setTimeout(function () { // Ensure the SW is still waiting (it may now be redundant). if (state === 'installed' && _this6._registration.waiting === sw) { _this6.dispatchEvent(new WorkboxEvent(eventPrefix + 'waiting', eventProps)); { if (isExternal) { logger.warn('An external service worker has installed but is ' + 'waiting for this client to close before activating...'); } else { logger.warn('The service worker has installed but is waiting ' + 'for existing clients to close before activating...'); } } } }, WAITING_TIMEOUT_DURATION); } else if (state === 'activating') { clearTimeout(this._waitingTimeout); if (!isExternal) { this._activeDeferred.resolve(sw); } } { switch (state) { case 'installed': if (isExternal) { logger.warn('An external service worker has installed. ' + 'You may want to suggest users reload this page.'); } else { logger.log('Registered service worker installed.'); } break; case 'activated': if (isExternal) { logger.warn('An external service worker has activated.'); } else { logger.log('Registered service worker activated.'); if (sw !== navigator.serviceWorker.controller) { logger.warn('The registered service worker is active but ' + 'not yet controlling the page. Reload or run ' + '`clients.claim()` in the service worker.'); } } break; case 'redundant': if (sw === this._compatibleControllingSW) { logger.log('Previously controlling service worker now redundant!'); } else if (!isExternal) { logger.log('Registered service worker now redundant!'); } break; } } }; /** * @private * @param {Event} originalEvent */ _proto._onControllerChange = function _onControllerChange(originalEvent) { var sw = this._sw; if (sw === navigator.serviceWorker.controller) { this.dispatchEvent(new WorkboxEvent('controlling', { sw: sw, originalEvent: originalEvent })); { logger.log('Registered service worker now controlling this page.'); } this._controllingDeferred.resolve(sw); } }; /** * @private * @param {Event} originalEvent */ _proto._onMessage = function _onMessage(originalEvent) { var data = originalEvent.data; this.dispatchEvent(new WorkboxEvent('message', { data: data, originalEvent: originalEvent })); }; _createClass(Workbox, [{ key: "active", get: function get() { return this._activeDeferred.promise; } /** * Resolves to the service worker registered by this instance as soon as it * is controlling the page. If a service worker was already controlling at * registration time then it will resolve to that if the script URLs (and * optionally script versions) match, otherwise it will wait until an update * is found and starts controlling the page. * Note: the first time a service worker is installed it will active but * not start controlling the page unless `clients.claim()` is called in the * service worker. * * @return {Promise} */ }, { key: "controlling", get: function get() { return this._controllingDeferred.promise; } }]); return Workbox; }(EventTargetShim); // The jsdoc comments below outline the events this instance may dispatch: /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ exports.Workbox = Workbox; exports.messageSW = messageSW; Object.defineProperty(exports, '__esModule', { value: true }); })); //# sourceMappingURL=workbox-window.dev.umd.js.map ================================================ FILE: public/assets/libs/workbox/workbox-window.prod.es5.mjs ================================================ try{self["workbox:window:4.1.1"]&&_()}catch(n){}var n=function(n,t){return new Promise(function(i){var e=new MessageChannel;e.port1.onmessage=function(n){return i(n.data)},n.postMessage(t,[e.port2])})};function t(n,t){for(var i=0;i0||!r(n.scriptURL,this.t)||performance.now()>this.L+6e4?(this.W=n,this.B.removeEventListener("updatefound",this.g)):(this.O=n,this.u.resolve(n)),++this.o,n.addEventListener("statechange",this.l)},d.l=function(n){var t=this,i=n.target,e=i.state,r=i===this.W,u=r?"external":"",a={sw:i,originalEvent:n};!r&&this.p&&(a.isUpdate=!0),this.dispatchEvent(new o(u+e,a)),"installed"===e?this._=setTimeout(function(){"installed"===e&&t.B.waiting===i&&t.dispatchEvent(new o(u+"waiting",a))},200):"activating"===e&&(clearTimeout(this._),r||this.s.resolve(i))},d.m=function(n){var t=this.O;t===navigator.serviceWorker.controller&&(this.dispatchEvent(new o("controlling",{sw:t,originalEvent:n})),this.h.resolve(t))},d.v=function(n){var t=n.data;this.dispatchEvent(new o("message",{data:t,originalEvent:n}))},l=v,(w=[{key:"active",get:function(){return this.s.promise}},{key:"controlling",get:function(){return this.h.promise}}])&&t(l.prototype,w),g&&t(l,g),v}(function(){function n(){this.D={}}var t=n.prototype;return t.addEventListener=function(n,t){this.T(n).add(t)},t.removeEventListener=function(n,t){this.T(n).delete(t)},t.dispatchEvent=function(n){n.target=this,this.T(n.type).forEach(function(t){return t(n)})},t.T=function(n){return this.D[n]=this.D[n]||new Set},n}());export{c as Workbox,n as messageSW}; //# sourceMappingURL=workbox-window.prod.es5.mjs.map ================================================ FILE: public/assets/libs/workbox/workbox-window.prod.mjs ================================================ try{self["workbox:window:4.1.1"]&&_()}catch(t){}const t=(t,s)=>new Promise(i=>{let e=new MessageChannel;e.port1.onmessage=(t=>i(t.data)),t.postMessage(s,[e.port2])});try{self["workbox:core:4.1.1"]&&_()}catch(t){}class s{constructor(){this.promise=new Promise((t,s)=>{this.resolve=t,this.reject=s})}}class i{constructor(){this.t={}}addEventListener(t,s){this.s(t).add(s)}removeEventListener(t,s){this.s(t).delete(s)}dispatchEvent(t){t.target=this,this.s(t.type).forEach(s=>s(t))}s(t){return this.t[t]=this.t[t]||new Set}}const e=(t,s)=>new URL(t,location).href===new URL(s,location).href;class n{constructor(t,s){Object.assign(this,s,{type:t})}}const h=200,a=6e4;class o extends i{constructor(t,i={}){super(),this.i=t,this.h=i,this.o=0,this.l=new s,this.g=new s,this.u=new s,this.m=this.m.bind(this),this.v=this.v.bind(this),this.p=this.p.bind(this),this._=this._.bind(this)}async register({immediate:t=!1}={}){t||"complete"===document.readyState||await new Promise(t=>addEventListener("load",t)),this.C=Boolean(navigator.serviceWorker.controller),this.W=this.L(),this.S=await this.B(),this.W&&(this.R=this.W,this.g.resolve(this.W),this.u.resolve(this.W),this.P(this.W),this.W.addEventListener("statechange",this.v,{once:!0}));const s=this.S.waiting;return s&&e(s.scriptURL,this.i)&&(this.R=s,Promise.resolve().then(()=>{this.dispatchEvent(new n("waiting",{sw:s,wasWaitingBeforeRegister:!0}))})),this.R&&this.l.resolve(this.R),this.S.addEventListener("updatefound",this.p),navigator.serviceWorker.addEventListener("controllerchange",this._,{once:!0}),"BroadcastChannel"in self&&(this.T=new BroadcastChannel("workbox"),this.T.addEventListener("message",this.m)),navigator.serviceWorker.addEventListener("message",this.m),this.S}get active(){return this.g.promise}get controlling(){return this.u.promise}async getSW(){return this.R||this.l.promise}async messageSW(s){const i=await this.getSW();return t(i,s)}L(){const t=navigator.serviceWorker.controller;if(t&&e(t.scriptURL,this.i))return t}async B(){try{const t=await navigator.serviceWorker.register(this.i,this.h);return this.U=performance.now(),t}catch(t){throw t}}P(s){t(s,{type:"WINDOW_READY",meta:"workbox-window"})}p(){const t=this.S.installing;this.o>0||!e(t.scriptURL,this.i)||performance.now()>this.U+a?(this.k=t,this.S.removeEventListener("updatefound",this.p)):(this.R=t,this.l.resolve(t)),++this.o,t.addEventListener("statechange",this.v)}v(t){const s=t.target,{state:i}=s,e=s===this.k,a=e?"external":"",o={sw:s,originalEvent:t};!e&&this.C&&(o.isUpdate=!0),this.dispatchEvent(new n(a+i,o)),"installed"===i?this.D=setTimeout(()=>{"installed"===i&&this.S.waiting===s&&this.dispatchEvent(new n(a+"waiting",o))},h):"activating"===i&&(clearTimeout(this.D),e||this.g.resolve(s))}_(t){const s=this.R;s===navigator.serviceWorker.controller&&(this.dispatchEvent(new n("controlling",{sw:s,originalEvent:t})),this.u.resolve(s))}m(t){const{data:s}=t;this.dispatchEvent(new n("message",{data:s,originalEvent:t}))}}export{o as Workbox,t as messageSW}; //# sourceMappingURL=workbox-window.prod.mjs.map ================================================ FILE: public/assets/libs/workbox/workbox-window.prod.umd.js ================================================ !function(n,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((n=n||self).workbox={})}(this,function(n){"use strict";try{self["workbox:window:4.1.1"]&&_()}catch(n){}var t=function(n,t){return new Promise(function(i){var e=new MessageChannel;e.port1.onmessage=function(n){return i(n.data)},n.postMessage(t,[e.port2])})};function i(n,t){for(var i=0;i0||!o(n.scriptURL,this.t)||performance.now()>this.C+6e4?(this.L=n,this.R.removeEventListener("updatefound",this.g)):(this._=n,this.u.resolve(n)),++this.o,n.addEventListener("statechange",this.l)},g.l=function(n){var t=this,i=n.target,e=i.state,r=i===this.L,o=r?"external":"",s={sw:i,originalEvent:n};!r&&this.p&&(s.isUpdate=!0),this.dispatchEvent(new u(o+e,s)),"installed"===e?this.W=setTimeout(function(){"installed"===e&&t.R.waiting===i&&t.dispatchEvent(new u(o+"waiting",s))},200):"activating"===e&&(clearTimeout(this.W),r||this.s.resolve(i))},g.m=function(n){var t=this._;t===navigator.serviceWorker.controller&&(this.dispatchEvent(new u("controlling",{sw:t,originalEvent:n})),this.h.resolve(t))},g.v=function(n){var t=n.data;this.dispatchEvent(new u("message",{data:t,originalEvent:n}))},l=v,(w=[{key:"active",get:function(){return this.s.promise}},{key:"controlling",get:function(){return this.h.promise}}])&&i(l.prototype,w),d&&i(l,d),v}(function(){function n(){this.D={}}var t=n.prototype;return t.addEventListener=function(n,t){this.M(n).add(t)},t.removeEventListener=function(n,t){this.M(n).delete(t)},t.dispatchEvent=function(n){n.target=this,this.M(n.type).forEach(function(t){return t(n)})},t.M=function(n){return this.D[n]=this.D[n]||new Set},n}());n.Workbox=f,n.messageSW=t,Object.defineProperty(n,"__esModule",{value:!0})}); //# sourceMappingURL=workbox-window.prod.umd.js.map ================================================ FILE: public/assets/materialsymbols.css ================================================ /* fallback */ @font-face { font-family: "Material Symbols Outlined"; font-style: normal; font-weight: 100 700; src: url(/assets/materialsymbols.woff2) format("woff2"); } .material-symbols-outlined { font-family: "Material Symbols Outlined"; font-weight: normal; font-style: normal; font-size: 24px; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-block; white-space: nowrap; word-wrap: normal; direction: ltr; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; } ================================================ FILE: public/assets/matter.css ================================================ /* Matter 0.2.2 */ /* Components */ /* Button Contained */ .matter-button-contained { --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243)); --matter-helper-ontheme: var(--matter-ontheme-rgb, var(--matter-onprimary-rgb, 255, 255, 255)); position: relative; display: inline-block; box-sizing: border-box; border: none; border-radius: 4px; padding: 0 16px; min-width: 64px; height: 36px; vertical-align: middle; text-align: center; text-overflow: ellipsis; color: rgb(var(--matter-helper-ontheme)); background-color: rgb(var(--matter-helper-theme)); box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12); font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 14px; font-weight: 500; line-height: 36px; outline: none; cursor: var(--cursor-pointer); transition: box-shadow 0.2s; } .matter-button-contained::-moz-focus-inner { border: none; } /* Highlight, Ripple */ .matter-button-contained::before, .matter-button-contained::after { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: inherit; opacity: 0; } .matter-button-contained::before { background-color: rgb(var(--matter-helper-ontheme)); transition: opacity 0.2s; } .matter-button-contained::after { background: radial-gradient(circle at center, currentColor 1%, transparent 1%) center/10000% 10000% no-repeat; transition: opacity 1s, background-size 0.5s; } /* Hover, Focus */ .matter-button-contained:hover, .matter-button-contained:focus { box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12); } .matter-button-contained:hover::before { opacity: 0.08; } .matter-button-contained:focus::before { opacity: 0.24; } .matter-button-contained:hover:focus::before { opacity: 0.32; } /* Active */ .matter-button-contained:active { box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); } .matter-button-contained:active::after { opacity: 0.32; background-size: 100% 100%; transition: background-size 0s; } /* Disabled */ .matter-button-contained:disabled { color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.12); box-shadow: none; cursor: initial; } .matter-button-contained:disabled::before, .matter-button-contained:disabled::after { opacity: 0; } /* Button Unelevated */ .matter-button-unelevated { --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243)); --matter-helper-ontheme: var(--matter-ontheme-rgb, var(--matter-onprimary-rgb, 255, 255, 255)); position: relative; display: inline-block; box-sizing: border-box; border: none; border-radius: 4px; padding: 0 16px; min-width: 64px; height: 36px; vertical-align: middle; text-align: center; text-overflow: ellipsis; color: rgb(var(--matter-helper-ontheme)); background-color: rgb(var(--matter-helper-theme)); font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 14px; font-weight: 500; line-height: 36px; outline: none; cursor: var(--cursor-pointer); } .matter-button-unelevated::-moz-focus-inner { border: none; } /* Highlight, Ripple */ .matter-button-unelevated::before, .matter-button-unelevated::after { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: inherit; opacity: 0; } .matter-button-unelevated::before { background-color: rgb(var(--matter-helper-ontheme)); transition: opacity 0.2s; } .matter-button-unelevated::after { background: radial-gradient(circle at center, currentColor 1%, transparent 1%) center/10000% 10000% no-repeat; transition: opacity 1s, background-size 0.5s; } /* Hover, Focus */ .matter-button-unelevated:hover::before { opacity: 0.08; } .matter-button-unelevated:focus::before { opacity: 0.24; } .matter-button-unelevated:hover:focus::before { opacity: 0.32; } /* Active */ .matter-button-unelevated:active::after { opacity: 0.32; background-size: 100% 100%; transition: background-size 0s; } /* Disabled */ .matter-button-unelevated:disabled { color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.12); cursor: initial; } .matter-button-unelevated:disabled::before, .matter-button-unelevated:disabled::after { opacity: 0; } /* Button Outlined */ .matter-button-outlined { --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243)); position: relative; display: inline-block; box-sizing: border-box; margin: 0; border: solid 1px; /* Safari */ border-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.24); border-radius: 4px; padding: 0 16px; min-width: 64px; height: 36px; vertical-align: middle; text-align: center; text-overflow: ellipsis; color: rgb(var(--matter-helper-theme)); background-color: transparent; font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 14px; font-weight: 500; line-height: 34px; outline: none; cursor: var(--cursor-pointer); } .matter-button-outlined::-moz-focus-inner { border: none; } /* Highlight, Ripple */ .matter-button-outlined::before, .matter-button-outlined::after { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 3px; opacity: 0; } .matter-button-outlined::before { background-color: rgb(var(--matter-helper-theme)); transition: opacity 0.2s; } .matter-button-outlined::after { background: radial-gradient(circle at center, currentColor 1%, transparent 1%) center/10000% 10000% no-repeat; transition: opacity 1s, background-size 0.5s; } /* Hover, Focus */ .matter-button-outlined:hover::before { opacity: 0.04; } .matter-button-outlined:focus::before { opacity: 0.12; } .matter-button-outlined:hover:focus::before { opacity: 0.16; } /* Active */ .matter-button-outlined:active::after { opacity: 0.16; background-size: 100% 100%; transition: background-size 0s; } /* Disabled */ .matter-button-outlined:disabled { color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); background-color: transparent; cursor: initial; } .matter-button-outlined:disabled::before, .matter-button-outlined:disabled::after { opacity: 0; } /* Button Text */ .matter-button-text { --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243)); position: relative; display: inline-block; box-sizing: border-box; margin: 0; border: none; border-radius: 4px; padding: 0 8px; min-width: 64px; height: 36px; vertical-align: middle; text-align: center; text-overflow: ellipsis; color: rgb(var(--matter-helper-theme)); background-color: transparent; font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 14px; font-weight: 500; line-height: 36px; outline: none; cursor: var(--cursor-pointer); } .matter-button-text::-moz-focus-inner { border: none; } /* Highlight, Ripple */ .matter-button-text::before, .matter-button-text::after { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: inherit; opacity: 0; } .matter-button-text::before { background-color: rgb(var(--matter-helper-theme)); transition: opacity 0.2s; } .matter-button-text::after { background: radial-gradient(circle at center, currentColor 1%, transparent 1%) center/10000% 10000% no-repeat; transition: opacity 1s, background-size 0.5s; } /* Hover, Focus */ .matter-button-text:hover::before { opacity: 0.04; } .matter-button-text:focus::before { opacity: 0.12; } .matter-button-text:hover:focus::before { opacity: 0.16; } /* Active */ .matter-button-text:active::after { opacity: 0.16; background-size: 100% 100%; transition: background-size 0s; } /* Disabled */ .matter-button-text:disabled { color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); background-color: transparent; cursor: initial; } .matter-button-text:disabled::before, .matter-button-text:disabled::after { opacity: 0; } /* Link */ .matter-link { --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243)); --matter-helper-safari1: rgba(var(--matter-helper-theme), 0.12); border-radius: 4px; color: rgb(var(--matter-helper-theme)); text-decoration: none; transition: background-color 0.2s, box-shadow 0.2s; } /* Hover */ .matter-link:hover { text-decoration: underline; } /* Focus */ .matter-link:focus { background-color: var(--matter-helper-safari1); box-shadow: 0 0 0 0.16em var(--matter-helper-safari1); outline: none; } /* Active */ .matter-link:active { background-color: transparent; box-shadow: none; } /* Progress Circular */ .matter-progress-circular { --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243)); -webkit-appearance: none; -moz-appearance: none; appearance: none; box-sizing: border-box; border: none; border-radius: 50%; padding: 0.25em; width: 3em; height: 3em; color: rgb(var(--matter-helper-theme)); background-color: transparent; font-size: 16px; overflow: hidden; } .matter-progress-circular::-webkit-progress-bar { background-color: transparent; } /* Indeterminate */ .matter-progress-circular:indeterminate { animation: matter-progress-circular 6s infinite cubic-bezier(0.3, 0.6, 1, 1); } :-ms-lang(x), .matter-progress-circular:indeterminate { animation: none; } .matter-progress-circular:indeterminate::before, .matter-progress-circular:indeterminate::-webkit-progress-value { content: ""; display: block; box-sizing: border-box; margin-bottom: 0.25em; border: solid 0.25em currentColor; border-radius: 50%; width: 100% !important; height: 100%; background-color: transparent; -webkit-clip-path: polygon(50% 50%, 37% 0, 50% 0, 50% 0, 50% 0, 50% 0); clip-path: polygon(50% 50%, 37% 0, 50% 0, 50% 0, 50% 0, 50% 0); animation: matter-progress-circular-pseudo 0.75s infinite linear alternate; animation-play-state: inherit; animation-delay: inherit; } .matter-progress-circular:indeterminate::-moz-progress-bar { box-sizing: border-box; border: solid 0.25em currentColor; border-radius: 50%; width: 100%; height: 100%; background-color: transparent; clip-path: polygon(50% 50%, 37% 0, 50% 0, 50% 0, 50% 0, 50% 0); animation: matter-progress-circular-pseudo 0.75s infinite linear alternate; animation-play-state: inherit; animation-delay: inherit; } .matter-progress-circular:indeterminate::-ms-fill { animation-name: -ms-ring; } @keyframes matter-progress-circular { 0% { transform: rotate(0deg); } 12.5% { transform: rotate(180deg); animation-timing-function: linear; } 25% { transform: rotate(630deg); } 37.5% { transform: rotate(810deg); animation-timing-function: linear; } 50% { transform: rotate(1260deg); } 62.5% { transform: rotate(1440deg); animation-timing-function: linear; } 75% { transform: rotate(1890deg); } 87.5% { transform: rotate(2070deg); animation-timing-function: linear; } 100% { transform: rotate(2520deg); } } @keyframes matter-progress-circular-pseudo { 0% { -webkit-clip-path: polygon(50% 50%, 37% 0, 50% 0, 50% 0, 50% 0, 50% 0); clip-path: polygon(50% 50%, 37% 0, 50% 0, 50% 0, 50% 0, 50% 0); } 18% { -webkit-clip-path: polygon(50% 50%, 37% 0, 100% 0, 100% 0, 100% 0, 100% 0); clip-path: polygon(50% 50%, 37% 0, 100% 0, 100% 0, 100% 0, 100% 0); } 53% { -webkit-clip-path: polygon(50% 50%, 37% 0, 100% 0, 100% 100%, 100% 100%, 100% 100%); clip-path: polygon(50% 50%, 37% 0, 100% 0, 100% 100%, 100% 100%, 100% 100%); } 88% { -webkit-clip-path: polygon(50% 50%, 37% 0, 100% 0, 100% 100%, 0 100%, 0 100%); clip-path: polygon(50% 50%, 37% 0, 100% 0, 100% 100%, 0 100%, 0 100%); } 100% { -webkit-clip-path: polygon(50% 50%, 37% 0, 100% 0, 100% 100%, 0 100%, 0 63%); clip-path: polygon(50% 50%, 37% 0, 100% 0, 100% 100%, 0 100%, 0 63%); } } /* Progress Linear */ .matter-progress-linear { --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243)); -webkit-appearance: none; -moz-appearance: none; appearance: none; border: none; width: 160px; height: 4px; vertical-align: middle; color: rgb(var(--matter-helper-theme)); background-color: rgba(var(--matter-helper-theme), 0.12); } .matter-progress-linear::-webkit-progress-bar { background-color: transparent; } /* Determinate */ .matter-progress-linear::-webkit-progress-value { background-color: currentColor; transition: all 0.2s; } .matter-progress-linear::-moz-progress-bar { background-color: currentColor; transition: all 0.2s; } .matter-progress-linear::-ms-fill { border: none; background-color: currentColor; transition: all 0.2s; } /* Indeterminate */ .matter-progress-linear:indeterminate { background-size: 200% 100%; background-image: linear-gradient(to right, currentColor 16%, transparent 16%), linear-gradient(to right, currentColor 16%, transparent 16%), linear-gradient(to right, currentColor 25%, transparent 25%); animation: matter-progress-linear 1.8s infinite linear; } .matter-progress-linear:indeterminate::-webkit-progress-value { background-color: transparent; } .matter-progress-linear:indeterminate::-moz-progress-bar { background-color: transparent; } .matter-progress-linear:indeterminate::-ms-fill { animation-name: none; } @keyframes matter-progress-linear { 0% { background-position: 32% 0, 32% 0, 50% 0; } 2% { background-position: 32% 0, 32% 0, 50% 0; } 21% { background-position: 32% 0, -18% 0, 0 0; } 42% { background-position: 32% 0, -68% 0, -27% 0; } 50% { background-position: 32% 0, -93% 0, -46% 0; } 56% { background-position: 32% 0, -118% 0, -68% 0; } 66% { background-position: -11% 0, -200% 0, -100% 0; } 71% { background-position: -32% 0, -200% 0, -100% 0; } 79% { background-position: -54% 0, -242% 0, -100% 0; } 86% { background-position: -68% 0, -268% 0, -100% 0; } 100% { background-position: -100% 0, -300% 0, -100% 0; } } /* Checkbox */ .matter-checkbox { --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243)); --matter-helper-ontheme: var(--matter-ontheme-rgb, var(--matter-onprimary-rgb, 255, 255, 255)); z-index: 0; position: relative; display: inline-block; color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87); font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 16px; line-height: 1.5; } /* Box */ .matter-checkbox > input { appearance: none; -moz-appearance: none; -webkit-appearance: none; z-index: 1; position: absolute; display: block; box-sizing: border-box; margin: 3px 1px; border: solid 2px; /* Safari */ border-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6); border-radius: 2px; width: 18px; height: 18px; outline: none; cursor: var(--cursor-pointer); transition: border-color 0.2s, background-color 0.2s; } /* Span */ .matter-checkbox > input + span { display: inline-block; box-sizing: border-box; padding-left: 30px; width: inherit; cursor: var(--cursor-pointer); } /* Highlight */ .matter-checkbox > input + span::before { content: ""; position: absolute; left: -10px; top: -8px; display: block; border-radius: 50%; width: 40px; height: 40px; background-color: rgb(var(--matter-onsurface-rgb, 0, 0, 0)); opacity: 0; transform: scale(1); pointer-events: none; transition: opacity 0.3s, transform 0.2s; } /* Check Mark */ .matter-checkbox > input + span::after { content: ""; z-index: 1; display: block; position: absolute; top: 3px; left: 1px; box-sizing: content-box; width: 10px; height: 5px; border: solid 2px transparent; border-right-width: 0; border-top-width: 0; pointer-events: none; transform: translate(3px, 4px) rotate(-45deg); transition: border-color 0.2s; } /* Checked, Indeterminate */ .matter-checkbox > input:checked, .matter-checkbox > input:indeterminate { border-color: rgb(var(--matter-helper-theme)); background-color: rgb(var(--matter-helper-theme)); } .matter-checkbox > input:checked + span::before, .matter-checkbox > input:indeterminate + span::before { background-color: rgb(var(--matter-helper-theme)); } .matter-checkbox > input:checked + span::after, .matter-checkbox > input:indeterminate + span::after { border-color: rgb(var(--matter-helper-ontheme, 255, 255, 255)); } .matter-checkbox > input:indeterminate + span::after { border-left-width: 0; transform: translate(4px, 3px); } /* Hover, Focus */ .matter-checkbox:hover > input + span::before { opacity: 0.04; } .matter-checkbox > input:focus + span::before { opacity: 0.12; } .matter-checkbox:hover > input:focus + span::before { opacity: 0.16; } /* Active */ .matter-checkbox:active > input, .matter-checkbox:active:hover > input { border-color: rgb(var(--matter-helper-theme)); } .matter-checkbox:active > input:checked { border-color: transparent; background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6); } .matter-checkbox:active > input + span::before { opacity: 1; transform: scale(0); transition: transform 0s, opacity 0s; } /* Disabled */ .matter-checkbox > input:disabled { border-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); cursor: initial; } .matter-checkbox > input:checked:disabled, .matter-checkbox > input:indeterminate:disabled { border-color: transparent; background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); } .matter-checkbox > input:disabled + span { color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); cursor: initial; } .matter-checkbox > input:disabled + span::before { opacity: 0; transform: scale(0); } /* Radio */ .matter-radio { --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243)); z-index: 0; position: relative; display: inline-block; color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87); font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 16px; line-height: 1.5; } /* Circle */ .matter-radio > input { appearance: none; -moz-appearance: none; -webkit-appearance: none; z-index: 1; position: absolute; display: block; box-sizing: border-box; margin: 2px 0; border: solid 2px; /* Safari */ border-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6); border-radius: 50%; width: 20px; height: 20px; outline: none; cursor: var(--cursor-pointer); transition: border-color 0.2s; } /* Span */ .matter-radio > input + span { display: inline-block; box-sizing: border-box; padding-left: 30px; width: inherit; cursor: var(--cursor-pointer); } /* Highlight */ .matter-radio > input + span::before { content: ""; position: absolute; left: -10px; top: -8px; display: block; border-radius: 50%; width: 40px; height: 40px; background-color: rgb(var(--matter-onsurface-rgb, 0, 0, 0)); opacity: 0; transform: scale(0); pointer-events: none; transition: opacity 0.3s, transform 0.2s; } /* Check Mark */ .matter-radio > input + span::after { content: ""; display: block; position: absolute; top: 2px; left: 0; border-radius: 50%; width: 10px; height: 10px; background-color: rgb(var(--matter-helper-theme)); transform: translate(5px, 5px) scale(0); transition: transform 0.2s; } /* Checked */ .matter-radio > input:checked { border-color: rgb(var(--matter-helper-theme)); } .matter-radio > input:checked + span::before { background-color: rgb(var(--matter-helper-theme)); } .matter-radio > input:checked + span::after { transform: translate(5px, 5px) scale(1); } /* Hover, Focus */ .matter-radio:hover > input + span::before { transform: scale(1); opacity: 0.04; } .matter-radio > input:focus + span::before { transform: scale(1); opacity: 0.12; } .matter-radio:hover > input:focus + span::before { transform: scale(1); opacity: 0.16; } /* Active */ .matter-radio:active > input { border-color: rgb(var(--matter-helper-theme)); } .matter-radio:active > input + span::before, .matter-radio:active:hover > input + span::before { opacity: 1; transform: scale(0); transition: transform 0s, opacity 0s; } /* Disabled */ .matter-radio > input:disabled { border-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); cursor: initial; } .matter-radio > input:disabled + span { color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); cursor: initial; } .matter-radio > input:disabled + span::before { opacity: 0; transform: scale(0); } .matter-radio > input:disabled + span::after { background-color: currentColor; } /* Switch */ .matter-switch { --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243)); z-index: 0; position: relative; display: inline-block; color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87); font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 16px; line-height: 1.5; } /* Track */ .matter-switch > input { appearance: none; -moz-appearance: none; -webkit-appearance: none; z-index: 1; position: relative; float: right; display: inline-block; margin: 0 0 0 5px; border: solid 5px transparent; border-radius: 12px; width: 46px; height: 24px; background-clip: padding-box; background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); outline: none; cursor: var(--cursor-pointer); transition: background-color 0.2s, opacity 0.2s; } /* Span */ .matter-switch > input + span { display: inline-block; box-sizing: border-box; margin-right: -51px; padding-right: 51px; width: inherit; cursor: var(--cursor-pointer); } /* Highlight */ .matter-switch > input + span::before { content: ""; position: absolute; right: 11px; top: -8px; display: block; border-radius: 50%; width: 40px; height: 40px; background-color: rgb(var(--matter-onsurface-rgb, 0, 0, 0)); opacity: 0; transform: scale(1); pointer-events: none; transition: opacity 0.3s 0.1s, transform 0.2s 0.1s; } /* Thumb */ .matter-switch > input + span::after { content: ""; z-index: 1; position: absolute; top: 2px; right: 21px; border-radius: 50%; width: 20px; height: 20px; background-color: rgb(var(--matter-surface-rgb, 255, 255, 255)); box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12); pointer-events: none; transition: background-color 0.2s, transform 0.2s; } /* Checked */ .matter-switch > input:checked { background-color: rgba(var(--matter-helper-theme), 0.6); } .matter-switch > input:checked + span::before { right: -5px; background-color: rgb(var(--matter-helper-theme)); } .matter-switch > input:checked + span::after { background-color: rgb(var(--matter-helper-theme)); transform: translateX(16px); } /* Hover, Focus */ .matter-switch:hover > input + span::before { opacity: 0.04; } .matter-switch > input:focus + span::before { opacity: 0.12; } .matter-switch:hover > input:focus + span::before { opacity: 0.16; } /* Active */ .matter-switch:active > input { background-color: rgba(var(--matter-helper-theme), 0.6); } .matter-switch:active > input:checked { background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); } .matter-switch:active > input + span::before { opacity: 1; transform: scale(0); transition: transform 0s, opacity 0s; } /* Disabled */ .matter-switch > input:disabled { background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); opacity: 0.38; cursor: default; } .matter-switch > input:checked:disabled { background-color: rgba(var(--matter-helper-theme), 0.6); } .matter-switch > input:disabled + span { color: rgba(var(--matter-onsurface-rgb, 0, 0, 0, 0.38)); cursor: default; } .matter-switch > input:disabled + span::before { z-index: 1; margin: 10px; width: 20px; height: 20px; background-color: rgb(var(--matter-surface-rgb, 255, 255, 255)); transform: scale(1); opacity: 1; transition: none; } .matter-switch > input:disabled + span::after { opacity: 0.38; } /* Textfield Standard */ .matter-textfield-standard { --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243)); position: relative; display: inline-block; font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 16px; line-height: 1.5; } /* Input, Textarea */ .matter-textfield-standard > input, .matter-textfield-standard > textarea { display: block; box-sizing: border-box; margin: 0; border: none; border-top: solid 24px transparent; border-bottom: solid 1px rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6); padding: 0 0 7px; width: 100%; height: inherit; color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87); -webkit-text-fill-color: currentColor; /* Safari */ background-color: transparent; box-shadow: none; /* Firefox */ font-family: inherit; font-size: inherit; line-height: inherit; caret-color: rgb(var(--matter-helper-theme)); transition: border-bottom 0.2s, background-color 0.2s; } /* Span */ .matter-textfield-standard > input + span, .matter-textfield-standard > textarea + span { position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: block; box-sizing: border-box; padding: 7px 0 0; color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6); font-size: 75%; line-height: 18px; pointer-events: none; transition: color 0.2s, font-size 0.2s, line-height 0.2s; } /* Underline */ .matter-textfield-standard > input + span::after, .matter-textfield-standard > textarea + span::after { content: ""; position: absolute; left: 0; bottom: 0; display: block; width: 100%; height: 2px; background-color: rgb(var(--matter-helper-theme)); transform-origin: bottom center; transform: scaleX(0); transition: transform 0.2s; } /* Hover */ .matter-textfield-standard:hover > input, .matter-textfield-standard:hover > textarea { border-bottom-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87); } /* Placeholder-shown */ .matter-textfield-standard > input:not(:focus):placeholder-shown + span, .matter-textfield-standard > textarea:not(:focus):placeholder-shown + span { font-size: inherit; line-height: 56px; } /* Focus */ .matter-textfield-standard > input:focus, .matter-textfield-standard > textarea:focus { outline: none; } .matter-textfield-standard > input:focus + span, .matter-textfield-standard > textarea:focus + span { color: rgb(var(--matter-helper-theme)); } .matter-textfield-standard > input:focus + span::after, .matter-textfield-standard > textarea:focus + span::after { transform: scale(1); } /* Disabled */ .matter-textfield-standard > input:disabled, .matter-textfield-standard > textarea:disabled { border-bottom-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); } .matter-textfield-standard > input:disabled + span, .matter-textfield-standard > textarea:disabled + span { color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); } /* Faster transition in Safari for less noticable fractional font-size issue */ @media not all and (min-resolution:.001dpcm) { @supports (-webkit-appearance:none) { .matter-textfield-standard > input, .matter-textfield-standard > input + span, .matter-textfield-standard > input + span::after, .matter-textfield-standard > textarea, .matter-textfield-standard > textarea + span, .matter-textfield-standard > textarea + span::after { transition-duration: 0.1s; } } } /* Textfield Filled */ .matter-textfield-filled { --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243)); position: relative; display: inline-block; font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 16px; line-height: 1.5; } /* Input, Textarea */ .matter-textfield-filled > input, .matter-textfield-filled > textarea { display: block; box-sizing: border-box; margin: 0; border: none; border-top: solid 24px transparent; border-bottom: solid 1px rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6); border-radius: 4px 4px 0 0; padding: 0 12px 7px; width: 100%; height: inherit; color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87); -webkit-text-fill-color: currentColor; /* Safari */ background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.04); box-shadow: none; /* Firefox */ font-family: inherit; font-size: inherit; line-height: inherit; caret-color: rgb(var(--matter-helper-theme)); transition: border-bottom 0.2s, background-color 0.2s; } /* Span */ .matter-textfield-filled > input + span, .matter-textfield-filled > textarea + span { position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: block; box-sizing: border-box; padding: 7px 12px 0; color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6); font-size: 75%; line-height: 18px; pointer-events: none; transition: color 0.2s, font-size 0.2s, line-height 0.2s; } /* Underline */ .matter-textfield-filled > input + span::after, .matter-textfield-filled > textarea + span::after { content: ""; position: absolute; left: 0; bottom: 0; display: block; width: 100%; height: 2px; background-color: rgb(var(--matter-helper-theme)); transform-origin: bottom center; transform: scaleX(0); transition: transform 0.3s; } /* Hover */ .matter-textfield-filled:hover > input, .matter-textfield-filled:hover > textarea { border-bottom-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87); background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.08); } /* Placeholder-shown */ .matter-textfield-filled > input:not(:focus):placeholder-shown + span, .matter-textfield-filled > textarea:not(:focus):placeholder-shown + span { font-size: inherit; line-height: 48px; } /* Focus */ .matter-textfield-filled > input:focus, .matter-textfield-filled > textarea:focus { outline: none; } .matter-textfield-filled > input:focus + span, .matter-textfield-filled > textarea:focus + span { color: rgb(var(--matter-helper-theme)); } .matter-textfield-filled > input:focus + span::after, .matter-textfield-filled > textarea:focus + span::after { transform: scale(1); } /* Disabled */ .matter-textfield-filled > input:disabled, .matter-textfield-filled > textarea:disabled { border-bottom-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.24); } .matter-textfield-filled > input:disabled + span, .matter-textfield-filled > textarea:disabled + span { color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); } /* Faster transition in Safari for less noticable fractional font-size issue */ @media not all and (min-resolution:.001dpcm) { @supports (-webkit-appearance:none) { .matter-textfield-filled > input, .matter-textfield-filled > input + span, .matter-textfield-filled > input + span::after, .matter-textfield-filled > textarea, .matter-textfield-filled > textarea + span, .matter-textfield-filled > textarea + span::after { transition-duration: 0.1s; } } } /* Textfield Outlined */ .matter-textfield-outlined { --matter-helper-theme: rgb(var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243))); --matter-helper-safari1: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); --matter-helper-safari2: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6); --matter-helper-safari3: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87); position: relative; display: inline-block; padding-top: 6px; font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 16px; line-height: 1.5; } /* Input, Textarea */ .matter-textfield-outlined > input, .matter-textfield-outlined > textarea { box-sizing: border-box; margin: 0; border-style: solid; border-width: 1px; border-color: transparent var(--matter-helper-safari2) var(--matter-helper-safari2); border-radius: 4px; padding: 15px 13px 15px; width: 100%; height: inherit; color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87); -webkit-text-fill-color: currentColor; /* Safari */ background-color: transparent; box-shadow: inset 1px 0 transparent, inset -1px 0 transparent, inset 0 -1px transparent; font-family: inherit; font-size: inherit; line-height: inherit; caret-color: var(--matter-helper-theme); transition: border 0.2s, box-shadow 0.2s; } .matter-textfield-outlined > input:not(:focus):placeholder-shown, .matter-textfield-outlined > textarea:not(:focus):placeholder-shown { border-top-color: var(--matter-helper-safari2); } /* Span */ .matter-textfield-outlined > input + span, .matter-textfield-outlined > textarea + span { position: absolute; top: 0; left: 0; display: flex; width: 100%; max-height: 100%; color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6); font-size: 75%; line-height: 15px; cursor: text; transition: color 0.2s, font-size 0.2s, line-height 0.2s; } .matter-textfield-outlined > input:not(:focus):placeholder-shown + span, .matter-textfield-outlined > textarea:not(:focus):placeholder-shown + span { font-size: inherit; line-height: 68px; } /* Corners */ .matter-textfield-outlined > input + span::before, .matter-textfield-outlined > input + span::after, .matter-textfield-outlined > textarea + span::before, .matter-textfield-outlined > textarea + span::after { content: ""; display: block; box-sizing: border-box; margin-top: 6px; border-top: solid 1px var(--matter-helper-safari2); min-width: 10px; height: 8px; pointer-events: none; box-shadow: inset 0 1px transparent; transition: border 0.2s, box-shadow 0.2s; } .matter-textfield-outlined > input + span::before, .matter-textfield-outlined > textarea + span::before { margin-right: 4px; border-left: solid 1px transparent; border-radius: 4px 0; } .matter-textfield-outlined > input + span::after, .matter-textfield-outlined > textarea + span::after { flex-grow: 1; margin-left: 4px; border-right: solid 1px transparent; border-radius: 0 4px; } .matter-textfield-outlined > input:not(:focus):placeholder-shown + span::before, .matter-textfield-outlined > textarea:not(:focus):placeholder-shown + span::before, .matter-textfield-outlined > input:not(:focus):placeholder-shown + span::after, .matter-textfield-outlined > textarea:not(:focus):placeholder-shown + span::after { border-top-color: transparent; } /* Hover */ .matter-textfield-outlined:hover > input, .matter-textfield-outlined:hover > textarea { border-color: transparent var(--matter-helper-safari3) var(--matter-helper-safari3); } .matter-textfield-outlined:hover > input + span::before, .matter-textfield-outlined:hover > textarea + span::before, .matter-textfield-outlined:hover > input + span::after, .matter-textfield-outlined:hover > textarea + span::after { border-top-color: var(--matter-helper-safari3); } .matter-textfield-outlined:hover > input:not(:focus):placeholder-shown, .matter-textfield-outlined:hover > textarea:not(:focus):placeholder-shown { border-color: var(--matter-helper-safari3); } /* Focus */ .matter-textfield-outlined > input:focus, .matter-textfield-outlined > textarea:focus { border-color: transparent var(--matter-helper-theme) var(--matter-helper-theme); box-shadow: inset 1px 0 var(--matter-helper-theme), inset -1px 0 var(--matter-helper-theme), inset 0 -1px var(--matter-helper-theme); outline: none; } .matter-textfield-outlined > input:focus + span, .matter-textfield-outlined > textarea:focus + span { color: var(--matter-helper-theme); } .matter-textfield-outlined > input:focus + span::before, .matter-textfield-outlined > input:focus + span::after, .matter-textfield-outlined > textarea:focus + span::before, .matter-textfield-outlined > textarea:focus + span::after { border-top-color: var(--matter-helper-theme) !important; box-shadow: inset 0 1px var(--matter-helper-theme); } /* Disabled */ .matter-textfield-outlined > input:disabled, .matter-textfield-outlined > input:disabled + span, .matter-textfield-outlined > textarea:disabled, .matter-textfield-outlined > textarea:disabled + span { border-color: transparent var(--matter-helper-safari1) var(--matter-helper-safari1) !important; color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38); pointer-events: none; } .matter-textfield-outlined > input:disabled + span::before, .matter-textfield-outlined > input:disabled + span::after, .matter-textfield-outlined > textarea:disabled + span::before, .matter-textfield-outlined > textarea:disabled + span::after { border-top-color: var(--matter-helper-safari1) !important; } .matter-textfield-outlined > input:disabled:placeholder-shown, .matter-textfield-outlined > input:disabled:placeholder-shown + span, .matter-textfield-outlined > textarea:disabled:placeholder-shown, .matter-textfield-outlined > textarea:disabled:placeholder-shown + span { border-top-color: var(--matter-helper-safari1) !important; } .matter-textfield-outlined > input:disabled:placeholder-shown + span::before, .matter-textfield-outlined > input:disabled:placeholder-shown + span::after, .matter-textfield-outlined > textarea:disabled:placeholder-shown + span::before, .matter-textfield-outlined > textarea:disabled:placeholder-shown + span::after { border-top-color: transparent !important; } /* Faster transition in Safari for less noticable fractional font-size issue */ @media not all and (min-resolution:.001dpcm) { @supports (-webkit-appearance:none) { .matter-textfield-outlined > input, .matter-textfield-outlined > input + span, .matter-textfield-outlined > textarea, .matter-textfield-outlined > textarea + span, .matter-textfield-outlined > input + span::before, .matter-textfield-outlined > input + span::after, .matter-textfield-outlined > textarea + span::before, .matter-textfield-outlined > textarea + span::after { transition-duration: 0.1s; } } } /* Tooltip */ .matter-tooltip, .matter-tooltip-top { z-index: 10; position: absolute; left: 0; right: 0; font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 10px; font-weight: 400; line-height: 16px; white-space: nowrap; text-transform: none; text-align: center; pointer-events: none; } .matter-tooltip { bottom: -40px; } .matter-tooltip-top { top: -40px; } .matter-tooltip > span, .matter-tooltip-top > span { position: -webkit-sticky; position: sticky; left: 0; right: 0; display: inline-block; box-sizing: content-box; margin: 0 -100vw; border: solid 8px transparent; border-radius: 12px; padding: 4px 8px; color: rgb(var(--matter-surface-rgb, 255, 255, 255)); background-clip: padding-box; background-image: linear-gradient(rgba(var(--matter-surface-rgb, 255, 255, 255), 0.34), rgba(var(--matter-surface-rgb, 255, 255, 255), 0.34)); background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.85); transform: scale(0); opacity: 0; pointer-events: auto; transition: transform 0.075s, opacity 0.075s; } :not(html):hover > .matter-tooltip > span, .matter-tooltip:hover > span, :not(html):hover > .matter-tooltip-top > span, .matter-tooltip-top:hover > span { transform: scale(1); opacity: 1; transition: transform 0.15s, opacity 0.15s; } :focus-within > .matter-tooltip > span, :focus-within > .matter-tooltip-top > span { transform: scale(1); opacity: 1; transition: transform 0.15s, opacity 0.15s; } /* Non-desktop */ @media (pointer: coarse), (hover: none) { .matter-tooltip, .matter-tooltip-top { font-size: 14px; line-height: 20px; } .matter-tooltip { bottom: -48px; } .matter-tooltip-top { top: -48px; } .matter-tooltip > span, .matter-tooltip-top > span { padding: 6px 16px; } } /* Utilities */ /* Colors */ /* Components */ .matter-primary { --matter-theme-rgb: var(--matter-primary-rgb, 33, 150, 243); --matter-ontheme-rgb: var(--matter-onprimary-rgb, 255, 255, 255); } .matter-secondary { --matter-theme-rgb: var(--matter-secondary-rgb, 102, 0, 238); --matter-ontheme-rgb: var(--matter-onsecondary-rgb, 255, 255, 255); } .matter-error { --matter-theme-rgb: var(--matter-error-rgb, 238, 0, 0); --matter-ontheme-rgb: var(--matter-error-rgb, 255, 255, 255); } .matter-warning { --matter-theme-rgb: var(--matter-warning-rgb, 238, 102, 0); --matter-ontheme-rgb: var(--matter-onwarning-rgb, 255, 255, 255); } .matter-success { --matter-theme-rgb: var(--matter-success-rgb, 17, 136, 34); --matter-ontheme-rgb: var(--matter-onsuccess-rgb, 255, 255, 255); } /* Text */ .matter-primary-text { color: rgb(var(--matter-primary-rgb, 33, 150, 243)); } .matter-secondary-text { color: rgb(var(--matter-secondary-rgb, 102, 0, 238)); } .matter-error-text { color: rgb(var(--matter-error-rgb, 238, 0, 0)); } .matter-warning-text { color: rgb(var(--matter-warning-rgb, 238, 102, 0)); } .matter-success-text { color: rgb(var(--matter-success-rgb, 17, 136, 34)); } /* Typography */ .matter-h1 { font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 96px; font-weight: 300; letter-spacing: -1.5px; line-height: 120px; } .matter-h2 { font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 60px; font-weight: 300; letter-spacing: -0.5px; line-height: 80px; } .matter-h3 { font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 48px; font-weight: 400; letter-spacing: 0; line-height: 64px; } .matter-h4 { font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 34px; font-weight: 400; letter-spacing: 0.25px; line-height: 48px; } .matter-h5 { font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 24px; font-weight: 400; letter-spacing: 0; line-height: 36px; } .matter-h6 { font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 20px; font-weight: 500; letter-spacing: 0.15px; line-height: 28px; } .matter-subtitle1 { font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 16px; font-weight: 400; letter-spacing: 0.15px; line-height: 24px; } .matter-subtitle2 { font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 14px; font-weight: 500; letter-spacing: 0.1px; line-height: 20px; } .matter-body1 { font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 16px; font-weight: 400; letter-spacing: 0.5px; line-height: 24px; } .matter-body2 { font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 14px; font-weight: 400; letter-spacing: 0.25px; line-height: 20px; } .matter-button { font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 14px; font-weight: 500; letter-spacing: 1.25px; text-transform: uppercase; line-height: 20px; } .matter-caption { font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 12px; font-weight: 400; letter-spacing: 0.4px; line-height: 20px; } .matter-overline { font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system); font-size: 10px; font-weight: 400; letter-spacing: 1.5px; text-transform: uppercase; line-height: 16px; } ================================================ FILE: public/cursor_changer.js ================================================ 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; --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); ================================================ FILE: public/lib/dreamland/all.js ================================================ // dreamland.js, MIT license const DLFEATURES = ['css', 'jsxLiterals', 'usestring', 'stores']; const DLVERSION = '0.0.24'; !function(e){const[t,n,r,s,o,l,i]=Array.from(Array(7),Symbol),f="dlcomponent",a={};let c;function u(){if(c)return!0;const e=document.createElement("style");e.textContent="@scope (.test) { :scope { color: red } }",document.head.appendChild(e);const t=document.createElement("div");t.className="test",document.body.appendChild(t);const n=getComputedStyle(t).color;return document.head.removeChild(e),document.body.removeChild(t),c="rgb(255, 0, 0)"==n,c}const d=50;function p(){return`${Array(4).fill(0).map((()=>Math.floor(36*Math.random()).toString(36))).join("")}`}const m=e=>function(t,...n){let r="";for(let e of t)r+=e+(n.shift()||"");return g("dl"+p(),r,e)},h=m(!1),b=m(!0);function g(e,t,n){let r=a[t];if(r)return r;a[t]=e;const s=document.createElement("style");document.head.appendChild(s);let o="",l="";for(t+="\n";;){let[e,...n]=t.split("\n");if(e.trim().endsWith("{"))break;if(l+=e+"\n",!(t=n.join("\n")))break}s.textContent=t;let i=!0;if(i=u(),n&&i){let t="";for(const n of s.sheet.cssRules)n.selectorText||n.media?n.selectorText?.startsWith(":")?(n.selectorText=`.${e}${n.selectorText}`,t+=n.cssText):o+=n.cssText:t+=n.cssText;s.textContent=`.${e} {${l}} @scope (.${e}) to (:not(.${e}).${f} *) { ${o} } ${t}`}else{let t="";n&&!i&&(t=function(e){let t=`:not(${e}).${f}`,n=(r,s)=>`${r} *${s>d?"":`:not(${n(r+" "+(s%2==0?e:t),s+1)})`}`;return`:not(${n(t,0)})`}(`.${e}`));const r=n=>{n.selectorText&&(n.selectorText=n.selectorText.split(",").map((n=>"&"===(n=n.trim())[0]?`.${e}${n.slice(1)}${t}`:":"===n[0]?`.${e}${n}${t}`:`.${e} ${n}${t}`)).join(", ")),o+=n.cssText};for(const e of s.sheet.cssRules)e.media&&e.media.mediaText?(o+=`@media(${e.media.mediaText}){`,Array.from(e.cssRules).map(r),o+="}"):r(e);s.textContent=`.${e} {${l}}${o}`}return e}let y=document;const $=Symbol();let v=!1;Object.defineProperty(window,"use",{get:()=>(v=!0,(e,o,...l)=>{if(e instanceof Array)return x(e,o,...l);T(e)||k(e),v=!1;let i={get value(){return function(e){let o=e[r],l=o[s],i=e[t],f=o[n];for(let e of l)if(f=f[e],!j(f))break;for(let e of i)f=e(f);return f}(i)}};if(k(e)){let n=[...e[t]];o&&n.push(o),i[r]=e[r],i[t]=n}else i[r]=e,i[t]=o?[o]:[];return i})}),Object.defineProperty(window,"useChange",{get:()=>(v=!0,(e,t)=>{v=!1,e=e instanceof Array?e:[e];for(let n of e)T(n)||k(n),A(use(n),t)})});const x=(e,...t)=>{v=!1;let n=w({});const r=[];for(const s in e)if(r.push(e[s]),t[s]){let e=t[s];if(T(e)&&(e=use(e)),k(e)){const t=r.length;let s;A(use(e),(e=>{r[t]=String(e);let o=r.join("");o!=s&&(n.string=o),s=o}))}else r.push(String(e))}return n.string=r.join(""),use(n.string)};let S=new Map;function w(e){j(e),e[o]=[],e[n]=e;let l=Symbol.toPrimitive,f=new Proxy(e,{get(e,o,i){if(v){let f=Symbol(),a=new Proxy({[n]:e,[r]:i,[s]:[o],[l]:()=>f},{get:(e,o)=>[n,r,s,t,l].includes(o)?e[o]:(o=S.get(o)||o,e[s].push(o),a)});return S.set(f,a),a}return Reflect.get(e,o,i)},set(e,t,n){let r=Reflect.set(e,t,n);for(let r of e[o])r(e,t,n);return e[i]&&e[i](e,t,e[t]),r}});return f}let j=e=>e instanceof Object;function L(e){return j(e)&&o in e}function T(e){return j(e)&&s in e}function k(e){return j(e)&&t in e}function A(e,l){k(e);let i,f=e[r],a=e[t],c=[];function u(){let e=f[n];for(i of c)if(e=e[i],!j(e))break;for(let t of a)e=t(e);l(e)}let d=(e,t)=>function r(s,l,i){if(l===c[t]&&e===s&&(u(),j(i))){let e=i[o];e&&!e.includes(r)&&e.push(d(i[n],t+1))}};for(let e in f[s]){let t=f[s][e];j(t)&&t[n]?A(t,(t=>{c[e]=t,u()})):c[e]=t}let p=d(f[n],0);f[n][o].push(p),p(f[n],c[0],f[n][c[0]])}function O(e,t,n){let r,s,o,l;A(e,(e=>{o=s?.[0],o&&(r=o.previousSibling||(l=o.parentNode)),s&&s.forEach((e=>e.remove())),s=E(n?e?n.then:n.otherwise:e,(e=>{r?(l?(r.prepend(e),l=null):r.after(e),r=e):t(e)}))}))}let N=e=>t=>{let n=e[r],o=e[s],l=0;for(;l{e?e=!1:(e=!0,s[l]=t)})),A(use(s[l]),(t=>{e?e=!1:(e=!0,o(t))}))}delete t[e]}}Object.assign(s,t),s.children=[];for(let e of n)E(e,s.children.push.bind(s.children));let o=e.apply(s);o.$=s,s.root=o;let l=o.classList,i=s.css,a=e.name.replaceAll("$","-");return i&&l.add(g(`${a}-${p()}`,i,!0)),s._leak||l.add(f),o.setAttribute("data-component",e.name),"function"==typeof s.mount&&s.mount(),o}let s=t?.xmlns,o=s?y.createElementNS(s,e):y.createElement(e);for(let e of n){E(e,o.append.bind(o))}if(!t)return o;((e,n)=>{if(!(e in t))return;n(t[e]),delete t[e]})("class",(e=>{if("string"==typeof e||e instanceof Array||k(e),"string"!=typeof e)if(k(e)){let t="";A(e,(e=>{for(let e of t.split(" "))e&&o.classList.remove(e);if("string"==typeof e){for(let t of e.split(" "))t&&o.classList.add(t);t=e}}))}else for(let t of e)if(k(t)){let e=null;A(t,(t=>{"string"==typeof e&&o.classList.remove(e),o.classList.add(t),e=t}))}else o.classList.add(t);else o.setAttribute("class",e)}));for(let e in t){let n=t[e];if(e.startsWith("bind:")){k(n);let s=e.substring(5),l=N(n[r]);"this"==s?l(o):"value"==s?(A(n,(e=>o.value=e)),o.addEventListener("change",(()=>l(o.value)))):"checked"==s&&(A(n,(e=>o.checked=e)),o.addEventListener("click",(()=>l(o.checked)))),delete t[e]}if(e.startsWith("class:")){let r=e.substring(6);k(n)?A(n,(e=>{e?o.classList.add(r):o.classList.remove(r)})):n&&o.classList.add(r),delete t[e]}if("style"==e&&j(n)&&!k(n)){for(let e in n){let t=n[e];k(t)?A(t,(t=>o.style[e]=t)):o.style[e]=t}delete t[e]}}for(let e in t){let n=t[e];k(n)?A(n,(t=>{P(o,e,t)})):P(o,e,n)}return s&&(o.innerHTML=o.innerHTML),o}function E(e,t){let n,r,s;if(k(e))O(e,t);else{if(!j(e)||!(l in e)){if(e instanceof Node)return t(e),[e];if(e instanceof Array){for(n of(r=[],e))r=r.concat(E(n,t));return r[0]||(r=E("",t)),r}return null==e&&(e=""),s=y.createTextNode(e),t(s),[s]}O(e[l],t,e)}}function P(e,t,n){if(!n&&e.hasAttribute(t)&&e.removeAttribute(t),n)if(t.startsWith("on:")){let r=t.substring(3);for(let t of r.split("$"))e.addEventListener(t,((...t)=>{self.$el=e,n(...t)}))}else e.setAttribute(t,n)}e.$if=function(e,t,n){return n??=y.createTextNode(""),k(e)?{[l]:e,then:t,otherwise:n}:e?t:n},e.$state=w,e.$store=function(e,{ident:t,backing:r,autosave:s}){let o,l;if("string"==typeof r){if("localstorage"===r)o=()=>localStorage.getItem(t),l=(e,t)=>{localStorage.setItem(e,t)}}else({read:o,write:l}=r);let f=()=>{console.info("[dreamland.js]: saving "+t);let n={},r=0,s=e=>{let t={stateful:L(e),values:{}},o=r++;n[o]=t;for(let n in e){let r=e[n];if(!k(r))switch(typeof r){case"string":case"number":case"boolean":case"undefined":t.values[n]=JSON.stringify(r);break;case"object":if(r instanceof Array){t.values[n]=r.map((e=>"object"==typeof e?s(e):JSON.stringify(e)));break}null===r?t.values[n]="null":(r.__proto__,Object.prototype,t.values[n]=s(r))}}return o};s(e);let o=JSON.stringify(n);l(t,o)},a=(e,t,r)=>{L(r)&&(r[n][i]=a),f()},c=JSON.parse(o(t));if(c){let t={},n=e=>{if(t[e])return t[e];let r=c[e],o={};for(let e in r.values){let t=r.values[e];o[e]="string"==typeof t?JSON.parse(t):t instanceof Array?t.map((e=>"string"==typeof e?JSON.parse(e):n(e))):n(t)}r.stateful&&"auto"==s&&(o[i]=a);let l=r.stateful?w(o):o;return t[e]=l,l};e=n(0)}switch(s){case"beforeunload":addEventListener("beforeunload",f);break;case"manual":break;case"auto":e[i]=a}return w(e)},e.Fragment=$,e.css=h,e.h=C,e.html=function(e,...t){e=[...e];let n="",r={};for(let s=0;s/.exec(e[s+1]);if(/< *$/.test(o)&&i&&(e[s+1]=e[s+1].substr(i.index+i[0].length)),n+=o,se===l));-1!==t?e=Object.keys(r)[t]:(e="h"+p(),r[e]=l),n+=e,i&&(n+=`>`)}}let s=(new DOMParser).parseFromString(n,"text/html");return s.body.children.length,function e(t){let n=t.nodeName.toLowerCase();if("#text"===n)return t.textContent;n in r&&(n=r[n]);let s=[...t.childNodes].map(e);for(let e=0;e `${str} *${i > depth ? '' : `:not(${g(str + ' ' + (i % 2 == 0 ? target : boundary), i + 1)})`}`; return `:not(${g(boundary, 0)})` } /* POLYFILL.SCOPE.END */ function genuid() { return `${Array(4) .fill(0) .map(() => { return Math.floor(Math.random() * 36).toString(36) }) .join('')}` } const csstag = (scoped) => function css(strings, ...values) { let str = ''; for (let f of strings) { str += f + (values.shift() || ''); } return genCss('dl' + genuid(), str, scoped) }; const css = csstag(false); const scope = csstag(true); function genCss(uid, str, scoped) { let cached = cssmap[str]; if (cached) return cached cssmap[str] = uid; const styleElement = document.createElement('style'); document.head.appendChild(styleElement); let newstr = ''; let selfstr = ''; str += '\n'; for (;;) { let [first, ...rest] = str.split('\n'); if (first.trim().endsWith('{')) break selfstr += first + '\n'; str = rest.join('\n'); if (!str) break } styleElement.textContent = str; let scopeSupported = true; /* POLYFILL.SCOPE.START */ scopeSupported = checkScopeSupported(); /* POLYFILL.SCOPE.END */ if (scoped && scopeSupported) { let extstr = ''; for (const rule of styleElement.sheet.cssRules) { if (!rule.selectorText && !rule.media) { extstr += rule.cssText; } else if (rule.selectorText?.startsWith(':')) { rule.selectorText = `.${uid}${rule.selectorText}`; extstr += rule.cssText; } else { newstr += rule.cssText; } } styleElement.textContent = `.${uid} {${selfstr}} @scope (.${uid}) to (:not(.${uid}).${cssBoundary} *) { ${newstr} } ${extstr}`; } else { let scopedSelector = ''; /* POLYFILL.SCOPE.START */ if (scoped && !scopeSupported) scopedSelector = polyfill_scope(`.${uid}`); /* POLYFILL.SCOPE.END */ const processRule = (rule) => { if (rule.selectorText) rule.selectorText = rule.selectorText .split(',') .map((x) => { x = x.trim(); if (x[0] === '&') { return `.${uid}${x.slice(1)}${scopedSelector}` } else if (x[0] === ':') { return `.${uid}${x}${scopedSelector}` } else { return `.${uid} ${x}${scopedSelector}` } }) .join(', '); newstr += rule.cssText; }; for (const rule of styleElement.sheet.cssRules) { if (rule.media && rule.media.mediaText) { newstr += `@media(${rule.media.mediaText}){`; Array.from(rule.cssRules).map(processRule); newstr += '}'; } else { processRule(rule); } } styleElement.textContent = `.${uid} {${selfstr}}${newstr}`; } return uid } /* FEATURE.CSS.END */ // saves a few characters, since document will never change let doc = document; const Fragment = Symbol(); // whether to return the true value from a stateful object or a "trap" containing the pointer let __use_trap = false; // Say you have some code like //// let state = $state({ //// a: $state({ //// b: 1 //// }) //// }) //// let elm =

{window.use(state.a.b)}

// // According to the standard, the order of events is as follows: // - the getter for window.use gets called, setting __use_trap true // - the proxy for state.a is triggered and instead of returning the normal value it returns the trap // - the trap proxy is triggered, storing ["a", "b"] as the order of events // - the function that the getter of `use` returns is called, setting __use_trap to false and restoring order // - the JSX factory h() is now passed the trap, which essentially contains a set of pointers pointing to the theoretical value of b // - with the setter on the stateful proxy, we can listen to any change in any of the nested layers and call whatever listeners registered // - the result is full intuitive reactivity with minimal overhead Object.defineProperty(window, 'use', { get: () => { __use_trap = true; return (ptr, transform, ...rest) => { /* FEATURE.USESTRING.START */ if (ptr instanceof Array) return usestr(ptr, transform, ...rest) /* FEATURE.USESTRING.END */ assert( isDLPtrInternal(ptr) || isDLPtr(ptr), 'a value was passed into use() that was not part of a stateful context' ); __use_trap = false; let newp = { get value() { return resolve(newp) }, }; if (isDLPtr(ptr)) { let cloned = [...ptr[USE_COMPUTED]]; if (transform) { cloned.push(transform); } newp[PROXY] = ptr[PROXY]; newp[USE_COMPUTED] = cloned; } else { newp[PROXY] = ptr; newp[USE_COMPUTED] = transform ? [transform] : []; } return newp } }, }); Object.defineProperty(window, 'useChange', { get: () => { __use_trap = true; return (ptrs, callback) => { __use_trap = false; ptrs = ptrs instanceof Array ? ptrs : [ptrs]; for (let ptr of ptrs) { assert( isDLPtrInternal(ptr) || isDLPtr(ptr), 'a value was passed into useChange() that was not part of a stateful context' ); handle(use(ptr), callback); } } }, }); /* FEATURE.USESTRING.START */ const usestr = (strings, ...values) => { __use_trap = false; let state = $state({}); const flattened_template = []; for (const i in strings) { flattened_template.push(strings[i]); if (values[i]) { let prop = values[i]; if (isDLPtrInternal(prop)) prop = use(prop); if (isDLPtr(prop)) { const current_i = flattened_template.length; let oldparsed; handle(use(prop), (val) => { flattened_template[current_i] = String(val); let parsed = flattened_template.join(''); if (parsed != oldparsed) state.string = parsed; oldparsed = parsed; }); } else { flattened_template.push(String(prop)); } } } state.string = flattened_template.join(''); return use(state.string) }; /* FEATURE.USESTRING.END */ let TRAPS = new Map(); // This wraps the target in a proxy, doing 2 things: // - whenever a property is accessed, return a "trap" that catches and records accessors // - whenever a property is set, notify the subscribed listeners // This is what makes our "pass-by-reference" magic work function $state(target) { assert(isobj(target), '$state() requires an object'); target[LISTENERS] = []; target[TARGET] = target; let TOPRIMITIVE = Symbol.toPrimitive; let proxy = new Proxy(target, { get(target, property, proxy) { if (__use_trap) { let sym = Symbol(); let trap = new Proxy( { [TARGET]: target, [PROXY]: proxy, [STEPS]: [property], [TOPRIMITIVE]: () => sym, }, { get(target, property) { if ( [ TARGET, PROXY, STEPS, USE_COMPUTED, TOPRIMITIVE, ].includes(property) ) return target[property] property = TRAPS.get(property) || property; target[STEPS].push(property); return trap }, } ); TRAPS.set(sym, trap); return trap } return Reflect.get(target, property, proxy) }, set(target, property, val) { let trap = Reflect.set(target, property, val); for (let listener of target[LISTENERS]) { listener(target, property, val); } /* FEATURE.STORES.START */ if (target[STATEHOOK]) target[STATEHOOK](target, property, target[property]); /* FEATURE.STORES.END */ return trap }, }); return proxy } let isobj = (o) => o instanceof Object; let isfn = (o) => typeof o === 'function'; function isStateful(obj) { return isobj(obj) && LISTENERS in obj } function isDLPtrInternal(arr) { return isobj(arr) && STEPS in arr } function isDLPtr(arr) { return isobj(arr) && USE_COMPUTED in arr } function $if(condition, then, otherwise) { otherwise ??= doc.createTextNode(''); if (!isDLPtr(condition)) return condition ? then : otherwise return { [IF]: condition, then, otherwise } } function resolve(exptr) { let proxy = exptr[PROXY]; let steps = proxy[STEPS]; let computed = exptr[USE_COMPUTED]; let val = proxy[TARGET]; for (let step of steps) { val = val[step]; if (!isobj(val)) break } for (let transform of computed) { val = transform(val); } return val } // This lets you subscribe to a stateful object function handle(exptr, callback) { assert(isDLPtr(exptr), 'handle() requires a stateful object'); assert(isfn(callback), 'handle() requires a callback function'); let ptr = exptr[PROXY], computed = exptr[USE_COMPUTED], step, resolvedSteps = []; function update() { let val = ptr[TARGET]; for (step of resolvedSteps) { val = val[step]; if (!isobj(val)) break } for (let transform of computed) { val = transform(val); } callback(val); } // inject ourselves into nested objects let curry = (target, i) => function subscription(tgt, prop, val) { if (prop === resolvedSteps[i] && target === tgt) { update(); if (isobj(val)) { let v = val[LISTENERS]; if (v && !v.includes(subscription)) { v.push(curry(val[TARGET], i + 1)); } } } }; // imagine we have a `use(state.a[state.b])` // simply recursively resolve any of the intermediate steps until we get to the final value // this will "misfire" occassionaly with a scenario like state.a[state.b][state.c] and call the listener more than needed // it is up to the caller to not implode for (let i in ptr[STEPS]) { let step = ptr[STEPS][i]; if (isobj(step) && step[TARGET]) { handle(step, (val) => { resolvedSteps[i] = val; update(); }); continue } resolvedSteps[i] = step; } let sub = curry(ptr[TARGET], 0); ptr[TARGET][LISTENERS].push(sub); sub(ptr[TARGET], resolvedSteps[0], ptr[TARGET][resolvedSteps[0]]); } function JSXAddFixedWrapper(ptr, cb, $if) { let before, appended, first, flag; handle(ptr, (val) => { first = appended?.[0]; if (first) before = first.previousSibling || (flag = first.parentNode); if (appended) appended.forEach((a) => a.remove()); appended = JSXAddChild( $if ? (val ? $if.then : $if.otherwise) : val, (el) => { if (before) { if (flag) { before.prepend(el); flag = null; } else before.after(el); before = el; } else cb(el); } ); }); } // returns a function that sets a reference // the currying is a small optimization let curryset = (ptr) => (val) => { let next = ptr[PROXY]; let steps = ptr[STEPS]; let i = 0; for (; i < steps.length - 1; i++) { next = next[steps[i]]; if (!isobj(next)) return } next[steps[i]] = val; }; // Actual JSX factory. Responsible for creating the HTML elements and all of the *reactive* syntactic sugar function h(type, props, ...children) { if (type == Fragment) return children if (typeof type == 'function') { // functional components. create the stateful object let newthis = $state(Object.create(type.prototype)); for (let name in props) { let ptr = props[name]; if (name.startsWith('bind:')) { assert( isDLPtr(ptr), 'bind: requires a reference pointer from use' ); let set = curryset(ptr[PROXY]); let propname = name.substring(5); if (propname == 'this') { set(newthis); } else { // component two way data binding!! (exact same behavior as svelte:bind) let isRecursive = false; handle(ptr, (value) => { if (isRecursive) { isRecursive = false; return } isRecursive = true; newthis[propname] = value; }); handle(use(newthis[propname]), (value) => { if (isRecursive) { isRecursive = false; return } isRecursive = true; set(value); }); } delete props[name]; } } Object.assign(newthis, props); newthis.children = []; for (let child of children) { JSXAddChild(child, newthis.children.push.bind(newthis.children)); } let elm = type.apply(newthis); assert( !(elm instanceof Array), 'Functional component cannot return a Fragment' ); assert(elm instanceof Node, 'Functional component must return a Node'); assert( !('$' in elm), 'Functional component cannot have another functional component at root level' ); // reasoning: it would overwrite data-component and make a mess of the css elm.$ = newthis; newthis.root = elm; /* FEATURE.CSS.START */ let cl = elm.classList; let css = newthis.css; let sanitizedName = type.name.replaceAll('$', '-'); if (css) { cl.add(genCss(`${sanitizedName}-${genuid()}`, css, true)); } // for ui toolkits, sometimes it's desirable to let outside css leak into the component. the caller has the responsibility of making sure outside css won't break the styles if (!newthis._leak) { cl.add(cssBoundary); } /* FEATURE.CSS.END */ elm.setAttribute('data-component', type.name); if (typeof newthis.mount === 'function') newthis.mount(); return elm } let xmlns = props?.xmlns; let elm = xmlns ? doc.createElementNS(xmlns, type) : doc.createElement(type); for (let child of children) { let bappend = elm.append.bind(elm); JSXAddChild(child, bappend); } if (!props) return elm let useProp = (name, callback) => { if (!(name in props)) return let prop = props[name]; callback(prop); delete props[name]; }; useProp('class', (classlist) => { assert( typeof classlist === 'string' || classlist instanceof Array || isDLPtr(classlist), 'class must be a string or ar ray (r pointer)' ); if (typeof classlist === 'string') { elm.setAttribute('class', classlist); return } /// this will be cleaned up once class arrays are removed if (isDLPtr(classlist)) { let oldvalue = ''; handle(classlist, (classname) => { for (let name of oldvalue.split(' ')) { if (name) elm.classList.remove(name); } if (typeof classname === 'string') { for (let name of classname.split(' ')) { if (name) elm.classList.add(name); } oldvalue = classname; } }); return } /* DEV.START */ if (!window.dlwarnedclassarrays) { console.error( "WARN: class arrays (eg,
) are deprecated and will be REMOVED in the next release" ); window.dlwarnedclassarrays = true; } /* DEV.END */ for (let name of classlist) { if (isDLPtr(name)) { let oldvalue = null; handle(name, (value) => { if (typeof oldvalue === 'string') { elm.classList.remove(oldvalue); } elm.classList.add(value); oldvalue = value; }); } else { elm.classList.add(name); } } }); for (let name in props) { let ptr = props[name]; if (name.startsWith('bind:')) { assert(isDLPtr(ptr), 'bind: requires a reference pointer from use'); let propname = name.substring(5); // create the function to set the value of the pointer let set = curryset(ptr[PROXY]); if (propname == 'this') { set(elm); } else if (propname == 'value') { handle(ptr, (value) => (elm.value = value)); elm.addEventListener('change', () => set(elm.value)); } else if (propname == 'checked') { handle(ptr, (value) => (elm.checked = value)); elm.addEventListener('click', () => set(elm.checked)); } delete props[name]; } if (name.startsWith('class:')) { let classname = name.substring(6); if (isDLPtr(ptr)) { handle(ptr, (value) => { if (value) { elm.classList.add(classname); } else { elm.classList.remove(classname); } }); } else { if (ptr) { elm.classList.add(classname); } } delete props[name]; } if (name == 'style' && isobj(ptr) && !isDLPtr(ptr)) { for (let key in ptr) { let prop = ptr[key]; if (isDLPtr(prop)) { handle(prop, (value) => (elm.style[key] = value)); } else { elm.style[key] = prop; } } delete props[name]; } } // apply the non-reactive properties for (let name in props) { let prop = props[name]; if (isDLPtr(prop)) { handle(prop, (val) => { JSXAddAttributes(elm, name, val); }); } else { JSXAddAttributes(elm, name, prop); } } // hack to fix svgs if (xmlns) elm.innerHTML = elm.innerHTML; return elm } // glue for nested children function JSXAddChild(child, cb) { let childchild, elms, node; if (isDLPtr(child)) { JSXAddFixedWrapper(child, cb); } else if (isobj(child) && IF in child) { JSXAddFixedWrapper(child[IF], cb, child); } else if (child instanceof Node) { cb(child); return [child] } else if (child instanceof Array) { elms = []; for (childchild of child) { elms = elms.concat(JSXAddChild(childchild, cb)); } if (!elms[0]) elms = JSXAddChild('', cb); return elms } else { // this is what makes it so that {null} won't render. the empty string would seem odd coming from other frameworks but it is for the best if (child === null || child === undefined) child = ''; node = doc.createTextNode(child); cb(node); return [node] } } // Where properties are assigned to elements, and where the *non-reactive* syntax sugar goes function JSXAddAttributes(elm, name, prop) { if (!prop && elm.hasAttribute(name)) elm.removeAttribute(name); if (!prop) return if (name.startsWith('on:')) { assert(typeof prop === 'function', 'on: requires a function'); let names = name.substring(3); for (let name of names.split('$')) { elm.addEventListener(name, (...args) => { self.$el = elm; prop(...args); }); } return } elm.setAttribute(name, prop); } function html(strings, ...values) { // normalize the strings array, it would otherwise give us an object strings = [...strings]; let flattened = ''; let markers = {}; for (let i = 0; i < strings.length; i++) { let string = strings[i]; let value = values[i]; // since self closing tags don't exist in regular html, look for the pattern enclosing a function, and replace it with `/.exec(strings[i + 1]); if (/< *$/.test(string) && match) { strings[i + 1] = strings[i + 1].substr( match.index + match[0].length ); } flattened += string; if (i < values.length) { let dupe = Object.values(markers).findIndex((v) => v === value); let marker; if (dupe !== -1) { marker = Object.keys(markers)[dupe]; } else { marker = 'h' + genuid(); markers[marker] = value; } flattened += marker; // close the self closing tag if (match) { flattened += `>`; } } } let dom = new DOMParser().parseFromString(flattened, 'text/html'); assert( dom.body.children.length == 1, 'html builder needs exactly one child' ); function wraph(elm) { let nodename = elm.nodeName.toLowerCase(); if (nodename === '#text') return elm.textContent if (nodename in markers) nodename = markers[nodename]; let children = [...elm.childNodes].map(wraph); for (let i = 0; i < children.length; i++) { let text = children[i]; if (typeof text !== 'string') continue for (const [marker, value] of Object.entries(markers)) { if (!text) break if (!text.includes(marker)) continue let before ;[before, text] = text.split(marker); children = [ ...children.slice(0, i), before, value, text, ...children.slice(i + 1), ]; i += 2; } } let attributes = {}; if (!elm.attributes) return elm // passthrough comments for (const attr of [...elm.attributes]) { let val = attr.nodeValue; if (val in markers) val = markers[val]; attributes[attr.name] = val; } return h(nodename, attributes, children) } return wraph(dom.body.children[0]) } function $store(target, { ident, backing, autosave }) { let read, write; if (typeof backing === 'string') { switch (backing) { case 'localstorage': read = () => localStorage.getItem(ident); write = (ident, data) => { localStorage.setItem(ident, data); }; break default: assert('Unknown store type: ' + backing); } } else { ({ read, write } = backing); } let save = () => { console.info('[dreamland.js]: saving ' + ident); // stack gets filled with "pointers" representing unique objects // this is to avoid circular references let serstack = {}; let vpointercount = 0; let ser = (tgt) => { let obj = { stateful: isStateful(tgt), values: {}, }; let i = vpointercount++; serstack[i] = obj; for (let key in tgt) { let value = tgt[key]; if (isDLPtr(value)) continue // i don"t think we should be serializing pointers? switch (typeof value) { case 'string': case 'number': case 'boolean': case 'undefined': // primitives, serialize as strings obj.values[key] = JSON.stringify(value); break case 'object': if (value instanceof Array) { obj.values[key] = value.map((v) => { if (typeof v === 'object') { return ser(v) } else { return JSON.stringify(v) } }); break } else if (value === null) { obj.values[key] = 'null'; } else { assert( value.__proto__ === Object.prototype, 'Only plain objects can be serialized in stores' ); // if it's not a primitive, store it as a number acting as a pointer obj.values[key] = ser(value); } break case 'symbol': case 'function': case 'bigint': assert('Unsupported type: ' + typeof value); break } } return i }; ser(target); let string = JSON.stringify(serstack); write(ident, string); }; let autohook = (target, prop, value) => { if (isStateful(value)) value[TARGET][STATEHOOK] = autohook; save(); }; let destack = JSON.parse(read(ident)); if (destack) { let objcache = {}; let de = (i) => { if (objcache[i]) return objcache[i] let obj = destack[i]; let tgt = {}; for (let key in obj.values) { let value = obj.values[key]; if (typeof value === 'string') { // it's a primitive, easy deser tgt[key] = JSON.parse(value); } else { if (value instanceof Array) { tgt[key] = value.map((v) => { if (typeof v === 'string') { return JSON.parse(v) } else { return de(v) } }); } else { tgt[key] = de(value); } } } if (obj.stateful && autosave == 'auto') tgt[STATEHOOK] = autohook; let newobj = obj.stateful ? $state(tgt) : tgt; objcache[i] = newobj; return newobj }; // "0" pointer is the root object target = de(0); } switch (autosave) { case 'beforeunload': addEventListener('beforeunload', save); break case 'manual': break case 'auto': target[STATEHOOK] = autohook; break default: assert('Unknown autosave type: ' + autosave); } return $state(target) } window.DREAMLAND_SECRET_DEV_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = { ...CONSTS, isDLPtrInternal, handle, }; log('Version: ' + DLVERSION); console.warn( 'This is a DEVELOPER build of dreamland.js. It is not suitable for production use.' ); console.info('Enabled features:', DLFEATURES.join(', ')); /* DEV.END */ exports.$if = $if; exports.$state = $state; exports.$store = $store; exports.Fragment = Fragment; exports.css = css; exports.h = h; exports.html = html; exports.isDLPtr = isDLPtr; exports.isStateful = isStateful; exports.scope = scope; })(window) //# sourceMappingURL=dev.js.map ================================================ FILE: public/lib/dreamland/minimal.js ================================================ // dreamland.js, MIT license const DLFEATURES = []; const DLVERSION = '0.0.24'; !function(e){const[t,n,r,i,l,s,o]=Array.from(Array(7),Symbol);let f=document;const u=Symbol();let c=!1;Object.defineProperty(window,"use",{get:()=>(c=!0,(e,l,...s)=>{p(e)||b(e),c=!1;let o={get value(){return function(e){let l=e[r],s=l[i],o=e[t],f=l[n];for(let e of s)if(f=f[e],!h(f))break;for(let e of o)f=e(f);return f}(o)}};if(b(e)){let n=[...e[t]];l&&n.push(l),o[r]=e[r],o[t]=n}else o[r]=e,o[t]=l?[l]:[];return o})}),Object.defineProperty(window,"useChange",{get:()=>(c=!0,(e,t)=>{c=!1,e=e instanceof Array?e:[e];for(let n of e)p(n)||b(n),y(use(n),t)})});let a=new Map;function d(e){e[l]=[],e[n]=e;let s=Symbol.toPrimitive,o=new Proxy(e,{get(e,l,o){if(c){let f=Symbol(),u=new Proxy({[n]:e,[r]:o,[i]:[l],[s]:()=>f},{get:(e,l)=>[n,r,i,t,s].includes(l)?e[l]:(l=a.get(l)||l,e[i].push(l),u)});return a.set(f,u),u}return Reflect.get(e,l,o)},set(e,t,n){let r=Reflect.set(e,t,n);for(let r of e[l])r(e,t,n);return r}});return o}let h=e=>e instanceof Object;function p(e){return h(e)&&i in e}function b(e){return h(e)&&t in e}function y(e,s){b(e);let o,f=e[r],u=e[t],c=[];function a(){let e=f[n];for(o of c)if(e=e[o],!h(e))break;for(let t of u)e=t(e);s(e)}let d=(e,t)=>function r(i,s,o){if(s===c[t]&&e===i&&(a(),h(o))){let e=o[l];e&&!e.includes(r)&&e.push(d(o[n],t+1))}};for(let e in f[i]){let t=f[i][e];h(t)&&t[n]?y(t,(t=>{c[e]=t,a()})):c[e]=t}let p=d(f[n],0);f[n][l].push(p),p(f[n],c[0],f[n][c[0]])}function g(e,t,n){let r,i,l,s;y(e,(e=>{l=i?.[0],l&&(r=l.previousSibling||(s=l.parentNode)),i&&i.forEach((e=>e.remove())),i=v(n?e?n.then:n.otherwise:e,(e=>{r?(s?(r.prepend(e),s=null):r.after(e),r=e):t(e)}))}))}let m=e=>t=>{let n=e[r],l=e[i],s=0;for(;s{self.$el=e,n(...t)}))}else e.setAttribute(t,n)}e.$if=function(e,t,n){return n??=f.createTextNode(""),b(e)?{[s]:e,then:t,otherwise:n}:e?t:n},e.$state=d,e.Fragment=u,e.h=function(e,t,...n){if(e==u)return n;if("function"==typeof e){let i=d(Object.create(e.prototype));for(let e in t){let n=t[e];if(e.startsWith("bind:")){b(n);let l=m(n[r]),s=e.substring(5);if("this"==s)l(i);else{let e=!1;y(n,(t=>{e?e=!1:(e=!0,i[s]=t)})),y(use(i[s]),(t=>{e?e=!1:(e=!0,l(t))}))}delete t[e]}}Object.assign(i,t),i.children=[];for(let e of n)v(e,i.children.push.bind(i.children));let l=e.apply(i);return l.$=i,i.root=l,l.setAttribute("data-component",e.name),"function"==typeof i.mount&&i.mount(),l}let i=t?.xmlns,l=i?f.createElementNS(i,e):f.createElement(e);for(let e of n){v(e,l.append.bind(l))}if(!t)return l;((e,n)=>{if(!(e in t))return;n(t[e]),delete t[e]})("class",(e=>{if("string"==typeof e||e instanceof Array||b(e),"string"!=typeof e)if(b(e)){let t="";y(e,(e=>{for(let e of t.split(" "))e&&l.classList.remove(e);if("string"==typeof e){for(let t of e.split(" "))t&&l.classList.add(t);t=e}}))}else for(let t of e)if(b(t)){let e=null;y(t,(t=>{"string"==typeof e&&l.classList.remove(e),l.classList.add(t),e=t}))}else l.classList.add(t);else l.setAttribute("class",e)}));for(let e in t){let n=t[e];if(e.startsWith("bind:")){b(n);let i=e.substring(5),s=m(n[r]);"this"==i?s(l):"value"==i?(y(n,(e=>l.value=e)),l.addEventListener("change",(()=>s(l.value)))):"checked"==i&&(y(n,(e=>l.checked=e)),l.addEventListener("click",(()=>s(l.checked)))),delete t[e]}if(e.startsWith("class:")){let r=e.substring(6);b(n)?y(n,(e=>{e?l.classList.add(r):l.classList.remove(r)})):n&&l.classList.add(r),delete t[e]}if("style"==e&&h(n)&&!b(n)){for(let e in n){let t=n[e];b(t)?y(t,(t=>l.style[e]=t)):l.style[e]=t}delete t[e]}}for(let e in t){let n=t[e];b(n)?y(n,(t=>{L(l,e,t)})):L(l,e,n)}return i&&(l.innerHTML=l.innerHTML),l},e.isDLPtr=b,e.isStateful=function(e){return h(e)&&l in e}}(window) //# sourceMappingURL=minimal.js.map ================================================ FILE: public/lib/dreamland/ssr.js ================================================ var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/ssr/main.js var main_exports = {}; __export(main_exports, { hSSR: () => hSSR, renderToString: () => renderToString }); module.exports = __toCommonJS(main_exports); var import_jsdom = require("jsdom"); function renderToString(component, props, children) { globalThis.h = hSSR; globalThis.use = (p) => p; return hSSR(component, props, children).outerHTML; } function hSSR(type, props, ...children) { const { document, HTMLElement } = new import_jsdom.JSDOM().window; if (typeof type == "function") { const newthis = {}; for (let key in props) { if (key.startsWith("bind:")) { const attr = key.slice(5); newthis[attr] = props[key]; continue; } newthis[key] = props[key]; } const elm = type.apply(newthis); elm.setAttribute("data-component", type.name); elm.setAttribute("ssr-data-component", type.name); return elm; } let el = document.createElement(type); for (let child of children) { if (typeof child == "object" && child != null && "remove" in child) { el.appendChild(child); } else { el.appendChild(document.createTextNode(child)); } } for (let key in props) { let val = props[key]; if (key == "class") { el.className = val; continue; } if (key == "style" && typeof val == "object") { for (let skey in val) { el.style[skey] = val[skey]; } continue; } if (key.startsWith("on:")) { continue; } if (key.startsWith("bind:")) { let attr = key.slice(5); el.setAttribute(attr, val); } el.setAttribute(key, props[key]); } return el; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { hSSR, renderToString }); ================================================ FILE: public/manifest.json ================================================ { "name": "Terbium WebOS", "description": "The next generation of Terbium WebOS Built for developers, enthusiests, and consumers to last", "short_name": "Terbium", "start_url": "/?boot=true", "display": "standalone", "background_color": "#D16FFF", "icons": [ { "src": "tb.svg", "sizes": "16x16 32x32 48x48 72x72 96x96 128x128 256x256 512x512", "type": "image/svg+xml", "purpose": "any maskable" } ], "display_override": ["window-controls-overlay", "standalone", "minimal-ui", "browser"] } ================================================ FILE: public/media_interactions.js ================================================ console.log("Media Interactions Injected"); function useSec(timeStr) { const [minutes, seconds] = timeStr.split(":").map(Number); return minutes * 60 + seconds; } function getBg(style) { const match = /url\(['"]?([^'"\)]+)['"]?\)/.exec(style); return match ? match[1] : null; } function runMp(config) { if (config.type === "video") { window.parent.tb.mediaplayer.video({ video_name: config.video_name, creator: config.creator, background: config.background, time: config.time, endtime: config.endtime, onBack: config.onBack, onPausePlay: config.onPausePlay, onNext: config.onNext, }); } else { window.parent.tb.mediaplayer.music({ track_name: config.track_name, artist: config.artist, background: config.background, time: config.time, endtime: config.endtime, onBack: config.onBack, onPausePlay: config.onPausePlay, onNext: config.onNext, }); } } async function newPlayer(elem, getConfig) { elem?.addEventListener("click", async () => { const exists = await window.parent.tb.mediaplayer.isExisting(); if (!exists) { runMp(getConfig()); } }); } setInterval(() => { // YouTube Music const ytmTab = document.querySelector(".ytmusic-app"); if (ytmTab) { const playButtons = [document.querySelector("ytmusic-play-button-renderer"), document.querySelector("tp-yt-paper-icon-button#play-pause-button")]; const nex = document.querySelector('tp-yt-paper-icon-button[title="Next"]'); const bac = document.querySelector('tp-yt-paper-icon-button[title="Previous"]'); const pic = document.querySelector(".image.style-scope.ytmusic-player-bar")?.src; const aname = document.querySelector("yt-formatted-string.byline.style-scope")?.innerHTML; const cmf = document.querySelector(".title.style-scope.ytmusic-player-bar")?.innerHTML; const timeText = document.querySelector("span.time-info.style-scope.ytmusic-player-bar")?.textContent.trim(); const [currTimeStr, endTimeStr] = timeText?.split(" / ") || []; const config = () => ({ track_name: cmf, artist: aname, background: pic, time: useSec(currTimeStr), endtime: useSec(endTimeStr), onBack: () => bac?.click(), onPausePlay: () => playButtons[0]?.click(), onNext: () => { nex?.click(); window.parent.tb.mediaplayer.hide(); }, }); playButtons.forEach(btn => newPlayer(btn, config)); } // SoundCloud [".sc-button-play.playButton", ".playControl.sc-ir", ".sc-button-play playButton.sc-button.sc-button-xlarge"].forEach(selector => { const playBtn = document.querySelector(selector); if (!playBtn) return; newPlayer(playBtn, () => { const imgStyle = document.querySelector("span.sc-artwork")?.getAttribute("style") || ""; const img = getBg(imgStyle); const aname = document.querySelector(".playbackSoundBadge__lightLink")?.title; const sname = document.querySelector(".playbackSoundBadge__titleLink")?.title; const curr = document.querySelector('.playbackTimeline__timePassed span[aria-hidden="true"]')?.textContent; const end = document.querySelector('.playbackTimeline__duration span[aria-hidden="true"]')?.textContent; const fw = document.querySelector(".playControls__next"); const bk = document.querySelector(".playControls__prev"); const seeker = document.querySelector(".playbackTimeline__progressWrapper.sc-mx-1x").ariaValueNow; return { track_name: sname.length > 18 ? sname.slice(0, 18) + "..." : sname, artist: aname, background: img, time: useSec(curr), endtime: useSec(end), onBack: () => bk?.click(), onPausePlay: () => playBtn?.click(), onNext: () => { fw?.click(); window.parent.tb.mediaplayer.hide(); }, onSeek: val => { seeker = val; }, }; }); }); // Spotify const spotTab = document.querySelector(".vnCew8qzJq3cVGlYFXRI"); if (spotTab) { newPlayer(spotTab, () => { const currTime = document.querySelector(".playback-bar__progress-time-elapsed")?.textContent; const endTime = document.querySelector(".kQqIrFPM5PjMWb5qUS56")?.textContent; const artist = document.querySelector('a[data-testid="context-item-info-artist"]')?.textContent; const track = document.querySelector('a[data-testid="context-item-link"]')?.textContent; const bgStyle = document.querySelector(".mMx2LUixlnN_Fu45JpFB")?.style.backgroundImage || ""; const bg = getBg(bgStyle); const bk = document.querySelector(".fn72ari9aEmKo4JcwteT"); const fw = document.querySelector(".mnipjT4SLDMgwiDCEnRC"); return { track_name: track, artist: artist, background: bg, time: useSec(currTime), endtime: useSec(endTime), onBack: () => bk?.click(), onPausePlay: () => spotTab?.click(), onNext: () => { fw?.click(); window.parent.tb.mediaplayer.hide(); }, }; }); } // YouTube vid const audtab = document.querySelector(".video-stream"); if (audtab) { const setupYT = async () => { const exists = await window.parent.tb.mediaplayer.isExisting(); if (!exists) { const fav = document.querySelector('link[rel="icon"]')?.href; const vidName = document.querySelector("yt-formatted-string.style-scope.ytd-watch-metadata")?.innerHTML; const creator = document.querySelector("a.yt-simple-endpoint.style-scope.yt-formatted-string")?.innerHTML; const duration = document.querySelector(".ytp-time-duration")?.innerHTML; runMp({ type: "video", video_name: vidName, creator, background: fav, endtime: useSec(duration), onPausePlay: () => document.querySelector(".ytp-play-button")?.click(), onNext: () => { document.querySelector(".ytp-next-button")?.click(); window.parent.tb.mediaplayer.hide(); }, }); } }; audtab.addEventListener("play", setupYT); audtab.addEventListener("click", setupYT); } // Snae [".-q.-u.-Y.da.-m.ha.Qc", ".-D.d.-K.-F.q.Bb"].forEach(selector => { const snaeRoot = document.querySelector(selector); if (!snaeRoot) return; const playBtn = snaeRoot.querySelector(".na.F.Q.-a.-Y.oa.a.m.pa.Va"); if (!playBtn) return; playBtn.addEventListener("click", async () => { const exists = await window.parent.tb.mediaplayer.isExisting(); if (exists) return; const snameEl = snaeRoot.querySelector(".I.T.-d") || snaeRoot.querySelector(".C.N.Y"); const sname = snameEl?.innerHTML || "Unknown Title"; const bgStyle = snaeRoot.querySelector(".fa.Na.Pa")?.style.backgroundImage || snaeRoot.querySelector(".fa.Na.Sc.Pa")?.style.backgroundImage; const backgroundMatch = bgStyle?.match(/url\(["']?(.*?)["']?\)/); const background = backgroundMatch ? backgroundMatch[1] : null; const parentDiv = snaeRoot.querySelector(".-J.-F.-S.da.zb"); const timeEl = parentDiv?.querySelector(".H.S.-c.fa.Ab:nth-of-type(1)"); const currTM = timeEl ? useSec(timeEl.textContent.trim()) : null; const etEl = parentDiv?.querySelector(".H.S.-c.fa.Ab:nth-of-type(2)"); const endTime = etEl ? useSec(etEl.textContent.trim()) : null; const fw = snaeRoot.querySelector('button[title="Play previous track (SHIFT+P)"]'); const bk = snaeRoot.querySelector('button[title="Play next track (SHIFT+N)"]'); runMp({ track_name: sname, artist: "Snae Player", background: background, time: currTM, endtime: endTime, onBack: () => bk?.click(), onPausePlay: () => playBtn?.click(), onNext: () => { fw?.click(); window.parent.tb.mediaplayer.hide(); }, }); }); }); }, 1000); ================================================ FILE: public/robots.txt ================================================ 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 ================================================ FILE: public/sitemap.xml ================================================ https://terbiumon.top/ 2026-02-10 weekly 1.0 ================================================ FILE: public/theme.css ================================================ /* 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; } ================================================ FILE: public/uv/uv.config.js ================================================ /*global Ultraviolet*/ self.__uv$config = { prefix: "/uv/service/", encodeUrl: Ultraviolet.codec.xor.encode, decodeUrl: Ultraviolet.codec.xor.decode, handler: "/uv/uv.handler.js", client: "/uv/uv.client.js", bundle: "/uv/uv.bundle.js", config: "/uv/uv.config.js", sw: "/uv/uv.sw.js", inject: [ { host: /discord\.com/, injectTo: "head", html: ` `, }, ], }; ================================================ FILE: server.ts ================================================ 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 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"; export function TServer() { config.config(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); console.log("Starting Terbium..."); 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) { console.log("Masqr is Enabled"); } else { console.log("Masqr is Disabled"); } async function MasqFail(c: any) { const host = c.req.header("host"); if (!host) { return c.html(fs.readFileSync("fail.html", "utf8")); } 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"); return c.html(failureFileLocal); } catch { return c.html(fs.readFileSync("fail.html", "utf8")); } } if (masqrCheck) { 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 (c.req.url.includes("/bare")) { await next(); return; } if (getCookie(c, "authcheck")) { await next(); return; } if (getCookie(c, "refreshcheck") !== "true") { setCookie(c, "refreshcheck", "true", { maxAge: 10000 }); return await MasqFail(c); } const authheader = c.req.header("authorization"); if (!authheader) { c.header("WWW-Authenticate", "Basic"); c.status(401); return await MasqFail(c); } 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), }); return c.redirect("/"); } return await MasqFail(c); }); } app.use( "*", serveStatic({ root: path.join(__dirname, "dist"), }), ); 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(nodeHandler); 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(); } }); server.listen(port, () => { console.log(` \x1b[38;2;50;174;98m@@@@@@@@@@@@@@~ B@@@@@@@@#G?. \x1b[38;2;50;174;98mB###&@@@@&####^ #@@@&PPPB@@@G. \x1b[38;2;50;174;98m .. ~@@@@J .. .#@@@P ~&@@@^ \x1b[38;2;60;195;240mWelcome to Terbium React v${version} \x1b[38;2;50;174;98m^@@@@? .#@@@@###&@@&7 \x1b[38;2;50;174;98m^@@@@? .#@@@#555P&@@B7 \x1b[38;2;182;182;182mTerbium is running on ${port} \x1b[38;2;50;174;98m^@@@@? .#@@@P G@@@@ \x1b[38;2;182;182;182mAny problems you encounter let us know! \x1b[38;2;50;174;98m^@@@@? .#@@@&GGG#@@@@Y \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), duplex: "half", } 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"); process.exit(); }); } ================================================ FILE: src/App.tsx ================================================ import Api from "./sys/Api"; import Desktop from "./sys/gui/Desktop"; function App() { Api(); return ( <> { e.preventDefault(); }} /> ); } export default App; ================================================ FILE: src/Boot.tsx ================================================ import { useEffect, useState } from "react"; import { version } from "../package.json"; import { dirExists, fileExists } from "./sys/types"; export default function Boot() { const [selected, setSelected] = useState(0); const [showCursor, setShowCursor] = useState(false); const [bootentries, setentries] = useState<{ name: string; action: void | any }[]>([]); 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(); }; useEffect(() => { const getEntries = async () => { let entries = []; if (!(await fileExists("/bootentries.json"))) { 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 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, action: eval(`(${entry.action})`), })); 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)"); const fsxr = await FilerDirExists("/system/etc/terbium"); if (fsxr) { sessionStorage.setItem("migrateFs", "true"); setentries(recreatedEntries); } else { setentries(bootent); } } else { setentries(recreatedEntries); } }; getEntries(); const getPlatform = () => { const mobileuas = /(android|bb\d+|meego).+mobile|armv7l|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series[46]0|samsungbrowser.*mobile|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino|android|ipad|playbook|silk|iPhone|iPad/i; const crosua = /CrOS/; if (mobileuas.test(navigator.userAgent) && !crosua.test(navigator.userAgent)) { return "mobile"; } else if (!mobileuas.test(navigator.userAgent) && navigator.maxTouchPoints > 1 && navigator.userAgent.indexOf("Macintosh") !== -1 && navigator.userAgent.indexOf("Safari") !== -1) { return "mobile"; } else { return "desktop"; } }; if (getPlatform() === "mobile") { setShowCursor(true); } const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "ArrowUp") { setSelected(prevSelected => (bootentries.length === 0 ? 0 : prevSelected === 0 ? bootentries.length - 1 : prevSelected - 1)); } else if (e.key === "ArrowDown") { setSelected(prevSelected => (prevSelected === bootentries.length - 1 ? 0 : prevSelected + 1)); } else if (e.key === "Enter") { const selectedEntry = bootentries[selected]; selectedEntry.action(); } else if (e.key === "Escape") { setShowCursor(prev => !prev); } }; window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [selected, bootentries]); return (
Terbium Boot Loader - Version {version}
{bootentries.map((entry, index) => ( { showCursor ? entry.action() : null; }} > {entry.name} ))}
Use the and keys to switch entry. Press the enter key to boot into the selection.
); } ================================================ FILE: src/CustomOS.tsx ================================================ import { useEffect, useState } from "react"; export default function CustomOS() { const [loaded, setloaded] = useState(false); useEffect(() => { const rep = (content: string) => { const parser = new DOMParser(); const doc = parser.parseFromString(content, "text/html"); console.log(`Terbium Bootloader v2.3.0 is now loading: ${sessionStorage.getItem("bootfile")}`); if (doc.body && doc.head) { const b = document.createElement("base"); b.href = `/fs/${sessionStorage.getItem("bootfile")!.replace(/\/?[^\/]+\.html$/, "")}/`; doc.head.insertBefore(b, doc.head.firstChild); document.body.innerHTML = doc.body.innerHTML; document.head.innerHTML = doc.head.innerHTML; const htmlAttrs = doc.documentElement.attributes; for (const attr of Array.from(htmlAttrs)) { document.documentElement.setAttribute(attr.name, attr.value); } const headAttrs = doc.head.attributes; for (const attr of Array.from(headAttrs)) { document.head.setAttribute(attr.name, attr.value); } const bodyAttrs = doc.body.attributes; for (const attr of Array.from(bodyAttrs)) { document.body.setAttribute(attr.name, attr.value); } const scripts = document.querySelectorAll("script"); for (const script of scripts) { const newScript = document.createElement("script"); for (const attr of script.attributes) { if (attr.name !== "src") { newScript.setAttribute(attr.name, attr.value); } } if (script.src) { let newSrc = script.src; if (script.src.includes("http")) { newSrc = script.src; } else if (!script.src.includes(`${window.location.origin}/fs/`)) { newSrc = `/fs/${sessionStorage.getItem("bootfile")!.replace(/\/?[^\/]+\.html$/, "")}${script.src.replace(window.location.origin, "")}`; } newScript.src = newSrc; } else { newScript.textContent = script.textContent; } document.head.appendChild(newScript); script.parentNode?.removeChild(script); } const styles: any = document.querySelectorAll("link[rel='stylesheet']"); for (const style of styles) { const newStyle = document.createElement("link"); for (const attr of style.attributes) { if (attr.name !== "href") { newStyle.setAttribute(attr.name, attr.value); } } if (style.href) { let newHref = style.href; if (style.href.includes("http")) { newHref = style.href; } else if (!style.href.includes(`${window.location.origin}/fs/`)) { newHref = `/fs/${sessionStorage.getItem("bootfile")!.replace(/\/?[^\/]+\.html$/, "")}${style.href.replace(window.location.origin, "")}`; } newStyle.href = newHref; } document.head.appendChild(newStyle); style.parentNode?.removeChild(style); } setloaded(true); } else { console.error(`Failed to boot: ${sessionStorage.getItem("bootfile")}`); sessionStorage.clear(); window.location.reload(); } }; window.tb.fs.promises .readFile(sessionStorage.getItem("bootfile")!, "utf8") .then(data => { rep(data); }) .catch(err => { console.error(`Failed to read bootfile because of: ${err}`); sessionStorage.clear(); window.location.reload(); }); }, []); useEffect(() => { const back = (e: KeyboardEvent) => { if (e.key === "Escape") { sessionStorage.clear(); window.location.reload(); } }; if (loaded) { window.removeEventListener("keydown", back); } else { window.addEventListener("keydown", back); } return () => { window.removeEventListener("keydown", back); }; }, [loaded]); return (
Terbium
Terbium Bootloader

Press ESC to return to boot menu

); } ================================================ FILE: src/Loading.tsx ================================================ import "./sys/gui/styles/loader.css"; export default function Loader() { return (
Terbium
TerbiumOS
); } ================================================ FILE: src/Login.tsx ================================================ import { useEffect, useState, useRef } from "react"; import "./sys/gui/styles/login.css"; import { GetTime, GetDate } from "./sys/apis/Date"; import pwd from "./sys/apis/Crypto"; import DialogContainer, { setDialogFn } from "./sys/apis/Dialogs"; import { User } from "./sys/types"; const pw = new pwd(); export default function Login() { const [isLoggingIn, setIsLoggingIn] = useState(false); const [time, setTime] = useState(GetTime()); const [date, setDate] = useState(GetDate()); const [hasPw, setHasPw] = useState(false); const [accounts, setAccounts] = useState([]); const [selectedUser, setSelectedUser] = useState(sessionStorage.getItem("currAcc") || "/home/user/"); const [profilePictures, setProfilePictures] = useState<{ [key: string]: string | null }>({}); const [wallpaper, setWallpaper] = useState(null); const [changingpw, setChangepw] = useState(false); const passwordRef = useRef(null); useEffect(() => { const intervalId = setInterval(() => { setTime(GetTime()); setDate(GetDate()); }, 1000); return () => clearInterval(intervalId); }, []); useEffect(() => { const FS = async () => { const entries = await window.tb.fs.promises.readdir("/home/"); const dirEntries = await Promise.all( entries.map(async entry => { const stat = await window.tb.fs.promises.stat(`/home/${entry}`); if (stat && stat.isDirectory()) { const exists = await window.tb.fs.promises.exists(`/home/${entry}/user.json`); if (exists) { return entry; } else { return null; } } }), ); const directories = dirEntries.filter(entry => entry !== null); // @ts-expect-error setAccounts(directories); }; FS(); }, []); useEffect(() => { const getDefUsr = async () => { const data = await window.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8"); const res = JSON.parse(data); setWallpaper(JSON.parse(await window.tb.fs.promises.readFile(`/home/${res.defaultUser}/settings.json`)).wallpaper); setSelectedUser(res.defaultUser); }; getDefUsr(); }, []); useEffect(() => { const getUsr = async () => { const pictures: { [key: string]: string | null } = {}; for (const account of accounts) { try { const res = JSON.parse(await window.tb.fs.promises.readFile(`/home/${account}/user.json`, "utf8")); pictures[account] = res.pfp || null; } catch (error) { console.error(`Error reading user data for ${account}:`, error); pictures[account] = null; } } if (selectedUser && selectedUser !== "/home/user/") { try { const res = JSON.parse(await window.tb.fs.promises.readFile(`/home/${selectedUser}/user.json`, "utf8")); setHasPw(res.password !== false); } catch (error) { console.error("Error reading user data:", error); } } setProfilePictures(pictures); }; if (accounts.length > 0) { getUsr(); } }, [accounts, selectedUser]); const login = async () => { // @ts-expect-error const passVal = passwordRef.current.value; if (passVal !== "") { const data = await window.tb.fs.promises.readFile(`/home/${selectedUser}/user.json`, "utf8"); const res = JSON.parse(data); const user_pass = res.password.toString(); const pass = pw.harden(passVal.toString()); if (user_pass === pass) { sessionStorage.setItem("logged-in", "true"); sessionStorage.setItem("currAcc", selectedUser); window.location.reload(); } else { Err(); } } }; const Err = () => { if (passwordRef.current) { passwordRef.current.classList.add("ring-[#ff7e7e5a]", "ring-[2px]", "border-[#ff7e7ed5]", "placeholder-[#ff7e7e6b]"); passwordRef.current.value = ""; passwordRef.current.placeholder = "Incorrect Password"; passwordRef.current.addEventListener("keydown", () => { if (passwordRef.current) { passwordRef.current.classList.remove("ring-[#ff7e7e5a]", "ring-[2px]", "border-[#ff7e7ed5]", "placeholder-[#ff7e7e6b]"); passwordRef.current.placeholder = "Password"; } }); } }; useEffect(() => { const keyCheck = async (e: KeyboardEvent) => { switch (e.key) { case "Enter": if (isLoggingIn && !changingpw) { login(); } break; case "Escape": if (isLoggingIn) { setIsLoggingIn(false); } else { } break; default: setIsLoggingIn(true); if (passwordRef.current && !changingpw) { passwordRef.current.focus(); } break; } }; window.addEventListener("keydown", keyCheck); return () => { window.removeEventListener("keydown", keyCheck); }; }, [isLoggingIn, selectedUser, changingpw]); return ( <>
{ if (!isLoggingIn) { const user = JSON.parse(localStorage.getItem("setup") || "{}"); if (user.password !== false) { setIsLoggingIn(true); setTimeout(() => { if (passwordRef.current && !changingpw) { passwordRef.current.focus(); } }, 200); } } }} > {accounts.length > 1 ? (
{accounts.map((account, i) => (
{ setSelectedUser(account); setWallpaper(JSON.parse(await window.tb.fs.promises.readFile(`/home/${account}/settings.json`)).wallpaper); }} >
))}
) : null}
{time}
{date}

Press any key to login

{time}
{date}
{accounts.length > 1 ? ( accounts.map((account, i) => (
{account}
)) ) : (
{selectedUser}
)}
{ if (e.key === "Enter") login(); }} />
{hasPw && (
{ const changepw = async () => { setChangepw(true); let settings: User = JSON.parse(await window.tb.fs.promises.readFile(`/home/${selectedUser}/user.json`, "utf8")); if (settings.securityQuestion) { setDialogFn("message", { title: `${settings.securityQuestion.question}`, onOk: (val: string) => { if (pw.harden(val) === settings.securityQuestion!.answer) { setDialogFn("message", { title: `Enter a new Password for the account: ${selectedUser}`, onOk: (val: string) => { settings.password = pw.harden(val); window.tb.fs.promises.writeFile(`/home/${selectedUser}/user.json`, JSON.stringify(settings, null, 4)); setChangepw(false); sessionStorage.setItem("logged-in", "true"); sessionStorage.setItem("currAcc", selectedUser); window.location.reload(); }, }); } else { setDialogFn("alert", { title: `Incorrect answer to the security question`, onOk: () => { changepw(); }, }); } }, }); } else { setDialogFn("message", { title: `Enter a new Password for the account: ${selectedUser}`, onOk: (val: string) => { settings.password = pw.harden(val); window.tb.fs.promises.writeFile(`/home/${selectedUser}/user.json`, JSON.stringify(settings, null, 4)); setChangepw(false); sessionStorage.setItem("logged-in", "true"); sessionStorage.setItem("currAcc", selectedUser); window.location.reload(); }, }); } }; changepw(); }} > Forgot Password?
)}
); } ================================================ FILE: src/Recovery.tsx ================================================ import { useEffect, useRef, useState } from "react"; import { version } from "../package.json"; import { libcurl } from "libcurl.js"; import { dirExists, unzip } from "./sys/types"; import { hash } from "./hash.json"; import apps from "./apps.json"; export default function Recovery() { const [selected, setSelected] = useState(0); const [showCursor, setShowCursor] = useState(false); const [msg, setMsg] = useState(null); const [action, setAction] = useState(""); const [progress, setProgress] = useState(0); const [updCache, setUpdCache] = useState(false); const msgbox = useRef(null); const main = useRef(null); const progresscheck = useRef(null); const statusref = useRef(null); async function copyDir(inp: string, dest: string, rn?: boolean) { if (rn === true) { if (!(await dirExists(dest))) { await window.tb.fs.promises.mkdir(dest); } } const files = await window.tb.fs.promises.readdir(inp); const totalFiles = files.length; for (const [index, file] of files.entries()) { const stats = await window.tb.fs.promises.stat(`${inp}/${file}`); if (stats && stats.isDirectory()) { await window.tb.fs.promises.mkdir(`${dest}/${file}`); await copyDir(`${inp}/${file}`, `${dest}/${file}`, true); } else { await window.tb.fs.promises.writeFile(`${dest}/${file}`, await window.tb.fs.promises.readFile(`${inp}/${file}`, "utf8")); } statusref.current!.innerText = `Creating a copy of: ${file}...`; setProgress(Math.floor(((index + 1) / totalFiles) * 100)); } } const getTmp = async () => { if (await dirExists("/system/tmp/terb-upd/")) { setUpdCache(true); } }; getTmp(); 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(); }; const prodins = async () => { setShowCursor(false); msgbox.current!.classList.remove("flex"); msgbox.current!.classList.add("hidden"); progresscheck.current!.classList.remove("hidden"); progresscheck.current!.classList.add("flex"); if (localStorage.getItem("setup")) { await window.tb.sh.format(); 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); await unzip("//uploaded.zip", "//"); await window.tb.fs.promises.mkdir("/system/tmp/"); await window.tb.sh.promises.rm("/home/Guest/desktop/", { recursive: true }); await window.tb.fs.promises.mkdir("/home/Guest/desktop/"); let r2 = []; let sysapps: { name: string; config: string; user: string }[] = []; let items: { name: string; item: string; position: { custom: boolean; top: number; left: number } }[] = []; for (let i = 0; i < apps.length; i++) { const app = apps[i]; const name = app.name.toLowerCase(); var topPos: number = 0; var leftPos: number = 0; if (i % 12 === 0) { topPos = 0; } else { topPos = i % 12; } if (i < 12) { leftPos = 0; } else { leftPos = 1; } if (topPos * 66 > window.innerHeight - 130) { leftPos = 1.15; if (r2.length === 0) { topPos = 0; } else { topPos = r2.length % 12; } r2.push({ name: app.name, }); } items.push({ name: app.name, item: `/home/Guest/desktop/${name}.lnk`, position: { custom: false, top: topPos, left: leftPos, }, }); sysapps.push({ name: app.name, config: `/apps/system/${name}.tapp/index.json`, user: "System", }); await window.tb.fs.promises.writeFile(`/home/Guest/desktop/.desktop.json`, JSON.stringify(items)); await window.tb.fs.promises.symlink(`/apps/system/${name}.tapp/index.json`, `/home/Guest/desktop/${name}.lnk`); } statusref.current!.innerText = "Cleaning up..."; setProgress(85); await window.tb.fs.promises.unlink("//uploaded.zip"); setProgress(100); statusref.current!.innerText = "Restarting..."; sessionStorage.clear(); sessionStorage.setItem("boot", "true"); localStorage.setItem("setup", "true"); window.location.reload(); }; // @ts-expect-error types window.prodins = prodins; const zipins = async () => { const fauxput = document.createElement("input"); fauxput.type = "file"; fauxput.accept = ".zip"; fauxput.onchange = async e => { const target = e.target as HTMLInputElement; if (target && target.files) { const file = target.files[0]; const content = await file.arrayBuffer(); setProgress(10); if (localStorage.getItem("setup")) { await window.tb.sh.format(); 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", window.tb.buffer.from(content), "arraybuffer"); setProgress(35); setShowCursor(false); main.current!.classList.remove("flex"); main.current!.classList.add("hidden"); progresscheck.current!.classList.remove("hidden"); progresscheck.current!.classList.add("flex"); await unzip("//uploaded.zip", "//"); setProgress(72); const users = await window.tb.fs.promises.readdir("/home/"); for (const user of users) { // note from XSTARS, this is a workaround that fixes the stupid symlink bug but it fucks over people with custom symlinks so be aware of that await window.tb.sh.promises.rm(`/home/${user}/desktop/`, { recursive: true }); await window.tb.fs.promises.mkdir(`/home/${user}/desktop/`); let r2 = []; let sysapps: { name: string; config: string; user: string }[] = []; let items: { name: string; item: string; position: { custom: boolean; top: number; left: number } }[] = []; for (let i = 0; i < apps.length; i++) { const app = apps[i]; const name = app.name.toLowerCase(); var topPos: number = 0; var leftPos: number = 0; if (i % 12 === 0) { topPos = 0; } else { topPos = i % 12; } if (i < 12) { leftPos = 0; } else { leftPos = 1; } if (topPos * 66 > window.innerHeight - 130) { leftPos = 1.15; if (r2.length === 0) { topPos = 0; } else { topPos = r2.length % 12; } r2.push({ name: app.name, }); } items.push({ name: app.name, item: `/home/${user}/desktop/${name}.lnk`, position: { custom: false, top: topPos, left: leftPos, }, }); sysapps.push({ name: app.name, config: `/apps/system/${name}.tapp/index.json`, user: "System", }); await window.tb.fs.promises.writeFile(`/home/${user}/desktop/.desktop.json`, JSON.stringify(items)); await window.tb.fs.promises.symlink(`/apps/system/${name}.tapp/index.json`, `/home/${user}/desktop/${name}.lnk`); } } await window.tb.fs.promises.mkdir("/system/tmp/"); statusref.current!.innerText = "Cleaning up..."; setProgress(85); await window.tb.fs.promises.unlink("//uploaded.zip"); setProgress(100); statusref.current!.innerText = "Restarting..."; sessionStorage.clear(); sessionStorage.setItem("boot", "true"); localStorage.setItem("setup", "true"); window.location.reload(); } }; fauxput.click(); }; async function download(url: string, location: string) { 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, 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 ? 5 : 4) : prevSelected - 1)); } else if (e.key === "ArrowDown") { 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.format(); 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) { msgbox.current!.classList.remove("hidden"); msgbox.current!.classList.add("flex"); main.current!.classList.add("hidden"); main.current!.classList.remove("flex"); setShowCursor(true); 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 === 4 && updCache) { setShowCursor(false); msgbox.current!.classList.remove("flex"); msgbox.current!.classList.add("hidden"); progresscheck.current!.classList.remove("hidden"); progresscheck.current!.classList.add("flex"); await copyDir("/system/tmp/terb-upd/", "/apps/", true); 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 ? 5 : 4)) { sessionStorage.clear(); window.location.reload(); } } else if (e.key === "Escape") { setShowCursor(prev => !prev); } }; const getPlatform = () => { const mobileuas = /(android|bb\d+|meego).+mobile|armv7l|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series[46]0|samsungbrowser.*mobile|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino|android|ipad|playbook|silk|iPhone|iPad/i; const crosua = /CrOS/; if (mobileuas.test(navigator.userAgent) && !crosua.test(navigator.userAgent)) { return "mobile"; } else if (!mobileuas.test(navigator.userAgent) && navigator.maxTouchPoints > 1 && navigator.userAgent.indexOf("Macintosh") !== -1 && navigator.userAgent.indexOf("Safari") !== -1) { return "mobile"; } else { return "desktop"; } }; if (getPlatform() === "mobile") { setShowCursor(true); } window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [selected]); return (
Terbium Recovery Utility - Version {version}
{msg}
Terbium
Terbium is installing

Please DO NOT close this tab

Downloading...

{ localStorage.clear(); sessionStorage.clear(); sessionStorage.setItem("boot", "true"); if (localStorage.getItem("setup")) { await window.tb.sh.format(); 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(); }} > Reinstall Terbium { msgbox.current!.classList.remove("hidden"); msgbox.current!.classList.add("flex"); main.current!.classList.add("hidden"); main.current!.classList.remove("flex"); setShowCursor(true); setMsg("BE AWARE if your static hosting this download will NOT work. Proceed?"); setAction("prodins()"); }} > Restore from Production Instance (Beta) { migrateFs(); }} > Migrate from Filer to OPFS { zipins(); }} > Restore from ZIP (Beta) {updCache && ( { setShowCursor(false); msgbox.current!.classList.remove("flex"); msgbox.current!.classList.add("hidden"); progresscheck.current!.classList.remove("hidden"); progresscheck.current!.classList.add("flex"); await copyDir("/system/tmp/terb-upd/", "/apps/", true); 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(); }} > Restore from Update Cache )} { sessionStorage.clear(); window.location.reload(); }} > Exit
Use the and keys to switch entry. Press the enter key to select.
); } ================================================ FILE: src/Setup.tsx ================================================ import Cropper from "cropperjs"; import Compressor from "compressorjs"; import { useState, useRef, useEffect } from "react"; import "./sys/gui/styles/login.css"; import "./sys/gui/styles/cropper.css"; import "./sys/gui/styles/oobe.css"; 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() { const [beforeSetup, setBeforeSetup] = useState(1); const [currentStep, setCurrentStep] = useState(1); const currentViewRef = useRef(null); var nextButtonClick = () => void 0; const Next = (step?: number) => { setBeforeSetup(currentStep); if (step) { setCurrentStep(step); } else { setCurrentStep(prevStep => Math.min(prevStep + 1, 5)); } }; const Back = () => { setBeforeSetup(currentStep); 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 () => { window.onbeforeunload = e => { e.preventDefault(); e.returnValue = "Terbium is still updating"; }; window.dispatchEvent(new CustomEvent("oobe-setupstage", { detail: "Initializing File System..." })); const int = await init(); console.log(`Init State: ${int}`); window.dispatchEvent(new CustomEvent("oobe-setupstage", { detail: "Setting up user..." })); let data: User = JSON.parse(sessionStorage.getItem("new-user") as string); sessionStorage.setItem("new-user", JSON.stringify(data)); const usr = data["username"]; data["id"] = usr; let pass: any; if (typeof data["password"] === "string" && data["password"].length > 0) { pass = pw.harden(data["password"]); } 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, password: pass, pfp: data["pfp"], perm: data["perm"], }; if (data.securityQuestion) { userInf["securityQuestion"] = { question: data.securityQuestion.question, 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); } window.dispatchEvent(new CustomEvent("oobe-setupstage", { detail: "Finalizing setup..." })); 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/`); await window.tb.fs.promises.mkdir(`/home/${usr}/images/`); await window.tb.fs.promises.mkdir(`/home/${usr}/videos/`); await window.tb.fs.promises.mkdir(`/home/${usr}/music/`); window.dispatchEvent(new CustomEvent("oobe-setupstage", { detail: "Finalizing settings..." })); let settings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${usr}/settings.json`, "utf8")); let syssettings = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8")); if (!syssettings["setup"] || syssettings["setup"] === false) { syssettings["setup"] = true; } syssettings["defaultUser"] = usr; const transport = sessionStorage.getItem("selectedTransport") || "Default (Epoxy)"; if (transport === "Default (Epoxy)") { settings["transport"] = "Default (Epoxy)"; } else if (transport === "Anura BCC") { settings["transport"] = "Anura BCC"; } else { settings["transport"] = "Libcurl"; } const wsrv = sessionStorage.getItem("selectedBare") || `${location.protocol.replace("http", "ws")}//${location.hostname}:${location.port}/wisp/`; settings["wispServer"] = wsrv; await window.tb.fs.promises.writeFile(`/home/${usr}/settings.json`, JSON.stringify(settings), "utf8"); await window.tb.fs.promises.writeFile("/system/etc/terbium/settings.json", JSON.stringify(syssettings), "utf8"); window.dispatchEvent(new CustomEvent("oobe-setupstage", { detail: "Finalizing setup..." })); const wispExist = await fileExists("//apps/system/settings.tapp/wisp-servers.json"); if (!wispExist) { const stockDat = [ { id: `${location.protocol.replace("http", "ws")}//${location.hostname}:${location.port}/wisp/`, name: "Backend" }, { id: "wss://wisp.terbiumon.top/wisp/", name: "TB Wisp Instance" }, ]; await window.tb.fs.promises.writeFile("//apps/system/settings.tapp/wisp-servers.json", JSON.stringify(stockDat)); } window.dispatchEvent(new CustomEvent("oobe-setupstage", { detail: "Restarting Terbium..." })); localStorage.setItem("setup", "true"); window.onbeforeunload = null; if (sessionStorage!.getItem("logged-in") === null || sessionStorage!.getItem("logged-in") === undefined || sessionStorage!.getItem("logged-in") === "false") { window.location.reload(); sessionStorage.setItem("firstRun", "true"); } else { window.location.reload(); sessionStorage.setItem("firstRun", "true"); } sessionStorage.removeItem("new-user"); }; const Step1 = () => { 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" >
The next generation of Terbium. Built to last.
); }; 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"); }, 150); const usernameRef = useRef(null); const passwordRef = useRef(null); const pfpRef = useRef(null); const secQ = useRef(null); const secA = useRef(null); const [showSec, setShowsec] = useState(false); nextButtonClick = () => { const pfp = pfpRef.current?.getAttribute("data-src"); 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; sessionStorage.setItem("new-user", JSON.stringify(passdata)); const password = passwordRef.current?.value || false; const username = usernameRef.current?.value || "Guest"; let data: any = JSON.parse(sessionStorage.getItem("new-user") as string) || {}; data["username"] = username; data["password"] = password; data["perm"] = "admin"; if (secA.current?.value && secQ.current?.value && secA.current.value.length > 0 && secQ.current.value.length > 0) { data["securityQuestion"] = { question: secQ.current.value, answer: secA.current.value, }; } sessionStorage.setItem("new-user", JSON.stringify(data)); currentViewRef.current?.classList.add("-translate-x-6"); currentViewRef.current?.classList.add("opacity-0"); setTimeout(() => { Next(3); }, 150); }; return (
{ currentViewRef.current = el; }} className="duration-150 -translate-x-6 opacity-0 flex-col justify-center items-center" >
Setup your account.
{ makePFP(); }} >

Upload

{ if (e.key === "Enter" && passwordRef.current) { passwordRef.current.focus(); } }} />
) => { if (e.target.value.length > 0) { setShowsec(true); } else { setShowsec(false); } }} />
{showSec ? (
Security Question (optional)
) : null}
); }; const Step3 = () => { setTimeout(() => { currentViewRef.current?.classList.remove("-translate-x-6"); currentViewRef.current?.classList.remove("opacity-0"); }, 150); nextButtonClick = () => { currentViewRef.current?.classList.add("-translate-x-6"); currentViewRef.current?.classList.add("opacity-0"); setTimeout(() => { Next(); }, 150); }; const [selectedProxy, setSelectedProxy] = useState(() => sessionStorage.getItem("selectedProxy") || "Scramjet"); const [proxyDropdownOpen, setProxyDropdownOpen] = useState(false); const toggleProxyDropDown = () => { setProxyDropdownOpen(prev => { return !prev; }); }; const proxyClick = (optionLabel: any) => { setSelectedProxy(optionLabel); sessionStorage.setItem("selectedProxy", optionLabel); setProxyDropdownOpen(false); }; return (
{ currentViewRef.current = el; }} className="duration-150 -translate-x-6 opacity-0 flex flex-col justify-center items-center" > Choose your default proxy.
{selectedProxy}
{proxyDropdownOpen && (
proxyClick("Ultraviolet")}> Ultraviolet
proxyClick("Scramjet")}> Scramjet
)}
); }; 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 isb64 = (v: any) => { if (!v || typeof v !== "string") return false; if (v.startsWith("data:")) return true; const stripped = v.replace(/\s+/g, ""); return /^[A-Za-z0-9+/=]+$/.test(stripped) && stripped.length > 200; }; const normalizeRaw = (r: any) => (typeof r === "string" && isb64(r) ? "Synced Wallpaper" : r); 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]) => { if (v && typeof v === "object" && !Array.isArray(v)) { return Object.entries(v).map(([kk, vv]) => ({ id: `settings.${k}.${kk}`, label: `${k}.${kk}`, raw: normalizeRaw(vv) })); } else { return [{ id: `settings.${k}`, label: k, raw: normalizeRaw(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: normalizeRaw(item) }; }); } if (val && typeof val === "object") { return Object.keys(val).map(k => ({ id: `${cat}.${k}`, label: k, raw: normalizeRaw(val[k]) })); } return [{ id: cat, label: `${cat}: ${String(val)}`, raw: normalizeRaw(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 .filter(opt => opt.id !== "apps.installed") .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"); currentViewRef.current?.classList.remove("opacity-0"); }, 150); nextButtonClick = () => { currentViewRef.current?.classList.add("-translate-x-6"); currentViewRef.current?.classList.add("opacity-0"); setTimeout(() => { Next(); }, 150); }; const [selectedBare, setSelectedBare] = useState(() => sessionStorage.getItem("selectedBare") || "Backend (Default)"); const [selectedTransport, setSelectedTransport] = useState(() => sessionStorage.getItem("selectedTransport") || "Default (Epoxy)"); const [bareDropdownOpen, setBareDropdownOpen] = useState(false); const [transportDropdownOpen, setTransportDropdownOpen] = useState(false); const [customServer, setCustomServer] = useState(""); const bareOptions = [{ label: "Backend (Default)" }, { label: "TB Wisp Instance" }, { label: "Custom Server" }]; const transportOptions = [{ label: "Default (Epoxy)" }, { label: "Libcurl" }, { label: "Anura BCC" }]; const bClick = (label: any) => { setSelectedBare(label); if (label === "Custom Server") { setCustomServer(""); } else if (label === "Backend (Default)") { sessionStorage.setItem("selectedBare", `${location.protocol.replace("http", "ws")}//${location.hostname}:${location.port}/wisp/`); } else if (label === "TB Wisp Instance") { sessionStorage.setItem("selectedBare", `wss://wisp.terbiumon.top/wisp/`); } setBareDropdownOpen(false); }; const transportOnClick = (label: any) => { setSelectedTransport(label); sessionStorage.setItem("selectedTransport", label); setTransportDropdownOpen(false); }; const ServerChange = async (e: any) => { const value = e.target.value; setCustomServer(value); sessionStorage.setItem("selectedBare", value); const stockDat = [ { id: `${location.protocol.replace("http", "ws")}//${location.hostname}:${location.port}/wisp/`, name: "Backend" }, { id: "wss://wisp.terbiumon.top/wisp/", name: "TB Wisp Instance" }, { id: value, name: "Custom Wisp" }, ]; await window.tb.fs.promises.writeFile("//apps/system/settings.tapp/wisp-servers.json", JSON.stringify(stockDat)); }; return (
{ currentViewRef.current = el; }} className="duration-150 -translate-x-6 opacity-0 flex flex-col justify-center items-center" > Customize Proxy Settings
setBareDropdownOpen(prev => !prev)}> {selectedBare}
{bareDropdownOpen && (
{bareOptions.map((option, index) => (
bClick(option.label)}> {option.label}
))}
)}
setTransportDropdownOpen(prev => !prev)}> {selectedTransport}
{transportDropdownOpen && (
{transportOptions.map((option, index) => (
transportOnClick(option.label)}> {option.label}
))}
)}
{selectedBare === "Custom Server" && ( )}
); }; const Step5 = () => { const ranRef = useRef(false); const actionRef = useRef(null); setTimeout(() => { currentViewRef.current?.classList.remove("-translate-x-6"); currentViewRef.current?.classList.remove("opacity-0"); }, 150); useEffect(() => { if (ranRef.current) return; ranRef.current = true; const updateActionText = (e: CustomEvent) => { if (actionRef.current) { actionRef.current.innerHTML = e.detail; } }; window.addEventListener("oobe-setupstage", updateActionText as EventListener); (async () => { await saveData(); })(); }, []); return (

{ currentViewRef.current = el; }} className="font-[700] text-[28px] duration-150 -translate-x-6 opacity-0" > Welcome to Terbium.

Starting Services...

); }; const Buttons = () => { const currentMotionEl = useRef(null); setTimeout(() => { currentMotionEl.current?.classList.remove("translate-y-8", "opacity-0"); }, 150); return (
{currentStep === 1 ? ( ) : null} {currentStep > 1 ? (
{ currentStep < 4 && (currentMotionEl.current = el); }} className={`${currentStep === 2 && beforeSetup !== 3 && "translate-y-8 opacity-0"} duration-150 w-full flex flex-wrap justify-between gap-2 max-w-full`} > {currentStep < 5 && ( )} {currentStep > 2 && currentStep < 5 && ( )}
) : null}
); }; return (
TB
{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}
); } ================================================ FILE: src/Updater.tsx ================================================ import { useEffect, useState, useRef } from "react"; import { dirExists, fileExists, unzip, 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); async function copyDir(inp: string, dest: string, rn?: boolean) { if (rn === true) { if (!(await dirExists(dest))) { await window.tb.fs.promises.mkdir(dest); } } const files = await window.tb.fs.promises.readdir(inp); const totalFiles = files.length; for (const [index, file] of files.entries()) { const stats = await window.tb.fs.promises.stat(`${inp}/${file}`); if (stats && stats.isDirectory()) { await window.tb.fs.promises.mkdir(`${dest}/${file}`); await copyDir(`${inp}/${file}`, `${dest}/${file}`, true); } else { await window.tb.fs.promises.writeFile(`${dest}/${file}`, await window.tb.fs.promises.readFile(`${inp}/${file}`, "utf8")); } statusref.current!.innerText = `Creating a copy of: ${file}...`; setProgress(Math.floor(((index + 1) / totalFiles) * 100)); } } useEffect(() => { const main = async () => { window.onbeforeunload = e => { e.preventDefault(); e.returnValue = "Terbium is still updating"; }; let sysapps = ["about.tapp", "app store.tapp", "browser.tapp", "calculator.tapp", "feedback.tapp", "files.tapp", "media viewer.tapp", "settings.tapp", "task manager.tapp", "terminal.tapp", "text editor.tapp"]; let sysscripts = [ "cat.js", "cd.js", "clear.js", "curl.js", "echo.js", "exit.js", "git.js", "help.js", "info.json", "info.schema.json", "ls.js", "mkdir.js", "node.js", "ping.js", "pkg.js", "pkill.js", "pwd.js", "qwick.js", "qwick_install.js", "rm.js", "rmdir.js", "sysfetch.js", "taskkill.js", "tb.js", "touch.js", "unzip.js", "ssh.js", "ssh-keygen.js", "nano.js", ]; if (await dirExists("/system/tmp/terb-upd/")) { await window.tb.sh.promises.rm(`/system/tmp/terb-upd/`, { recursive: true }); } statusref.current!.innerText = "Installing latest version of TB..."; await window.tb.fs.promises.mkdir("/system/tmp/terb-upd/"); const apps = await window.tb.fs.promises.readdir("/apps/system/"); const scripts = await window.tb.fs.promises.readdir("/apps/system/terminal.tapp/scripts/"); setProgress(20); statusref.current!.innerText = "Creating a backup"; if (await fileExists("/apps/system/settings.tapp/wisp-servers.json")) { await window.tb.fs.promises.writeFile("/system/tmp/terb-upd/wisp-servers.json", await window.tb.fs.promises.readFile("/apps/system/settings.tapp/wisp-servers.json")); } else { const stockDat = [ { id: `${location.protocol.replace("http", "ws")}//${location.hostname}:${location.port}/wisp/`, name: "Backend" }, { id: "wss://wisp.terbiumon.top/wisp/", name: "TB Wisp Instance" }, ]; await window.tb.fs.promises.writeFile("/system/tmp/terb-upd/wisp-servers.json", JSON.stringify(stockDat)); } for (const item of apps) { setProgress(prevProgress => prevProgress + 1); if (sysapps.includes(item)) { if (item === "terminal.tapp") { for (const item of scripts) { setProgress(prevProgress => prevProgress + 1); if (!sysscripts.includes(item)) { console.log(`Skipping ${item}...`); } } } else { await copyDir(`/apps/system/${item}/`, `/system/tmp/terb-upd/${item}.old`, true); await window.tb.sh.promises.rm(`/apps/system/${item}/`, { recursive: true }); } } else { console.log(`Skipping ${item}...`); } } setProgress(50); statusref.current!.innerText = "Updating Terbium..."; setProgress(0); for (const item of paths) { setProgress(prevProgress => prevProgress + 1); statusref.current!.innerText = `Installing ${item}...`; const isDir = item.toString().endsWith("/"); if (isDir) { try { // @ts-expect-error await window.tb.fs.promises.mkdir(`/apps/system/${item.toString()}`, { recursive: true }); } catch (err) { console.error(err); } } else if (item.toString().endsWith(".tapp.zip")) { const res = await fetch(`/apps/${item.toString()}`); if (!res.ok) { console.error(`Failed to fetch /apps/${item.toString()}: ${res.status} ${res.statusText}`); continue; } const data = await res.arrayBuffer(); await window.tb.fs.promises.writeFile(`/apps/system/${item}`, window.tb.buffer.from(data)); await unzip(`/apps/system/${item}`, `/apps/system/${item.slice(0, -4)}`); await window.tb.fs.promises.unlink(`/apps/system/${item}`); } else { const path = `/apps/system/${item.toString()}`; const dir = path.substring(0, path.lastIndexOf("/")); try { if (!(await dirExists(dir))) { // @ts-expect-error await window.tb.fs.promises.mkdir(dir, { recursive: true }); } const res = await fetch(`/apps/${item.toString()}`); const data = await res.text(); await window.tb.fs.promises.writeFile(path, data); } catch (err) { console.error(err); } } } await window.tb.fs.promises.writeFile("/apps/system/settings.tapp/wisp-servers.json", await window.tb.fs.promises.readFile("/system/tmp/terb-upd/wisp-servers.json")); await window.tb.fs.promises.writeFile("/system/etc/terbium/hash.cache", hash); const user = sessionStorage.getItem("currAcc") || JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8")).defaultUser; // v2.0-Beta2 update if (!(await fileExists("/apps/installed.json"))) { statusref.current!.innerText = "Installing Terbium v2.0-Beta2 prerequisites..."; let insapps = [ { name: "About", config: "/apps/system/about.tapp/index.json", user: "System", }, { name: "App Store", config: "/apps/system/app store.tapp/index.json", user: "System", }, { name: "Calculator", config: "/apps/system/calculator.tapp/index.json", user: "System", }, { name: "Feedback", config: "/apps/system/feedback.tapp/index.json", user: "System", }, { name: "Files", config: "/apps/system/files.tapp/index.json", user: "System", }, { name: "Media Viewer", config: "/apps/system/media viewer.tapp/index.json", user: "System", }, { name: "Settings", config: "/apps/system/settings.tapp/index.json", user: "System", }, { name: "Task Manager", config: "/apps/system/task manager.tapp/index.json", user: "System", }, { name: "Terminal", config: "/apps/system/terminal.tapp/index.json", user: "System", }, { name: "Text Editor", config: "/apps/system/text editor.tapp/index.json", user: "System", }, { name: "Anura File Manager", config: "/system/etc/anura/configs/Anura File Manager.json", user: "System", }, ]; const startApps = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/start.json", "utf8")).system_apps; for (const app of startApps) { let appName = ""; let appTitle = ""; if (typeof app.title === "object" && app.title !== null && "text" in app.title) { appTitle = app.title.text; } else if (typeof app.title === "string") { appTitle = app.title; } else if (typeof app.name === "string") { appTitle = app.name; } appName = appTitle?.toLowerCase?.() || ""; const isSysApp = sysapps.some(s => s.toLowerCase() === appName) || appName === "anura file manager" || appName === "browser"; const alreadyInstalled = insapps.some(a => a.name?.toLowerCase?.() === appName || a.name?.toLowerCase?.() === appTitle?.toLowerCase?.()); if (!isSysApp && !alreadyInstalled) { let configPath = "/apps/"; if (app.name) { let appDir = `/apps/system/${app.name}/`; let configFile = ""; if (await fileExists(`${appDir}.tbconfig`)) { configFile = `${appDir}.tbconfig`; } else { if (user) { appDir = `/apps/user/${user}/${app.name}/`; } else { appDir = `/apps/user/${JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8")).defaultUser}/${app.name}/`; } if (await fileExists(`${appDir}.tbconfig`)) { configFile = `${appDir}.tbconfig`; } else { configFile = `${appDir}index.json`; } } if (configFile) { configPath = configFile; } } insapps.push({ name: appTitle, config: configPath, user: user || "System", }); } } await window.tb.fs.promises.mkdir("/system/etc/anura/configs/"); await window.tb.fs.promises.writeFile("/apps/installed.json", JSON.stringify(insapps)); await window.tb.fs.promises.writeFile("/system/var/terbium/recent.json", JSON.stringify([])); } // v2.1 update if (!(await fileExists(`/apps/user/${user}/app store/repos.json`))) { await window.tb.fs.promises.mkdir(`/apps/user/${user}/app store/`); await window.tb.fs.promises.writeFile( `/apps/user/${user}/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", }, ]), ); } if (!(await fileExists(`/apps/user/${user}/browser/favorites.json`))) { 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([])); } // 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)); } // hotfix for v2.2.1 & migration from v2.0.0-beta.x to v2.1.x if (await fileExists(`/home/${user}/desktop/browser.lnk`)) { await window.tb.fs.promises.writeFile(`/home/${user}/desktop/browser.lnk`, "symlink:/apps/system/browser.tapp/index.json:file"); const desktopItems = JSON.parse(await window.tb.fs.promises.readFile(`/home/${user}/desktop/.desktop.json`, "utf8")); const hasBrowserShortcut = desktopItems.some((item: any) => item.name?.toLowerCase?.() === "browser" || item.item === `/home/${user}/desktop/browser.lnk`); if (!hasBrowserShortcut) { desktopItems.push({ name: "Browser", item: `/home/${user}/desktop/browser.lnk`, position: { custom: false, top: desktopItems.length, left: 0, }, }); await window.tb.fs.promises.writeFile(`/home/${user}/desktop/.desktop.json`, JSON.stringify(desktopItems, null, 4)); } } } if (!(await fileExists("/system/etc/terbium/taccs.json"))) { await window.tb.fs.promises.writeFile("/system/etc/terbium/taccs.json", JSON.stringify({})); } if (!(await fileExists("/system/var/terbium/startup.json"))) { const users = await window.tb.fs.promises.readdir("/home/"); const startupObj = { System: {}, ...Object.fromEntries(users.map(user => [user, {}])), }; await window.tb.fs.promises.writeFile("/system/var/terbium/startup.json", JSON.stringify(startupObj), "utf8"); } // v2.3 update const dockConfig = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/dock.json", "utf8")); const startConfig = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/start.json", "utf8")); try { let changed = false; const termHtml = `
\n\n
\n
\n
\n\n
\n
\n
`; function upgradeEntry(entry: any): boolean { if (!entry || typeof entry !== "object") return false; let updated = false; if (typeof entry.title === "string" && entry.title.toLowerCase() === "terminal") { entry.title = { text: "Terminal", html: termHtml }; updated = true; } if (entry.title && typeof entry.title === "object" && entry.title.text && entry.title.text.toLowerCase() === "terminal" && !entry.title.html) { entry.title.html = termHtml; updated = true; } if (entry.src && typeof entry.src === "string" && entry.src.includes("terminal.tapp")) { if (typeof entry.title === "string") { entry.title = { text: "Terminal", html: termHtml }; updated = true; } else if (entry.title && typeof entry.title === "object" && !entry.title.html) { entry.title.html = termHtml; updated = true; } } if (entry.size && typeof entry.size === "object" && entry.size.height === 400) { entry.size.height = 415; updated = true; } return updated; } if (Array.isArray(dockConfig)) { dockConfig.forEach((e: any) => { if (upgradeEntry(e)) changed = true; }); } else if (dockConfig && typeof dockConfig === "object") { if (Array.isArray(dockConfig.pinned_apps)) { dockConfig.pinned_apps.forEach((e: any) => { if (upgradeEntry(e)) changed = true; }); } if (Array.isArray(dockConfig.apps)) { dockConfig.apps.forEach((e: any) => { if (upgradeEntry(e)) changed = true; }); } if (Array.isArray(dockConfig.items)) { dockConfig.items.forEach((e: any) => { if (upgradeEntry(e)) changed = true; }); } } if (startConfig && Array.isArray(startConfig.system_apps)) { startConfig.system_apps.forEach((app: any) => { if (upgradeEntry(app)) changed = true; }); } if (changed) { await window.tb.fs.promises.writeFile("/system/var/terbium/dock.json", JSON.stringify(dockConfig, null, 4), "utf8"); await window.tb.fs.promises.writeFile("/system/var/terbium/start.json", JSON.stringify(startConfig, null, 4), "utf8"); console.log("Migrated terminal entries in dock.json/start.json to new metadata"); } } catch (err) { console.warn("Failed to migrate dock/start config:", err); } 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(); }; 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 (
Terbium
Terbium is updating

Please DO NOT close this tab

Downloading Updates...

); } ================================================ FILE: src/index.css ================================================ @import "tailwindcss"; @theme { --shadow-tb-border-shadow: 0px 0px 6px 0px #00000052, inset 0 0 0 0.5px #ffffff38; --shadow-tb-border: inset 0 0 0 0.5px #ffffff38; --shadow-window-shadow: 0px 0px 10px 1px #00000052; --cursor-text: var(--cursor-text); --cursor-pointer: var(--cursor-pointer); --cursor-default: var(--cursor-normal); --cursor-crosshair: var(--cursor-crosshair); --cursor-wait: var(--cursor-wait); --cursor-nw-resize: var(--cursor-nw-resize); --cursor-ne-resize: var(--cursor-ne-resize); --cursor-sw-resize: var(--cursor-sw-resize); --cursor-se-resize: var(--cursor-se-resize); --cursor-n-resize: var(--cursor-n-resize); --cursor-s-resize: var(--cursor-s-resize); --cursor-e-resize: var(--cursor-e-resize); --cursor-w-resize: var(--cursor-w-resize); } /* The default border color has changed to `currentColor` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the same as it did with Tailwind CSS v3. If we ever want to remove these styles, we need to add an explicit border color utility to any element that depends on these defaults. */ @layer base { *, ::after, ::before, ::backdrop, ::file-selector-button { border-color: var(--color-gray-200, currentColor); } } /* The default border color has changed to `currentColor` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the same as it did with Tailwind CSS v3. If we ever want to remove these styles, we need to add an explicit border color utility to any element that depends on these defaults. */ @layer base { *, ::after, ::before, ::backdrop, ::file-selector-button { border-color: var(--color-gray-200, currentColor); } } @font-face { font-family: Inter; src: url("/fonts/Inter.ttf") format("truetype"); } :root { font-family: Inter; color-scheme: light dark; color: #ffffffde; background-color: #000000; --cursor-normal: url("/cursors/dark/normal.svg") 6 0, default; --cursor-pointer: url("/cursors/dark/pointer.svg") 6 0, pointer; --cursor-text: url("/cursors/dark/text.svg") 10 0, text; --cursor-crosshair: url("/cursors/dark/crosshair.svg") 0 0, crosshair; --cursor-wait: url("/cursors/dark/wait.svg") 0 0, wait; --cursor-nw-resize: url("/cursors/dark/resize-l.svg") 0 0, nw-resize; --cursor-ne-resize: url("/cursors/dark/resize-r.svg") 0 0, ne-resize; --cursor-sw-resize: url("/cursors/dark/resize-r.svg") 0 0, sw-resize; --cursor-se-resize: url("/cursors/dark/resize-l.svg") 0 0, se-resize; --cursor-n-resize: url("/cursors/dark/resize-v.svg") 0 0, n-resize; --cursor-s-resize: url("/cursors/dark/resize-v.svg") 0 0, s-resize; --cursor-e-resize: url("/cursors/dark/resize-h.svg") 0 0, e-resize; --cursor-w-resize: url("/cursors/dark/resize-h.svg") 0 0, w-resize; } html, body { height: 100%; margin: 0; cursor: var(--cursor-normal); } iframe { background: transparent !important; } .grainy { background: repeating-conic-gradient(#0000 0.000045%, #000 0.0005%); mix-blend-mode: overlay; } ================================================ FILE: src/init/fs.init.ts ================================================ import paths from "../installer.json"; import { unzip } from "../sys/types"; export async function copyfs() { for (const item of paths) { const p = item.toString(); if (p.includes("browser.tapp")) continue; window.dispatchEvent(new CustomEvent("oobe-setupstage", { detail: `Copying ${p} to File System...` })); if (p.toLowerCase().includes(".tapp.zip")) { 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.arrayBuffer(); await window.tb.fs.promises.writeFile(`/apps/system/${p}`, window.tb.buffer.from(data)); await unzip(`/apps/system/${p}`, `/apps/system/${p.slice(0, -4)}`); await window.tb.fs.promises.unlink(`/apps/system/${p}`); continue; } if (p.endsWith("/")) { await window.tb.fs.promises.mkdir(`/apps/system/${p}`); } else { 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"; } ================================================ FILE: src/init/index.ts ================================================ import { dirExists, TAuthSSData, UserSettings } from "../sys/types"; import apps from "../apps.json"; import { copyfs } from "./fs.init"; import { hash } from "../hash.json"; export async function init() { /** * create home structure */ console.log("Initing File System please wait..."); if (!(await dirExists("/home"))) { await window.tb.fs.promises.mkdir("/home"); } const user = JSON.parse(`${sessionStorage.getItem("new-user")}`).username; /** * create apps structure */ if (!(await dirExists("/apps"))) { await window.tb.fs.promises.mkdir("/apps"); await window.tb.fs.promises.mkdir("/apps/system"); await copyfs(); window.dispatchEvent(new CustomEvent("oobe-setupstage", { detail: "Initializing File System..." })); await window.tb.fs.promises.mkdir("/apps/user"); await window.tb.fs.promises.writeFile("/apps/web_apps.json", JSON.stringify({ apps: [] })); } else { if (!(await dirExists("/apps/user"))) { await window.tb.fs.promises.mkdir("/apps/user"); } } if (!(await dirExists(`/apps/user/${user}`))) { await window.tb.fs.promises.mkdir(`/apps/user/${user}`); await window.tb.fs.promises.mkdir(`/apps/user/${user}/files`); await window.tb.fs.promises.mkdir(`/apps/user/${user}/terminal`); } /** * create system structure */ if (!(await dirExists("/system"))) { await window.tb.fs.promises.mkdir("/system"); await window.tb.fs.promises.mkdir("/system/trash"); await window.tb.fs.promises.mkdir("/system/bin"); await window.tb.fs.promises.mkdir("/system/etc"); await window.tb.fs.promises.mkdir("/system/etc/terbium"); let stockSettings = { theme: "dark", "system-blur": true, "dock-full": false, fileAssociatedApps: { text: "text-editor", image: "media-viewer", video: "media-viewer", audio: "media-viewer", }, location: "40.7831,-73.9712", weather: { unit: "Celsius", }, "host-name": "terbium", }; await window.tb.fs.promises.writeFile("/system/etc/terbium/settings.json", JSON.stringify(stockSettings)); await window.tb.fs.promises.writeFile("/system/etc/terbium/sudousers.json", JSON.stringify([])); await window.tb.fs.promises.mkdir("/system/etc/terbium/wallpapers"); await window.tb.fs.promises.mkdir("/system/var"); await window.tb.fs.promises.mkdir("/system/var/terbium"); await window.tb.fs.promises.writeFile("/system/etc/terbium/hash.cache", hash); let startApps = { system_apps: apps.map(app => app.config), pinned_apps: [], }; await window.tb.fs.promises.writeFile("/system/var/terbium/start.json", JSON.stringify(startApps)); await window.tb.fs.promises.writeFile(`/apps/installed.json`, JSON.stringify([])); await window.tb.fs.promises.mkdir("/apps/anura/"); let dockPins = [ { title: { text: "Terminal", html: '
\n\n
\n
\n
\n\n
\n
\n
', }, icon: "/fs/apps/system/terminal.tapp/icon.svg", isPinnable: true, src: "/fs/apps/system/terminal.tapp/index.html", size: { width: 612, height: 415, }, }, { title: "Files", icon: "/fs/apps/system/files.tapp/icon.svg", isPinnable: true, src: "/fs/apps/system/files.tapp/index.html", size: { width: 600, height: 500, }, }, { title: "Settings", icon: "/fs/apps/system/settings.tapp/icon.svg", isPinnable: true, src: "/fs/apps/system/settings.tapp/index.html", single: true, }, { title: "Feedback", icon: "/fs/apps/system/feedback.tapp/icon.svg", proxy: true, isPinnable: true, src: "https://forms.gle/m664xxmrugWQADQt9", size: { width: 600, height: 500, }, }, ]; await window.tb.fs.promises.writeFile("/system/var/terbium/dock.json", JSON.stringify(dockPins)); await window.tb.fs.promises.writeFile("/system/var/terbium/startup.json", JSON.stringify({ System: {}, [user]: {} }), "utf8"); await window.tb.fs.promises.mkdir("/system/lib"); await window.tb.fs.promises.mkdir("/system/lib/anura"); await window.tb.fs.promises.mkdir("/system/tmp"); 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: 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/`, "battery-percent": false, accent: "#32ae62", times: { format: "12h", 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 = []; let sysapps: { name: string; config: string; user: string }[] = []; for (let i = 0; i < apps.length; i++) { const app = apps[i]; const name = app.name.toLowerCase(); var topPos: number = 0; var leftPos: number = 0; if (i % 12 === 0) { topPos = 0; } else { topPos = i % 12; } if (i < 12) { leftPos = 0; } else { leftPos = 1; } if (topPos * 66 > window.innerHeight - 130) { leftPos = 1.15; if (r2.length === 0) { topPos = 0; } else { topPos = r2.length % 12; } r2.push({ name: app.name, }); } items.push({ name: app.name, item: `/home/${user}/desktop/${name}.lnk`, position: { custom: false, top: topPos, left: leftPos, }, }); await window.tb.fs.promises.mkdir(`/apps/system/${name}.tapp`); await window.tb.fs.promises.writeFile( `/apps/system/${name}.tapp/index.json`, JSON.stringify({ name: app.name, config: app.config, icon: app.config.icon, }), ); sysapps.push({ name: app.name, config: `/apps/system/${name}.tapp/index.json`, user: "System", }); await window.tb.fs.promises.symlink(`/apps/system/${name}.tapp/index.json`, `/home/${user}/desktop/${name}.lnk`); } await window.tb.fs.promises.writeFile(`/home/${user}/desktop/.desktop.json`, JSON.stringify(items)); if (tcaccSettings && Array.isArray(tcaccSettings) && tcaccSettings[0]) { 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}/`, ...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([])); await window.tb.fs.promises.writeFile(`/apps/installed.json`, JSON.stringify(sysapps)); const response = await fetch("/apps/files.tapp/icons.json"); const dat = await response.json(); const iconNames = Object.keys(dat["name-to-path"]); var iconArrays: { [key: string]: string } = {}; await window.tb.fs.promises.mkdir(`/system/etc/terbium/file-icons`); 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({ "ext-to-name": dat["ext-to-name"], "name-to-path": iconArrays, }), ); await window.tb.fs.promises.writeFile( `/apps/user/${user}/files/quick-center.json`, JSON.stringify({ paths: { Desktop: `/home/${user}/desktop`, Documents: `/home/${user}/documents`, Images: `/home/${user}/images`, Videos: `/home/${user}/videos`, Music: `/home/${user}/music`, Trash: `/system/trash`, }, }), "utf8", ); await window.tb.fs.promises.writeFile(`/apps/user/${user}/terminal/info.json`, JSON.stringify({})); await window.tb.fs.promises.mkdir(`/apps/user/${user}/app store/`); if (tcaccSettings && Array.isArray(tcaccSettings) && tcaccSettings[0].apps) { await window.tb.fs.promises.writeFile(`/apps/user/${user}/app store/repos.json`, JSON.stringify(tcaccSettings[0].apps.repos), "utf8"); } else { await window.tb.fs.promises.writeFile( `/apps/user/${user}/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; } ================================================ FILE: src/main.tsx ================================================ import { StrictMode, useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; import App from "./App.tsx"; import "./index.css"; import { BareMuxConnection } from "@mercuryworkshop/bare-mux"; import Boot from "./Boot.tsx"; import CustomOS from "./CustomOS.tsx"; import { hash } from "./hash.json"; import Loader from "./Loading.tsx"; import Login from "./Login.tsx"; import Recovery from "./Recovery.tsx"; import Setup from "./Setup.tsx"; import { fileExists } from "./sys/types.ts"; import Updater from "./Updater.tsx"; const Root = () => { const [currPag, setPag] = useState(); 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: `${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/", files: { wasm: "/scram/scramjet.wasm.wasm", all: "/scram/scramjet.all.js", sync: "/scram/scramjet.sync.js", }, defaultFlags: { rewriterLogs: false, }, codec: { encode: function encode(input: string): string { let result = ""; let len = input.length; for (let i = 0; i < len; i++) { const char = input[i]; result += i % 2 ? String.fromCharCode(char.charCodeAt(0) ^ 2) : char; } return encodeURIComponent(result); }, decode: function decode(input: string): string { if (!input) return input; input = decodeURIComponent(input); let result = ""; let len = input.length; for (let i = 0; i < len; i++) { const char = input[i]; result += i % 2 ? String.fromCharCode(char.charCodeAt(0) ^ 2) : char; } return result; }, }, }; window.scramjet = new ScramjetController(scramjetTb); scramjet.init(); navigator.serviceWorker.register("/anura-sw.js"); }; tempTransport(); if (sessionStorage.getItem("recovery")) { setPag(); } else if (sessionStorage.getItem("boot") || params.get("boot")) { const upd = async () => { let sha; if (await fileExists("/system/etc/terbium/hash.cache")) { sha = await window.tb.fs.promises.readFile("/system/etc/terbium/hash.cache", "utf8"); } else { sha = hash; } if (localStorage.getItem("setup")) { if (localStorage.getItem("setup") && (sha !== hash || sessionStorage.getItem("migrateFs"))) { setPag(); } else { if (sessionStorage.getItem("logged-in") && sessionStorage.getItem("logged-in") === "true") { setPag(); } else { setPag(); } } } else { setPag(); } }; upd(); } else if (sessionStorage.getItem("cusboot")) { setPag(); } else { setPag(); } }, []); return currPag; }; createRoot(document.getElementById("root")!).render( , ); ================================================ FILE: src/sys/Api.ts ================================================ import { BareMuxConnection } from "@mercuryworkshop/bare-mux"; import type { ScramjetController } from "@mercuryworkshop/scramjet"; import * as fflate from "fflate"; import { libcurl } from "libcurl.js"; import apps from "../apps.json"; import { hash } from "../hash.json"; import pwd from "./apis/Crypto"; import { setDialogFn } from "./apis/Dialogs"; import { hideFn, isExistingFn, setMusicFn, setVideoFn } from "./apis/Mediaisland"; import { dismissNotifFn, setNotifFn } from "./apis/Notifications"; import { registry } from "./apis/Registry"; import { System } from "./apis/System"; import { XOR } from "./apis/Xor"; import { type AppIslandProps, clearControls, clearInfo, updateControls } from "./gui/AppIsland"; import type { TDockItem } from "./gui/Dock"; import { createWindow } from "./gui/WindowArea"; import { Lemonade } from "./lemonade"; import { AliceWM } from "./liquor/AliceWM"; import { Anura } from "./liquor/Anura"; import { LocalFS } from "./liquor/api/LocalFS"; import { AnuraBareClient } from "./liquor/bcc"; import { ExternalApp } from "./liquor/coreapps/ExternalApp"; import { ExternalLib } from "./liquor/libs/ExternalLib"; 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"; import { launchProcs, addStartupProc, removeStartupProc, enableProc, disableProc } from "./apis/utils/startupHandler"; const system = new System(); const pw = new pwd(); declare const tb: COM; declare global { interface Window { tb: COM; Filer: FilerType; ScramjetController: ScramjetController; } var scramjetTb: any; var scramjet: ScramjetController; } 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")); settings["battery-percent"] = true; await window.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(settings)); window.dispatchEvent(new CustomEvent("controlBatteryPercentVisibility", { detail: true })); return "Success"; }, async hidePercentage() { const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); settings["battery-percent"] = false; await window.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(settings)); window.dispatchEvent(new CustomEvent("controlBatteryPercentVisibility", { detail: false })); return "Success"; }, async canUse() { if ("BatteryManager" in window) { const battery = await navigator.getBattery(); return battery ? true : false; } return false; }, }, launcher: { async addApp(props: launcherProps) { const apps: any = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/start.json", "utf8")); if (!props.name) throw new Error("Name is required"); if (!props.icon) throw new Error("Icon is required"); if (apps.system_apps.some((app: any) => app.title === props.name)) { throw new Error("App with the same name already exists"); } apps.system_apps.push(props); await window.tb.fs.promises.writeFile("/system/var/terbium/start.json", JSON.stringify(apps, null, 2)); window.dispatchEvent(new Event("updApps")); return true; }, async removeApp(name: string) { if (!name) throw new Error("Name is required"); const data: any = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/start.json", "utf8")); const apps = data.system_apps; const realName = String(name) .replace(/[^a-zA-Z0-9]/g, "") .toLowerCase(); const appIndex = apps.findIndex((app: any) => { const n = app.name ? app.name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase() : ""; return n === realName; }); if (appIndex !== -1) { apps.splice(appIndex, 1); } else { throw new Error(`App with name '${name}' not found`); } await window.tb.fs.promises.writeFile("/system/var/terbium/start.json", JSON.stringify(data, null, 2)); window.dispatchEvent(new Event("updApps")); return true; }, }, theme: { async get() { return JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8"))["theme"]; }, async set(data: string) { return new Promise(async resolve => { const settings: SysSettings = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8")); settings["theme"] = data; await window.tb.fs.promises.writeFile("/system/etc/terbium/settings.json", JSON.stringify(settings), "utf8"); resolve(true); }); }, }, desktop: { preferences: { async setTheme(color: string) { color.toString().includes('"') ? (color = color.replace(/"/g, "")) : (color = color); document.body.setAttribute("theme", color); const settings: SysSettings = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8")); settings["theme"] = color; await window.tb.fs.promises.writeFile("/system/etc/terbium/settings.json", JSON.stringify(settings), "utf8"); }, async theme() { const settings: SysSettings = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8")); return settings["theme"]; }, async setAccent(color: string) { const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); settings["accent"] = color; await window.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(settings), "utf8"); }, async getAccent() { return JSON.parse(await window.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8"))["accent"]; }, }, wallpaper: { async set(path: string) { const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); settings["wallpaper"] = path; await window.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(settings)); window.dispatchEvent(new Event("updWallpaper")); }, async contain() { const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); settings["wallpaperMode"] = "contain"; await window.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(settings), "utf8"); window.dispatchEvent(new Event("updWallpaper")); }, async stretch() { const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); settings["wallpaperMode"] = "stretch"; await window.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(settings), "utf8"); window.dispatchEvent(new Event("updWallpaper")); }, async cover() { const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); settings["wallpaperMode"] = "cover"; await window.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(settings), "utf8"); window.dispatchEvent(new Event("updWallpaper")); }, async fillMode() { return new Promise(async resolve => { resolve(JSON.parse(await window.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8"))["wallpaperMode"]); }); }, }, dock: { async pin(app: TDockItem) { const apps: Array = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/dock.json")); apps.push(app); await window.tb.fs.promises.writeFile("/system/var/terbium/dock.json", JSON.stringify(apps)); window.dispatchEvent(new Event("updPins")); return "Success"; }, async unpin(app: string) { let apps: Array = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/dock.json")); const appExists = apps.some(appIndex => appIndex.title === app); if (!appExists) { throw new Error(`App with title "${app}" not found in the dock.`); } apps = apps.filter(appIndex => appIndex.title !== app); apps.filter(appIndex => appIndex.title !== app); await window.tb.fs.promises.writeFile("/system/var/terbium/dock.json", JSON.stringify(apps)); window.dispatchEvent(new Event("updPins")); return "Success"; }, }, }, window: { getId() { return useWindowStore.getState().currentPID; }, create(props: any) { createWindow(props); }, content: { get() { return new Promise(resolve => { const getContent = (e: CustomEvent) => { window.removeEventListener("curr-win-content", getContent as EventListener); resolve(e.detail); }; window.addEventListener("curr-win-content", getContent as EventListener); window.dispatchEvent(new CustomEvent("get-content", { detail: useWindowStore.getState().currentPID })); }); }, set(html: string | HTMLElement) { const msg = { currWin: useWindowStore.getState().currentPID, content: html, }; window.dispatchEvent(new CustomEvent("upd-wincont", { detail: JSON.stringify(msg) })); }, }, titlebar: { setColor(hex: string) { const msg = { currWin: useWindowStore.getState().currentPID, color: hex, }; window.dispatchEvent(new CustomEvent("upd-winbarcol", { detail: JSON.stringify(msg) })); }, setText(text: string) { const msg = { currWin: useWindowStore.getState().currentPID, txt: text, }; window.dispatchEvent(new CustomEvent("upd-winbartxt", { detail: JSON.stringify(msg) })); }, setBackgroundColor(hex: string) { const msg = { currWin: useWindowStore.getState().currentPID, color: hex, }; window.dispatchEvent(new CustomEvent("upd-winbarbg", { detail: JSON.stringify(msg) })); }, }, island: { addControl(args: AppIslandProps) { if (!args.text) throw new Error("text is required"); if (!args.click) throw new Error("click function is required"); if (!args.appname) throw new Error("appname is required"); if (!args.id) throw new Error("control_id is required"); updateControls({ text: args.text, appname: args.appname, id: args.id, click: () => { if (args.click) { args.click(); } }, }); }, removeControl(control_id: string) { if (!control_id) throw new Error("control_id is required"); clearControls(control_id); }, }, changeSrc(src: string) { const currWin = useWindowStore.getState().currentPID; window.dispatchEvent(new CustomEvent("upd-src", { detail: JSON.stringify({ pid: currWin, url: src }) })); }, reload() { const currWin = useWindowStore.getState().currentPID; window.dispatchEvent(new CustomEvent("reload-win", { detail: currWin })); }, minimize() { const currWin = useWindowStore.getState().currentPID; window.dispatchEvent(new CustomEvent("min-win", { detail: currWin })); }, maximize() { const currWin = useWindowStore.getState().currentPID; window.dispatchEvent(new CustomEvent("max-win", { detail: currWin })); }, close() { const currWin = useWindowStore.getState().currentPID; clearInfo(); useWindowStore.getState().killWindow(currWin); }, }, contextmenu: { create(props: cmprops) { let adjustedX = props.x; let adjustedY = props.y; let shouldAdjust = false; if (props.iframe === true) { shouldAdjust = true; } else if (props.iframe === false) { shouldAdjust = false; } else { try { const stack = new Error().stack || ""; const isCalledFromIframe = stack.includes("about:srcdoc") || /at\s+.*?\/apps\/.*?\.tapp\//.test(stack) || stack.split("\n").some(line => { return line.includes("blob:") || line.includes("/apps/"); }); shouldAdjust = isCalledFromIframe; } catch (err) { console.warn("Could not detect iframe context for context menu:", err); } } if (shouldAdjust) { try { const currentPID = useWindowStore.getState().currentPID; const windows = useWindowStore.getState().windows; if (currentPID && windows) { const windowConfig = windows.find((w: any) => w.pid === currentPID); if (windowConfig?.wid && windowConfig?.src) { const windowElement = document.getElementById(windowConfig.wid); if (windowElement) { const iframe = windowElement.querySelector("iframe"); if (iframe) { const rect = iframe.getBoundingClientRect(); adjustedX = props.x + rect.left; adjustedY = props.y + rect.top; } } } } } catch (err) { console.warn("Could not calculate iframe offset for context menu:", err); } } window.dispatchEvent( new CustomEvent("ctxm", { detail: { props: { titlebar: props.titlebar || false, x: adjustedX, y: adjustedY, options: props.options, }, }, }), ); }, close() { window.dispatchEvent(new Event("close-ctxm")); }, }, user: { async username() { try { const username = JSON.parse(await window.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, "utf8"))["username"]; return username || "Guest"; } catch (error) { console.error("Error Fetching username:", error); return "Guest"; } }, async pfp() { try { return JSON.parse(await window.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/user.json`, "utf8"))["pfp"] || "/assets/img/defualt - blue.png"; } catch (error) { console.error("Error Fetching pfp:", error); return "/assets/img/defualt - blue.png"; } }, }, proxy: { async get() { const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); return settings["proxy"]; }, async set(proxy: "Ultraviolet" | "Scramjet") { const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); settings["proxy"] = proxy; await window.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, JSON.stringify(settings, null, 2), "utf8"); window.tb.proxy.updateSWs(); return true; }, async updateSWs() { await navigator.serviceWorker.getRegistrations().then(registrations => { registrations.forEach(registration => { registration.unregister().catch(error => { console.error("Error unregistering service worker:", error); }); }); }); const request = indexedDB.open("$scramjet"); request.onsuccess = () => { const db = request.result; if (db.objectStoreNames.length === 0) { db.close(); const deleteRequest = indexedDB.deleteDatabase("$scramjet"); deleteRequest.onsuccess = () => { console.log("Cleared SJ DB"); }; deleteRequest.onerror = err => { console.error(err); }; } else { console.log("Scramjet is fine"); } }; request.onerror = err => { console.error(err); }; const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`)); const updateTransport = async () => { const wispserver = settings.wispServer || `${window.location.origin.replace(/^https?:\/\//, "ws://")}/wisp/`; const connection = new BareMuxConnection("/baremux/worker.js"); if (settings.transport === "Default (Epoxy)") { await connection.setTransport("/epoxy/index.mjs", [{ wisp: wispserver }]); } else if (settings.transport === "Anura BCC") { // @ts-expect-error await connection.setRemoteTransport(new AnuraBareClient(), "AnuraBareClient"); } else { await connection.setTransport("/libcurl/index.mjs", [{ wisp: wispserver }]); } }; const { ScramjetController } = $scramjetLoadController(); window.scramjet = new ScramjetController(window.scramjetTb); scramjet.init(); navigator.serviceWorker .register("anura-sw.js", { scope: "/", }) .then(() => { updateTransport(); }); navigator.serviceWorker.ready.then(async () => { await updateTransport(); }); if (settings.wispServer === null) { // @ts-expect-error window.tb.libcurl.set_websocket(`${location.protocol.replace("http", "ws")}//${location.hostname}:${location.port}/wisp/`); } else { window.tb.libcurl.set_websocket(settings.wispServer); } return true; }, async encode(url: string, encoder: string) { if (encoder === "xor" || encoder === "XOR") { const enc = await XOR.encode(url); return enc; } throw new Error("Encoder not found"); // Stubbed for future addition of say AES }, async decode(url: string, decoder: string) { if (decoder === "xor" || decoder === "XOR") { const dec = await XOR.decode(url); return dec; } throw new Error("Encoder not found"); // Stubbed for future addition of say AES }, }, notification: { Message(props: NotificationProps) { setNotifFn("message", props); }, Toast(props: NotificationProps) { setNotifFn("toast", props); }, Installing(props: NotificationProps, task?: Promise | (() => Promise), doneToast?: Partial | null, failToast?: Partial | null) { if (!task) { setNotifFn("installing", props); return; } const notifId = setNotifFn("installing", props); const runTask = typeof task === "function" ? task() : task; return Promise.resolve(runTask) .then(result => { dismissNotifFn(notifId); if (doneToast !== null) { setNotifFn("toast", { application: doneToast?.application || props.application, iconSrc: doneToast?.iconSrc || props.iconSrc, message: doneToast?.message || `${props.message} complete`, time: doneToast?.time, }); } return result; }) .catch(error => { dismissNotifFn(notifId); if (failToast !== null) { setNotifFn("toast", { application: failToast?.application || props.application, iconSrc: failToast?.iconSrc || props.iconSrc, message: failToast?.message || `${props.message} failed`, time: failToast?.time, }); } throw error; }); }, }, dialog: { Alert(props: dialogProps) { setDialogFn("alert", props); }, Message(props: dialogProps) { setDialogFn("message", props); }, Select(props: dialogProps) { setDialogFn("select", props); }, Auth(props: dialogProps, options: { sudo: boolean }) { setDialogFn("auth", props, options); }, Permissions(props: dialogProps) { setDialogFn("permissions", props); }, FileBrowser(props: dialogProps) { setDialogFn("filebrowser", props); }, DirectoryBrowser(props: dialogProps) { setDialogFn("directorybrowser", props); }, SaveFile(props: dialogProps) { setDialogFn("savefile", props); }, Cropper(props: dialogProps) { setDialogFn("cropper", props); }, WebAuth(props: dialogProps) { setDialogFn("webauth", props); }, }, system: { version: () => { return system.version("string"); }, instance: system.instance, openApp: async (pkg: string) => { const apps = JSON.parse(await window.tb.fs.promises.readFile("/apps/installed.json", "utf8")); const app = apps.find((a: any) => a.name.toLowerCase() === pkg.toLowerCase()); if (!app) throw new Error(`App "${pkg}" not found`); let type: "anura" | "terbium"; if (app.config.endsWith("manifest.json") || app.config.endsWith(`${pkg}.json`)) { type = "anura"; } else { type = "terbium"; } let config: any; if (type === "anura") { let aPath = app.config; if (aPath.endsWith("manifest.json")) { aPath = aPath.replace(/manifest\.json$/, ""); config = JSON.parse(await window.tb.fs.promises.readFile(app.config, "utf8")); } else if (aPath.endsWith(`${pkg}.json`)) { config = JSON.parse(await window.tb.fs.promises.readFile(app.config, "utf8")).manifest; aPath = aPath.replace(new RegExp(`${pkg}\\.json$`), ""); } const src = config.index || config.src || "index.html"; config.title = config.name; config.src = src.startsWith(".") || src.startsWith("/") ? `/fs${aPath}${src.replace(/^\.\//, "")}` : `/fs${aPath}${src}`; config.icon = config.icon ? (config.icon.startsWith(".") || config.icon.startsWith("/") ? `/fs${aPath}${config.icon.replace(/^\.\//, "")}` : `/fs${aPath}${config.icon}`) : undefined; } else { const conf: WindowConfig = JSON.parse(await window.tb.fs.promises.readFile(app.config, "utf8")).wmArgs || JSON.parse(await window.tb.fs.promises.readFile(app.config, "utf8")).config; const aPath = app.config.replace(/[^/]+$/, ""); let src = conf.src || "index.html"; if (src.startsWith("./")) { src = `/fs${aPath}${src.slice(2)}`; } else if (!src.startsWith("/")) { src = `/fs${aPath}${src}`; } conf.src = src; let icon = conf.icon || "icon.png"; if (icon.startsWith("./")) { icon = `/fs${aPath}${icon.slice(2)}`; } else if (!icon.startsWith("/")) { icon = `/fs${aPath}${icon}`; } conf.icon = icon; config = conf; } window.tb.window.create(config); }, download: async (url: string, location: string) => { try { const response: Response = await window.tb.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, window.tb.buffer.from(content), "arraybuffer"); console.log(`File saved successfully at: ${location}`); } catch (error) { console.error(error); } }, exportfs: async (startPath = "/", filename = "tbfs.backup.zip") => { const files: Record = {}; const normalizeZipPath = (p: string) => p.replace(/^\/+/, ""); const toUint8 = (raw: any): Uint8Array => { if (raw instanceof Uint8Array) return raw; if (raw instanceof ArrayBuffer) return new Uint8Array(raw); if (typeof raw === "string") return new TextEncoder().encode(raw); if (raw?.buffer instanceof ArrayBuffer) return new Uint8Array(raw.buffer); return new Uint8Array(raw); }; const walk = async (fsPath: string, zipPrefix: string) => { try { const stat = await window.tb.fs.promises.stat(fsPath); if (stat!.type === "DIRECTORY") { const entries = await window.tb.fs.promises.readdir(fsPath); const dirName = normalizeZipPath(zipPrefix); if (entries.length === 0 && dirName) { files[dirName.endsWith("/") ? dirName : dirName + "/"] = new Uint8Array(0); } await Promise.all( entries.map(async entry => { const childFsPath = `${fsPath.endsWith("/") ? fsPath : fsPath + "/"}${entry}`; const childZipPath = zipPrefix ? `${zipPrefix}/${entry}` : entry; await walk(childFsPath, childZipPath); }), ); } else { const raw = await window.tb.fs.promises.readFile(fsPath, "arraybuffer"); const name = normalizeZipPath(zipPrefix || fsPath.split("/").pop() || "file"); files[name] = toUint8(raw); } } catch (err) { console.warn("exportfs: skipping path", fsPath, err); } }; try { const normalizedStart = normalizeZipPath(startPath); await walk(startPath, normalizedStart === "" || normalizedStart === "." ? "" : normalizedStart); const zipped = fflate.zipSync(files, { level: 1 }); // @ts-expect-error blobs work fine const blob = new Blob([zipped], { type: "application/zip" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); setTimeout(() => URL.revokeObjectURL(url), 100); return url; } catch (err) { console.error("exportfs failed", err); throw err; } }, users: { async list() { const usersDir = await window.tb.fs.promises.readdir("/home/"); const users: string[] = []; for (const user of usersDir) { if (await fileExists(`/home/${user}/user.json`)) { const userData: User = JSON.parse(await window.tb.fs.promises.readFile(`/home/${user}/user.json`, "utf8")); users.push(userData.username); } } return users; }, async add(user: User) { const { username, password, pfp, perm, securityQuestion } = user; const userDir = `/home/${username}`; await window.tb.fs.promises.mkdir(userDir); const userJson: User = { id: username, username: username, password: password, pfp: pfp, perm: perm, }; if (securityQuestion) { userJson.securityQuestion = { question: securityQuestion.question, answer: securityQuestion.answer, }; } await window.tb.fs.promises.writeFile(`${userDir}/user.json`, JSON.stringify(userJson)); const userSettings: UserSettings = { wallpaper: "/assets/wallpapers/1.png", wallpaperMode: "cover", animations: true, proxy: "Scramjet", transport: "Default (Epoxy)", wispServer: `${location.protocol.replace("http", "ws")}//${location.hostname}:${location.port}/wisp/`, "battery-percent": false, accent: "#32ae62", times: { format: "12h", 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"]; defaultDirs.forEach(async dir => { await window.tb.fs.promises.mkdir(`${userDir}/${dir}`); }); await window.tb.fs.promises.mkdir(`/apps/user/${username}`); await window.tb.fs.promises.mkdir(`/apps/user/${username}/files`); await window.tb.fs.promises.writeFile( `/apps/user/${username}/files/config.json`, JSON.stringify({ "quick-center": true, "sidebar-width": 180, drives: { "File System": `/home/${username}/`, }, storage: { "File System": "storage-device", localStorage: "storage-device", }, "open-collapsibles": { "quick-center": true, drives: true, }, }), "utf8", ); await window.tb.fs.promises.writeFile(`/apps/user/${username}/files/davs.json`, JSON.stringify([])); const response = await fetch("/apps/files.tapp/icons.json"); const dat = await response.json(); await window.tb.fs.promises.writeFile(`/apps/user/${username}/files/icns.json`, JSON.stringify(dat)); await window.tb.fs.promises.writeFile( `/apps/user/${username}/files/quick-center.json`, JSON.stringify({ paths: { Documents: `/home/${username}/documents`, Images: `/home/${username}/images`, Videos: `/home/${username}/videos`, Music: `/home/${username}/music`, Trash: "/system/trash", }, }), "utf8", ); const items: any[] = []; const r2 = []; for (let i = 0; i < apps.length; i++) { const app = apps[i]; const name = app.name.toLowerCase(); var topPos = 0; var leftPos = 0; if (i % 12 === 0) { topPos = 0; } else { topPos = i % 12; } if (i < 12) { leftPos = 0; } else { leftPos = 1; } if (topPos * 66 > parent.innerHeight - 130) { leftPos = 1.15; if (r2.length === 0) { topPos = 0; } else { topPos = r2.length % 12; } r2.push({ name: app.name, }); } items.push({ name: app.name, item: `/home/${username}/desktop/${name}.lnk`, position: { custom: false, top: topPos, left: leftPos, }, }); 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 && uDir.type === "DIRECTORY") { await window.tb.sh.promises.rm(userDir, { recursive: true }); } } catch (err: any) { throw new Error(err.message); } try { const appDir = await window.tb.fs.promises.stat(`/apps/user/${id}`); if (appDir && appDir.type === "DIRECTORY") { await window.tb.sh.promises.rm(`/apps/user/${id}`, { recursive: true }); } } catch (err: any) { throw new Error(err.message); } const sudoUsers: string[] = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/sudousers.json", "utf8")); const users = await window.tb.fs.promises.readdir("/home/"); const idx = sudoUsers.indexOf(id); if (idx !== -1) { sudoUsers.splice(idx, 1); await window.tb.fs.promises.writeFile("/system/etc/terbium/sudousers.json", JSON.stringify(sudoUsers, null, 2), "utf8"); if (sudoUsers.length === 0) { window.tb.dialog.Select({ title: "Select new sudo user", message: "Please select a new sudo user", options: users.map(u => ({ text: u, value: u })), onOk: async (selected: string) => { await window.tb.fs.promises.writeFile("/system/etc/terbium/sudousers.json", JSON.stringify({ id: selected }), "utf8"); window.tb.notification.Toast({ application: "System", iconSrc: "/fs/apps/system/about.tapp/icon.svg", message: `Sudo user changed to ${selected}`, }); const syssettings: SysSettings = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8")); if (id === syssettings.defaultUser) { syssettings.defaultUser = selected; await window.tb.fs.promises.writeFile("/system/etc/terbium/settings.json", JSON.stringify(syssettings, null, 2), "utf8"); } if (id === sessionStorage.getItem("currAcc")) { sessionStorage.setItem("logged-in", "false"); sessionStorage.removeItem("currAcc"); window.location.reload(); } }, }); } } return true; }, async update(user: User) { const { username, password, pfp, perm, securityQuestion } = user; const userDir = `/home/${username}`; const userConfig = JSON.parse(await window.tb.fs.promises.readFile(`${userDir}/user.json`, "utf8")); await window.tb.fs.promises.writeFile( `${userDir}/user.json`, JSON.stringify({ id: userConfig.id, username: username === userConfig.username ? userConfig.username : username, password: password === userConfig.password ? userConfig.password : password, pfp: pfp === userConfig.pfp ? userConfig.pfp : pfp, perm: perm === userConfig.perm ? userConfig.perm : perm, ...(securityQuestion !== undefined ? { securityQuestion: securityQuestion === userConfig.securityQuestion ? userConfig.securityQuestion : securityQuestion } : userConfig.securityQuestion !== undefined ? { securityQuestion: userConfig.securityQuestion } : {}), }), ); }, async renameUser(olduser: string, newuser: string) { const userData = JSON.parse(await window.tb.fs.promises.readFile(`/home/${olduser}/user.json`, "utf8")); userData["username"] = newuser; await window.tb.fs.promises.writeFile(`/home/${olduser}/user.json`, JSON.stringify(userData)); let linkpaths = []; for (const item of await window.tb.fs.promises.readdir(`/home/${olduser}/desktop/`)) { const stat = await window.tb.fs.promises.stat(`/home/${olduser}/desktop/${item}`); if (stat && stat.type === "SYMLINK") { linkpaths.push(await window.tb.fs.promises.readlink(`/home/${olduser}/desktop/${item}`)); try { await window.tb.fs.promises.unlink(`/home/${olduser}/desktop/${item}`); } catch (e) { console.log(e); } } } await window.tb.fs.promises.rename(`/home/${olduser}`, `/home/${newuser}`); sessionStorage.setItem("currAcc", newuser); for (const link of linkpaths) { const tappMatch = link.match(/([^/]+)(?=\.tapp(?:\/|$))/); const parts = link.split("/").filter(Boolean); let linkName = ""; if (tappMatch) { linkName = tappMatch[1]; } else if (parts.length > 1) { const last = parts[parts.length - 1]; linkName = last.includes(".") ? parts[parts.length - 2] : last; } else { linkName = parts[0] || ""; } linkName = linkName.replace(/\.tapp$/, ""); await window.tb.fs.promises.symlink(link, `/home/${newuser}/desktop/${linkName}.lnk`); } const desktopItems = JSON.parse(await window.tb.fs.promises.readFile(`/home/${newuser}/desktop/.desktop.json`, "utf8")); for (const item of desktopItems) { if (item.position && item.name) { const name = item.name.toLowerCase(); item.item = `/home/${newuser}/desktop/${name}.lnk`; } } await window.tb.fs.promises.writeFile(`/home/${newuser}/desktop/.desktop.json`, JSON.stringify(desktopItems), "utf8"); await window.tb.fs.promises.rename(`/apps/user/${olduser}`, `/apps/user/${newuser}`); const sysSettings: SysSettings = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8")); if (sysSettings["defaultUser"] === olduser) { sysSettings["defaultUser"] = newuser; } const sudousers = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/sudousers.json", "utf8")); const idx = sudousers.indexOf(olduser); if (idx !== -1) { sudousers[idx] = newuser; await window.tb.fs.promises.writeFile("/system/etc/terbium/sudousers.json", JSON.stringify(sudousers), "utf8"); } await window.tb.fs.promises.writeFile("/system/etc/terbium/settings.json", JSON.stringify(sysSettings)); const fcfg = JSON.parse(await window.tb.fs.promises.readFile(`/apps/user/${newuser}/files/config.json`, "utf8")); fcfg.drives["File System"] = `/home/${newuser}/`; await window.tb.fs.promises.writeFile(`/apps/user/${newuser}/files/config.json`, JSON.stringify(fcfg)); const qcfg = JSON.parse(await window.tb.fs.promises.readFile(`/apps/user/${newuser}/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(olduser, newuser); } } await window.tb.fs.promises.writeFile(`/apps/user/${newuser}/files/quick-center.json`, JSON.stringify(qcfg)); window.location.reload(); }, }, bootmenu: { async addEntry(name: string, file: string) { const data = JSON.parse(await window.tb.fs.promises.readFile("/bootentries.json", "utf8")); data.push({ name: name, action: `() => { sessionStorage.setItem("cusboot", "true"); sessionStorage.setItem("bootfile", "${file}"); window.location.reload(); }`, }); await window.tb.fs.promises.writeFile("/bootentries.json", JSON.stringify(data, null, 2)); }, async removeEntry(name: string) { const data = JSON.parse(await window.tb.fs.promises.readFile("/bootentries.json", "utf8")); const dat = data.filter((entry: any) => entry.name !== name); await window.tb.fs.promises.writeFile("/bootentries.json", JSON.stringify(dat, null, 2)); }, }, startup: { async addProc(pkgorname: string, target: "System" | "User", cmd?: string) { await addStartupProc(pkgorname, target, cmd); }, async removeProc(pkgorname: string, target: "System" | "User") { await removeStartupProc(pkgorname, target); }, async enable(pkgorname: string, target: "System" | "User") { await enableProc(pkgorname, target); }, async disable(pkgorname: string, target: "System" | "User") { await disableProc(pkgorname, target); }, async list() { const procs = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/startup.json", "utf8")); return procs; }, }, }, 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, rememberMe: true, 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")); const existingIndex = conf.findIndex((acc: any) => acc && (acc.id === response.data.user.id || acc.email === response.data.user.email)); if (existingIndex !== -1) { conf[existingIndex] = { username: response.data.user.name, perm: "admin", pfp: response.data.user.image, email: response.data.user.email, id: response.data.user.id, }; console.log("[TAUTH] Updated existing Account Info in FS"); } else { conf.push({ username: response.data.user.name, perm: "admin", pfp: response.data.user.image, email: response.data.user.email, id: response.data.user.id, }); console.log("[TAUTH] Saved Account Info to FS"); } await window.tb.fs.promises.writeFile("/system/etc/terbium/taccs.json", JSON.stringify(conf, null, 2), "utf8"); 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"); }, }); } } }, reauth: async () => { return new Promise((resolve, reject) => { window.tb.dialog.WebAuth({ title: "Verify Identity", 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, rememberMe: true, fetchOptions: { onSuccess: async () => { await window.tb.tauth.sync.retreive(); resolve(true); }, onError: error => { reject(new Error(error.error.message)); }, }, }); }, onCancel: () => { reject(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(null, null, "tbs"); console.log("[TAUTH] Retrieved synced data from cloud"); 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"); await window.tb.fs.promises.writeFile(`/apps/user/${info.username}/app store/repos.json`, JSON.stringify(data.settings[0].apps.repos || [], 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; let settings: UserSettings = 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")); if (!settings.wallpaper.startsWith("/assets/wallpapers") && !settings.wallpaper.startsWith("data:")) { const res = await fetch(`/fs/${settings.wallpaper}`); const blob = await res.blob(); const reader = new FileReader(); const dataURL: Promise = new Promise((resolve, reject) => { reader.onloadend = () => { if (typeof reader.result === "string") { resolve(reader.result); } else { reject(new Error("Failed to convert wallpaper to data URL")); } }; reader.onerror = () => { reject(new Error("Failed to read wallpaper blob")); }; }); reader.readAsDataURL(blob); settings.wallpaper = await dataURL; } const toupload = [ { settings: settings, apps: { repos: JSON.parse(await window.tb.fs.promises.readFile(`/apps/user/${info.username}/app store/repos.json`, "utf8")), installed: [], }, davs: davs, }, ]; setinfo(null, null, "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(), isReady: false, start: () => { initializeWebContainer(); }, stop: () => { if (window.tb.node.isReady) { // @ts-expect-error window.tb.node.webContainer.teardown(); window.tb.node.isReady = false; return true; } throw new Error("No WebContainer is running"); }, }, crypto: async (pass: string, file?: string) => { const newpw = pw.harden(pass); if (file) { await window.tb.fs.promises.writeFile(file, newpw); return "Complete"; } return newpw; }, platform: { async getPlatform() { const mobileuas = /(android|bb\d+|meego).+mobile|armv7l|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series[46]0|samsungbrowser.*mobile|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino|android|ipad|playbook|silk|iPhone|iPad/i; const crosua = /CrOS/; if (mobileuas.test(navigator.userAgent) && !crosua.test(navigator.userAgent)) { return "mobile"; } if (!mobileuas.test(navigator.userAgent) && navigator.maxTouchPoints > 1 && navigator.userAgent.indexOf("Macintosh") !== -1 && navigator.userAgent.indexOf("Safari") !== -1) { return "mobile"; } return "desktop"; }, }, process: { procs: { 0: { name: "Terbium Service Worker", wid: null, pid: 0, src: null, size: null, icon: null, type: "runtime", onKill: async () => { await window.tb.proxy.updateSWs(); window.location.reload(); }, }, 1: { name: "Terbium Alexa Desktop Experience", wid: null, pid: 1, src: null, size: null, icon: null, type: "runtime", onKill: () => { window.location.reload(); }, }, } as Record, kill(pid: string | number) { const pd = Number(pid); const proc = Object.values(window.tb.process.list()).find((p: any) => { return Number(p.pid) === pd; }); if (proc) { if (proc.type === "window") { clearInfo(); useWindowStore.getState().killWindow(String(pd)); delete window.tb.process.procs[proc.pid]; } else if (proc.type === "runtime") { delete window.tb.process.procs[proc.pid]; if (proc.onKill) proc.onKill(); } } else { throw new Error(`Process with PID ${pid} not found`); } }, list() { return window.tb.process.procs; }, parse: { build(src: string) { parse.build(src); }, }, create(type: "window" | "runtime", config: any) { if (type === "window") { createWindow({ title: { text: "Generic Window", }, src: "about:blank", }); } else { const latestProcId = Math.max(...Object.keys(window.tb.process.procs).map(Number)) + 1; window.tb.process.procs[latestProcId] = { ...config, pid: latestProcId, type: "runtime" }; return latestProcId; } }, }, screen: { async captureScreen() { if (!navigator.mediaDevices.getDisplayMedia) throw new Error("API Not Avalible on your browser"); // @ts-expect-error const stream = await navigator.mediaDevices.getDisplayMedia({ preferCurrentTab: true }); const capture = new ImageCapture(stream.getVideoTracks()[0]); const frame = await capture.grabFrame(); stream.getVideoTracks()[0].stop(); const canvas: HTMLCanvasElement = document.createElement("canvas"); const ctx: any = canvas.getContext("2d"); canvas.width = frame.width; canvas.height = frame.height; ctx.drawImage(frame, 0, 0, frame.width, frame.height); const dataURI = await new Promise(res => { canvas.toBlob(blobImage => { res(blobImage); }); }); canvas.remove(); const obj = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result as ArrayBuffer); reader.onerror = () => reject(new Error("Failed to read blob")); // @ts-expect-error reader.readAsArrayBuffer(dataURI); }); tb.dialog.SaveFile({ title: "Save screenshot", filename: "screenshot.png", onOk: async (filePath: string) => { await window.tb.fs.promises.writeFile(filePath, window.tb.buffer.from(obj)); }, }); }, }, mediaplayer: { music(props: MediaProps) { setMusicFn(props); }, video(props: MediaProps) { setVideoFn(props); }, hide: () => { hideFn(); }, pauseplay: () => { window.dispatchEvent(new Event("tb-pause-isl")); }, isExisting: () => { return new Promise(resolve => { isExistingFn(); const getContent = (e: CustomEvent) => { window.removeEventListener("isExistingMP", getContent as EventListener); resolve(e.detail); }; window.addEventListener("isExistingMP", getContent as EventListener); }); }, }, file: { handler: { openFile: async (path: string, type: string) => { const message = { type: "process", path: path }; const settings = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8")); const fApps = settings["fileAssociatedApps"]; const customHandler = fApps?.[type]; if (customHandler) { try { const installed = JSON.parse(await window.tb.fs.promises.readFile("/apps/installed.json", "utf8")); let appInfo = installed.find((a: any) => a.name.toLowerCase() === customHandler.toLowerCase()); if (!appInfo) { try { const altAppInfo = installed.find((a: any) => a.name.toLowerCase() === `${customHandler.toLowerCase()}.tapp`); if (altAppInfo) { appInfo = altAppInfo; } } catch { console.error(`App "${customHandler}" not found in installed apps`); } } if (appInfo.user === "System") return; const appConfigRaw = JSON.parse(await window.tb.fs.promises.readFile(appInfo.config, "utf8")); let windowConfig; if (appConfigRaw.wmArgs) { windowConfig = { ...appConfigRaw.wmArgs }; const configDir = appInfo.config.replace(/\/(\.tbconfig|index\.json)$/, ""); if (windowConfig.src && !windowConfig.src.startsWith("/")) { windowConfig.src = `/fs/${configDir}/${windowConfig.src}`; } if (windowConfig.icon && !windowConfig.icon.startsWith("/")) { windowConfig.icon = `/fs/${configDir}/${windowConfig.icon}`; } } createWindow({ ...windowConfig, message: JSON.stringify(message), }); return; } catch {} } switch (type) { case "text": createWindow({ title: "Text Editor", src: "/fs/apps/system/text editor.tapp/index.html", size: { width: 460, height: 460, minWidth: 160, minHeight: 160, }, icon: "/fs/apps/system/text editor.tapp/icon.svg", message: JSON.stringify(message), }); break; case "image": case "video": case "audio": case "pdf": createWindow({ title: "Media Viewer", src: "/fs/apps/system/media viewer.tapp/index.html", size: { width: 460, height: 460, minWidth: 160, minHeight: 160, }, icon: "/fs/apps/system/media viewer.tapp/icon.svg", message: JSON.stringify(message), }); break; case "webpage": createWindow({ title: "Terbium Webview", src: `/fs/${path}`, size: { width: 460, height: 460, minWidth: 160, minHeight: 160, }, icon: "/apps/browser.tapp/icon.svg", }); break; } }, addHandler: async (app: string, ext: string) => { const settings: SysSettings = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8")); (settings.fileAssociatedApps as Record)[ext] = app; await window.tb.fs.promises.writeFile("/system/etc/terbium/settings.json", JSON.stringify(settings, null, 2), "utf8"); return true; }, removeHandler: async (ext: string) => { const settings: SysSettings = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8")); delete (settings.fileAssociatedApps as Record)[ext]; await window.tb.fs.promises.writeFile("/system/etc/terbium/settings.json", JSON.stringify(settings, null, 2), "utf8"); return true; }, }, icons: { get: async (ext: string) => { const fileExts = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/file-icons.json", "utf8")); const extName = fileExts["ext-to-name"][ext.toLowerCase()]; if (extName && fileExts["name-to-path"][extName]) { return fileExts["name-to-path"][extName]; } return fileExts["name-to-path"]["Unknown"]; }, set: async (ext: string, iconPath: string) => { const fileExts = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/file-icons.json", "utf8")); const normalizedExt = ext.toLowerCase().replace(/^\./, ""); const extName = normalizedExt.charAt(0).toUpperCase() + normalizedExt.slice(1); fileExts["ext-to-name"][normalizedExt] = extName; fileExts["name-to-path"][extName] = iconPath; await window.tb.fs.promises.writeFile("/system/etc/terbium/file-icons.json", JSON.stringify(fileExts, null, 2), "utf8"); return true; }, remove: async (ext: string) => { const fileExts = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/file-icons.json", "utf8")); const normalizedExt = ext.toLowerCase().replace(/^\./, ""); const extName = fileExts["ext-to-name"][normalizedExt]; if (extName) { delete fileExts["ext-to-name"][normalizedExt]; delete fileExts["name-to-path"][extName]; await window.tb.fs.promises.writeFile("/system/etc/terbium/file-icons.json", JSON.stringify(fileExts, null, 2), "utf8"); } return true; }, }, }, }; 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.loadLock = true; const anura = await Anura.new({ milestone: 5, FileExts: { txt: { handler_type: "module", id: "anura.fileviewer" }, mp3: { handler_type: "module", id: "anura.fileviewer" }, flac: { handler_type: "module", id: "anura.fileviewer" }, wav: { handler_type: "module", id: "anura.fileviewer" }, ogg: { handler_type: "module", id: "anura.fileviewer" }, mp4: { handler_type: "module", id: "anura.fileviewer" }, mov: { handler_type: "module", id: "anura.fileviewer" }, webm: { handler_type: "module", id: "anura.fileviewer" }, gif: { handler_type: "module", id: "anura.fileviewer" }, png: { handler_type: "module", id: "anura.fileviewer" }, jpg: { handler_type: "module", id: "anura.fileviewer" }, jpeg: { handler_type: "module", id: "anura.fileviewer" }, svg: { handler_type: "module", id: "anura.fileviewer" }, pdf: { handler_type: "module", id: "anura.fileviewer" }, py: { handler_type: "module", id: "anura.fileviewer" }, js: { handler_type: "module", id: "anura.fileviewer" }, mjs: { handler_type: "module", id: "anura.fileviewer" }, cjs: { handler_type: "module", id: "anura.fileviewer" }, json: { handler_type: "module", id: "anura.fileviewer" }, html: { handler_type: "module", id: "anura.fileviewer" }, css: { handler_type: "module", id: "anura.fileviewer" }, default: { handler_type: "module", id: "anura.fileviewer" }, }, "handler-migration-complete": true, apps: ["apps/fsapp.app"], defaultsettings: { "use-sw-cache": true, applist: ["anura.browser", "anura.settings", "anura.fsapp", "anura.term"], "relay-url": "wss://relay.widgetry.org/", directories: { apps: "/apps/anura/", libs: "/system/lib/anura/", init: "/system/etc/anura/init/", bin: "/system/bin/anura/", }, }, x86: { debian: { bzimage: "/images/debian-boot/vmlinuz-6.1.0-11-686", initrd: "/images/debian-boot/initrd.img-6.1.0-11-686", rootfs: [ "images/debian-rootfs/aa", "images/debian-rootfs/ab", "images/debian-rootfs/ac", "images/debian-rootfs/ad", "images/debian-rootfs/ae", "images/debian-rootfs/af", "images/debian-rootfs/ag", "images/debian-rootfs/ah", "images/debian-rootfs/ai", "images/debian-rootfs/aj", "images/debian-rootfs/ak", "images/debian-rootfs/al", "images/debian-rootfs/am", "images/debian-rootfs/an", "images/debian-rootfs/ao", "images/debian-rootfs/ap", "images/debian-rootfs/aq", "images/debian-rootfs/ar", "images/debian-rootfs/as", "images/debian-rootfs/at", "images/debian-rootfs/au", "images/debian-rootfs/av", "images/debian-rootfs/aw", "images/debian-rootfs/ax", "images/debian-rootfs/ay", "images/debian-rootfs/az", "images/debian-rootfs/ba", "images/debian-rootfs/bb", "images/debian-rootfs/bc", "images/debian-rootfs/bd", "images/debian-rootfs/be", "images/debian-rootfs/bf", "images/debian-rootfs/bg", "images/debian-rootfs/bh", "images/debian-rootfs/bi", "images/debian-rootfs/bj", "images/debian-rootfs/bk", "images/debian-rootfs/bl", "images/debian-rootfs/bm", "images/debian-rootfs/bn", "images/debian-rootfs/bo", ], }, }, }); window.anura = anura; // @ts-expect-error For backwards compatibility window.anura.fs.Shell = window.tfs.sh; window.AliceWM = AliceWM; window.LocalFS = LocalFS; 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", message: "A new version of terbium is ready to install", onOk: async () => { window.location.reload(); }, }); } }; if (!(await fileExists("/system/etc/terbium/hash.cache"))) { 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", message: "A new version of terbium is ready to install", onOk: async () => { window.location.reload(); }, }); } else { getupds(); } setInterval(() => { getupds(); }, 300000); const libcurlload = (srv: any) => { window.tb.libcurl.set_websocket(srv); }; const wsld = async () => { const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")); if (settings.wispServer === null) { libcurlload(`${location.protocol.replace("http", "ws")}//${location.hostname}:${location.port}/wisp/`); } else { libcurlload(settings.wispServer); } }; let triggered = false; const down = (e: KeyboardEvent) => { if (e.altKey && e.shiftKey) { if (!triggered) { window.tb.screen.captureScreen(); triggered = true; } } }; const up = (e: KeyboardEvent) => { if (!e.altKey || !e.shiftKey) { triggered = false; } }; document.addEventListener("keydown", down); 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()) { await window.tb.tauth.sync.retreive(); 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 === false) { 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 === false) { window.tb.tauth.sync.upload(); } }); window.tb.fs.watch(`/apps/user/${await window.tb.user.username()}/app store/repos.json`, { recursive: true }, (e: string, _f: string) => { if (e === "change" && window.tb.tauth.sync.isSyncing === false) { window.tb.tauth.sync.upload(); } }); } launchProcs(); document.addEventListener("libcurl_load", wsld); window.tb.node.webContainer = await initializeWebContainer(); } ================================================ FILE: src/sys/Filer.d.ts ================================================ declare let Filer: FilerType; declare let $el: any; // Note: this is different from the Anura Filesystem type because file descriptors are internally stored as numbers rather than the AnuraFD type. // This should still be fully compatible as file descriptors are obtained from other methods and are not created directly. This will only be a // problem if someone for some reason tries to create a file descriptor manually or did some external logic based on the file descriptor being a // number. type FilerFS = { watch(filename: string, listener: (event: string, filename: string) => void, options?: { recursive: boolean }): void; Shell: { new (): { cd: (t: string, r?: any) => void; pwd: () => string; env: () => Record; fs: () => any; rm: (directory: string, options?: { recursive: boolean; force: boolean }) => void; promises: { cat: () => Promise; cd: (t: string, r?: any) => Promise; exec: (command: string) => Promise; find: ( path: string, options?: { name?: string; regex?: RegExp | string; exec?: boolean | Function; }, ) => Promise; ls: (dir: string) => Promise; mkdirp: (dir: string) => Promise; rm: (path: string, options?: { recursive?: boolean; force?: boolean }) => Promise; tempDir: () => Promise; touch: (filePath: string) => Promise; }; }; }; rename(oldPath: string, newPath: string, callback?: (err: Error | null) => void): void; ftruncate(fd: number, len: number, callback?: (err: Error | null, fd: number) => void): void; truncate(path: string, len: number, callback?: (err: Error | null) => void): void; stat(path: string, callback?: (err: Error | null, stats: TStats) => void): void; fstat(fd: number, callback?: (err: Error | null, stats: TStats) => void): void; lstat(path: string, callback?: (err: Error | null, stats: TStats) => void): void; /** @deprecated fs.exists() is an anachronism and exists only for historical reasons. */ exists(path: string, callback?: (exists: boolean) => void): void; link(srcPath: string, dstPath: string, callback?: (err: Error | null) => void): void; symlink(srcPath: string, dstPath: string, type: string, callback?: (err: Error | null) => void): void; symlink(srcPath: string, dstPath: string, callback?: (err: Error | null) => void): void; readlink(path: string, callback?: (err: Error | null, linkContents: string) => void): void; unlink(path: string, callback?: (err: Error | null) => void): void; mknod(path: string, mode: number, callback?: (err: Error | null) => void): void; rmdir(path: string, callback?: (err: Error | null) => void): void; mkdir(path: string, mode: number, callback?: (err: Error | null) => void): void; mkdir(path: string, callback?: (err: Error | null) => void): void; access(path: string, mode: number, callback?: (err: Error | null) => void): void; access(path: string, callback?: (err: Error | null) => void): void; mkdtemp(prefix: string, options: { encoding: string } | string, callback?: (err: Error | null, path: string) => void): void; mkdtemp(prefix: string, callback?: (err: Error | null, path: string) => void): void; readdir(path: string, options: { encoding: string; withFileTypes: boolean } | string, callback?: (err: Error | null, files: string[]) => void): void; readdir(path: string, callback?: (err: Error | null, files: string[]) => void): void; close(fd: number, callback?: (err: Error | null) => void): void; open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode: number, callback?: (err: Error | null, fd: number) => void): void; open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", callback?: (err: Error | null, fd: number) => void): void; utimes(path: string, atime: number | Date, mtime: number | Date, callback?: (err: Error | null) => void): void; futimes(fd: number, atime: number | Date, mtime: number | Date, callback?: (err: Error | null) => void): void; chown(path: string, uid: number, gid: number, callback?: (err: Error | null) => void): void; fchown(fd: number, uid: number, gid: number, callback?: (err: Error | null) => void): void; chmod(path: string, mode: number, callback?: (err: Error | null) => void): void; fchmod(fd: number, mode: number, callback?: (err: Error | null) => void): void; fsync(fd: number, callback?: (err: Error | null) => void): void; write(fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null, callback?: (err: Error | null, nbytes: number) => void): void; read(fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null, callback?: (err: Error | null, nbytes: number, buffer: Uint8Array) => void): void; readFile(path: string, callback?: (err: Error | null, data: Uint8Array) => void): void; writeFile(path: string, data: Uint8Array | string, options: { encoding: string; flag: "r" | "r+" | "w" | "w+" | "a" | "a+" } | string, callback?: (err: Error | null) => void): void; writeFile(path: string, data: Uint8Array | string, callback?: (err: Error | null) => void): void; appendFile(path: string, data: Uint8Array, callback?: (err: Error | null) => void): void; setxattr(path: string, name: string, value: string | object, flag: "CREATE" | "REPLACE", callback?: (err: Error | null) => void): void; setxattr(path: string, name: string, value: string | object, callback?: (err: Error | null) => void): void; fsetxattr(fd: number, name: string, value: string | object, flag: "CREATE" | "REPLACE", callback?: (err: Error | null) => void): void; fsetxattr(fd: number, name: string, value: string | object, callback?: (err: Error | null) => void): void; getxattr(path: string, name: string, callback?: (err: Error | null, value: string | object) => void): void; fgetxattr(fd: number, name: string, callback?: (err: Error | null, value: string | object) => void): void; removexattr(path: string, name: string, callback?: (err: Error | null) => void): void; fremovexattr(fd: number, name: string, callback?: (err: Error | null) => void): void; /* * Asynchronous FS operations */ promises: { appendFile(path: string, data: Uint8Array, options: { encoding: string; mode: number; flag: string }): Promise; access(path: string, mode?: number): Promise; chown(path: string, uid: number, gid: number): Promise; chmod(path: string, mode: number): Promise; getxattr(path: string, name: string): Promise; link(srcPath: string, dstPath: string): Promise; lstat(path: string): Promise; mkdir(path: string, mode?: number): Promise; mkdtemp(prefix: string, options?: { encoding: string }): Promise; mknod(path: string, mode: number): Promise; open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode?: number): Promise; readdir(path: string, options?: string | { encoding: string; withFileTypes: boolean }): Promise; readFile(path: string, encoding?: string): Promise; readlink(path: string): Promise; removexattr(path: string, name: string): Promise; rename(oldPath: string, newPath: string): Promise; rmdir(path: string): Promise; setxattr(path: string, name: string, value: string | object, flag?: "CREATE" | "REPLACE"): Promise; stat(path: string, callback?: void | any): Promise; symlink(srcPath: string, dstPath: string, type?: string): Promise; truncate(path: string, len: number): Promise; unlink(path: string): Promise; utimes(path: string, atime: number | Date, mtime: number | Date): Promise; writeFile(path: string, data: any | string, encoding?: string, mode?: number, flag?: string): Promise; }; }; type FilerType = { fs: FilerFS; promises: FilerFS.promises; Buffer: any; Path: any; FileSystem: FilerFS.constructor; }; ================================================ FILE: src/sys/FilerWP.d.ts ================================================ interface TStats { node: string; // internal node id (unique) dev: string; // file system name name: string; // the entry's name (basename) size: number; // file size in bytes nlinks: number; // number of links atime: Date; // last access time as JS Date Object mtime: Date; // last modification time as JS Date Object ctime: Date; // creation time as JS Date Object atimeMs: number; // last access time as Unix Timestamp mtimeMs: number; // last modification time as Unix Timestamp ctimeMs: number; // creation time as Unix Timestamp type: string; // file type (FILE, DIRECTORY, SYMLINK) gid: number; // group name uid: number; // owner name mode: number; // permission version: number; // version of the node isFile(): boolean; // Returns true if the node is a file. isDirectory(): boolean; // Returns true if the node is a directory. isBlockDevice(): boolean; // Not implemented, returns false. isCharacterDevice(): boolean; // Not implemented, returns false. isSymbolicLink(): boolean; // Returns true if the node is a symbolic link. isFIFO(): boolean; // Not implemented, returns false. isSocket(): boolean; // Not implemented, returns false; } interface IAccessModes { F_OK: boolean; // Test for existence of file. R_OK: boolean; // Test whether the file exists and grants read permission. W_OK: boolean; // Test whether the file exists and grants write permission. X_OK: boolean; // Test whether the file exists and grants execute permission. } declare namespace Filer { namespace fs { namespace promises { function rename(oldPath: string, newPath: string): Promise; function ftruncate(fd: number, len: number): Promise; function truncate(fd: number, len: number): Promise; function stat(path: string): TStats; function fstat(fd: number): TStats; function lstat(path: string): TStats; function exists(path: string): boolean; function link(srcPath: string, dstPath: string): Promise; function symlink(srcPath: string, dstPath: string): Promise; function readlink(srcPath: string): string; function unlink(path: string): Promise; function mknod(path: string, mode: "FILE" | "DIRECTORY" | string): Promise; function rmdir(path: string): Promise; function mkdir(path: string, mode?: number): Promise; function access(path: string, mode: IAccessModes): Promise; function mkdtemp(path: string): string; function readdir(path: string, options?: { encoding: string; withFileTypes: boolean } | string): string[]; function close(fd: number): void; function open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode?: number): number; function utimes(path: string, atime: number | Date, mtime: number | Date): Promise; function futimes(fd: number, atime: number | Date, mtime: number | Date): Promise; function chown(path: string, uid: number, gid: number): Promise; function fchown(fd: number, uid: number, gid: number): Promise; function chmod(path: string, mode: number | string): Promise; function fchmod(fd: number, mode: number | string): Promise; function fsync(fd: number): Promise; function write(fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null): number; function read(fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null): number; function readFile(path: string, options?: { encoding: string; flag: "r" | "r+" | "w" | "w+" | "a" | "a+" } | string): Uint8Array; function writeFile(path: string, data: Uint8Array | string, options?: { encoding: string; flag: "r" | "r+" | "w" | "w+" | "a" | "a+" } | string): Promise; function appendFile(filename: string, data: Uint8Array | string, options?: { encoding: string; flag: "r" | "r+" | "w" | "w+" | "a" | "a+" } | string): Promise; function setxattr(path: string, name: string, value: string | object, flag?: "XATTR_CREATE" | "XATTR_REPLACE" | string): Promise; function fsetxattr(fd: number, name: string, value: string | object, flag?: "XATTR_CREATE" | "XATTR_REPLACE" | string): Promise; function getxattr(path: string, name: string): string | object; function fgetxattr(fd: number, name: string): string | object; function removexattr(path: string, name: string): Promise; function fremovexattr(fd: number, name: string): Promise; } /** * Renames the file at `oldPath` to `newPath`. Asynchronous [rename(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/rename.html). Callback gets no additional arguments. * @see [fs.rename](https://github.com/filerjs/filer?tab=readme-ov-file#rename) */ function rename(oldPath: string, newPath: string, callback: (err: Error | null) => void): void; /** * Change the size of the file represented by the open file descriptor `fd` to be length `len` bytes. * Asynchronous [ftruncate(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/ftruncate.html). If the file is larger than `len`, the extra bytes will be discarded; * if smaller, its size will be increased, and the extended area will appear as if it were zero-filled. * * @see [fs.ftruncate](https://github.com/filerjs/filer?tab=readme-ov-file#ftruncate) * @see also [fs.truncate()](https://github.com/filerjs/filer?tab=readme-ov-file#truncate) */ function ftruncate(fd: number, len: number, callback: (err: Error | null, fd: number) => void): void; /** * Change the size of the file at `path` to be length `len` bytes. * Asynchronous [truncate(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/truncate.html). If the file is larger than `len`, the extra bytes will be discarded; * if smaller, its size will be increased, and the extended area will appear as if it were zero-filled. * @see [fs.truncate](https://github.com/filerjs/filer?tab=readme-ov-file#truncate) * @see also [fs.ftruncate](https://github.com/filerjs/filer?tab=readme-ov-file#ftruncate) */ function truncate(fd: number, len: number, callback: (err: Error | null) => void): void; /** * Obtain file status about the file at `path`. Asynchronous [stat(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/stat.html). Callback gets `(error, stats)`, where `stats` is an object with the following properties: * - `node` internal node id (unique) * - `dev` file system name * - `name` the entry's name (basename) * - `size` file size in bytes * - `nlinks` number of links * - `atime` last access time as JS Date Object * - `mtime` last modification time as JS Date Object * - `ctime` creation time as JS Date Object * - `atimeMs` last access time as Unix Timestamp * - `mtimeMs` last modification time as Unix Timestamp * - `ctimeMs` creation time as Unix Timestamp * - `type` file type (FILE, DIRECTORY, SYMLINK) * - `gid` group name * - `uid` owner name * - `mode` permission * - `version` version of the node * The following convenience methods are also present on the callback's `stats`: * - `isFile()` Returns true if the node is a file. * - `isDirectory()` Returns true if the node is a directory. * - `isBlockDevice()` Not implemented, returns false. * - `isCharacterDevice()` Not implemented, returns false. * - `isSymbolicLink()` Returns true if the node is a symbolic link. * - `isFIFO()` Not implemented, returns false. * - `isSocket()` Not implemented, returns false. * If the file at `path` is a symbolic link, the file to which it links will be used instead. To get the status of a symbolic link file, use [fs.lstat()](https://github.com/filerjs/filer?tab=readme-ov-file#lstat) instead. * @see [fs.stat](https://github.com/filerjs/filer?tab=readme-ov-file#stat) * @see also [fs.lstat](https://github.com/filerjs/filer?tab=readme-ov-file#stat) */ function stat(path: string, callback: (err: Error | null, stats: TStats) => void): void; /** * Obtain information about the open file known by the file descriptor `fd`. * Asynchronous [fstat(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/fstat.html). Callback gets `(error, stats)`. `fstat()` is identical to `stat()`, except that the file to be stat-ed is specified by the open file descriptor `fd` instead of a path. * @see [fs.fstat](https://github.com/filerjs/filer?tab=readme-ov-file#fstat) * @see also [fs.stat](https://github.com/filerjs/filer?tab=readme-ov-file#stat) */ function fstat(fd: number, callback: (err: Error | null, stats: TStats) => void): void; /** * Obtain information about the file at `path` (i.e., the symbolic link file itself) vs. the destination file to which it links. * Asynchronous [lstat(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/lstat.html). Callback gets `(error, stats)`. * @see [fs.lstat](https://github.com/filerjs/filer?tab=readme-ov-file#lstat) * @see also [fs.stat](https://github.com/filerjs/filer?tab=readme-ov-file#stat) */ function lstat(path: string, callback: (err: Error | null, stats: TStats) => void): void; /** * Test whether or not the given path exists by checking with the file system. Then call the callback argument with either true or false. * @deprecated `fs.exists()` is an anachronism and exists only for historical reasons. There should almost never be a reason to use it in your own code. * @see [fs.exists](https://github.com/filerjs/filer?tab=readme-ov-file#exists) */ function exists(path: string, callback: (exists: boolean) => void): void; /** * Create a (hard) link to the file at `srcPath` named `dstPath`. * Asynchronous [link(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/link.html). Callback gets no additional arguments. Links are directory entries that point to the same file node. * @see [fs.link](https://github.com/filerjs/filer?tab=readme-ov-file#link) */ function link(srcPath: string, dstPath: string, callback: (err: Error | null) => void): void; /** * Create a symbolic link to the file at `dstPath` containing the path `srcPath`. * Asynchronous [symlink(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/symlink.html). Callback gets no additional arguments. Symbolic links are files that point to other paths. * NOTE: Filer allows for, but ignores the optional `type` parameter used in node.js. The `srcPath` may be a relative path, which will be resolved relative to `dstPath`. * @see [fs.symlink](https://github.com/filerjs/filer?tab=readme-ov-file#symlink) */ function symlink(srcPath: string, dstPath: string, callback: (err: Error | null) => void): void; /** * Reads the contents of a symbolic link. * Asynchronous [readlink(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/readlink.html). Callback gets `(error, linkContents)`, where `linkContents` is a string containing the symbolic link's link path. * If the original `srcPath` given to `symlink()` was a relative path, it will be fully resolved relative to `dstPath` when returned by `readlink()`. * @see [fs.readlink](https://github.com/filerjs/filer?tab=readme-ov-file#readlink) */ function readlink(srcPath: string, callback: (err: Error | null, linkContents: string) => void): void; /** * Removes the directory entry located at `path`. * Asynchronous [unlink(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/unlink.html). Callback gets no additional arguments. If `path` names a symbolic link, the symbolic link will be removed (i.e., not the linked file). Otherwise, the filed named by `path` will be removed (i.e., deleted). * @see [fs.unlink](https://github.com/filerjs/filer?tab=readme-ov-file#unlink) */ function unlink(path: string, callback: (err: Error | null) => void): void; /** * Creates a node at `path` based on the mode passed which is either `FILE` or `DIRECTORY`. * Asynchronous [mknod(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/mknod.html). Callback gets no additional arguments. * @see [fs.mknod](https://github.com/filerjs/filer?tab=readme-ov-file#mknod) */ function mknod(path: string, mode: "FILE" | "DIRECTORY" | string, callback: (err: Error | null) => void): void; /** * Removes the directory at `path`. * Asynchronous [rmdir(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/rmdir.html). Callback gets no additional arguments. The operation will fail if the directory at `path` is not empty. * @see [fs.rmdir](https://github.com/filerjs/filer?tab=readme-ov-file#rmdir) */ function rmdir(path: string, callback: (err: Error | null) => void): void; /** * Makes a directory with name supplied in `path` argument. * Asynchronous [mkdir(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/mkdir.html). Callback gets no additional arguments. * NOTE: Filer allows for, but ignores the optional `mode` argument used in node.js. * @see [fs.mkdir](https://github.com/filerjs/filer?tab=readme-ov-file#mkdir) */ function mkdir(path: string, mode?: number, callback: (err: Error | null) => void): void; /** * Tests a user's permissions for the file or directory supplied in `path` argument. * Asynchronous [access(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/access.html). Callback gets no additional arguments. The `mode` argument can be one of the following (constants are available on `fs.constants` and `fs`): * - `F_OK`: Test for existence of file. * - `R_OK`: Test whether the file exists and grants read permission. * - `W_OK`: Test whether the file exists and grants write permission. * - `X_OK`: Test whether the file exists and grants execute permission. * NOTE: you can also create a mask consisting of the bitwise OR of two or more values (e.g. `fs.constants.W_OK | fs.constants.R_OK`). * @see [fs.access](https://github.com/filerjs/filer?tab=readme-ov-file#access) */ function access(path: string, mode: IAccessModes, callback: (err: Error | null) => void): void; /** * Makes a temporary directory with prefix supplied in `path` argument. Method will append six random characters directly to the prefix. Asynchronous. Callback gets `(error, path)`, where path is the path to the created directory. * NOTE: Filer allows for, but ignores the `optional` options argument used in node.js. * @see [fs.mkdtemp](https://github.com/filerjs/filer?tab=readme-ov-file#mkdtemp) */ function mkdtemp(path: string, callback: (err: Error | null, path: string) => void): void; /** * Reads the contents of a directory. * Asynchronous [readdir(3)](http://pubs.opengroup.org/onlinepubs/009695399/functions/readdir.html). Callback gets `(error, files)`, where `files` is an array containing the names of each directory entry (i.e., file, directory, link) in the directory, excluding `.` and `..`. * Optionally accepts an options parameter, which can be either an encoding (e.g. "utf8") or an object with optional properties `encoding` and `withFileTypes`. * The `encoding` property is a `string` which will determine the character encoding to use for the names of each directory entry. The `withFileTypes` property is a `boolean` which defaults to `false`. If `true`, this method will return an array of [fs.Dirent](https://nodejs.org/api/fs.html#fs_class_fs_dirent) objects. * The `name` property on the [fs.Dirent](https://nodejs.org/api/fs.html#fs_class_fs_dirent) objects will be encoded using the specified character encoding. * @see [fs.readdir](https://github.com/filerjs/filer?tab=readme-ov-file#readdir) */ function readdir(path: string, options?: { encoding: string; withFileTypes: boolean } | string, callback: (err: Error | null, files: string[]) => void): void; /** * Closes a file descriptor. * Asynchronous [close(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/close.html). Callback gets no additional arguments. * @see [fs.close](https://github.com/filerjs/filer?tab=readme-ov-file#close) */ function close(fd: number, callback: (err: Error | null) => void): void; /** * Opens a file. * Asynchronous [open(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/open.html). Callback gets `(error, fd)`, where `fd` is the file descriptor. The `flags` argument can be: * - `'r'`: Open file for reading. An exception occurs if the file does not exist. * - `'r'`: Open file for reading. An exception occurs if the file does not exist. * - `'r+'`: Open file for reading and writing. An exception occurs if the file does not exist. * - `'w'`: Open file for writing. The file is created (if it does not exist) or truncated (if it exists). * - `'w+'`: Open file for reading and writing. The file is created (if it does not exist) or truncated (if it exists). * - `'a'`: Open file for appending. The file is created if it does not exist. * - `'a+'`: Open file for reading and appending. The file is created if it does not exist. * NOTE: Filer allows for, but ignores the optional `mode` argument used in node.js. * @see [fs.open](https://github.com/filerjs/filer?tab=readme-ov-file#open) */ function open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode?: number, callback: (err: Error | null, fd: number) => void): void; /** * Changes the file timestamps for the file given at path `path`. * Asynchronous [utimes(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/utimes.html). Callback gets no additional arguments. Both `atime` (access time) and `mtime` (modified time) arguments should be a JavaScript Date or Number. * @see [fs.utimes](https://github.com/filerjs/filer?tab=readme-ov-file#utimes) */ function utimes(path: string, atime: number | Date, mtime: number | Date, callback: (err: Error | null) => void): void; /** * Changes the file timestamps for the open file represented by the file descriptor `fd`. * Asynchronous [utimes(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/utimes.html). Callback gets no additional arguments. Both `atime` (access time) and `mtime` (modified time) arguments should be a JavaScript Date or Number. * @see [fs.futimes](https://github.com/filerjs/filer?tab=readme-ov-file#futimes) */ function futimes(fd: number, atime: number | Date, mtime: number | Date, callback: (err: Error | null) => void): void; /** * Changes the owner and group of a file. * Asynchronous [chown(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/chown.html). Callback gets no additional arguments. Both `uid` (user id) and `gid` (group id) arguments should be a JavaScript Number. By default, `0x0` is used (i.e., `root:root` ownership). * @see [fs.chown](https://github.com/filerjs/filer?tab=readme-ov-file#chown) */ function chown(path: string, uid: number, gid: number, callback: (err: Error | null) => void): void; /** * Changes the owner and group of a file. * Asynchronous [chown(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/chown.html). Callback gets no additional arguments. Both `uid` (user id) and `gid` (group id) arguments should be a JavaScript Number. By default, `0x0` is used (i.e., `root:root` ownership). * @see [fs.fchown](https://github.com/filerjs/filer?tab=readme-ov-file#fchown) */ function fchown(fd: number, uid: number, gid: number, callback: (err: Error | null) => void): void; /** * Changes the mode of a file. * Asynchronous [chmod(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/chmod.html). Callback gets no additional arguments. The `mode` argument should be a JavaScript Number, which combines file type and permission information. Here are a list of common values useful for setting the `mode`: * - File type `S_IFREG=0x8000` * - Dir type `S_IFDIR=0x4000` * - Link type `S_IFLNK=0xA000` * - Permissions `755=0x1ED` * - Permissions `644=0x1A4` * - Permissions `777=0x1FF` * - Permissions `666=0x1B6` * By default, directories use `(0x4000 | 0x1ED)` and files use `(0x8000 | 0x1A4)`. * @see [fs.chmod](https://github.com/filerjs/filer?tab=readme-ov-file#chmod) */ function chmod(path: string, mode: number | string, callback: (err: Error | null) => void): void; /** * Changes the mode of a file. * Asynchronous [chmod(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/chmod.html). Callback gets no additional arguments. The `mode` argument should be a JavaScript Number, which combines file type and permission information. By default, `755` (dir) and `644` (file) are used. * @see [fs.fchmod](https://github.com/filerjs/filer?tab=readme-ov-file#fchmod) */ function fchmod(fd: number, mode: number | string, callback: (err: Error | null) => void): void; /** * Synchronize the data and metadata for the file referred to by `fd` to disk. * Asynchronous [fsync(2)](http://man7.org/linux/man-pages/man2/fsync.2.html). The callback gets `(error)`. * @see [fs.fsync](https://github.com/filerjs/filer?tab=readme-ov-file#fsync) */ function fsync(fd: number, callback: (err: Error | null) => void): void; /** * Writes bytes from `buffer` to the file specified by `fd`. * Asynchronous [write(2), pwrite(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/write.html). The `offset` and `length` arguments describe the part of the buffer to be written. The `position` refers to the offset from the beginning of the file where this data should be written. If `position` is `null`, the data will be written at the current position. The callback gets `(error, nbytes)`, where `nbytes` is the number of bytes written. * NOTE: Filer currently writes the entire buffer in a single operation. However, future versions may do it in chunks. * @see [fs.write](https://github.com/filerjs/filer?tab=readme-ov-file#write) */ function write(fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null, callback: (err: Error | null, nbytes: number) => void): void; /** * Read bytes from the file specified by `fd` into `buffer`. * Asynchronous [read(2), pread(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/read.html). The `offset` and `length` arguments describe the part of the buffer to be used. The `position` refers to the offset from the beginning of the file where this data should be read. If `position` is `null`, the data will be written at the current position. The callback gets `(error, nbytes)`, where `nbytes` is the number of bytes read. * NOTE: Filer currently reads into the buffer in a single operation. However, future versions may do it in chunks. * @see [fs.read](https://github.com/filerjs/filer?tab=readme-ov-file#read) */ function read(fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null, callback: (err: Error | null, nbytes: number, buffer: Uint8Array) => void): void; /** * Reads the entire contents of a file. The `options` argument is optional, and can take the form `"utf8"` (i.e., an encoding) or be an object literal: `{ encoding: "utf8", flag: "r" }`. If no encoding is specified, the raw binary buffer is returned via the callback. The callback gets `(error, data)`, where data is the contents of the file. * @see [fs.readFile](https://github.com/filerjs/filer?tab=readme-ov-file#readfile) */ function readFile(path: string, options?: { encoding: string; flag: "r" | "r+" | "w" | "w+" | "a" | "a+" } | string, callback: (err: Error | null, data: Uint8Array) => void): void; /** * Writes data to a file. `data` can be a string or `Buffer`, in which case any encoding option is ignored. The `options` argument is optional, and can take the form `"utf8"` (i.e., an encoding) or be an object literal: `{ encoding: "utf8", flag: "w" }`. If no encoding is specified, and `data` is a string, the encoding defaults to `'utf8'`. The callback gets `(error)`. * @see [fs.writeFile](https://github.com/filerjs/filer?tab=readme-ov-file#writefile) */ function writeFile(path: string, data: Uint8Array | string, options?: { encoding: string; flag: "r" | "r+" | "w" | "w+" | "a" | "a+" } | string, callback: (err: Error | null) => void): void; /** * Writes data to the end of a file. `data` can be a string or a `Buffer`, in which case any encoding option is ignored. The `options` argument is optional, and can take the form `"utf8"` (i.e., an encoding) or be an object literal: `{ encoding: "utf8", flag: "w" }`. If no encoding is specified, and `data` is a string, the encoding defaults to `'utf8'`. The callback gets `(error)`. * @see [fs.appendFile](https://github.com/filerjs/filer?tab=readme-ov-file#appendfile) */ function appendFile(filename: string, data: Uint8Array | string, options?: { encoding: string; flag: "r" | "r+" | "w" | "w+" | "a" | "a+" } | string, callback: (err: Error | null) => void): void; /** * Sets an extended attribute of a file or directory named `path`. * Asynchronous [setxattr(2)](http://man7.org/linux/man-pages/man2/setxattr.2.html). The optional `flag` parameter can be set to the following: * - `XATTR_CREATE`: ensures that the extended attribute with the given name will be new and not previously set. If an attribute with the given name already exists, it will return an `EExists` error to the callback. * - `XATTR_REPLACE`: ensures that an extended attribute with the given name already exists. If an attribute with the given name does not exist, it will return an `ENoAttr` error to the callback. * Callback gets no additional arguments. * @see [fs.setxattr](https://github.com/filerjs/filer?tab=readme-ov-file#setxattr) */ function setxattr(path: string, name: string, value: string | object, flag?: "XATTR_CREATE" | "XATTR_REPLACE" | string, callback: (err: Error | null) => void): void; /** * Sets an extended attribute of the file represented by the open file descriptor `fd`. * Asynchronous [setxattr(2)](http://man7.org/linux/man-pages/man2/setxattr.2.html). See `fs.setxattr` for more details. Callback gets no additional arguments. * @see [fs.fsetxattr](https://github.com/filerjs/filer?tab=readme-ov-file#fsetxattr) */ function fsetxattr(fd: number, name: string, value: string | object, flag?: "XATTR_CREATE" | "XATTR_REPLACE" | string, callback: (err: Error | null) => void): void; /** * Gets an extended attribute value for a file or directory. * Asynchronous [getxattr(2)](http://man7.org/linux/man-pages/man2/getxattr.2.html). Callback gets `(error, value)`, where `value` is the value for the extended attribute named `name`. * @see [fs.getxattr](https://github.com/filerjs/filer?tab=readme-ov-file#getxattr) */ function getxattr(path: string, name: string, callback: (err: Error | null, value: string | object) => void): void; /** * Gets an extended attribute value for the file represented by the open file descriptor `fd`. * Asynchronous [getxattr(2)](http://man7.org/linux/man-pages/man2/getxattr.2.html). See `fs.getxattr` for more details. Callback gets `(error, value)`, where `value` is the value for the extended attribute named `name`. * @see [fs.fgetxattr](https://github.com/filerjs/filer?tab=readme-ov-file#fgetxattr) */ function fgetxattr(fd: number, name: string, callback: (err: Error | null, value: string | object) => void): void; /** * Removes the extended attribute identified by `name` for the file given at `path`. * Asynchronous [removexattr(2)](http://man7.org/linux/man-pages/man2/removexattr.2.html). Callback gets no additional arguments. * @see [fs.removexattr](https://github.com/filerjs/filer?tab=readme-ov-file#removexattr) */ function removexattr(path: string, name: string, callback: (err: Error | null) => void): void; /** * Removes the extended attribute identified by `name` for the file represented by the open file descriptor `fd`. * Asynchronous [removexattr(2)](http://man7.org/linux/man-pages/man2/removexattr.2.html). See `fs.removexattr` for more details. Callback gets no additional arguments. * @see [fs.fremovexattr](https://github.com/filerjs/filer?tab=readme-ov-file#fremovexattr) */ function fremovexattr(fd: number, name: string, callback: (err: Error | null) => void): void; /** * Watch for changes to a file or directory at filename. The object returned is an FSWatcher, which is an [`EventEmitter`](http://nodejs.org/api/events.html)` with the following additional method: * - `close()` - stops listening for changes, and removes all listeners from this instance. Use this to stop watching a file or directory after calling `fs.watch()`. * The only supported option is `recursive`, which if `true` will cause a watch to be placed on a directory, and all sub-directories and files beneath it. * The `listener` callback gets two arguments `(event, filename)`. `event` is either `'rename'` or `'change'`, (currenty only `'rename'` is supported) and `filename` is the name of the file/dir which triggered the event. * Unlike node.js, all watch events return a path. Also, all returned paths are absolute from the root vs. just a relative filename. * @see [fs.watch](https://github.com/filerjs/filer?tab=readme-ov-file#watch) */ function watch(filename: string, options?: { recursive: boolean } | string, listener: (event: string, filename: string) => void): void; } } ================================================ FILE: src/sys/Node/runtimes/Webcontainers/nodeFSIntegration.ts ================================================ ================================================ FILE: src/sys/Node/runtimes/Webcontainers/nodeProc.ts ================================================ import { WebContainer } from "@webcontainer/api"; import getFileTree from "./util/getFileTree"; /** * Initialize and boot WebContainer with mounted file tree mirrored from Terbium's FS * @returns Promise resolving to WebContainer instance */ export async function initializeWebContainer(): Promise { const webContainer = await WebContainer.boot(); // Start the Nodebox runtime and setup FS mirroring from Terbium's FS on it const fileTree = await getFileTree(); await webContainer.mount(fileTree); if (!window.tb.node.servers) { window.tb.node.servers = new Map(); } webContainer.on("server-ready", (port, url) => { window.tb.node.servers.set(port, url); console.info(`[Node.js Subsystem] Server ready on port ${port}: ${url}`); }); console.info("[Node.js Subsystem] WebContainer has been initialized!"); window.tb.node.isReady = true; window.tb.process.create("runtime", { name: "Terbium Node.js Runtime", wid: null, src: null, size: null, icon: null, onKill: () => { window.tb.node.stop(); }, }); return webContainer; } ================================================ FILE: src/sys/Node/runtimes/Webcontainers/util/getFileTree.ts ================================================ /** * @module getFileTree * A WebContainers-compatible version that adapts the generic file tree used in the Nodebox implementation * @see https://webcontainers.io/guides/working-with-the-file-system */ import getFileTreeGeneric from "../../util/getFileTree"; import type { FileSystemTree, DirectoryNode, FileNode } from "@webcontainer/api"; /** * Converts a flat file tree (path -> contents) to WebContainer's FileSystemTree format * @param flatTree - Flat tree with full paths as keys and file contents as values * @returns FileSystemTree object compatible with WebContainers */ function convertToWebContainerTree(flatTree: Record): FileSystemTree { const tree: FileSystemTree = {}; for (const [path, contents] of Object.entries(flatTree)) { const pathParts = path.replace(/^\//, "").split("/").filter(Boolean); if (pathParts.length === 0) continue; let currentLevel = tree; for (let i = 0; i < pathParts.length - 1; i++) { const dirName = pathParts[i]; if (!currentLevel[dirName]) { // Create a new directory node currentLevel[dirName] = { directory: {}, } as DirectoryNode; } // Advance to the next level currentLevel = (currentLevel[dirName] as DirectoryNode).directory; } // Attach the file contents to the final level const fileName = pathParts[pathParts.length - 1]; currentLevel[fileName] = { file: { contents, }, } as FileNode; } return tree; } /** * Builds a file tree-compatible with WebContainers by adapting the generic file tree * @param path The path to build the tree from * @returns FileSystemTree An object representing the file tree for WebContainers */ export default async function getFileTree(path = "/"): Promise { // Get the flat file tree from the generic function const flatTree = await getFileTreeGeneric(path); // Convert to the WebContainers format const webContainerTree = convertToWebContainerTree(flatTree); if (path !== "/" && path !== "") { const pathParts = path.replace(/^\//, "").split("/").filter(Boolean); let subtree = webContainerTree; // Navigate to the requested path for (const part of pathParts) { if (subtree[part] && "directory" in subtree[part]) { // Navigate subtree = (subtree[part] as DirectoryNode).directory; } else { return {}; } } return subtree; } return webContainerTree; } ================================================ FILE: src/sys/Node/runtimes/shims/apis/child_process.ts ================================================ /** * The JSDoc is taken from Node itself */ import type * as NodeChildProcess from "node:child_process"; import type { ObjectEncodingOptions as NodeObjectEncodingOptions } from "node:fs"; import type { Readable, Writable } from "node:stream"; // Re-export the types from Node export type { ChildProcess, ChildProcessWithoutNullStreams, ChildProcessByStdio, ExecOptions, ExecException, ExecFileOptions, ExecFileException, ExecFileOptionsWithBufferEncoding, ExecFileOptionsWithStringEncoding, ExecFileOptionsWithOtherEncoding, ExecSyncOptions, ExecSyncOptionsWithStringEncoding, ExecSyncOptionsWithBufferEncoding, ExecFileSyncOptions, ExecFileSyncOptionsWithStringEncoding, ExecFileSyncOptionsWithBufferEncoding, SpawnOptions, SpawnOptionsWithoutStdio, SpawnOptionsWithStdioTuple, SpawnSyncOptions, SpawnSyncOptionsWithStringEncoding, SpawnSyncOptionsWithBufferEncoding, SpawnSyncReturns, ForkOptions, PromiseWithChild, StdioPipe, StdioNull, } from "node:child_process"; /** * Spawns a shell then executes the `command` within that shell, buffering any * generated output. The `command` string passed to the exec function is processed * directly by the shell and special characters (vary based on [shell](https://en.wikipedia.org/wiki/List_of_command-line_interpreters)) * need to be dealt with accordingly: * * ```js * import { exec } from 'node:child_process'; * * exec('"/path/to/test file/test.sh" arg1 arg2'); * // Double quotes are used so that the space in the path is not interpreted as * // a delimiter of multiple arguments. * * exec('echo "The \\$HOME variable is $HOME"'); * // The $HOME variable is escaped in the first instance, but not in the second. * ``` * * **Never pass unsanitized user input to this function. Any input containing shell** * **metacharacters may be used to trigger arbitrary command execution.** * * If a `callback` function is provided, it is called with the arguments `(error, stdout, stderr)`. On success, `error` will be `null`. On error, `error` will be an instance of `Error`. The * `error.code` property will be * the exit code of the process. By convention, any exit code other than `0` indicates an error. `error.signal` will be the signal that terminated the * process. * * The `stdout` and `stderr` arguments passed to the callback will contain the * stdout and stderr output of the child process. By default, Node.js will decode * the output as UTF-8 and pass strings to the callback. The `encoding` option * can be used to specify the character encoding used to decode the stdout and * stderr output. If `encoding` is `'buffer'`, or an unrecognized character * encoding, `Buffer` objects will be passed to the callback instead. * * ```js * import { exec } from 'node:child_process'; * exec('cat *.js missing_file | wc -l', (error, stdout, stderr) => { * if (error) { * console.error(`exec error: ${error}`); * return; * } * console.log(`stdout: ${stdout}`); * console.error(`stderr: ${stderr}`); * }); * ``` * * If `timeout` is greater than `0`, the parent will send the signal * identified by the `killSignal` property (the default is `'SIGTERM'`) if the * child runs longer than `timeout` milliseconds. * * Unlike the [`exec(3)`](http://man7.org/linux/man-pages/man3/exec.3.html) POSIX system call, `child_process.exec()` does not replace * the existing process and uses a shell to execute the command. * * If this method is invoked as its `util.promisify()` ed version, it returns * a `Promise` for an `Object` with `stdout` and `stderr` properties. The returned `ChildProcess` instance is attached to the `Promise` as a `child` property. In * case of an error (including any error resulting in an exit code other than 0), a * rejected promise is returned, with the same `error` object given in the * callback, but with two additional properties `stdout` and `stderr`. * * ```js * import util from 'node:util'; * import child_process from 'node:child_process'; * const exec = util.promisify(child_process.exec); * * async function lsExample() { * const { stdout, stderr } = await exec('ls'); * console.log('stdout:', stdout); * console.error('stderr:', stderr); * } * lsExample(); * ``` * * If the `signal` option is enabled, calling `.abort()` on the corresponding `AbortController` is similar to calling `.kill()` on the child process except * the error passed to the callback will be an `AbortError`: * * ```js * import { exec } from 'node:child_process'; * const controller = new AbortController(); * const { signal } = controller; * const child = exec('grep ssh', { signal }, (error) => { * console.error(error); // an AbortError * }); * controller.abort(); * ``` * @since v0.1.90 * @param command The command to run, with space-separated arguments. * @param callback called with the output when process terminates. */ export function exec(command: string, callback?: (error: NodeChildProcess.ExecException | null, stdout: string, stderr: string) => void): NodeChildProcess.ChildProcess; export function exec( command: string, options: { encoding: "buffer" | null; } & NodeChildProcess.ExecOptions, callback?: (error: NodeChildProcess.ExecException | null, stdout: Buffer, stderr: Buffer) => void, ): NodeChildProcess.ChildProcess; export function exec( command: string, options: { encoding: BufferEncoding; } & NodeChildProcess.ExecOptions, callback?: (error: NodeChildProcess.ExecException | null, stdout: string, stderr: string) => void, ): NodeChildProcess.ChildProcess; export function exec( command: string, options: { encoding: BufferEncoding; } & NodeChildProcess.ExecOptions, callback?: (error: NodeChildProcess.ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => void, ): NodeChildProcess.ChildProcess; export function exec(command: string, options: NodeChildProcess.ExecOptions, callback?: (error: NodeChildProcess.ExecException | null, stdout: string, stderr: string) => void): NodeChildProcess.ChildProcess; export function exec(command: string, options: (NodeObjectEncodingOptions & NodeChildProcess.ExecOptions) | undefined | null, callback?: (error: NodeChildProcess.ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => void): NodeChildProcess.ChildProcess; export function exec( _command: string, // biome-ignore lint/suspicious/noExplicitAny: I'll figure this out later ..._args: any[] ): NodeChildProcess.ChildProcess { // TODO: Implement this stub throw new Error("execFileSync is not yet implemented"); } export function execFile(file: string): NodeChildProcess.ChildProcess; export function execFile(file: string, options: (NodeObjectEncodingOptions & NodeChildProcess.ExecFileOptions) | undefined | null): NodeChildProcess.ChildProcess; export function execFile(file: string, args?: readonly string[] | null): NodeChildProcess.ChildProcess; export function execFile(file: string, args: readonly string[] | undefined | null, options: (NodeObjectEncodingOptions & NodeChildProcess.ExecFileOptions) | undefined | null): NodeChildProcess.ChildProcess; export function execFile(file: string, callback: (error: NodeChildProcess.ExecFileException | null, stdout: string, stderr: string) => void): NodeChildProcess.ChildProcess; export function execFile(file: string, args: readonly string[] | undefined | null, callback: (error: NodeChildProcess.ExecFileException | null, stdout: string, stderr: string) => void): NodeChildProcess.ChildProcess; export function execFile(file: string, options: NodeChildProcess.ExecFileOptionsWithBufferEncoding, callback: (error: NodeChildProcess.ExecFileException | null, stdout: Buffer, stderr: Buffer) => void): NodeChildProcess.ChildProcess; export function execFile(file: string, args: readonly string[] | undefined | null, options: NodeChildProcess.ExecFileOptionsWithBufferEncoding, callback: (error: NodeChildProcess.ExecFileException | null, stdout: Buffer, stderr: Buffer) => void): NodeChildProcess.ChildProcess; export function execFile(file: string, options: NodeChildProcess.ExecFileOptionsWithStringEncoding, callback: (error: NodeChildProcess.ExecFileException | null, stdout: string, stderr: string) => void): NodeChildProcess.ChildProcess; export function execFile(file: string, args: readonly string[] | undefined | null, options: NodeChildProcess.ExecFileOptionsWithStringEncoding, callback: (error: NodeChildProcess.ExecFileException | null, stdout: string, stderr: string) => void): NodeChildProcess.ChildProcess; export function execFile(file: string, options: NodeChildProcess.ExecFileOptionsWithOtherEncoding, callback: (error: NodeChildProcess.ExecFileException | null, stdout: string | Buffer, stderr: string | Buffer) => void): NodeChildProcess.ChildProcess; export function execFile(file: string, args: readonly string[] | undefined | null, options: NodeChildProcess.ExecFileOptionsWithOtherEncoding, callback: (error: NodeChildProcess.ExecFileException | null, stdout: string | Buffer, stderr: string | Buffer) => void): NodeChildProcess.ChildProcess; export function execFile(file: string, options: NodeChildProcess.ExecFileOptions, callback: (error: NodeChildProcess.ExecFileException | null, stdout: string, stderr: string) => void): NodeChildProcess.ChildProcess; export function execFile(file: string, args: readonly string[] | undefined | null, options: NodeChildProcess.ExecFileOptions, callback: (error: NodeChildProcess.ExecFileException | null, stdout: string, stderr: string) => void): NodeChildProcess.ChildProcess; export function execFile(file: string, options: (NodeObjectEncodingOptions & NodeChildProcess.ExecFileOptions) | undefined | null, callback: ((error: NodeChildProcess.ExecFileException | null, stdout: string | Buffer, stderr: string | Buffer) => void) | undefined | null): NodeChildProcess.ChildProcess; export function execFile( file: string, args: readonly string[] | undefined | null, options: (NodeObjectEncodingOptions & NodeChildProcess.ExecFileOptions) | undefined | null, callback: ((error: NodeChildProcess.ExecFileException | null, stdout: string | Buffer, stderr: string | Buffer) => void) | undefined | null, ): NodeChildProcess.ChildProcess; export function execFile( _file: string, // biome-ignore lint/suspicious/noExplicitAny: I'll figure this out later ..._args: any[] ): NodeChildProcess.ChildProcess { // TODO: Implement this stub throw new Error("execFile from node:child-process is not yet implemented"); } // spawn function with all Node.js overloads export function spawn(command: string, options?: NodeChildProcess.SpawnOptionsWithoutStdio): NodeChildProcess.ChildProcessWithoutNullStreams; export function spawn(command: string, options: NodeChildProcess.SpawnOptionsWithStdioTuple): NodeChildProcess.ChildProcessByStdio; export function spawn(command: string, options: NodeChildProcess.SpawnOptionsWithStdioTuple): NodeChildProcess.ChildProcessByStdio; export function spawn(command: string, options: NodeChildProcess.SpawnOptionsWithStdioTuple): NodeChildProcess.ChildProcessByStdio; export function spawn(command: string, options: NodeChildProcess.SpawnOptionsWithStdioTuple): NodeChildProcess.ChildProcessByStdio; export function spawn(command: string, options: NodeChildProcess.SpawnOptionsWithStdioTuple): NodeChildProcess.ChildProcessByStdio; export function spawn(command: string, options: NodeChildProcess.SpawnOptionsWithStdioTuple): NodeChildProcess.ChildProcessByStdio; export function spawn(command: string, options: NodeChildProcess.SpawnOptionsWithStdioTuple): NodeChildProcess.ChildProcessByStdio; export function spawn(command: string, options: NodeChildProcess.SpawnOptionsWithStdioTuple): NodeChildProcess.ChildProcessByStdio; export function spawn(command: string, options: NodeChildProcess.SpawnOptions): NodeChildProcess.ChildProcess; export function spawn(command: string, args?: readonly string[], options?: NodeChildProcess.SpawnOptionsWithoutStdio): NodeChildProcess.ChildProcessWithoutNullStreams; export function spawn(command: string, args: readonly string[], options: NodeChildProcess.SpawnOptionsWithStdioTuple): NodeChildProcess.ChildProcessByStdio; export function spawn(command: string, args: readonly string[], options: NodeChildProcess.SpawnOptionsWithStdioTuple): NodeChildProcess.ChildProcessByStdio; export function spawn(command: string, args: readonly string[], options: NodeChildProcess.SpawnOptionsWithStdioTuple): NodeChildProcess.ChildProcessByStdio; export function spawn(command: string, args: readonly string[], options: NodeChildProcess.SpawnOptionsWithStdioTuple): NodeChildProcess.ChildProcessByStdio; export function spawn(command: string, args: readonly string[], options: NodeChildProcess.SpawnOptionsWithStdioTuple): NodeChildProcess.ChildProcessByStdio; export function spawn(command: string, args: readonly string[], options: NodeChildProcess.SpawnOptionsWithStdioTuple): NodeChildProcess.ChildProcessByStdio; export function spawn(command: string, args: readonly string[], options: NodeChildProcess.SpawnOptionsWithStdioTuple): NodeChildProcess.ChildProcessByStdio; export function spawn(command: string, args: readonly string[], options: NodeChildProcess.SpawnOptionsWithStdioTuple): NodeChildProcess.ChildProcessByStdio; export function spawn(command: string, args: readonly string[], options: NodeChildProcess.SpawnOptions): NodeChildProcess.ChildProcess; export function fork(modulePath: string | URL, options?: NodeChildProcess.ForkOptions): NodeChildProcess.ChildProcess; export function fork(modulePath: string | URL, args?: readonly string[], options?: NodeChildProcess.ForkOptions): NodeChildProcess.ChildProcess; // execSync function with Node.js signatures export function execSync(command: string): Buffer; export function execSync(command: string, options: NodeChildProcess.ExecSyncOptionsWithStringEncoding): string; export function execSync(command: string, options: NodeChildProcess.ExecSyncOptionsWithBufferEncoding): Buffer; export function execSync(command: string, options?: NodeChildProcess.ExecSyncOptions): string | Buffer; export function execSync(command: string, options?: NodeChildProcess.ExecSyncOptions): string | Buffer { // TODO: Implement the actual execSync functionality throw new Error("execSync is not yet implemented"); } // execFileSync function with Node.js signatures export function execFileSync(file: string): Buffer; export function execFileSync(file: string, options: NodeChildProcess.ExecFileSyncOptionsWithStringEncoding): string; export function execFileSync(file: string, options: NodeChildProcess.ExecFileSyncOptionsWithBufferEncoding): Buffer; export function execFileSync(file: string, options?: NodeChildProcess.ExecFileSyncOptions): string | Buffer; export function execFileSync(file: string, args: readonly string[]): Buffer; export function execFileSync(file: string, args: readonly string[], options: NodeChildProcess.ExecFileSyncOptionsWithStringEncoding): string; export function execFileSync(file: string, args: readonly string[], options: NodeChildProcess.ExecFileSyncOptionsWithBufferEncoding): Buffer; export function execFileSync(file: string, args?: readonly string[], options?: NodeChildProcess.ExecFileSyncOptions): string | Buffer; export function execFileSync(file: string, ...args: any[]): string | Buffer { // TODO: Implement the actual execFileSync functionality throw new Error("execFileSync is not yet implemented"); } // spawnSync function with Node.js signatures export function spawnSync(command: string): NodeChildProcess.SpawnSyncReturns; export function spawnSync(command: string, options: NodeChildProcess.SpawnSyncOptionsWithStringEncoding): NodeChildProcess.SpawnSyncReturns; export function spawnSync(command: string, options: NodeChildProcess.SpawnSyncOptionsWithBufferEncoding): NodeChildProcess.SpawnSyncReturns; export function spawnSync(command: string, options?: NodeChildProcess.SpawnSyncOptions): NodeChildProcess.SpawnSyncReturns; export function spawnSync(command: string, args: readonly string[]): NodeChildProcess.SpawnSyncReturns; export function spawnSync(command: string, args: readonly string[], options: NodeChildProcess.SpawnSyncOptionsWithStringEncoding): NodeChildProcess.SpawnSyncReturns; export function spawnSync(command: string, args: readonly string[], options: NodeChildProcess.SpawnSyncOptionsWithBufferEncoding): NodeChildProcess.SpawnSyncReturns; export function spawnSync(command: string, args?: readonly string[], options?: NodeChildProcess.SpawnSyncOptions): NodeChildProcess.SpawnSyncReturns {} ================================================ FILE: src/sys/Node/runtimes/shims/apis/http.ts ================================================ ================================================ FILE: src/sys/Node/runtimes/shims/path-remapper.ts ================================================ ================================================ FILE: src/sys/Node/runtimes/shims/util/Stub.ts ================================================ export default class NotImplementedError extends Error { public readonly methodName?: string; constructor(methodName: string, packageName: string) { super(`${methodName}() from ${packageName} is not implemented yet`); this.name = "NotImplementedError"; this.methodName = methodName; Object.setPrototypeOf(this, NotImplementedError.prototype); } } ================================================ FILE: src/sys/Node/runtimes/util/getFileTree.ts ================================================ /** * @module getFileTree */ /** * Builds a full file tree for Terbium's File System * @param path The path to start at * @returns The flat file tree */ export default async function getFileTree(path = "/") { const tree: Record = {}; async function traverse(path: string): Promise { const stat = await window.parent.tb.fs.promises.stat(path); if (stat.isDirectory()) { const entries = await window.parent.tb.fs.promises.readdir(path); for (const entry of entries) { if (entry === "." || entry === "..") continue; const newPath = path.endsWith("/") ? `${path}${entry}` : `${path}/${entry}`; await traverse(newPath); } } else if (stat.isFile()) { const contents = await window.parent.tb.fs.promises.readFile(path, "utf8"); tree[path] = contents; } } await traverse(path); return tree; } ================================================ FILE: src/sys/Parser.ts ================================================ import * as htmlparser from "htmlparser2"; var isNative: boolean = false; var region: HTMLElement | null = null; var tbWindow: HTMLElement | null = null; // @ts-expect-error API Stub, Declaration will be read eventually var appTitle: string | null = null; const parse = { /** * THIS MAY NOT MAKE IT TO PRODUCTION * * Parses the given HTML or TML file and returns a built Terbium window * @param src The source code of the file * @returns new Terbium window * @example parse.build('My Window') * @example parse.build('https://example.com/window.tml') * @function `parse.build` */ build: async (src: string) => { const baseURL = new URL(src, window.location.href).href; const response = await window.tb.libcurl.fetch(baseURL); if (!response) throw new Error(`Failed to fetch the source from ${src}`); const data = await response.text(); if (data.startsWith(`@native`) || (src.endsWith(".tml") && !data.startsWith(""))) { isNative = true; } if (!isNative) return; console.warn("This functionality is not refined and may not work as expected."); tbWindow = document.createElement("window-body"); const shadow = tbWindow.attachShadow({ mode: "open" }); const parser: htmlparser.Parser | undefined = new htmlparser.Parser( { onopentag: (name: string, attribs: { [s: string]: string }) => { if (name === "region") { region = document.createElement("region"); for (const key in attribs) { region.setAttribute(key, attribs[key]); } } const element = document.createElement(name); for (const key in attribs) { element.setAttribute(key, attribs[key]); } shadow.appendChild(element); }, }, { decodeEntities: true }, ); parser!.write(data); parser!.end(); console.log(tbWindow); return tbWindow; }, }; export default parse; ================================================ FILE: src/sys/Store.ts ================================================ import React from "react"; import { create } from "zustand"; import { WindowConfig, cmprops, fileExists } from "./types"; import { init } from "@paralleldrive/cuid2"; import { updateInfo } from "./gui/AppIsland"; interface WindowState { windows: WindowConfig[]; wid?: string; pid?: string; matchedWindows: any; addWindow: (config: WindowConfig) => void; killWindow: (wid: any) => void; removeWindow: (wid: any) => void; arrange: (wid: any) => void; minimize: (wid: any) => void; getWindow: (wid: any) => void; currentPID?: string; } interface ContextMenuState { menu: cmprops; setContextMenu: (options: any) => void; clearContextMenu: () => void; } interface SearchMenuState { open: boolean; setOpen: (open: boolean) => void; searchRef: React.RefObject; searchMenuRef: React.RefObject; } let lastPID: number = 0; function ensureLastPID() { if (lastPID === 0) { if (window.tb?.process?.list) { try { const list = window.tb.process.list(); lastPID = Math.max(...Object.keys(list).map(Number)); } catch (e) { console.warn(e); lastPID = 2; } } else { lastPID = 2; } } } export const createPID = () => { ensureLastPID(); lastPID += 1; return lastPID.toString(); }; export const createWID = () => { const cuid = init({ length: 10, }); return "w-" + cuid(); }; const useWindowStore = create()(set => ({ windows: [], matchedWindows: [], addWindow: async (config: WindowConfig) => { const recentApps = (await fileExists("/system/var/terbium/recent.json")) ? JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/recent.json", "utf8")) : (await window.tb.fs.promises.writeFile("/system/var/terbium/recent.json", JSON.stringify([], null, 2), "utf8").catch((err: any) => console.error(err)), []); const updateState = async (state: WindowState) => { const indexes = state.windows.map(w => w.zIndex ?? 0); config.zIndex = Math.max(...indexes) + 1; config.focused = true; if (config.zIndex === -Infinity) { config.zIndex = 2; } state.windows.forEach(w => { if (w.wid !== config.wid) { w.focused = false; if (w.zIndex !== undefined) { Math.max(0, w.zIndex - 1); } } }); // @ts-expect-error const matched = state.matchedWindows.findIndex(group => // @ts-expect-error group.some(w => (typeof w.title === "string" ? w.title : w.title?.text) === (typeof config.title === "string" ? config.title : config.title?.text)), ); if (matched !== -1) { state.matchedWindows[matched].push(config); } else { state.matchedWindows.push([config]); } config.wid = createWID(); config.pid = createPID(); const appName = typeof config.title === "string" ? config.title : config.title?.text; let configData: any = null; try { const data = JSON.parse(await window.tb.fs.promises.readFile(`/apps/system/${appName.toLowerCase()}.tapp/index.json`, "utf8")).config; configData = { ...data, weight: 1, }; } catch (err) { configData = { title: appName, icon: config.icon, src: config.src, size: config.size, proxy: config.proxy, weight: 1, }; } if (recentApps.length > 10) { const lowestWeight = Math.min(...recentApps.map((app: any) => app.weight)); const lowestWeightIndex = recentApps.findIndex((app: any) => app.weight === lowestWeight); recentApps.splice(lowestWeightIndex, 1); } const recentAppIndex = recentApps.findIndex((app: any) => { return (typeof app.title === "string" ? app.title.toLowerCase() : app.title?.text.toLowerCase()) === (typeof configData.title === "string" ? configData.title.toLowerCase() : configData.title?.text.toLowerCase()); }); if (recentAppIndex === -1) { recentApps.push(configData); } else { recentApps[recentAppIndex].weight += 1; } await window.tb.fs.promises.writeFile("/system/var/terbium/recent.json", JSON.stringify(recentApps, null, 2), "utf8").catch((err: any) => { console.error("Error writing recent apps file:", err); }); window.dispatchEvent(new CustomEvent("selwin-upd", { detail: typeof config.title === "string" ? config.title : config.title?.text })); window.tb.process.procs[config.pid as any] = { name: typeof config.title === "string" ? config.title : config.title.text, wid: config.wid, icon: config.icon!, // @ts-expect-error pid: config.pid!, src: config.src, // @ts-expect-error size: config.size || { width: 800, height: 600 }, type: "window", }; return { windows: [...state.windows, config], matchedWindows: [...state.matchedWindows], currentPID: config.pid, }; }; const newState = await updateState(useWindowStore.getState()); set(newState); }, killWindow: (pid: string) => set((state: any) => { const windows = state.windows.filter((w: any) => w.pid !== pid); const matchedWindows = state.matchedWindows .map((group: any) => { const newGroup = group.filter((w: any) => w.pid !== pid); return newGroup.length > 0 ? newGroup : null; }) .filter((group: any) => group !== null); const indexes = windows.map((w: any) => w.zIndex ?? 0); const highest = Math.max(...indexes); const win = windows.find((w: any) => w.zIndex === highest); if (win) { win.focused = true; updateInfo({ appname: typeof win.title === "string" ? win.title : win.title?.text }); window.dispatchEvent(new CustomEvent("selwin-upd", { detail: typeof win.title === "string" ? win.title : win.title?.text })); } return { windows, matchedWindows, }; }), removeWindow: (wid: string) => { set((state: any) => { const windows = state.windows.filter((w: any) => w.wid !== wid); const matchedWindows = state.matchedWindows .map((group: any) => { const newGroup = group.filter((w: any) => w.wid !== wid); return newGroup.length > 0 ? newGroup : null; }) .filter((group: any) => group !== null); const indexes = windows.map((w: any) => w.zIndex ?? 0); const highest = Math.max(...indexes); const win = windows.find((w: any) => w.zIndex === highest); if (win) { win.focused = true; updateInfo({ appname: typeof win.title === "string" ? win.title : win.title?.text }); window.dispatchEvent(new CustomEvent("selwin-upd", { detail: typeof win.title === "string" ? win.title : win.title?.text })); } return { windows, matchedWindows, }; }); }, arrange: (wid: string) => 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; window.focused = true; state.windows.forEach(w => { if (w.wid !== wid) { w.focused = false; } }); return { windows: [...state.windows], // Create new array reference for React to detect change }; }), minimize: (wid: string) => set((state: WindowState) => { const window = state.windows.find(w => w.wid === wid); if (!window) return state; window.focused = false; return { windows: state.windows, }; }), getWindow: (wid: string) => { const state = useWindowStore.getState(); return state.windows.find(w => w.wid === wid); }, })); const useContextMenuStore = create()(set => ({ menu: { x: 0, y: 0, options: [] }, setContextMenu: (options: any) => set({ menu: options }), clearContextMenu: () => set({ menu: { x: 0, y: 0, options: [] } }), })); const useSearchMenuStore = create()(set => ({ open: false, setOpen: (open: boolean) => set({ open }), searchRef: React.createRef(), searchMenuRef: React.createRef(), })); export { useWindowStore, useContextMenuStore, useSearchMenuStore }; ================================================ FILE: src/sys/apis/Crypto.ts ================================================ // @ts-expect-error stfu import { SHA256 } from "crypto-js"; export default class pwd { harden(password: string) { const hash = SHA256(password).toString(); return hash; } } ================================================ FILE: src/sys/apis/Date.ts ================================================ export function GetTime() { const date = new Date(); const hours = date.getHours(); const minutes = date.getMinutes(); const ampm = hours >= 12 ? "PM" : "AM"; const formattedHours = hours % 12 || 12; const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes; const timeString = `${formattedHours}:${formattedMinutes} ${ampm}`; return timeString; } export function GetDate() { const dateObj = new Date(); const month = dateObj.getMonth(); const day = dateObj.getDate(); const year = dateObj.getFullYear(); const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; const output = `${days[dateObj.getDay()]} ${months[month]} ${day} ${year}`; return output; } ================================================ FILE: src/sys/apis/Dialogs.tsx ================================================ import { dialogProps } from "../types"; import { useState, useEffect, useRef } from "react"; import "../gui/styles/dialog.css"; import "../gui/styles/cropper.css"; import Cropper from "cropperjs"; import Compressor from "compressorjs"; export type dialogType = "alert" | "message" | "select" | "auth" | "permissions" | "filebrowser" | "directorybrowser" | "savefile" | "cropper" | "webauth"; export let setDialogFn: (type: dialogType, props: dialogProps, options?: { sudo: boolean }) => void; export let removeFn: () => void; export default function DialogContainer() { const [dialogType, setdialogType] = useState(null); const [dialogProps, setdialogProps] = useState({}); const [sudo, setSudo] = useState(null); const remove = () => { setdialogType(null); setdialogProps({}); }; const setDialog = (type: dialogType, props: dialogProps, options?: { sudo: boolean }) => { setdialogType(type); setdialogProps(props); setSudo(options?.sudo || null); }; /** * @returns Components for COM * @author XSTARS */ useEffect(() => { setDialogFn = setDialog; removeFn = remove; }, []); return ( <> {dialogType === "alert" && } {dialogType === "message" && } {dialogType === "select" &&
); } export function Select({ title, options, onOk, onCancel }: dialogProps) { if (!title) throw new Error("title is required"); const container = useRef(null); const dialog = useRef(null); const OK = (value: string) => { if (container.current) { container.current.classList.add("fade-out"); setTimeout(() => { container.current?.remove(); if (onOk) { onOk(value); } }, 200); removeFn(); } }; const Cancel = () => { if (container.current) { container.current.classList.add("fade-out"); setTimeout(() => { container.current?.remove(); if (onCancel) { onCancel(); } }, 200); removeFn(); } }; useEffect(() => { document.addEventListener("mousedown", e => { if (container.current && e.target !== dialog.current && e.target === container.current) { Cancel(); } }); }); return (
{title}
{options && options.map((option: { text: string; value: string }) => ( ))}
); } export function Auth({ title, defaultUsername, onOk, onCancel, sudo }: dialogProps) { if (!title) throw new Error("title is required"); const container = useRef(null); const dialog = useRef(null); const usernameRef = useRef(null); const passwordRef = useRef(null); const [sudoList, setSudoList] = useState([]); const OK = () => { const username = usernameRef.current?.value; const password = passwordRef.current?.value; if (container.current) { container.current.classList.add("fade-out"); setTimeout(() => { if (container.current) { removeFn(); } if (onOk && username && password) { onOk(username, password); } }, 200); } }; const Cancel = () => { if (container.current) { container.current.classList.add("fade-out"); setTimeout(() => { if (container.current) { removeFn(); } if (onCancel) { onCancel(); } }, 200); } }; const onDown = (event: React.KeyboardEvent) => { if (event.key === "Enter") { OK(); } }; useEffect(() => { document.addEventListener("mousedown", e => { if (container.current && e.target !== dialog.current && e.target === container.current) { Cancel(); } }); }); return (
{title}
); } export function Permissions({ title, message, onOk, onCancel }: dialogProps) { if (!message) throw new Error("message is required"); const container = useRef(null); const dialog = useRef(null); const OK = () => { if (container.current) { container.current.classList.add("fade-out"); setTimeout(() => { container.current?.remove(); if (onOk) { onOk(); } }, 200); removeFn(); } }; const Cancel = () => { if (container.current) { container.current.classList.add("fade-out"); setTimeout(() => { container.current?.remove(); if (onCancel) { onCancel(); } }, 200); removeFn(); } }; useEffect(() => { document.addEventListener("mousedown", e => { if (container.current && e.target !== dialog.current && e.target === container.current) { Cancel(); } }); }); return (
{title}
{message}
); } 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("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; 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; } } 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); } }, [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 { setSelectedEntry(`${currentDirectory}/${entry}`); const files = document.querySelectorAll(".file-item"); files.forEach(file => { if (file.getAttribute("data-entry") !== `${entry}`) { file.classList.remove("bg-[#ffffff18]"); } }); } }; const OK = () => { setTimeout(() => { removeFn(); if (onOk) { onOk(selectedEntry); } }, 300); }; const Cancel = () => { return new Promise(reject => { setTimeout(() => { reject("Canceled"); removeFn(); if (onCancel) { onCancel(); } }, 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 (
{title}
{loading ? (
Loading...
) : (
{fileEntries.length === 0 ? (
No files found
) : ( fileEntries.map(({ entry, isDirectory, type, connected }) => (
{ entClick(entry, isDirectory, type); const files = document.querySelectorAll(".file-item"); files.forEach(file => { file.classList.remove("bg-[#ffffff18]"); }); e.currentTarget.classList.add("bg-[#ffffff18]"); }} >
{currentDirectory === "storage devices" ? ( type === "external" ? ( ) : ( ) ) : isDirectory ? ( ) : ( )}
{entry}
{currentDirectory === "storage devices" && entry === "File System" && storageInfo ? (
{(() => { return `${formatSize(storageInfo.usage)} of ${formatSize(storageInfo.quota)}`; })()}
) : type === "external" && currentDirectory === "storage devices" ? (
WebDav Device
) : null}
)) )}
)}
{showBackButton && ( )} setCurrentDirectory(e.target.value)} />
); } export function DirectoryBrowser({ title, defualtDir, local, onOk, onCancel }: dialogProps) { const [selectedEntry, setSelectedEntry] = useState(null); const [fileEntries, setFileEntries] = useState([]); const [loading, setLoading] = useState(true); const [currentDirectory, setCurrentDirectory] = useState(defualtDir || "storage devices"); 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 { 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 .filter((entry: any) => entry.type === "directory") .map((entry: any) => ({ entry: entry.basename, isDirectory: true, type: "external", connected: true, })); setFileEntries(entriesInfo); setShowBackButton(true); setLoading(false); return; } } 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}`); return { entry, isDirectory: fileInfo.isDirectory(), type: "internal" }; }), ); const directories = entriesInfo.filter(info => info.isDirectory); setFileEntries(directories); setShowBackButton(true); if (directory.startsWith("//")) { directory = directory.slice(1); } setCurrentDirectory(directory); } catch (error) { console.error(error); } finally { setLoading(false); } }; useEffect(() => { if (currentDirectory === "storage devices") { navigator.storage.estimate().then(({ usage, quota }) => { setStorageInfo({ usage: usage as number, quota: quota as number }); }); 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); setLoading(false); } else { openDirectory(currentDirectory); } }, [currentDirectory]); const Select = () => { if (selectedEntry) { setTimeout(() => { removeFn(); if (onOk) { onOk(selectedEntry); } }, 300); } }; const Cancel = () => { return new Promise(reject => { setTimeout(() => { reject("Canceled"); removeFn(); if (onCancel) { onCancel(); } }, 300); }); }; const onChange = (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 (
{title}
{loading ? (
Loading...
) : (
{fileEntries.length === 0 ? (
No directories found
) : ( fileEntries.map(({ entry, isDirectory, type, connected }) => (
{ if (currentDirectory === "storage devices") { if (type === "internal") { setCurrentDirectory("//"); } else { window.tb.vfs.setServer(entry); setCurrentDirectory(`/mnt/${entry}`); } } else { if (currentDirectory.endsWith("/")) { setCurrentDirectory(`${currentDirectory}${entry}`); } else { setCurrentDirectory(`${currentDirectory}/${entry}`); } } }} onMouseDown={(e: React.MouseEvent) => { let path; if (currentDirectory === "storage devices") { if (type === "internal") { path = "//"; } else { path = `/mnt/${entry}`; } } else { if (currentDirectory.endsWith("/")) { path = `${currentDirectory}${entry}`; } else { path = `${currentDirectory}/${entry}`; } } setSelectedEntry(path); const files = document.querySelectorAll(".file-item"); files.forEach(file => { if (file.getAttribute("data-entry") !== `${entry}`) { file.classList.remove("bg-[#ffffff18]"); } }); e.currentTarget.classList.add("bg-[#ffffff18]"); }} >
{currentDirectory === "storage devices" ? ( type === "external" ? ( ) : ( ) ) : isDirectory ? ( ) : ( )}
{entry}
{currentDirectory === "storage devices" && entry === "File System" && storageInfo ? (
{(() => { return `${formatSize(storageInfo.usage)} of ${formatSize(storageInfo.quota)}`; })()}
) : type === "external" && currentDirectory === "storage devices" ? (
WebDav Device
) : null}
)) )}
)}
{showBackButton && ( )} setCurrentDirectory(e.target.value)} />
); } export function SaveFile({ title, defualtDir, filename, local, onOk, onCancel }: dialogProps) { if (!title) throw new Error("title is required"); const [selectedEntry, setSelectedEntry] = useState(null); const [fileEntries, setFileEntries] = useState([]); const [loading, setLoading] = useState(true); const [currentDirectory, setCurrentDirectory] = useState(defualtDir || "storage devices"); const [showBackButton, setShowBackButton] = useState(false); const [storageInfo, setStorageInfo] = useState<{ usage: number; quota: number } | null>(null); const fileInp = useRef(null); const anura = window.parent.anura; 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 .filter((entry: any) => entry.type === "directory") .map((entry: any) => ({ entry: entry.basename, isDirectory: true, type: "external", connected: true, })); setFileEntries(entriesInfo); setShowBackButton(true); setLoading(false); return; } } 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}`); return { entry, isDirectory: fileInfo.isDirectory(), type: "internal" }; }), ); const directories = entriesInfo.filter(info => info.isDirectory); setFileEntries(directories); setShowBackButton(true); setLoading(false); } catch (error) { console.error(error); setLoading(false); } }; useEffect(() => { if (currentDirectory === "storage devices") { navigator.storage.estimate().then(({ usage, quota }) => { setStorageInfo({ usage: usage as number, quota: quota as number }); }); 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); setLoading(false); } else { openDirectory(currentDirectory); } }, [currentDirectory]); const Select = (entry: string, isDirectory: boolean, type: string) => { let path; if (currentDirectory === "storage devices") { if (type === "internal") { path = "//"; } else { window.tb.vfs.setServer(entry); path = `/mnt/${entry}`; } setCurrentDirectory(path); return; } if (isDirectory) { if (currentDirectory.endsWith("/")) { path = `${currentDirectory}${entry}`; } else { path = `${currentDirectory}/${entry}`; } setCurrentDirectory(path); if (fileInp.current) { fileInp.current.value = `${path}/${filename || "file.txt"}`; } } else { if (currentDirectory.endsWith("/")) { path = `${currentDirectory}${entry}`; } else { path = `${currentDirectory}/${entry}`; } setSelectedEntry(path); if (fileInp.current) { fileInp.current.value = `${path}`; } } }; const onSave = () => { const fileName = fileInp.current?.value; removeFn(); if (fileName) { setTimeout(() => { if (onOk) { onOk(fileName); } }, 300); } }; const Cancel = () => { removeFn(); if (onCancel) { onCancel(); } }; const onPathChange = (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 (
{title}
{loading ? (
Loading...
) : (
{fileEntries.length === 0 ? (
No directories found
) : ( fileEntries.map(({ entry, isDirectory, type, connected }) => (
Select(entry, isDirectory, type)}>
{currentDirectory === "storage devices" ? ( type === "external" ? ( ) : ( ) ) : isDirectory ? ( ) : ( )}
{entry}
{currentDirectory === "storage devices" && entry === "File System" && storageInfo ? (
{(() => { return `${formatSize(storageInfo.usage)} of ${formatSize(storageInfo.quota)}`; })()}
) : type === "external" && currentDirectory === "storage devices" ? (
WebDav Device
) : null}
)) )}
)}
{showBackButton && ( )} { if (e.key === "Enter") { const inputPath = fileInp.current!.value; if (inputPath.endsWith("/")) { setCurrentDirectory(inputPath); } else { onSave(); } } }} onChange={e => onPathChange(e as unknown as React.KeyboardEvent)} />
); } export function Crop({ title, img, onOk, onCancel }: dialogProps) { const imgRef = useRef(null); const cropperRef = useRef(null); useEffect(() => { if (imgRef.current && img) { imgRef.current.src = img; cropperRef.current = new Cropper(imgRef.current, { aspectRatio: 1, viewMode: 1, cropBoxResizable: false, movable: true, rotatable: true, scalable: true, responsive: true, }); } return () => { if (cropperRef.current) { cropperRef.current.destroy(); } }; }, [img]); const onSave = () => { if (!cropperRef.current) return; const canvas = cropperRef.current.getCroppedCanvas(); canvas.toBlob((blob: any) => { new Compressor(blob as Blob, { quality: 0.5, success(result) { const reader = new FileReader(); reader.readAsDataURL(result); reader.onload = () => { removeFn(); setTimeout(() => { if (onOk) { onOk(reader.result); } }, 300); }; }, }); }); }; const Cancel = () => { removeFn(); if (onCancel) { onCancel(); } }; return (
{title}
); } export function WebAuth({ title, defaultUsername, onOk, onCancel }: dialogProps) { if (!title) throw new Error("title is required"); const container = useRef(null); const dialog = useRef(null); const usernameRef = useRef(null); const passwordRef = useRef(null); const OK = () => { const username = usernameRef.current?.value; const password = passwordRef.current?.value; if (container.current) { container.current.classList.add("fade-out"); setTimeout(() => { if (container.current) { removeFn(); } if (onOk && username && password) { onOk(username, password); } }, 200); } }; const Cancel = () => { if (container.current) { container.current.classList.add("fade-out"); setTimeout(() => { if (container.current) { removeFn(); } if (onCancel) { onCancel(); } }, 200); } }; const onDown = (event: React.KeyboardEvent) => { if (event.key === "Enter") { OK(); } }; useEffect(() => { document.addEventListener("mousedown", e => { if (container.current && e.target !== dialog.current && e.target === container.current) { Cancel(); } }); }); return (
{title}
); } ================================================ FILE: src/sys/apis/Mediaisland.tsx ================================================ import { useState, useEffect, useRef, useCallback } from "react"; import "../gui/styles/mediaisland.css"; import { MediaProps } from "../types"; export let setMusicFn: (props: MediaProps) => void; export let setVideoFn: (props: MediaProps) => void; export let hideFn: () => void; export let isExistingFn: () => void; export default function MediaIsland() { const [mediaType, setMediaType] = useState<"music" | "video" | null>(null); const [mediaProps, setMediaProps] = useState({}); const removeMedia = () => { setMediaType(null); setMediaProps({}); }; const setMusic = (props: MediaProps) => { setMediaType("music"); setMediaProps(props); }; const setVideo = (props: MediaProps) => { setMediaType("video"); setMediaProps(props); }; /** * @returns Components for COM * @author XSTARS */ useEffect(() => { setMusicFn = setMusic; setVideoFn = setVideo; hideFn = removeMedia; isExistingFn = () => { window.dispatchEvent(new CustomEvent("isExistingMP", { detail: mediaType !== null })); }; }, []); return (
{mediaType === "music" && } {mediaType === "video" &&
); } function Music({ track_name, artist, endtime, onRemove, onPausePlay, onNext, onBack, onSeek, time }: MediaProps & { onRemove: () => void }) { const [isPaused, setIsPaused] = useState(false); const [elapsedTime, setElapsedTime] = useState(() => (typeof time === "number" ? Math.max(0, Math.min(time, endtime)) : 0)); const [track, setTrack] = useState(track_name); const rafRef = useRef(null); const lastClientXRef = useRef(null); useEffect(() => { if (typeof time === "number") { setElapsedTime(Math.max(0, Math.min(time, endtime))); } }, [time, endtime]); useEffect(() => { let cancelled = false; let localId: ReturnType | null = null; const runTick = () => { if (isPaused || cancelled) return; localId = setTimeout(() => { setElapsedTime(prev => { const next = prev + 1; if (next >= endtime) { onRemove(); return prev; } return next; }); if (!isPaused && !cancelled) runTick(); }, 1000); }; if (!isPaused) runTick(); return () => { cancelled = true; if (localId) clearTimeout(localId); }; }, [isPaused]); const PausePlay = useCallback(() => { setIsPaused(prev => !prev); // @ts-expect-error if (onPausePlay) { // @ts-expect-error onPausePlay(); } }, [onPausePlay]); useEffect(() => { const handler = () => PausePlay(); window.addEventListener("tb-pause-isl", handler); return () => window.removeEventListener("tb-pause-isl", handler); }, [PausePlay]); useEffect(() => { return () => { if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; } }; }, []); const next = () => { if (onNext) { // @ts-ignore onNext(); } else { onRemove(); } }; const back = () => { if (onBack) { // @ts-ignore onBack(); } else { onRemove(); } }; const seek = (value: number) => { const clamped = Math.max(0, Math.min(value, endtime)); setElapsedTime(clamped); if (onSeek) { // @ts-expect-error onSeek(clamped); } }; const formatTime = (time: number) => { const minutes = Math.floor(time / 60); const seconds = time % 60; return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`; }; useEffect(() => { if (track.length > 21) { setTrack(track.slice(0, 21) + "..."); } }, [track_name]); const updSeek = (clientX: number, el: HTMLDivElement) => { const rect = el.getBoundingClientRect(); const rel = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); seek(Math.round(rel * endtime)); }; return (

{track}

{artist}

{ next(); }} className="back cursor-pointer" viewBox="0 0 16 9" fill="none" xmlns="http://www.w3.org/2000/svg" > {isPaused ? ( ) : ( )} { back(); }} className="forward cursor-pointer" viewBox="0 0 16 9" fill="none" xmlns="http://www.w3.org/2000/svg" >

{formatTime(elapsedTime)}

{ if (e.key === "ArrowLeft") seek(Math.max(0, elapsedTime - 5)); if (e.key === "ArrowRight") seek(Math.min(endtime, elapsedTime + 5)); }} onPointerDown={e => { e.preventDefault(); const el = e.currentTarget as HTMLDivElement; try { el.setPointerCapture(e.pointerId); } catch {} const handle = el.querySelector(".custom-handle") as HTMLDivElement | null; if (handle) { handle.style.opacity = "1"; handle.style.transform = "translate(-50%, -50%) scale(1.25)"; } lastClientXRef.current = e.clientX; updSeek(e.clientX, el); }} onPointerMove={e => { if (!(e.buttons & 1)) return; const el = e.currentTarget as HTMLDivElement; lastClientXRef.current = e.clientX; if (rafRef.current == null) { rafRef.current = requestAnimationFrame(() => { rafRef.current = null; if (lastClientXRef.current != null) { updSeek(lastClientXRef.current, el); } }); } }} onPointerUp={e => { const el = e.currentTarget as HTMLDivElement; try { el.releasePointerCapture(e.pointerId); } catch {} const handle = el.querySelector(".custom-handle") as HTMLDivElement | null; if (handle) { handle.style.opacity = "0"; handle.style.transform = "translate(-50%, -50%) scale(1)"; } if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; } lastClientXRef.current = null; }} onPointerCancel={e => { const el = e.currentTarget as HTMLDivElement; const handle = el.querySelector(".custom-handle") as HTMLDivElement | null; if (handle) { handle.style.opacity = "0"; handle.style.transform = "translate(-50%, -50%) scale(1)"; } if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; } lastClientXRef.current = null; }} >

{formatTime(endtime)}

); } function Video({ video_name, creator, endtime, onRemove, onPausePlay, onBack, onNext, onSeek, time }: MediaProps & { onRemove: () => void }) { const [isPaused, setIsPaused] = useState(false); const [elapsedTime, setElapsedTime] = useState(() => (typeof time === "number" ? Math.max(0, Math.min(time, endtime)) : 0)); const [video, setVideo] = useState(video_name); const rafRef = useRef(null); const lastClientXRef = useRef(null); useEffect(() => { if (typeof time === "number") { setElapsedTime(Math.max(0, Math.min(time, endtime))); } }, [time, endtime]); useEffect(() => { let cancelled = false; let localId: ReturnType | null = null; const runTick = () => { if (isPaused || cancelled) return; localId = setTimeout(() => { setElapsedTime(prev => { const next = prev + 1; if (next >= endtime) { onRemove(); return prev; } return next; }); if (!isPaused && !cancelled) runTick(); }, 1000); }; if (!isPaused) runTick(); return () => { cancelled = true; if (localId) clearTimeout(localId); }; }, [isPaused]); const PausePlay = useCallback(() => { setIsPaused(prev => !prev); // @ts-expect-error if (onPausePlay) { // @ts-expect-error onPausePlay(); } }, [onPausePlay]); useEffect(() => { const handler = () => PausePlay(); window.addEventListener("tb-pause-isl", handler); return () => window.removeEventListener("tb-pause-isl", handler); }, [PausePlay]); useEffect(() => { return () => { if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; } }; }, []); const next = () => { if (onNext) { // @ts-ignore onNext(); } else { onRemove(); } }; const back = () => { if (onBack) { // @ts-ignore onBack(); } else { onRemove(); } }; const seek = (value: number) => { const clamped = Math.max(0, Math.min(value, endtime)); setElapsedTime(clamped); if (onSeek) { // @ts-expect-error onSeek(clamped); } }; const formatTime = (time: number) => { const minutes = Math.floor(time / 60); const seconds = time % 60; return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`; }; useEffect(() => { if (video.length > 21) { setVideo(video.slice(0, 21) + "..."); } }, [video_name]); const updSeek = (clientX: number, el: HTMLDivElement) => { const rect = el.getBoundingClientRect(); const rel = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); seek(Math.round(rel * endtime)); }; return (

{video}

{creator}

{ next(); }} className="back cursor-pointer" viewBox="0 0 16 9" fill="none" xmlns="http://www.w3.org/2000/svg" > {isPaused ? ( ) : ( )} { back(); }} className="forward cursor-pointer" viewBox="0 0 16 9" fill="none" xmlns="http://www.w3.org/2000/svg" >

{formatTime(elapsedTime)}

{ if (e.key === "ArrowLeft") seek(Math.max(0, elapsedTime - 5)); if (e.key === "ArrowRight") seek(Math.min(endtime, elapsedTime + 5)); }} onPointerDown={e => { e.preventDefault(); const el = e.currentTarget as HTMLDivElement; try { el.setPointerCapture(e.pointerId); } catch {} const handle = el.querySelector(".custom-handle") as HTMLDivElement | null; if (handle) { handle.style.opacity = "1"; handle.style.transform = "translate(-50%, -50%) scale(1.25)"; } lastClientXRef.current = e.clientX; updSeek(e.clientX, el); }} onPointerMove={e => { if (!(e.buttons & 1)) return; const el = e.currentTarget as HTMLDivElement; lastClientXRef.current = e.clientX; if (rafRef.current == null) { rafRef.current = requestAnimationFrame(() => { rafRef.current = null; if (lastClientXRef.current != null) { updSeek(lastClientXRef.current, el); } }); } }} onPointerUp={e => { const el = e.currentTarget as HTMLDivElement; try { el.releasePointerCapture(e.pointerId); } catch {} const handle = el.querySelector(".custom-handle") as HTMLDivElement | null; if (handle) { handle.style.opacity = "0"; handle.style.transform = "translate(-50%, -50%) scale(1)"; } if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; } lastClientXRef.current = null; }} onPointerCancel={e => { const el = e.currentTarget as HTMLDivElement; const handle = el.querySelector(".custom-handle") as HTMLDivElement | null; if (handle) { handle.style.opacity = "0"; handle.style.transform = "translate(-50%, -50%) scale(1)"; } if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; } lastClientXRef.current = null; }} >

{formatTime(endtime)}

); } ================================================ FILE: src/sys/apis/Notifications.tsx ================================================ import { NotificationProps } from "../types"; import { useState, useEffect } from "react"; import "../gui/styles/notification.css"; export let setNotifFn: (type: "message" | "toast" | "installing", props: NotificationProps) => number; export let dismissNotifFn: (id: number) => void; let notificationId = 0; let notificationCount = 0; export default function NotificationContainer() { const [notifications, setNotifications] = useState<{ id: number; type: "message" | "toast" | "installing"; props: NotificationProps }[]>([]); const remove = (id: number) => { setNotifications(prev => prev.filter(notif => notif.id !== id)); }; const setNotif = (type: "message" | "toast" | "installing", props: NotificationProps) => { const id = notificationId++; const newNotification = { id, type, props }; setNotifications(prev => [...prev, newNotification]); return id; }; /** * @returns Components for COM * @author XSTARS */ useEffect(() => { setNotifFn = setNotif; dismissNotifFn = remove; }, []); return (
{notifications.map(({ id, type, props }) => { if (type === "message") { return remove(id)} />; } else if (type === "toast") { return remove(id)} />; } else if (type === "installing") { return remove(id)} />; } })}
); } export function Message({ iconSrc, application, message, txt, onOk, onCancel, time, remove }: NotificationProps & { remove: () => void }) { if (!message) throw new Error("message is required"); const [inputValue, setInputValue] = useState(txt || ""); const [elapsedTime, setElapsedTime] = useState("Now"); useEffect(() => { const startTime = Date.now(); const int = setInterval(() => { const elapsed = Math.floor((Date.now() - startTime) / 60000); if (elapsed > 0) { setElapsedTime(`${elapsed}min ago`); } else if (elapsed > 60) { setElapsedTime(`${Math.floor(elapsed / 60)}h ago`); } else if (elapsed > 1440) { setElapsedTime(`${Math.floor(elapsed / 1440)}d ago`); } else if (elapsed > 10080) { setElapsedTime(`${Math.floor(elapsed / 10080)}w ago`); } }, 60000); const tID = setTimeout(() => { Cancel(); }, time || 10000); return () => { clearInterval(int); clearTimeout(tID); }; }, [time]); const OK = () => { setTimeout(() => { remove(); if (onOk) onOk(inputValue); }, 200); }; const Cancel = () => { setTimeout(() => { remove(); notificationCount += 1; window.dispatchEvent( new CustomEvent("notification-count", { detail: { count: notificationCount }, }), ); SaveNotification({ iconSrc, application, message, onOk }); if (onCancel) onCancel(); }, 200); }; const onDown = (event: React.KeyboardEvent) => { if (event.key === "Enter") { OK(); } }; return (
{application}
{application || "Unknown App"}
{elapsedTime}
{message}
setInputValue(e.target.value)} onKeyDown={onDown} className="w-full p-2 rounded-md leading-none text-lg bg-[#ffffff20] shadow-tb-border-shadow cursor-[var(--cursor-text)] focus-within:outline-hidden" />
); } export function Toast({ iconSrc, application, message, time, onOk, onCancel, remove }: NotificationProps & { remove: () => void }) { const [elapsedTime, setElapsedTime] = useState("Now"); useEffect(() => { const startTime = Date.now(); const int = setInterval(() => { const elapsed = Math.floor((Date.now() - startTime) / 60000); if (elapsed > 0) { setElapsedTime(`${elapsed}min ago`); } else if (elapsed > 60) { setElapsedTime(`${Math.floor(elapsed / 60)}h ago`); } else if (elapsed > 1440) { setElapsedTime(`${Math.floor(elapsed / 1440)}d ago`); } else if (elapsed > 10080) { setElapsedTime(`${Math.floor(elapsed / 10080)}w ago`); } }, 60000); const tID = setTimeout(() => { Cancel(); }, time || 10000); return () => { clearInterval(int); clearTimeout(tID); }; }, [time]); const Cancel = () => { setTimeout(() => { remove(); notificationCount += 1; window.dispatchEvent( new CustomEvent("notification-count", { detail: { count: notificationCount }, }), ); SaveNotification({ iconSrc, application, message, onOk }); if (onCancel) onCancel(); }, 200); }; const OK = () => { setTimeout(() => { remove(); if (onOk) onOk(); }, 200); }; return (
{application}
{application || "Unkown App"}
{elapsedTime}
{message}
); } export function Installing({ iconSrc, application, message, time, onOk, remove }: NotificationProps & { remove: () => void }) { const [currentAnimation, setCurrentAnimation] = useState(0); useEffect(() => { if (typeof time !== "number" || time <= 0) return; const tID = setTimeout(() => { OK(); }, time); return () => { clearTimeout(tID); }; }, [time]); const anim0 = `anim0 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite`; const anim1 = `anim1 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) 1.15s infinite`; useEffect(() => { const int = setInterval(() => { setCurrentAnimation(prev => (prev === 0 ? 1 : 0)); }, 2100); return () => clearInterval(int); }, []); const OK = () => { setTimeout(() => { remove(); if (onOk) onOk(); }, 200); }; return (
Icon
{application || "com.tb.genericapp"}
{message}
{currentAnimation === 0 ?
:
}
); } export async function SaveNotification({ iconSrc, application, message, onOk }: NotificationProps) { const notifications = JSON.parse(sessionStorage.getItem("notifications") || "[]"); if (onOk) { const notificationObject = { message: message, icon: iconSrc || "/assets/img/logo.png", application: application || "com.tb.genericapp", time: new Date().toISOString(), onOk: { code: await onOk.toString(), }, }; console.log(notificationObject); notifications.push(notificationObject); sessionStorage.setItem("notifications", JSON.stringify(notifications)); window.dispatchEvent(new CustomEvent("notification-update")); } else { const notificationObject = { message: message, icon: iconSrc || "/assets/img/logo.png", application: application || "com.tb.genericapp", time: new Date().toISOString(), }; notifications.push(notificationObject); sessionStorage.setItem("notifications", JSON.stringify(notifications)); window.dispatchEvent(new CustomEvent("notification-update")); } } ================================================ FILE: src/sys/apis/Registry.ts ================================================ export const registry = { cache: { state: "unready", }, async get(data: any): Promise { if (this.cache.state == "unready") { // Defer if not loaded console.log("Unable to get data, retrying in 2 seconds"); await new Promise(r => setTimeout(r, 2000)); return (await this.get(data)) as any; } // @ts-expect-error return this.cache[data.path] as any; }, async set(data: any) { if (this.cache.state == "unready") { // Defer if not loaded console.log("Unable to get data, retrying in 2 seconds"); await new Promise(r => setTimeout(r, 2000)); await this.set(data); } // void, nothing happens here for now other than storing changes for the session //@ts-ignore this.cache[data.path] = data.content; await window.tb.fs.promises.writeFile("/system/etc/terbium/settings.json", JSON.stringify(this.cache)); }, exists(data: any) { // @ts-expect-error if (this.cache[data]) { return true; } return false; }, }; ================================================ FILE: src/sys/apis/SysSearch.ts ================================================ import { WindowConfig } from "../types"; import { fileStat, isFilePathString } from "./utils/file"; type TSearchTerm = string | object | File | ArrayBuffer | Blob | null; export const searchFiles = async (searchTerm: TSearchTerm): Promise => { const sh = window.tb.sh; const searchResults: any[] = []; const searchTermString = typeof searchTerm === "string" ? searchTerm : JSON.stringify(searchTerm); const files = await sh.promises.find("/", { name: `*${searchTermString}` }); if (!isFilePathString(searchTermString)) return false; for (const file of files) { const filePath = file.replace(/\\/g, "/"); const fileName = await fileStat.name(filePath); const fileDir = await fileStat.dir(filePath); const fileSize = await fileStat.size(filePath); const fileType = await fileStat.type(filePath); const fileMTime = await fileStat.mTime(filePath); const fileCTime = await fileStat.cTime(filePath); const fileExt = await fileStat.ext(filePath); if (fileName.toLowerCase().includes(searchTermString.toLowerCase())) { let result = { name: fileName, ext: fileExt, type: fileType, size: fileSize, cTime: fileCTime, mTime: fileMTime, path: filePath, dir: fileDir, }; searchResults.push(result); } } if (searchResults.length > 0) { return searchResults; } else { return false; } }; interface IAppName { text: string; weight?: number; html?: string; } type TAppName = string | IAppName; export const searchApps = async (searchTerm: TSearchTerm): Promise => { let searchTermString = typeof searchTerm === "string" ? searchTerm.toLowerCase() : JSON.stringify(searchTerm).toLowerCase(); if (searchTermString.endsWith(".tapp")) searchTermString = searchTermString.replace(".tapp", ""); let installed = JSON.parse(await window.tb.fs.promises.readFile("/apps/installed.json", "utf8")); const searchResults: any[] = []; for (const app of installed) { if (app.name && app.name.toLowerCase().includes(searchTermString)) { let cfg = JSON.parse(await window.tb.fs.promises.readFile(app.config, "utf8")); let icon: string | null = null; let appDir = app.config.replace(/\/[^\/]+\.json$|\/[^\/]+\.tbconfig$/i, ""); try { if (cfg.icon) { icon = cfg.icon.includes("http") ? cfg.icon : cfg.icon; } else if (cfg.config && cfg.config.icon) { icon = cfg.config.icon.includes("http") ? cfg.config.icon : cfg.config.icon; } else if (cfg.wmArgs) { icon = cfg.wmArgs.icon.includes("http") ? cfg.config.icon : cfg.config.icon; } } catch { icon = ` `; } if (!icon) { icon = ` `; } if (cfg.manifest) { cfg.config = { title: cfg.manifest.name, icon: cfg.manifest.name.includes("Anura File Manager") ? "/apps/fsapp.app/files.png" : `${appDir}/${cfg.manifest.icon}`, src: cfg.manifest.name.includes("Anura File Manager") ? "/apps/fsapp.app/index.html" : `${appDir}/${cfg.manifest.index}`, }; } let title: string = cfg.name; if (cfg.config && cfg.config.title) { title = typeof cfg.config.title === "object" ? cfg.config.title.text : cfg.config.title; } else if (cfg.wmArgs && cfg.wmArgs.title) { title = typeof cfg.wmArgs.title === "object" ? cfg.wmArgs.title.text : cfg.wmArgs.title; } searchResults.push({ dir: title.includes("Anura File Manager") ? "int://apps/fsapp.app/" : appDir, icon: title.includes("Anura File Manager") ? "/apps/fsapp.app/files.png" : icon, name: title, cfg: cfg.config, }); } } return searchResults.length > 0 ? searchResults : false; }; ================================================ FILE: src/sys/apis/System.ts ================================================ import { version } from "../../../package.json"; import { hash, repository } from "../../hash.json"; export class System { version(type: string | number) { if (type === "string") return version.toString(); else if (type === "number") return Number(version); } instance = { repo: repository, hash: hash, }; } ================================================ FILE: src/sys/apis/Time.ts ================================================ let format: string = "12h"; let internet: boolean; let showSeconds: boolean; const getTime = () => { window.tb.fs.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, (err: any, data: any) => { if (err) return console.error(err); const settings = JSON.parse(data); format = settings["times"]["format"]; internet = settings["times"]["internet"]; if (settings["times"]["showSeconds"] === true) { showSeconds = true; } else { showSeconds = false; } }); const date = new Date(); let hours: any; let minutes: any; let seconds: any; let time: string = ""; if (format === "24h") { hours = date.getHours(); } else if (format === "12h") { hours = date.getHours(); hours = hours % 12; hours = hours ? hours : 12; } let ampm: string; // set am or pm if (date.getHours() >= 12) { ampm = "PM"; } else { ampm = "AM"; } minutes = date.getMinutes(); seconds = date.getSeconds(); hours = hours < 10 ? "0" + hours : hours; minutes = minutes < 10 ? "0" + minutes : minutes; seconds = seconds < 10 ? "0" + seconds : seconds; if (format === "24h") { if (showSeconds === true) { time = `${hours}:${minutes}:${seconds}`; } else { time = `${hours}:${minutes}`; } } else if (format === "12h") { if (showSeconds === true) { time = `${hours}:${minutes}:${seconds} ${ampm}`; } else { time = `${hours}:${minutes} ${ampm}`; } } return time; }; export default getTime; ================================================ FILE: src/sys/apis/Xor.ts ================================================ export const XOR = { encode(input: string): string { let result = ""; let len = input.length; for (let i = 0; i < len; i++) { const char = input[i]; result += i % 2 ? String.fromCharCode(char.charCodeAt(0) ^ 2) : char; } return encodeURIComponent(result); }, decode(input: string): string { if (!input) return input; input = decodeURIComponent(input); let result = ""; let len = input.length; for (let i = 0; i < len; i++) { const char = input[i]; result += i % 2 ? String.fromCharCode(char.charCodeAt(0) ^ 2) : char; } return result; }, }; ================================================ FILE: src/sys/apis/utils/WindowPerformanceMonitor.ts ================================================ /** * Window Performance Monitor * * This utility helps measure window rendering performance. * Usage in browser console: * * const monitor = new WindowPerformanceMonitor(); * monitor.start(); * // Perform window operations (drag, resize, open/close) * monitor.stop(); * monitor.getReport(); */ class WindowPerformanceMonitor { private startTime: number = 0; private metrics: { frameRates: number[]; paintTimes: number[]; layoutTimes: number[]; dragCount: number; resizeCount: number; renderCount: number; } = { frameRates: [], paintTimes: [], layoutTimes: [], dragCount: 0, resizeCount: 0, renderCount: 0, }; private observer: PerformanceObserver | null = null; private rafId: number | null = null; private lastFrameTime: number = 0; start() { console.log("🚀 Window Performance Monitor started"); this.startTime = performance.now(); this.lastFrameTime = this.startTime; this.observer = new PerformanceObserver(list => { for (const entry of list.getEntries()) { if (entry.entryType === "paint") { this.metrics.paintTimes.push(entry.startTime); } else if (entry.entryType === "measure") { if (entry.name.includes("layout")) { this.metrics.layoutTimes.push(entry.duration); } } } }); this.observer.observe({ entryTypes: ["paint", "measure"] }); const measureFrameRate = () => { const now = performance.now(); const delta = now - this.lastFrameTime; const fps = 1000 / delta; this.metrics.frameRates.push(fps); this.lastFrameTime = now; this.rafId = requestAnimationFrame(measureFrameRate); }; this.rafId = requestAnimationFrame(measureFrameRate); const mutationObserver = new MutationObserver(mutations => { this.metrics.renderCount += mutations.length; }); const windowArea = document.querySelector("window-area"); if (windowArea) { mutationObserver.observe(windowArea, { attributes: true, childList: true, subtree: true, }); } this.monitorWindowOperations(); } private monitorWindowOperations() { const originalAddEventListener = window.addEventListener; let isDragging = false; let isResizing = false; window.addEventListener = function (type: string, listener: any, options?: any) { if (type === "mousemove") { const wrappedListener = (e: MouseEvent) => { if (isDragging) { // @ts-ignore this.metrics.dragCount++; } if (isResizing) { // @ts-ignore this.metrics.resizeCount++; } listener(e); }; // @ts-ignore return originalAddEventListener.call(window, type, wrappedListener, options); } return originalAddEventListener.call(window, type, listener, options); }.bind(this); window.addEventListener("mousedown", (e: MouseEvent) => { const target = e.target as HTMLElement; if (target.closest(".region")) { isDragging = true; } if (target.dataset.resizer) { isResizing = true; } }); window.addEventListener("mouseup", () => { isDragging = false; isResizing = false; }); } stop() { console.log("Window Performance Monitor stopped"); if (this.observer) { this.observer.disconnect(); } if (this.rafId) { cancelAnimationFrame(this.rafId); } } getReport() { const duration = performance.now() - this.startTime; const avgFPS = this.metrics.frameRates.reduce((a, b) => a + b, 0) / this.metrics.frameRates.length; const minFPS = Math.min(...this.metrics.frameRates); const maxFPS = Math.max(...this.metrics.frameRates); const avgPaintTime = this.metrics.paintTimes.reduce((a, b) => a + b, 0) / this.metrics.paintTimes.length; const avgLayoutTime = this.metrics.layoutTimes.reduce((a, b) => a + b, 0) / this.metrics.layoutTimes.length; const report = { duration: `${(duration / 1000).toFixed(2)}s`, frameRate: { average: `${avgFPS.toFixed(2)} FPS`, min: `${minFPS.toFixed(2)} FPS`, max: `${maxFPS.toFixed(2)} FPS`, samples: this.metrics.frameRates.length, }, performance: { avgPaintTime: `${avgPaintTime.toFixed(2)}ms`, avgLayoutTime: `${avgLayoutTime.toFixed(2)}ms`, }, operations: { dragOperations: this.metrics.dragCount, resizeOperations: this.metrics.resizeCount, renders: this.metrics.renderCount, }, health: this.getPerformanceHealth(avgFPS, minFPS), }; console.table(report); return report; } private getPerformanceHealth(avgFPS: number, minFPS: number) { if (avgFPS >= 55 && minFPS >= 45) { return "Excellent (60 FPS target met)"; } else if (avgFPS >= 45 && minFPS >= 30) { return "Good (some frame drops)"; } else if (avgFPS >= 30) { return "Fair (noticeable lag)"; } else { return "Poor (significant performance issues)"; } } getDetailedMetrics() { return { frameRates: this.metrics.frameRates, paintTimes: this.metrics.paintTimes, layoutTimes: this.metrics.layoutTimes, dragCount: this.metrics.dragCount, resizeCount: this.metrics.resizeCount, renderCount: this.metrics.renderCount, }; } } if (typeof window !== "undefined") { // @ts-ignore window.WindowPerformanceMonitor = WindowPerformanceMonitor; console.log("💡 Window Performance Monitor loaded. Usage:"); console.log(" const monitor = new WindowPerformanceMonitor();"); console.log(" monitor.start();"); console.log(" // Perform window operations"); console.log(" monitor.stop();"); console.log(" monitor.getReport();"); } export default WindowPerformanceMonitor; ================================================ FILE: src/sys/apis/utils/file.ts ================================================ import path from "path-browserify"; export interface FileStats { name: (path: string) => Promise; ext: (path: string) => Promise; type: (path: string) => Promise; size: (path: string) => Promise; cTime: (path: string) => Promise; mTime: (path: string) => Promise; isFile: (path: string) => Promise; isDir: (path: string) => Promise; dir: (path: string) => Promise; } export const extensionToNameMap: Record = { html: "HTML", htm: "HTML", css: "CSS", js: "JavaScript", jsx: "JavaScript (JSX)", ts: "TypeScript", tsx: "TypeScript (TSX)", json: "JSON", xml: "XML", yml: "YAML", yaml: "YAML", php: "PHP", asp: "ASP", aspx: "ASP.NET", vue: "Vue.js", svelte: "Svelte", ejs: "Embedded JavaScript (EJS)", handlebars: "Handlebars", mustache: "Mustache", java: "Java", py: "Python", pyc: "Compiled Python", rb: "Ruby", pl: "Perl", swift: "Swift", go: "Go", rs: "Rust", c: "C", h: "C Header", cpp: "C++", cxx: "C++", cc: "C++", cs: "C#", kotlin: "Kotlin", kt: "Kotlin", scala: "Scala", dart: "Dart", lua: "Lua", r: "R", sh: "Shell Script", bash: "Bash Script", zsh: "Zsh Script", bat: "Batch File", ps1: "PowerShell Script", sql: "SQL", ini: "INI Config", toml: "TOML Config", cfg: "Config File", env: "Environment Config", csv: "CSV", tsv: "TSV", md: "Markdown", rst: "reStructuredText", latex: "LaTeX", tex: "TeX", bib: "BibTeX", log: "Log File", png: "PNG Image", jpg: "JPEG Image", jpeg: "JPEG Image", gif: "GIF Image", bmp: "Bitmap Image", svg: "SVG Vector Image", webp: "WebP Image", ico: "Icon File", tiff: "TIFF Image", heic: "HEIC Image", mp4: "MPEG-4 Video", mov: "QuickTime Video", avi: "AVI Video", webm: "WebM Video", mkv: "Matroska Video", flv: "Flash Video", m4v: "MPEG-4 Video", "3gp": "3GP Video", mp3: "MP3 Audio", wav: "WAV Audio", ogg: "OGG Audio", flac: "FLAC Audio", aac: "AAC Audio", m4a: "MPEG-4 Audio", wma: "Windows Media Audio", opus: "Opus Audio", zip: "ZIP Archive", rar: "RAR Archive", "7z": "7-Zip Archive", tar: "TAR Archive", gz: "Gzip Archive", bz2: "Bzip2 Archive", xz: "XZ Archive", tgz: "Tarball Gzip Archive", "tar.gz": "Tarball Gzip Archive", pdf: "PDF Document", doc: "Microsoft Word Document", docx: "Microsoft Word (OpenXML)", xls: "Microsoft Excel Spreadsheet", xlsx: "Microsoft Excel (OpenXML)", ppt: "Microsoft PowerPoint", pptx: "Microsoft PowerPoint (OpenXML)", odt: "OpenDocument Text", ods: "OpenDocument Spreadsheet", odp: "OpenDocument Presentation", rtf: "Rich Text Format", txt: "Plain Text", ttf: "TrueType Font", otf: "OpenType Font", woff: "Web Open Font Format", woff2: "Web Open Font Format 2", exe: "Windows Executable", dll: "Dynamic Link Library", deb: "Debian Package", rpm: "Red Hat Package Manager", dmg: "macOS Disk Image", iso: "ISO Disk Image", app: "macOS App Package", bin: "Binary File", jar: "Java Archive", war: "Web Application Archive", class: "Java Class File", wasm: "WebAssembly", node: "Node.js Binary", lock: "Lock File", bak: "Backup File", tmp: "Temporary File", crt: "Certificate File", pem: "PEM Certificate", key: "Private Key File", pub: "Public Key File", sig: "Signature File", dat: "Data File", }; export const nameToExtensionMap: Record = Object.entries(extensionToNameMap).reduce((acc: Record, [ext, name]) => { const key = name.toLowerCase(); if (!acc[key]) acc[key] = ext; return acc; }, {}); /** * Get a human-readable file type name from a file extension. * @param ext The file extension (e.g., "js", "pdf"). * @returns A human-readable name or "Unknown" if not found. */ export function getNameFromExtension(ext: string): string { return extensionToNameMap[ext.toLowerCase()] || "Unknown"; } /** * Get a typical file extension for a human-readable file type name. * @param name The human-readable name (e.g., "JavaScript"). * @returns A file extension or "unknown" if not found. */ export function getExtensionFromName(name: string): string { return nameToExtensionMap[name.toLowerCase()] || "unknown"; } /** * Check if a string is a valid file path (contains a file name and extension). * @param str The string to check. * @returns True if it's a valid file path, false otherwise. */ export const isFilePathString = (str: string): boolean => { const last = str.split("/").pop(); return !!last && /\.[a-zA-Z0-9]+$/.test(last); // checks for an extension }; export const fileStat: FileStats = { name: async (file: string): Promise => { const stats = await window.tb.fs.promises.stat(file); if (stats.type.toLowerCase() === "file") { const name = path.basename(stats.name); return name; } return "unknown"; }, ext: async (file: string): Promise => { const stats = await window.tb.fs.promises.stat(file); if (stats.type.toLowerCase() === "file") { const ext = path.extname(stats.name).replace(".", "").toLowerCase(); return ext; } return "unknown"; }, type: async (file: string): Promise => { const stats = await window.tb.fs.promises.stat(file); if (stats.type.toLowerCase() === "file") { const ext = path.extname(stats.name).replace(".", "").toLowerCase(); const type = getNameFromExtension(ext); return type; } return "unknown"; }, size: async (file: string): Promise => { const stats = await window.tb.fs.promises.stat(file); if (stats.type.toLowerCase() === "file") { const size = stats.size; const units = ["B", "KB", "MB", "GB", "TB"]; let i = 0; let sizeInUnits = size; while (sizeInUnits >= 1024 && i < units.length - 1) { sizeInUnits /= 1024; i++; } return `${sizeInUnits.toFixed(2)} ${units[i]}`; } return "0 B"; }, cTime: async (file: string): Promise => { const stats = await window.tb.fs.promises.stat(file); if (stats.type.toLowerCase() === "file") { const cTime = new Date(stats.ctime).toLocaleString("en-US", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", }); return cTime; } return "unknown"; }, mTime: async (file: string): Promise => { const stats = await window.tb.fs.promises.stat(file); if (stats.type.toLowerCase() === "file") { const mTime = new Date(stats.mtime).toLocaleString("en-US", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", }); return mTime; } return "unknown"; }, isFile: async (file: string): Promise => { const stats = await window.tb.fs.promises.stat(file); if (stats.type.toLowerCase() === "file") { return true; } return false; }, isDir: async (file: string): Promise => { const stats = await window.tb.fs.promises.stat(file); if (stats.type.toLowerCase() === "directory") { return true; } return false; }, dir: async (file: string): Promise => { const stats = await window.tb.fs.promises.stat(file); if (stats.type.toLowerCase() === "file") { return path.dirname(file); } return "unknown"; }, }; ================================================ FILE: src/sys/apis/utils/startupHandler.ts ================================================ import { fileExists } from "../../types"; export async function launchProcs(): Promise { const toLaunch = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/startup.json", "utf8")); const currUser = sessionStorage.getItem("currAcc") || "Guest"; const userProcs = toLaunch[currUser] || {}; const systemProcs = toLaunch["System"] || {}; for (const proc in systemProcs) { setTimeout(() => {}, 1000); if (systemProcs[proc].enabled) { try { eval(systemProcs[proc].start); } catch (e) { console.error(`Failed to launch system startup process ${proc}:`, e); } } } for (const proc in userProcs) { setTimeout(() => {}, 1000); if (userProcs[proc].enabled) { try { eval(userProcs[proc].start); } catch (e) { console.error(`Failed to launch user startup process ${proc}:`, e); } } } } export async function addStartupProc(proc: string, target: "System" | "User", cmd?: string): Promise { if (!(await fileExists("/system/var/terbium/startup.json"))) { const users = await window.tb.fs.promises.readdir("/home/"); const startupObj = { System: {}, ...Object.fromEntries(users.map(user => [user, {}])), }; await window.tb.fs.promises.writeFile("/system/var/terbium/startup.json", JSON.stringify(startupObj), "utf8"); } const startupData = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/startup.json", "utf8")); const currUser = sessionStorage.getItem("currAcc") || "Guest"; const relTarget = target === "System" ? "System" : currUser; if (!startupData[relTarget]) { startupData[relTarget] = {}; } startupData[relTarget][proc] = { start: cmd || `tb.system.openApp('${proc.toLowerCase().replace(/\s+/g, "")}')`, installedby: currUser, enabled: false, }; await window.tb.fs.promises.writeFile("/system/var/terbium/startup.json", JSON.stringify(startupData, null, 4), "utf8"); } export async function removeStartupProc(proc: string, target: "System" | "User"): Promise { if (!(await fileExists("/system/var/terbium/startup.json"))) { return; } const startupData = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/startup.json", "utf8")); const currUser = sessionStorage.getItem("currAcc") || "Guest"; const relTarget = target === "System" ? "System" : currUser; if (!startupData[relTarget] || !startupData[relTarget][proc]) { return; } delete startupData[relTarget][proc]; await window.tb.fs.promises.writeFile("/system/var/terbium/startup.json", JSON.stringify(startupData, null, 4), "utf8"); } export async function enableProc(proc: string, target: "System" | "User"): Promise { if (!(await fileExists("/system/var/terbium/startup.json"))) { return; } const startupData = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/startup.json", "utf8")); const currUser = sessionStorage.getItem("currAcc") || "Guest"; const relTarget = target === "System" ? "System" : currUser; if (!startupData[relTarget] || !startupData[relTarget][proc]) { return; } startupData[relTarget][proc].enabled = true; await window.tb.fs.promises.writeFile("/system/var/terbium/startup.json", JSON.stringify(startupData, null, 4), "utf8"); } export async function disableProc(proc: string, target: "System" | "User"): Promise { if (!(await fileExists("/system/var/terbium/startup.json"))) { return; } const startupData = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/startup.json", "utf8")); const currUser = sessionStorage.getItem("currAcc") || "Guest"; const relTarget = target === "System" ? "System" : currUser; if (!startupData[relTarget] || !startupData[relTarget][proc]) { return; } startupData[relTarget][proc].enabled = false; await window.tb.fs.promises.writeFile("/system/var/terbium/startup.json", JSON.stringify(startupData, null, 4), "utf8"); } ================================================ FILE: src/sys/apis/utils/tauth.ts ================================================ import { TAuthReturnType } from "../../types"; import { createAuthClient } from "better-auth/client"; import { libcurl } from "libcurl.js"; export const auth = createAuthClient({ baseURL: "https://auth.terbiumon.top", fetchOptions: { customFetchImpl: async (input: string | URL | Request, init?: RequestInit | undefined) => { 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/`); console.log("libcurl wasm loaded"); } const savedCookies = localStorage.getItem("libcurl_cookies") || ""; const session = new libcurl.HTTPSession({ enable_cookies: true, cookie_jar: savedCookies, }); session.import_cookies(); try { const headers = new Headers(init?.headers); if (!headers.has("origin") && !headers.has("referer")) { headers.set("origin", window.location.origin); } const response = await session.fetch(input.toString(), { ...init, headers, }); return response; } finally { setTimeout(() => { const currentCookies = session.export_cookies(); localStorage.setItem("libcurl_cookies", currentCookies); session.close(); console.log("HTTP Session Destroyed"); }, 100); } }, }, }); export async function getinfo(user?: string | null, pass?: string | null, setting?: string): Promise { if (user && pass) { console.log("[TAUTH] Signing in with provided credentials..."); await auth.signIn.email({ email: user, password: pass, rememberMe: true, }); } const response = await auth.$fetch("https://auth.terbiumon.top/user/info", { credentials: "include", method: "GET", headers: { "Content-Type": "application/json", }, }); const uinf = !response.error ? response.data : { error: "Failed to fetch user info" }; const sr = setting ? await auth.$fetch(`https://auth.terbiumon.top/kv/retrieve/${setting}`, { credentials: "include", method: "GET", headers: { "Content-Type": "application/json", }, }) : null; const settings = sr === null ? null : !sr.error ? sr.data : { error: "Failed to fetch settings" }; return { user: uinf, settings: settings ? (() => { try { // @ts-expect-error no return JSON.parse(settings.value); } catch (e) { window.tb.notification.Toast({ message: "Your session is out of date. Click OK to sign in to Terbium Cloud again", application: "System", iconSrc: "/fs/apps/system/about.tapp/icon.svg", onOk: async () => { await window.tb.tauth.reauth(); }, time: 6000, }); return null; } })() : null, }; } export async function setinfo(user?: string | null, pass?: string | null, setting?: string, toset?: any) { if (user && pass) { console.log("[TAUTH] Signing in with provided credentials..."); await auth.signIn.email({ email: user, password: pass, rememberMe: true, }); } if (!setting || !toset) { return { error: "No setting or value to set provided" }; } const response = await auth.$fetch(`https://auth.terbiumon.top/kv/set/${setting}`, { credentials: "include", method: "POST", body: JSON.stringify({ value: JSON.stringify(toset), }), headers: { "Content-Type": "application/json", }, }); const changer = !response.error ? response.data : { error: "Failed to set info" }; return changer; } ================================================ FILE: src/sys/apis/utils/winPreview.ts ================================================ // utility for showing a lightweight preview of an element // call getPrev(elem) to display the preview, hidePrev() to remove it. // Uses modern-screenshot for static snapshots to avoid RAM spikes from live content like iframes. import { domToCanvas } from "modern-screenshot"; let container: HTMLDivElement | null = null; let snapshotCache = new WeakMap(); let currentTarget: HTMLElement | null = null; let pendingTarget: HTMLElement | null = null; let pendingOptions: PreviewOptions | undefined; let updateScheduled = false; interface PreviewOptions { maxSize?: number; x?: number; y?: number; scale?: number; } function ensureContainer() { if (container) return; if (!document.getElementById("win-preview-styles")) { const styleEl = document.createElement("style"); styleEl.id = "win-preview-styles"; styleEl.textContent = ` .win-preview-container { position: fixed; top: 0; left: 0; pointer-events: none; z-index: 1000000; transition: opacity 0.1s; opacity: 0; will-change: transform,opacity; } .win-preview-container * { pointer-events: none; } `; document.head.appendChild(styleEl); } container = document.createElement("div"); container.className = "win-preview-container"; document.body.appendChild(container); } function scheduleUpdate(target: HTMLElement, options?: PreviewOptions) { pendingTarget = target; pendingOptions = options; if (!updateScheduled) { updateScheduled = true; requestAnimationFrame(performUpdate); } } function performUpdate() { updateScheduled = false; if (!pendingTarget) return; const target = pendingTarget; const opts = pendingOptions || {}; if (target === currentTarget) { positionContainer(opts); return; } ensureContainer(); // check cache first let canvas = snapshotCache.get(target); if (!canvas) { currentTarget = target; // capture snapshot asynchronously const filter = (node: Node) => { const tagName = (node as Element).tagName; if (tagName === "IFRAME" || tagName === "SCRIPT" || tagName === "NOSCRIPT") return false; return true; }; const drawResolvedCanvas = (newCanvas: HTMLCanvasElement) => { snapshotCache.set(target, newCanvas); if (currentTarget === target) { updateContainerWithCanvas(newCanvas, target, opts); } }; domToCanvas(target, { scale: 1, backgroundColor: null, filter, features: { restoreScrollPosition: true, }, }) .then(newCanvas => { drawResolvedCanvas(newCanvas); }) .catch(() => { // silently fail to avoid spamming console }); // for now, show a placeholder or nothing return; } updateContainerWithCanvas(canvas, target, opts); currentTarget = target; } function updateContainerWithCanvas(canvas: HTMLCanvasElement, target: HTMLElement, opts: PreviewOptions) { if (!container) return; // clear and add canvas container.innerHTML = ""; container.appendChild(canvas); const rect = target.getBoundingClientRect(); let scale = opts.scale ?? 1; if (opts.maxSize) { const maxSz = opts.maxSize; const factor = Math.min(maxSz / rect.width, maxSz / rect.height, 1); scale = Math.min(scale, factor); } canvas.style.transformOrigin = "top left"; canvas.style.transform = `scale(${scale})`; container.style.width = `${rect.width * scale}px`; container.style.height = `${rect.height * scale}px`; positionContainer(opts); container.style.opacity = "1"; } function positionContainer(opts: PreviewOptions) { if (!container) return; if (opts.x != null && opts.y != null) { container.style.transform = `translate(${opts.x}px, ${opts.y}px) ${container.style.transform.replace(/translate\([^)]*\)/, "")}`; } } /** * Show a preview of the given element. The preview is positioned fixed relative to the viewport. * * @param el the element to preview * @param options optional configuration: maxSize, x/y coordinate, scale */ export function getPrev(el: HTMLElement, options?: PreviewOptions) { if (!el || !(el instanceof HTMLElement)) return; scheduleUpdate(el, options); } /** * Hide the preview immediately. */ export function hidePrev() { if (container) container.style.opacity = "0"; currentTarget = null; pendingTarget = null; } export function previewAtMouse(el: HTMLElement, evt: MouseEvent, options?: PreviewOptions) { const coords = { x: evt.clientX + 10, y: evt.clientY + 10 }; getPrev(el, { ...options, ...coords }); } export default { getPrev, hidePrev, previewAtMouse, }; ================================================ FILE: src/sys/gui/AppIsland.tsx ================================================ import { useEffect, useState } from "react"; import "./styles/shell.css"; import { createId } from "@paralleldrive/cuid2"; export interface AppIslandProps { text?: string; click?: () => void; appname?: string; id?: string; } type IslandState = { props: AppIslandProps | null; controls: React.JSX.Element[]; }; export let updateInfo: (props: AppIslandProps) => void; export let updateControls: (props: AppIslandProps) => void; export let clearInfo: () => void; export let clearControls: (appname: string) => void; export default function AppIsland() { const [islands, setIslands] = useState<{ [appname: string]: IslandState }>({}); const [activeApp, setActiveApp] = useState(null); const onUpdate = (props: AppIslandProps) => { if (!props.appname) return; setIslands(prev => ({ ...prev, [props.appname!]: { ...(prev[props.appname!] || { props: null, controls: [] }), props: { ...prev[props.appname!]?.props, ...props }, }, })); setActiveApp(props.appname); window.dispatchEvent(new CustomEvent("selwin-upd", { detail: props.appname })); }; const updconts = (props: AppIslandProps) => { if (!props.appname) return; const appname = props.appname; const controlId = props.id || createId(); const whenClick = props.click ? props.click : () => {}; setIslands(prev => { const existingControls = prev[appname]?.controls || []; if (existingControls.some(control => control.key === controlId)) return prev; const control = ( ); return { ...prev, [appname]: { props: prev[appname]?.props || null, controls: [...existingControls, control], }, }; }); window.dispatchEvent(new CustomEvent("selwin-upd", { detail: appname })); }; const clear = (appname: string) => { setIslands(prev => ({ ...prev, [appname]: { ...prev[appname], controls: [], }, })); }; const clearinf = () => { setActiveApp(null); }; useEffect(() => { updateInfo = onUpdate; updateControls = updconts; clearInfo = clearinf; clearControls = clear; }); return (
{Object.entries(islands).map(([appname, island]) => (
{appname}
{island.controls.length > 0 &&
{island.controls}
}
))}
); } ================================================ FILE: src/sys/gui/Battery.tsx ================================================ import { useEffect, useRef, useState } from "react"; import { UserSettings } from "../types"; declare global { interface Navigator { getBattery(): Promise; userAgent: string; } interface BatteryManager { level: number; charging: boolean; chargingTime: number; onchargingchange: (() => void) | null; onlevelchange: (() => void) | null; } } export default function Battery() { const [batteryStatus, setBattery] = useState("100%"); const [charging, setCharging] = useState(false); const [shouldRender, setShouldRender] = useState(true); const [showPercent, setShowPercent] = useState(false); const [batteryNumber, setBatteryNumber] = useState(0); const percentRef = useRef(null); useEffect(() => { const controlBatteryPercentVisibility = (value: CustomEvent) => { if (typeof value.detail !== "boolean") return; setShowPercent(value.detail); }; window.addEventListener("controlBatteryPercentVisibility", controlBatteryPercentVisibility as EventListener); return () => { window.removeEventListener("controlBatteryPercentVisibility", controlBatteryPercentVisibility as EventListener); }; }, []); useEffect(() => { const getBattery = async () => { if ("getBattery" in navigator) { try { const battery = await navigator.getBattery(); const batteryChange = () => { if (battery.charging) { setBattery(`${Math.floor(battery.level * 100)}%`); setBatteryNumber(Math.floor(battery.level * 100)); setCharging(true); } else { setBattery(`${Math.floor(battery.level * 100)}%`); setBatteryNumber(Math.floor(battery.level * 100)); setCharging(false); } }; battery.onchargingchange = batteryChange; battery.onlevelchange = batteryChange; batteryChange(); } catch (error) { console.error(`Error fetching battery: ${error}`); setBattery("none"); } } else { setBattery("none"); setShouldRender(false); } }; setInterval(getBattery); getBattery(); const getShowPercent = async () => { const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8")); setShowPercent(settings["battery-percent"]); }; getShowPercent(); }, []); function renderBattery() { return ( shouldRender && (
15 ? "#fee685" : "#ffffff"}`} />
15 ? "text-amber-200" : "text-[#ffffffc9]"} ${showPercent === true ? "" : "opacity-0 translate-x-2"} duration-150 `} > {batteryStatus != "none" ? batteryStatus : "N/A"}
) ); } return renderBattery(); } ================================================ FILE: src/sys/gui/ContextMenu.tsx ================================================ import { useEffect, useState, useRef } from "react"; import "./styles/contextmenu.css"; import { useContextMenuStore } from "../Store"; const ContextMenuArea = () => { const contextMenuStore = useContextMenuStore(); const menuAreaRef = useRef(null); const menuRef = useRef(null); const [menuOpen, setMenuOpen] = useState(false); const [menuPos, setmenuPos] = useState({ x: 0, y: 0 }); useEffect(() => { const ctx = (e: CustomEvent) => { contextMenuStore.setContextMenu(e.detail.props); setTimeout(() => { setMenuOpen(true); }, 50); }; const withinRadius = (e: MouseEvent) => { if (!menuRef.current) return false; const rect = menuRef.current.getBoundingClientRect(); const xBound = e.clientX >= rect.left - 75 && e.clientX <= rect.right + 75; const yBound = e.clientY >= rect.top - 75 && e.clientY <= rect.bottom + 75; return xBound && yBound; }; const onDown = (e: MouseEvent) => { if (e.button === 0) { if (!menuRef.current?.contains(e.target as Node) && !withinRadius(e)) { setMenuOpen(false); setTimeout(() => { contextMenuStore.clearContextMenu(); }, 150); } } }; const close = () => { setMenuOpen(false); setTimeout(() => { contextMenuStore.clearContextMenu(); }, 1000); }; if (contextMenuStore.menu.options.length > 0) { let x = contextMenuStore.menu.x; let y = contextMenuStore.menu.y; if (x > window.innerWidth - 190) { x = window.innerWidth - 190; } if (y > window.innerHeight - 160) { y = window.innerHeight - 160; } setmenuPos({ x, y }); } window.addEventListener("ctxm", ctx as unknown as EventListener); window.addEventListener("close-ctxm", close); document.addEventListener("click", onDown); return () => { window.removeEventListener("ctxm", ctx as unknown as EventListener); window.removeEventListener("close-ctxm", close); document.removeEventListener("click", onDown); }; }, [contextMenuStore]); return (
{}}> {contextMenuStore.menu.options.length > 0 && (
{contextMenuStore.menu.titlebar ? typeof contextMenuStore.menu.titlebar === "string" ?
{contextMenuStore.menu.titlebar}
: contextMenuStore.menu.titlebar : null}
{contextMenuStore.menu.options.map((option, i) => { return ( ); })}
)}
); }; export default ContextMenuArea; ================================================ FILE: src/sys/gui/Desktop.tsx ================================================ import { FC, createElement, useRef, useState, useEffect } from "react"; import Dock, { TDockItem } from "./Dock"; import WindowArea from "./WindowArea"; import Shell from "./Shell"; import { WispMenu } from "./Wifi"; import DialogContainer from "../apis/Dialogs"; import NotificationContainer from "../apis/Notifications"; import { NotificationMenu } from "./NotificationCenter"; import { dirExists, UserSettings, WindowConfig } from "../types"; import WinSwitcher from "./WinSwitcher"; import ContextMenuArea from "./ContextMenu"; import { domToCanvas } from "modern-screenshot"; // Mostly for testing stuff but these are the prod settings rn. Feel free to change in dev const PREVIEW_DEBUG = false; const THUMB_WIDTH = 250; const THUMB_HEIGHT = 200; interface IDesktopProps { desktop: number; onContextMenu?: (e: MouseEvent) => void; } const Desktop: FC = ({ desktop, onContextMenu }) => { const desktopRef = useRef(null); const [showMenu, setShowMenu] = useState(false); const [showNotif, setShowNotif] = useState(false); const [pinned, setPinned] = useState>([]); const [winPrev, setWinPrev] = useState<{ open: boolean; windows: any; location: string } | null>(null); const [previewWin, setPreviewWin] = useState(null); const [windowOptimizationsEnabled, setWindowOptimizationsEnabled] = useState(false); const [thumbnailFallbacks, setThumbnailFallbacks] = useState>(new Set()); const [thumbnailLoading, setThumbnailLoading] = useState>(new Set()); const thumbnailCacheRef = useRef>(new Map()); const setThumbnailLoadingState = (winId: string, loading: boolean) => { setThumbnailLoading(prev => { const isLoading = prev.has(winId); if (loading === isLoading) return prev; const next = new Set(prev); if (loading) { next.add(winId); } else { next.delete(winId); } return next; }); }; const markThumbnailFallback = (winId: string) => { setThumbnailFallbacks(prev => { if (prev.has(winId)) return prev; const next = new Set(prev); next.add(winId); return next; }); }; const drawCanvasFitted = (ctx: CanvasRenderingContext2D, source: HTMLCanvasElement, targetWidth: number, targetHeight: number) => { ctx.clearRect(0, 0, targetWidth, targetHeight); ctx.fillStyle = "#111"; ctx.fillRect(0, 0, targetWidth, targetHeight); if (source.width <= 0 || source.height <= 0) return; const scale = Math.min(targetWidth / source.width, targetHeight / source.height); const drawWidth = source.width * scale; const drawHeight = source.height * scale; const offsetX = (targetWidth - drawWidth) / 2; const offsetY = (targetHeight - drawHeight) / 2; ctx.drawImage(source, offsetX, offsetY, drawWidth, drawHeight); }; const renderWindowThumbnail = (winId: string, canvasEl: HTMLCanvasElement, attempt = 0) => { setThumbnailLoadingState(winId, true); if (windowOptimizationsEnabled) { markThumbnailFallback(winId); setThumbnailLoadingState(winId, false); return; } const ctx = canvasEl.getContext("2d"); if (!ctx) { if (PREVIEW_DEBUG) console.warn("[win-preview] no 2d context", { winId, attempt }); setThumbnailLoadingState(winId, false); return; } const cachedCanvas = thumbnailCacheRef.current.get(winId); if (cachedCanvas) { if (PREVIEW_DEBUG) console.debug("[win-preview] draw from cache", { winId, attempt, width: cachedCanvas.width, height: cachedCanvas.height }); drawCanvasFitted(ctx, cachedCanvas, THUMB_WIDTH, THUMB_HEIGHT); canvasEl.dataset.rendered = "1"; setThumbnailLoadingState(winId, false); return; } const parElem = document.getElementById(winId); const mainparElem = parElem?.querySelector(".w-full.h-full"); let elem = mainparElem; if (mainparElem?.querySelector("iframe")) { elem = mainparElem.querySelector("iframe") as HTMLElement | null; } if (!elem) { if (PREVIEW_DEBUG) console.warn("[win-preview] missing element", { winId }); markThumbnailFallback(winId); canvasEl.dataset.rendered = "0"; setThumbnailLoadingState(winId, false); return; } const rect = elem.getBoundingClientRect(); if (PREVIEW_DEBUG) console.debug("[win-preview] element rect", { winId, attempt, width: rect.width, height: rect.height, x: rect.x, y: rect.y }); if (rect.width <= 0 || rect.height <= 0) { if (attempt < 2) { if (PREVIEW_DEBUG) console.warn("[win-preview] zero rect, retrying", { winId, attempt }); requestAnimationFrame(() => renderWindowThumbnail(winId, canvasEl, attempt + 1)); return; } if (PREVIEW_DEBUG) console.error("[win-preview] zero rect after retries", { winId }); markThumbnailFallback(winId); canvasEl.dataset.rendered = "0"; setThumbnailLoadingState(winId, false); return; } const filter = (node: Node) => { const tagName = (node as Element).tagName; if (tagName === "IFRAME" || tagName === "SCRIPT" || tagName === "NOSCRIPT") return false; return true; }; if (PREVIEW_DEBUG) console.debug("[win-preview] rendering via modern-screenshot domToCanvas", { winId, attempt }); domToCanvas(elem, { scale: 1, backgroundColor: null, filter, features: { restoreScrollPosition: true, }, }) .then(fullCanvas => { if (PREVIEW_DEBUG) console.debug("[win-preview] modern-screenshot success", { winId, width: fullCanvas.width, height: fullCanvas.height }); thumbnailCacheRef.current.set(winId, fullCanvas); drawCanvasFitted(ctx, fullCanvas, THUMB_WIDTH, THUMB_HEIGHT); canvasEl.dataset.rendered = "1"; setThumbnailLoadingState(winId, false); }) .catch(err => { if (PREVIEW_DEBUG) console.error("[win-preview] modern-screenshot failed", { winId, attempt, err }); if (attempt < 2) { requestAnimationFrame(() => renderWindowThumbnail(winId, canvasEl, attempt + 1)); return; } markThumbnailFallback(winId); canvasEl.dataset.rendered = "0"; setThumbnailLoadingState(winId, false); }); }; useEffect(() => { const menu = () => { setShowMenu(prev => !prev); }; const nMenu = () => { setShowNotif(prev => !prev); }; const getWallpaper = async () => { const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`)); setWindowOptimizationsEnabled(settings.windowOptimizations ?? false); if (settings.wallpaper.startsWith("/system")) { if (!desktopRef.current) return; desktopRef.current.style.backgroundImage = `url("/fs/${settings.wallpaper}")`; desktopRef.current.style.backgroundSize = settings.wallpaperMode === "stretch" ? "100% 100%" : settings.wallpaperMode; desktopRef.current.style.backgroundPosition = "center"; desktopRef.current.style.backgroundRepeat = "no-repeat"; } else { if (!desktopRef.current) return; desktopRef.current.style.backgroundImage = `url("${settings.wallpaper}")`; desktopRef.current.style.backgroundSize = settings.wallpaperMode === "stretch" ? "100% 100%" : settings.wallpaperMode; desktopRef.current.style.backgroundPosition = "center"; desktopRef.current.style.backgroundRepeat = "no-repeat"; } }; const showWinPrev = (e: CustomEvent) => { const data = JSON.parse(e.detail); setWinPrev(data); if (!data.open) { setPreviewWin(null); setThumbnailLoading(new Set()); document.querySelectorAll(".window-element").forEach(el => { (el as HTMLElement).style.opacity = "1"; }); } }; const getPins = async () => { if (await dirExists("/system")) { setPinned(JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/dock.json", "utf8"))); } }; getPins(); window.addEventListener("tfsready", getWallpaper); window.addEventListener("open-net", menu); window.addEventListener("open-notif", nMenu); window.addEventListener("load", getWallpaper); window.addEventListener("updWallpaper", getWallpaper); window.addEventListener("updPins", getPins); // @ts-expect-error window.addEventListener("windows-prev", showWinPrev); return () => { window.removeEventListener("open-net", menu); window.removeEventListener("open-notif", nMenu); window.removeEventListener("load", getWallpaper); window.removeEventListener("updWallpaper", getWallpaper); window.removeEventListener("updPins", getPins); // @ts-expect-error window.removeEventListener("windows-prev", showWinPrev); window.removeEventListener("tfsready", getWallpaper); }; }, [showNotif, winPrev, pinned]); return (
) => { onContextMenu?.(e.nativeEvent); }} >
{ setPreviewWin(null); document.querySelectorAll(".window-element").forEach(el => { (el as HTMLElement).style.opacity = "1"; }); thumbnailCacheRef.current.clear(); setThumbnailFallbacks(new Set()); setThumbnailLoading(new Set()); }} > {winPrev?.windows && winPrev.windows.length > 0 && (
{winPrev.windows[0].map((win: WindowConfig) => { const winId = win.wid; if (!winId) return null; const shouldUseLogoFallback = windowOptimizationsEnabled || thumbnailFallbacks.has(winId); const isThumbnailLoading = !shouldUseLogoFallback && thumbnailLoading.has(winId); const iconSrc = win.icon ?? "/assets/img/null.svg"; return (
{ const canvasEl = e.currentTarget.querySelector("canvas") as HTMLCanvasElement | null; if (!shouldUseLogoFallback && canvasEl && canvasEl.dataset.rendered !== "1") { if (PREVIEW_DEBUG) console.debug("[win-preview] hover-triggered thumbnail retry", { winId }); renderWindowThumbnail(winId, canvasEl); } if (previewWin !== winId) { setPreviewWin(winId); document.querySelectorAll(".window-element").forEach(otherEl => { if (otherEl.id !== winId) { (otherEl as HTMLElement).style.opacity = "0"; } else { (otherEl as HTMLElement).style.opacity = "1"; if (!otherEl.classList.contains("shadow-window-shadow")) { otherEl.classList.add("shadow-window-shadow"); } } }); } }} onClick={() => { window.dispatchEvent(new CustomEvent("sel-win", { detail: winId })); window.dispatchEvent(new CustomEvent("currWID", { detail: winId })); setPreviewWin(null); document.querySelectorAll(".window-element").forEach(el => { (el as HTMLElement).style.opacity = "1"; }); setWinPrev(prev => ({ ...prev, open: false, windows: prev?.windows, location: prev?.location ?? "", })); }} >
Window icon {typeof win.title === "string" ? win.title : win.title?.text}
{shouldUseLogoFallback ? (
App logo
) : ( { if (canvasRef && canvasRef.dataset.rendered !== "1") { renderWindowThumbnail(winId, canvasRef); } }} /> )} {isThumbnailLoading && (
)}
); })}
)}
); }; export const createDesktop = (amount: number) => { for (let i = 0; i < amount; i++) { createElement(Desktop, { desktop: i }); } }; export default Desktop; ================================================ FILE: src/sys/gui/Dock.tsx ================================================ import { FC, ReactNode, useEffect, useRef, useState } from "react"; import { MagnifyingGlassIcon, ChevronRightIcon, PuzzlePieceIcon } from "@heroicons/react/24/solid"; import "./styles/dock.css"; import { dirExists, isURL, WindowConfig } from "../types"; import { useWindowStore, useSearchMenuStore } from "../Store"; import SearchMenu from "./Search"; export type TDockItem = { className?: string; title: string; icon: string | undefined; src: string; size?: number[] | any; children?: Array; isPinnable?: boolean; snapable?: boolean; pid?: string; wid?: string; proxy?: boolean; user?: string; onClick?: (e: MouseEvent) => void; onContextMenu?: (e: MouseEvent) => void; }; export type TStartItem = { title: string; icon: string | ReactNode | undefined; pid: string | undefined; onClick?: (e: MouseEvent) => void; inPins?: boolean; className?: string; src?: string; proxy?: boolean; size?: { width: number; height: number }; snapable?: boolean; }; interface IDockProps { showPins?: boolean; pinned: Array | null; } interface IUser { pfp: string | undefined; username: string | undefined; } const Dock: FC = ({ pinned }) => { const windowStore = useWindowStore(); const searchMenuStore = useSearchMenuStore(); const [isStartOpen, setStartOpen] = useState(false); const [user, setUser] = useState({ pfp: undefined, username: undefined, }); const startRef = useRef(null); const searchRef = useRef(null); const searchDockRef = useRef(null); const [searchHasText, setSearchHasText] = useState((searchRef.current?.value?.length ?? 0) >= 1); const [searchActive, setSearchActive] = useState(false); const placeholderRef = useRef(null); const openAppsRef = useRef(null); const startButtonRef = useRef(null); const systemAppsRef = useRef(null); const pinnedAppsRef = useRef(null); const searchMatchRef = useRef(null); const pinnedAppsDockRef = useRef(null); const openedAppsDockRef = useRef(null); const userOptsRef = useRef(null); const [searchMatch, setSearchMatch] = useState(false); const [systemApps, setSysApps] = useState>([]); const [pins, setPins] = useState>([]); useEffect(() => { const fetchData = async () => { if (await dirExists("/system")) { setSysApps(JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/start.json", "utf8")).system_apps); setPins(JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/start.json", "utf8")).pinned_apps); } }; fetchData(); if (isStartOpen === true) { setStartOpen(false); } window.addEventListener("updApps", fetchData); return () => window.removeEventListener("updApps", fetchData); }, []); useEffect(() => { const fetchUser = async () => { const pfp = await window.tb.user.pfp(); const username = await window.tb.user.username(); const user = { pfp: pfp, username: username, }; setUser(user); }; window.addEventListener("accUpd", fetchUser); window.addEventListener("tfsready", fetchUser); return () => { window.removeEventListener("accUpd", fetchUser); window.removeEventListener("tfsready", fetchUser); }; }, []); const filteredSysApps = systemApps.filter((item, index, self) => index === self.findIndex(t => t.title === item.title && t.icon === item.icon && t.src === item.src) && (!item.user || item.user === user.username)); // @ts-expect-error const filteredPins = pinned.filter((item, index, self) => index === self.findIndex(t => t.src === item.src && t.title === item.title && t.icon === item.icon)); var openStart = (focusSearch?: boolean | null, close?: boolean) => { if (close) { setStartOpen(false); setSearchHasText(false); setSearchMatch(false); if (searchRef.current) { searchRef.current.value = ""; } const systemApps = systemAppsRef.current; const pinnedApps = pinnedAppsRef.current; if (systemApps !== null && pinnedApps !== null) { const systemAppsChildren = systemApps.children; const pinnedAppsChildren = pinnedApps.children; for (let i = 0; i < systemAppsChildren.length; i++) { const child: Element = systemAppsChildren[i]; child.classList.remove("hidden"); child.classList.remove("-translate-x-2"); child.classList.remove("opacity-0"); } for (let i = 0; i < pinnedAppsChildren.length; i++) { const child: Element = pinnedAppsChildren[i]; child.classList.remove("hidden"); child.classList.remove("-translate-x-2"); child.classList.remove("opacity-0"); } } return; } const clickElsewhere = (e: MouseEvent) => { if (e.target !== startRef.current && e.target !== startButtonRef.current && !startRef.current?.contains(e.target as Node) && !searchDockRef.current?.contains(e.target as Node)) { setStartOpen(false); setSearchHasText(false); setSearchMatch(false); if (searchRef.current) { searchRef.current.value = ""; } const systemApps = systemAppsRef.current; const pinnedApps = pinnedAppsRef.current; if (systemApps !== null && pinnedApps !== null) { const systemAppsChildren = systemApps.children; const pinnedAppsChildren = pinnedApps.children; for (let i = 0; i < systemAppsChildren.length; i++) { const child: Element = systemAppsChildren[i]; child.classList.remove("hidden"); child.classList.remove("-translate-x-2"); child.classList.remove("opacity-0"); } for (let i = 0; i < pinnedAppsChildren.length; i++) { const child: Element = pinnedAppsChildren[i]; child.classList.remove("hidden"); child.classList.remove("-translate-x-2"); child.classList.remove("opacity-0"); } } window.removeEventListener("mousedown", clickElsewhere); } }; window.addEventListener("mousedown", clickElsewhere); setStartOpen(prev => !prev); openSearchMenu(true); if (focusSearch) { setTimeout(() => { searchRef.current?.focus(); }, 150); } setTimeout(() => { if (searchRef.current) { searchRef.current.value = ""; } setSearchHasText(false); setSearchMatch(false); const systemApps = systemAppsRef.current; const pinnedApps = pinnedAppsRef.current; if (systemApps !== null && pinnedApps !== null) { const systemAppsChildren = systemApps.children; const pinnedAppsChildren = pinnedApps.children; for (let i = 0; i < systemAppsChildren.length; i++) { const child: Element = systemAppsChildren[i]; child.classList.remove("hidden"); child.classList.remove("-translate-x-2"); child.classList.remove("opacity-0"); } for (let i = 0; i < pinnedAppsChildren.length; i++) { const child: Element = pinnedAppsChildren[i]; child.classList.remove("hidden"); child.classList.remove("-translate-x-2"); child.classList.remove("opacity-0"); } } }, 200); }; var openSearchMenu = (close?: boolean) => { const clearout = () => { searchMenuStore.setOpen(false); }; if (close) { clearout(); return; } const clickElsewhere = (e: MouseEvent) => { if (e.target !== searchDockRef.current && e.target !== startButtonRef.current && !searchDockRef.current?.contains(e.target as Node) && !searchMenuStore.searchMenuRef.current?.contains(e.target as Node)) { clearout(); window.removeEventListener("mousedown", clickElsewhere); } }; window.addEventListener("mousedown", clickElsewhere); searchMenuStore.setOpen(!searchMenuStore.open); openStart(null, true); }; return (
Search { setSearchActive(true); }} onBlur={() => { setSearchActive(false); }} onInput={(e: React.FormEvent) => { if (e.currentTarget.value.length > 0) { setSearchHasText(true); } else { setSearchHasText(false); } const query = e.currentTarget.value.toLowerCase(); const systemApps = systemAppsRef.current; const pinnedApps = pinnedAppsRef.current; console.log(systemApps, pinnedApps); if (systemApps !== null) { const systemAppsChildren = systemApps.children; let systemAppsMatch = 0; for (let i = 0; i < systemAppsChildren.length; i++) { const child: Element = systemAppsChildren[i]; if (child.textContent && child.textContent.toLowerCase().includes(query)) { child.classList.remove("hidden"); setTimeout(() => { child.classList.remove("opacity-0"); child.classList.remove("-translate-x-2"); }, 150); systemAppsMatch++; } else { child.classList.add("-translate-x-2"); child.classList.add("opacity-0"); setTimeout(() => { child.classList.add("hidden"); }, 150); } } if (pinnedApps !== null) { const pinnedAppsChildren = pinnedApps.children; let pinnedAppsMatch = 0; for (let i = 0; i < pinnedAppsChildren.length; i++) { const child: Element = pinnedAppsChildren[i]; if (child.textContent && child.textContent.toLowerCase().includes(query)) { child.classList.remove("hidden"); setTimeout(() => { child.classList.remove("opacity-0"); child.classList.remove("-translate-x-2"); }, 150); pinnedAppsMatch++; } else { child.classList.add("-translate-x-2"); child.classList.add("opacity-0"); setTimeout(() => { child.classList.add("hidden"); }, 150); } } if (systemAppsMatch === 0 && pinnedAppsMatch === 0) { setSearchMatch(true); } else { setSearchMatch(false); } } else { if (systemAppsMatch === 0) { setSearchMatch(true); } else { setSearchMatch(false); } } } }} />
0 ? "max-h-47 grid-cols-2" : "w-full grid-cols-3"} gap-1 overflow-y-auto `} > {filteredSysApps.map((item, index) => ( { item.onClick?.(new MouseEvent("click")); windowStore.addWindow({ src: item.src, size: item.size, icon: typeof item.icon === "string" ? item.icon : undefined, title: item.title, proxy: item.proxy, snapable: item.snapable, }); setStartOpen(false); }} /> ))}
{pins.length > 0 ? (
Pinned Apps
{pins.length > 0 ? pins.map((item, index) => ( { if (e.button === 0) item.onClick?.(new MouseEvent("click")); windowStore.addWindow({ src: item.src, icon: typeof item.icon === "string" ? item.icon : undefined, size: item.size, title: item.title, proxy: item.proxy, snapable: item.snapable, }); setStartOpen(false); }} /> )) : null}
) : null}
No results found
{ window.tb.contextmenu.create({ x: userOptsRef.current?.getBoundingClientRect().x ?? 0, y: userOptsRef.current ? userOptsRef.current.getBoundingClientRect().y - 75 : 0, options: [ { text: "Manage Account", click: () => { window.tb.window.create({ title: "Settings", src: "/fs/apps/system/settings.tapp/index.html", icon: "/fs/apps/system/settings.tapp/icon.svg", single: true, message: JSON.stringify({ page: "privacy" }), }); }, }, { text: "Sign out", click: () => { sessionStorage.setItem("logged-in", "false"); window.location.reload(); }, }, ], }); }} > {user.username} {user.username}
{ window.tb.window.create({ title: "Settings", src: "/fs/apps/system/settings.tapp/index.html", icon: "/fs/apps/system/settings.tapp/icon.svg", }); setStartOpen(false); }} > Settings { window.tb.window.create({ title: "About", src: "/fs/apps/system/about.tapp/index.html", icon: "/fs/apps/system/about.tapp/icon.svg", }); setStartOpen(false); }} > About { sessionStorage.setItem("ldir", `/home/${user.username}/Documents`); window.tb.window.create({ title: "Files", icon: "/fs/apps/system/files.tapp/icon.svg", src: "/fs/apps/system/files.tapp/index.html", size: { width: 600, height: 500, }, }); setStartOpen(false); }} > Documents
openStart(false)}>
openSearchMenu()}> Search
0 && windowStore.windows.length > 0) || pinned.length > 0 || windowStore.windows.length > 0 ? "translate-x-0 opacity-100" : "translate-y-3 opacity-0 pointer-events-none") : null} `} style={{ backgroundImage: "url(/assets/img/grain.png)" }} > {pinned != null && pinned.length > 0 ? (
{filteredPins.map((item, index) => ( { e.preventDefault(); }} /> ))}
) : null} {(pinned?.length ?? 0) > 0 && windowStore.windows.length > 0 ? : null}
0 ? "flex" : "hidden"} `} > {windowStore.windows .filter((item, index, self) => index === self.findIndex(t => (typeof t.title === "string" ? t.title : t.title?.text) === (typeof item.title === "string" ? item.title : item.title?.text))) .map((item, index) => ( ))}
); }; const DockItem: FC = ({ className, icon, title, src, onClick, onContextMenu, size, snapable, pid, wid, proxy }) => { const windowStore = useWindowStore(); const dockItemRef = useRef(null); const [currWID, setcurrWID] = useState(wid); const [winfocused, setwinfocused] = useState(windowStore.windows.find((w: any) => w.wid === currWID)?.focused); const mm = (e: MouseEvent) => { const target = e.target; if (target instanceof Element && target.closest("[data-win-preview='true']")) { return; } const withinRadius = (e: MouseEvent) => { if (!dockItemRef.current) return false; const rect = dockItemRef.current.getBoundingClientRect(); const xDistance = Math.abs(e.clientX - (rect.left + rect.width / 2)); const yDistance = Math.abs(e.clientY - (rect.top + rect.height / 2)); const distance = Math.sqrt(xDistance * xDistance + yDistance * yDistance); return distance > 350; }; if (withinRadius(e)) { window.removeEventListener("mousemove", mm); window.dispatchEvent(new CustomEvent("windows-prev", { detail: JSON.stringify({ open: false, location: null }) })); } }; useEffect(() => { const setWID = (e: CustomEvent) => { setcurrWID(e.detail); setwinfocused(windowStore.windows.find((w: any) => w.wid === e.detail)?.focused); }; const updsel = (e: CustomEvent) => { if (e.detail !== title) { setwinfocused(false); } else { setwinfocused(true); } }; window.addEventListener("selwin-upd", updsel as EventListener); window.addEventListener("currWID", setWID as EventListener); return () => { window.removeEventListener("currWID", setWID as EventListener); window.removeEventListener("mousemove", mm); }; }, [currWID, winfocused]); return ( { setTimeout(() => { const rect = dockItemRef.current?.getBoundingClientRect(); const x = rect ? rect.x : 0; window.addEventListener("mousemove", mm); window.dispatchEvent( new CustomEvent("windows-prev", { detail: JSON.stringify({ open: true, windows: [ windowStore.matchedWindows.find((group: any[]) => group.some((w: WindowConfig) => { if (typeof w.title === "string") { return w.title === title; } else if (w.title && w.title.text) { return w.title.text === title; } return false; }), ), ], location: x, }), }), ); }, 950); }} onClick={() => { onClick?.(new MouseEvent("click")); window.dispatchEvent(new CustomEvent("sel-win", { detail: currWID })); }} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); onContextMenu?.(new MouseEvent("contextmenu")); const { clientX, clientY } = e; window.tb.contextmenu.create({ x: clientX - 10, y: clientY - 150, options: [ { text: "New Window", click: () => { windowStore.addWindow({ src: src, icon: typeof icon === "string" ? icon : undefined, size: size, title: title, proxy: proxy, snapable: snapable, }); }, }, { text: "Pin to Dock", click: () => { window.tb.desktop.dock.pin({ // @ts-expect-error ignore this title: typeof title === "string" ? title : title?.text, icon: typeof icon === "string" ? icon : undefined, isPinnable: true, src: src, proxy: proxy, snapable: snapable, size: { width: size?.width ?? 600, height: size?.height ?? 400, }, }); }, }, { text: "Close", click: () => { window.tb.process.kill(pid); }, }, ], }); }} > {typeof icon === "undefined" || icon === null ? ( {title} ) : typeof icon === "string" ? ( {title} ) : (
{icon}
)}
); }; const PinnedDockItem: FC = ({ className, icon, title, src, onClick, onContextMenu, size, snapable, proxy }) => { const windowStore = useWindowStore(); return ( { return; }} title={title} onClick={() => { onClick?.(new MouseEvent("click")); windowStore.addWindow({ src: src, icon: typeof icon === "string" ? icon : undefined, size: size, title: title, proxy: proxy, snapable: snapable, }); }} onContextMenuCapture={(e: React.MouseEvent) => { e.preventDefault(); onContextMenu?.(new MouseEvent("contextmenu")); const { clientX, clientY } = e; window.tb.contextmenu.create({ x: clientX - 10, y: clientY - 100, options: [ { text: "New Window", click: () => { windowStore.addWindow({ src: src, icon: typeof icon === "string" ? icon : undefined, size: size, title: title, proxy: proxy, snapable: snapable, }); }, }, { text: "Unpin from Dock", click: () => { window.tb.desktop.dock.unpin(title); }, }, ], }); }} > {typeof icon === "undefined" || icon === null ? ( {title} ) : typeof icon === "string" ? ( {title} ) : (
{icon}
)}
); }; export const StartItem: FC = ({ icon, title, onClick, inPins, className, src, proxy, size, snapable }) => { // @ts-expect-error const chars = typeof title === "string" ? title.split("") : title?.text.split(""); const [resolvedIcon, setResolvedIcon] = useState(false); let sysapps = [{ title: { text: "Terminal" } }, { title: "Files" }, { title: "Settings" }, { title: { text: "App Store" } }, { title: "Browser" }, { title: "Calculator" }, { title: "Feedback" }, { title: "About" }, { title: "Text Editor" }, { title: "Task Manager" }, { title: "Anura File Manager" }]; // @ts-expect-error const isSystemApp = sysapps.map(app => (typeof app.title === "string" ? app.title : app.title.text)).includes(typeof title === "string" ? title : title?.text); useEffect(() => { const checkIcon = async (icon: string | ReactNode | null) => { if (typeof icon === "undefined" || icon === null) { setResolvedIcon(false); } else if (typeof icon === "string") { setResolvedIcon(true); if (icon.startsWith("/")) { let origin: string; let path: string; let url: string; if (icon.match(isURL)) { url = icon; } else { origin = window.location.origin; path = icon.startsWith("/") ? icon : `/${icon}`; url = `${origin}${path}`; } try { const response = await fetch(url); const data = await response.text(); if (!data.startsWith(" onClick?.(new MouseEvent("click"))} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); const { clientX, clientY } = e; console.log( src, src ?.replace("/fs", "") .replace(/\/[^/]+\.html$/, "/") .replace(/\/\.\//, "/"), ); window.tb.contextmenu.create({ x: clientX - 10, y: clientY - 150, options: [ { text: "Open", click: () => { onClick?.(new MouseEvent("click")); }, }, { text: "Pin to Dock", click: async () => { window.tb.desktop.dock.pin({ // @ts-expect-error ignore this title: typeof title === "string" ? title : title?.text, icon: typeof icon === "string" ? icon : undefined, isPinnable: true, src: src, proxy: proxy, snapable: snapable, size: { width: size?.width ?? 600, height: size?.height ?? 400, }, }); }, }, { text: "Unpin from Start", click: async () => { const apps: any = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/start.json", "utf8")); apps.pinned_apps = apps.pinned_apps.filter((app: any) => !(app.title === title && app.icon === icon) && !(app.name === title && app.icon === icon)); await window.tb.fs.promises.writeFile("/system/var/terbium/start.json", JSON.stringify(apps, null, 2)); window.dispatchEvent(new Event("updApps")); }, }, ...(isSystemApp ? [] : [ { text: "Uninstall", click: async () => { let appPath = ""; const appName = typeof title === "string" ? title : title && typeof title === "object" && "text" in title ? (title as { text: string }).text : ""; if (src?.startsWith("/fs/apps/user/")) { if (src.includes(".tapp")) { appPath = `/apps/user/${await window.tb.user.username()}/${appName.toLowerCase()}.tapp`; } else { appPath = `/apps/user/${await window.tb.user.username()}/${appName.toLowerCase()}`; } } else if (src?.startsWith("/fs/apps/system/")) { appPath = `/apps/system/${appName.toLowerCase()}.tapp`; } else if (src?.includes("/apps/anura/")) { appPath = `/apps/anura/${appName.toLowerCase()}`; } else { appPath = `/apps/user/${await window.tb.user.username()}/${appName}`; } let installedApps = JSON.parse(await window.tb.fs.promises.readFile(`/apps/installed.json`, "utf8")); installedApps = installedApps.filter((app: any) => app.title === title); await window.tb.fs.promises.writeFile(`/apps/installed.json`, JSON.stringify(installedApps)); await window.tb.sh.promises.rm(appPath, { recursive: true }); await window.tb.launcher.removeApp(chars); window.dispatchEvent(new Event("updApps")); }, }, ]), ], }); }} >
{ // @ts-expect-error resolvedIcon === true ? :
{}
} {chars.length > 10 ? chars.slice(0, 10).join("") + "..." : chars.join("")}
{ const apps: any = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/start.json", "utf8")); apps.pinned_apps = apps.pinned_apps.filter((app: any) => !(app.title === title && app.icon === icon)); await window.tb.fs.promises.writeFile("/system/var/terbium/start.json", JSON.stringify(apps, null, 2)); window.dispatchEvent(new Event("updApps")); }} className="size-7 bg-[#ffffff18] backdrop-blur-[20px] shadow-tb-border-shadow p-1.5 rounded-full text-white stroke-current stroke-[3px] opacity-0 group-hover:opacity-100 duration-150 ease-in" />
) : (
{ if (e.button === 0) onClick?.(new MouseEvent("click")); }} onContextMenu={async (e: React.MouseEvent) => { e.preventDefault(); const { clientX, clientY } = e; const appsStart: any = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/start.json", "utf8")); const appsDock: any = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/dock.json", "utf8")); const isPinnedStart = appsStart.pinned_apps.some((app: any) => app.title === title && app.icon === icon); const isPinnedDock = appsDock.some((app: any) => app.src === src && app.icon === icon); window.tb.contextmenu.create({ x: clientX - 10, y: clientY - 150, options: [ { text: "Open", click: () => { onClick?.(new MouseEvent("click")); }, }, isPinnedDock ? { text: "Unpin from Dock", click: () => { window.tb.desktop.dock.unpin(title); }, } : { text: "Pin to Dock", click: async () => { window.tb.desktop.dock.pin({ // @ts-expect-error ignore this title: typeof title === "string" ? title : title?.text, icon: typeof icon === "string" ? icon : undefined, isPinnable: true, src: src, proxy: proxy, snapable: snapable, size: { width: size?.width ?? 600, height: size?.height ?? 400, }, }); }, }, isPinnedStart ? { text: "Unpin from Start", click: async () => { const apps: any = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/start.json", "utf8")); apps.pinned_apps = apps.pinned_apps.filter((app: any) => !(app.title === title && app.icon === icon)); await window.tb.fs.promises.writeFile("/system/var/terbium/start.json", JSON.stringify(apps, null, 2)); window.dispatchEvent(new Event("updApps")); }, } : { text: "Pin to Start", click: async () => { const path = src ?.replace("/fs", "") .replace(/\/[^/]+\.html$/, "/") .replace(/\/\.\//, "/"); const appConfig = JSON.parse(await window.tb.fs.promises.readFile(path + "index.json", "utf8")); if (appsStart.pinned_apps.some((app: any) => app.title === appConfig.config.title && app.icon === appConfig.config.icon)) { return; } appsStart.pinned_apps.push({ name: typeof appConfig.config.title === "string" ? appConfig.config.title : appConfig.config.title.text, ...appConfig.config, }); await window.tb.fs.promises.writeFile("/system/var/terbium/start.json", JSON.stringify(appsStart, null, 2)); window.dispatchEvent(new Event("updApps")); }, }, ...(isSystemApp ? [] : [ { text: "Uninstall", click: async () => { let appPath = ""; const appName = typeof title === "string" ? title : title && typeof title === "object" && "text" in title ? (title as { text: string }).text : ""; if (src?.startsWith("/fs/apps/user/")) { if (src.includes(".tapp")) { appPath = `/apps/user/${await window.tb.user.username()}/${appName.toLowerCase()}.tapp`; } else { appPath = `/apps/user/${await window.tb.user.username()}/${appName.toLowerCase()}`; } } else if (src?.startsWith("/fs/apps/system/")) { appPath = `/apps/system/${appName.toLowerCase()}.tapp`; } else if (src?.includes("/apps/anura/")) { appPath = `/apps/anura/${appName.toLowerCase()}`; } else { appPath = `/apps/user/${await window.tb.user.username()}/${appName}`; } let installedApps = JSON.parse(await window.tb.fs.promises.readFile(`/apps/installed.json`, "utf8")); installedApps = installedApps.filter((app: any) => app.title === title); await window.tb.fs.promises.writeFile(`/apps/installed.json`, JSON.stringify(installedApps)); await window.tb.sh.promises.rm(appPath, { recursive: true }); await window.tb.launcher.removeApp(chars); window.dispatchEvent(new Event("updApps")); }, }, ]), ], }); }} >
{ // @ts-expect-error resolvedIcon === true ? :
{}
} {chars.length > 10 ? chars.slice(0, 10).join("") + "..." : chars.join("")}
{ const apps: any = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/start.json", "utf8")); apps.pinned_apps.push({ title: title, icon: icon, src: src, }); await window.tb.fs.promises.writeFile("/system/var/terbium/start.json", JSON.stringify(apps, null, 2)); window.dispatchEvent(new Event("updApps")); }} className="size-7 bg-[#ffffff18] backdrop-blur-[20px] shadow-tb-border-shadow p-1.5 rounded-full text-white stroke-current stroke-[3px] opacity-0 group-hover:opacity-100 duration-150 ease-in" />
); }; export default Dock; ================================================ FILE: src/sys/gui/FPSCounter.tsx ================================================ import { useEffect, useState, useRef } from "react"; export const FPSCounter = () => { const [fps, setFps] = useState(0); const [showFPS, setShowFPS] = useState(false); const frameCountRef = useRef(0); const lastTimeRef = useRef(performance.now()); const rafRef = useRef(null); useEffect(() => { const loadSettings = async () => { try { const settings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8")); setShowFPS(settings.showFPS ?? false); } catch (err) { console.error("Failed to load FPS counter setting:", err); } }; loadSettings(); const handleSettingsChange = (e: CustomEvent) => { if (e.detail?.showFPS !== undefined) { setShowFPS(e.detail.showFPS); } }; window.addEventListener("settings-changed", handleSettingsChange as EventListener); return () => window.removeEventListener("settings-changed", handleSettingsChange as EventListener); }, []); useEffect(() => { if (!showFPS) { if (rafRef.current) { cancelAnimationFrame(rafRef.current); } return; } const updateFPS = () => { frameCountRef.current++; const now = performance.now(); const delta = now - lastTimeRef.current; if (delta >= 1000) { const currentFPS = Math.round((frameCountRef.current * 1000) / delta); setFps(currentFPS); frameCountRef.current = 0; lastTimeRef.current = now; } rafRef.current = requestAnimationFrame(updateFPS); }; rafRef.current = requestAnimationFrame(updateFPS); return () => { if (rafRef.current) { cancelAnimationFrame(rafRef.current); } }; }, [showFPS]); if (!showFPS) return null; return (
{fps} FPS
); }; ================================================ FILE: src/sys/gui/NotificationCenter.tsx ================================================ import { useRef, useState, useEffect } from "react"; import "./styles/notification.css"; interface Notification { message: string; application: string; icon: string; onOk?: { code: string }; time?: string; } export let totalNotifs: number; export default function NotificationCenter() { const [notificationCount, setNotificationCount] = useState(() => { const savedCount = JSON.parse(sessionStorage.getItem("notifications") || "[]").length; return savedCount ? parseInt(savedCount) : 0; }); const iconRef = useRef(null); const updateIcon = () => { if (iconRef.current) { iconRef.current.src = `/assets/img/notif_${notificationCount <= 9 ? notificationCount : "plus"}.svg`; } }; useEffect(() => { const updateCount = (event: CustomEvent) => { setNotificationCount(event.detail.count); }; window.addEventListener("notification-count", updateCount as EventListener); updateIcon(); return () => { window.removeEventListener("notification-count", updateCount as EventListener); }; }, [notificationCount]); useEffect(() => { totalNotifs = notificationCount; }); return ( notifimg 9 ? "plus" : Math.max(0, Math.min(notificationCount, 9))}.svg`} className="tooltip_item w-6 h-6 cursor-pointer duration-150 select-none" onMouseUp={() => { iconRef.current?.classList.remove("scale-90"); window.dispatchEvent(new Event("open-notif")); }} onMouseLeave={() => { iconRef.current?.classList.remove("scale-90"); }} onMouseOver={() => { iconRef.current?.classList.add("scale-90"); }} onMouseDown={() => { iconRef.current?.classList.add("scale-90"); }} > ); } interface INotificationProps { isOpen: boolean; } const NotificationMenu = ({ isOpen }: INotificationProps) => { const [notifications, setNotifications] = useState(() => { const savedNotifs = sessionStorage.getItem("notifications"); return savedNotifs ? JSON.parse(savedNotifs) : []; }); const updateNotifications = () => { const notifs = JSON.parse(sessionStorage.getItem("notifications") || "[]"); setNotifications(notifs); }; window.addEventListener("notification-update", updateNotifications as EventListener); useEffect(() => { const leave = (e: MouseEvent) => { const withinRadius = (e: MouseEvent) => { if (!notificationCenterRef.current) return false; const rect = notificationCenterRef.current.getBoundingClientRect(); const xBound = e.clientX >= rect.left - 5 && e.clientX <= rect.right + 5; const yBound = e.clientY >= rect.top - 5 && e.clientY <= rect.bottom + 5; return xBound && yBound; }; if (e.button === 0) { if (!notificationCenterRef.current?.contains(e.target as Node) && !withinRadius(e)) { setTimeout(() => { window.dispatchEvent(new Event("open-notif")); }, 150); } } }; if (!isOpen) { document.removeEventListener("mousedown", leave); } else { document.addEventListener("mousedown", leave); } return () => document.removeEventListener("mousedown", leave); }, [isOpen]); const notificationCenterRef = useRef(null); const dismiss = (index: number) => { const notifData = notifications.filter((_, i) => i !== index); setNotifications(notifData); if (totalNotifs > 0) window.dispatchEvent( new CustomEvent("notification-count", { detail: { count: (totalNotifs -= 1) }, }), ); sessionStorage.setItem("notifications", JSON.stringify(notifData)); }; return (

Notifications

{notifications.length > 0 ? (
{notifications.map((notification, index) => { var time = "Now"; const currentTime = new Date().getTime(); const timeDiff = notification.time ? currentTime - new Date(notification.time).getTime() : 0; if (timeDiff < 60000) { time = "Just now"; } else if (timeDiff < 3600000) { time = `${Math.floor(timeDiff / 60000)}min ago`; } else if (timeDiff < 86400000) { time = `${Math.floor(timeDiff / 3600000)}h ago`; } else if (timeDiff < 604800000) { time = `${Math.floor(timeDiff / 86400000)}d ago`; } else { time = `${Math.floor(timeDiff / 604800000)}w ago`; } return (
Icon
{notification.application}
{notification.time ?
{time}
: null}
{notification.message}
); })}
) : (
No Notifications yet.
)}
); }; export { NotificationMenu }; ================================================ FILE: src/sys/gui/Power.tsx ================================================ import { useState, useRef, useEffect } from "react"; import { PowerIcon, MoonIcon, LockClosedIcon, ArrowPathIcon } from "@heroicons/react/24/solid"; export default function Power() { const [showMenu, setShowMenu] = useState(false); const [showHardRestart, setShowHardRestart] = useState(false); const menu = useRef(null); const iconRef = useRef(null); useEffect(() => { const leave = (e: MouseEvent) => { if (showMenu) { if (e.target instanceof HTMLElement) { if (e.target !== menu.current && !menu.current?.contains(e.target)) { setShowMenu(false); setShowHardRestart(false); } } } }; document.addEventListener("mousedown", leave); return () => document.removeEventListener("mousedown", leave); }); useEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === "Shift") { setShowHardRestart(true); } }; const up = (e: KeyboardEvent) => { if (e.key === "Shift") { setShowHardRestart(false); } }; document.addEventListener("keydown", down); document.addEventListener("keyup", up); return () => { document.removeEventListener("keydown", down); document.removeEventListener("keyup", up); }; }, []); return ( <> { iconRef.current?.classList.remove("scale-90"); setShowMenu(prev => !prev); }} onMouseLeave={() => { iconRef.current?.classList.remove("scale-90"); }} onMouseOver={() => { iconRef.current?.classList.add("scale-90"); }} onClick={() => { iconRef.current?.classList.add("scale-90"); }} />
{showHardRestart && ( )}
); } ================================================ FILE: src/sys/gui/Search.tsx ================================================ import { FC, useEffect, useRef, useState } from "react"; import { MagnifyingGlassIcon } from "@heroicons/react/24/solid"; import { useSearchMenuStore } from "../Store"; import { StartItem } from "./Dock"; import { searchApps, searchFiles } from "../apis/SysSearch"; import { createWindow } from "./WindowArea"; interface SearchProps { className: string; searchRef?: React.RefObject; } const SearchMenu: FC = ({ className }) => { const searchMenuStore = useSearchMenuStore(); const [searchMatch, setSearchMatch] = useState(false); const [resultOpen, setResultOpen] = useState(false); const [searchHasText, setSearchHasText] = useState(false); const [searchActive, setSearchActive] = useState(false); const [recentApps, setRecentApps] = useState([]); const searchMenuRef = useRef(null); const searchRefRef = useRef(null); const placeholderRef = useRef(null); const containerRef = useRef(null); const resultRef = useRef(null); const recentAppsRef = useRef(null); const [results, setResults] = useState([]); const [noResutls, setNoResults] = useState(false); useEffect(() => { const getRecentApps = async () => { const recentApps = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/recent.json")); const recentAppsList = recentApps.map((app: any) => { return app; }); setRecentApps(recentAppsList); searchRefRef.current!.focus(); }; searchMenuStore.open ? getRecentApps() : null; if (!searchMenuStore.open) { searchRefRef.current!.value = ""; setNoResults(false); setSearchMatch(false); setSearchActive(false); setSearchHasText(false); setResultOpen(false); setTimeout(() => { recentAppsRef.current!.classList.add("col-span-2"); containerRef.current!.classList.remove("grid-cols-2"); containerRef.current!.classList.add("grid-cols-1"); }, 200); setTimeout(() => { resultRef.current!.classList.add("absolute"); }, 300); setResults([]); } getRecentApps(); }, [searchMenuStore]); useEffect(() => { searchMenuStore.searchRef = { current: searchRefRef.current }; searchMenuStore.searchMenuRef = { current: searchMenuRef.current }; }); return (
Search for apps and files setSearchActive(true)} onBlur={() => setSearchActive(false)} onChange={async e => { const value = (e.target as HTMLInputElement).value; if (value.length > 0) { setSearchActive(true); resultRef.current!.classList.remove("absolute"); recentAppsRef.current!.classList.remove("col-span-2"); setTimeout(() => { containerRef.current!.classList.remove("grid-cols-1"); containerRef.current!.classList.add("grid-cols-2"); }, 200); setTimeout(() => { setResultOpen(true); }, 200); setSearchHasText(true); const appres = await searchApps(value); const filesres = await searchFiles(value); if (appres && Array.isArray(appres) && appres.length > 0) { setSearchMatch(true); const app = appres[0]; let iconHtml = ""; if (typeof app.icon === "string") { if (app.icon.trim().startsWith("`; } } const appName = typeof app.name === "string" ? app.name : app.name && typeof app.name.text === "string" ? app.name.text : ""; setResults([ [ { icon: iconHtml, name: appName.charAt(0).toUpperCase() + appName.slice(1), dir: app.dir || "Unknown Path", config: app.cfg, click: () => { createWindow(app.cfg); searchMenuStore.open = false; searchRefRef.current!.value = ""; setSearchMatch(false); setSearchActive(false); setSearchHasText(false); setResultOpen(false); setTimeout(() => { recentAppsRef.current!.classList.add("col-span-2"); containerRef.current!.classList.remove("grid-cols-2"); containerRef.current!.classList.add("grid-cols-1"); }, 200); setTimeout(() => { resultRef.current!.classList.add("absolute"); }, 300); setResults([]); }, }, ], [], ]); setNoResults(false); setSearchActive(false); } else if (filesres && Array.isArray(filesres) && filesres.length > 0) { setSearchMatch(true); window.tb.fs.promises.readFile("/system/etc/terbium/file-icons.json", "utf8").then(async (data: string) => { const fileIconsData = JSON.parse(data); const getIcon = (ext: string) => { let iconName = fileIconsData["ext-to-name"][ext]; let iconPath = fileIconsData["name-to-path"][iconName]; if (iconPath) { return iconPath; } else { return fileIconsData["name-to-path"]["Unknown"]; } }; const fileItems = await Promise.all( filesres.map(async (f: any) => { const iconSvg = await window.tb.fs.promises.readFile(getIcon(f.ext), "utf8"); function rewriteSvgSize(svg: string) { return svg.replace(/]*)>/, (_, attrs) => { let newAttrs = attrs.replace(/\swidth=['"][^'"]*['"]/, "").replace(/\sheight=['"][^'"]*['"]/, ""); return ``; }); } const newSvg = rewriteSvgSize(iconSvg); return { icon: newSvg, name: f.name.charAt(0).toUpperCase() + f.name.slice(1) || value.charAt(0).toUpperCase() + value.slice(1), path: f.path || "", ext: f.ext, dir: f.dir, onClick: async () => { let handlers = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8"))["fileAssociatedApps"]; handlers = Object.entries(handlers).filter(([type, app]) => { return !((type === "text" && app === "text-editor") || (type === "image" && app === "media-viewer") || (type === "video" && app === "media-viewer") || (type === "audio" && app === "media-viewer")); }); const dat = JSON.parse(await window.tb.fs.promises.readFile("/apps/system/files.tapp/extensions.json", "utf8")); let hands: { text: string; value: string }[] = []; for (const [type, app] of handlers) { hands.push({ text: app, value: type }); } await window.tb.dialog.Select({ title: `Select a application to open: ${f.name}`, options: [{ text: "Text Editor", value: "text" }, { text: "Media Viewer", value: "media" }, { text: "Webview", value: "webview" }, ...hands, { text: "Other", value: "other" }], onOk: async (val: string) => { switch (val) { case "text": parent.window.tb.file.handler.openFile(f.path, "text"); break; case "media": const ext = f.name.split(".").pop(); if (dat["image"].includes(ext)) { parent.window.tb.file.handler.openFile(f.path, "image"); } else if (dat["video"].includes(ext)) { parent.window.tb.file.handler.openFile(f.path, "video"); } else if (dat["audio"].includes(ext)) { parent.window.tb.file.handler.openFile(f.path, "audio"); } break; case "webview": parent.window.tb.file.handler.openFile(f.path, "webpage"); break; case "other": parent.window.tb.dialog.DirectoryBrowser({ title: "Select a application", filter: ".tapp", onOk: async (val: string) => { const app = JSON.parse(await window.tb.fs.promises.readFile(`${val}/.tbconfig`, "utf8")); window.parent.tb.window.create({ ...app.wmArgs, message: { type: "process", path: f.dir }, }); }, }); break; default: if (hands.length === 0) { parent.window.tb.file.handler.openFile(f.path, "text"); } else { parent.window.tb.file.handler.openFile(f.path, val); } break; } searchMenuStore.open = false; searchRefRef.current!.value = ""; setNoResults(false); setSearchMatch(false); setSearchActive(false); setSearchHasText(false); setResultOpen(false); setTimeout(() => { recentAppsRef.current!.classList.add("col-span-2"); containerRef.current!.classList.remove("grid-cols-2"); containerRef.current!.classList.add("grid-cols-1"); }, 200); setTimeout(() => { resultRef.current!.classList.add("absolute"); }, 300); setResults([]); }, }); }, }; }), ); setNoResults(false); setResults([[], fileItems]); setSearchActive(false); }); } else if (appres === false && filesres === false) { setSearchMatch(false); setNoResults(true); } } else { setSearchMatch(false); setSearchHasText(false); setResults([]); setResultOpen(false); setNoResults(false); setTimeout(() => { recentAppsRef.current!.classList.add("col-span-2"); containerRef.current!.classList.remove("grid-cols-2"); containerRef.current!.classList.add("grid-cols-1"); }, 200); setTimeout(() => { resultRef.current!.classList.add("absolute"); }, 300); } }} />
0 ? "" : "items-center justify-center"} ` } > {recentApps.length <= 0 && (

No recent apps

)}
0 ? "duration-150" : "opacity-0 pointer-events-none translate-4 duration-200"} ` } >

Recent apps

0 ? "grid-cols-1" : "grid-cols-2"} ` } > {recentApps.length > 0 ? recentApps .sort((a: any, b: any) => { const valueDiff = (b.value ?? 0) - (a.value ?? 0); if (valueDiff !== 0) return valueDiff; return (b.weight ?? 0) - (a.weight ?? 0); }) .slice(0, 8) .map((app: any, i: number) => ( { createWindow({ src: app.src, size: app.size, icon: typeof app.icon === "string" ? app.icon : undefined, title: app.title, proxy: app.proxy, snapable: app.snapable, }); searchMenuStore.open = false; }} /> )) : null}

Search results

{noResutls ? (
No apps or files relevant to searches
) : results.length > 0 && searchActive ? (
Searching...
) : (
{Array.isArray(results[0]) && results[0].length > 0 ? results[0].map((app: any, i: number) => (
{ app.click(); }} >
{typeof app.icon === "string" ? app.icon.trim().startsWith(" : {app.name} : app.icon}

{app.name}

{app.dir || "Unknown Path"}

)) : Array.isArray(results[1]) && results[1].length > 0 ? results[1].map((f: any, i: number) => (
{ f.onClick(); }} >
{typeof f.icon === "string" ? f.icon.trim().startsWith(" : {f.name} : f.icon}

{f.name}

{f.path || ""}

)) : null}
)}
{recentApps.length === 0 || (searchMatch === false && (
No recent apps or relevant searches
))}
); }; export default SearchMenu; ================================================ FILE: src/sys/gui/Shell.tsx ================================================ import Mediaisland from "../apis/Mediaisland"; import Battery from "./Battery"; import "./styles/shell.css"; import Wifi from "./Wifi"; import getTime from "../apis/Time"; import { useEffect, useState } from "react"; import Weather from "./Weather"; import { FPSCounter } from "./FPSCounter"; import NotificationCenter from "./NotificationCenter"; import AppIsland from "./AppIsland"; import Power from "./Power"; const Shell = () => { const [time, setTime] = useState(0); useEffect(() => { const int = setInterval(() => { // @ts-expect-error setTime(getTime()); }, 100); return () => clearInterval(int); }, []); return (
{time}
{/* Desktop */}
window.dispatchEvent(new Event("min-wins"))}>
); }; export default Shell; ================================================ FILE: src/sys/gui/Weather.tsx ================================================ import { useEffect, useState } from "react"; import getTime from "../apis/Time"; import { SysSettings } from "../types"; interface LocationData { properties: { forecast: string; }; } interface ForecastData { properties: { periods: Period[]; }; } interface WeatherData { temp: number; unit: string; icn: string; } interface Period { temperature: number; shortForecast: string; } export default function Weather() { const [weatherData, setWeatherData] = useState(null); const [loaded, setLoaded] = useState(true); const [error, setError] = useState(null); useEffect(() => { const getWeather = async () => { try { const settings: SysSettings = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/settings.json")); const defaultLocation = "40.7590322,-74.0516312"; const loc = settings.location || defaultLocation; const locationResponse = await fetch(`https://api.weather.gov/points/${loc}`, { method: "GET", headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0 Safari/537.36 Terbium-Browser/2.3.0", }, }); const locationData: LocationData = await locationResponse.json(); const forecastResponse = await fetch(locationData.properties.forecast, { method: "GET", headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0 Safari/537.36 Terbium-Browser/2.3.0", }, }); const forecastData: ForecastData = await forecastResponse.json(); const currentPeriod = forecastData.properties.periods[0]; let temp = currentPeriod.temperature; let unit = "°F"; if (settings.weather) { const { unit: userUnit } = settings.weather; ({ temp, unit } = FormatTemp(temp, userUnit)); } const icn = getIcon(currentPeriod.shortForecast); setWeatherData({ temp, unit, icn }); setLoaded(true); } catch (err: any) { setError(err); } }; getWeather(); window.addEventListener("updWeather", getWeather); return () => window.removeEventListener("updWeather", getWeather); }, []); return loaded && !error ? (
{weatherData?.temp} {weatherData?.unit}
) : null; } function FormatTemp(temp: number, unit: string): { temp: number; unit: string } { switch (unit) { case "Celsius": return { temp: Math.round(((temp - 32) * 5) / 9), unit: "°C" }; case "Kelvin": return { temp: Math.round(((temp - 32) * 5) / 9 + 273.15), unit: " K" }; default: return { temp, unit: "°F" }; } } function getIcon(sky: string): string { const time = getTime(); const [timePart, period] = time.split(" "); const [hours] = timePart.split(":").map(Number); const hour24 = period === "PM" && hours !== 12 ? hours + 12 : period === "AM" && hours === 12 ? 0 : hours; let icn = "/assets/img/day.svg"; if (sky === "Sunny" || sky === "Clear" || sky === "Mostly Clear") { icn = hour24 >= 18 ? "/assets/img/night.svg" : "/assets/img/day.svg"; } else if (sky.includes("Partly")) { icn = hour24 >= 18 ? "/assets/img/cloudy_night.svg" : "/assets/img/cloudy_day.svg"; } else if (sky.includes("Cloudy")) { icn = "/assets/img/cloudy.svg"; } else if (sky.includes("Rain")) { icn = "/assets/img/rainy.svg"; } else if (sky.includes("Snow") || sky.includes("Hail")) { icn = "/assets/img/snowy.svg"; } return icn; } ================================================ FILE: src/sys/gui/Wifi.tsx ================================================ import { useEffect, useRef, useState } from "react"; import "./styles/wifi.css"; import { fileExists } from "../types"; interface Server { id: string; name: string; latency?: string; connected?: boolean; isOnline?: boolean; } function ping(id: string): Promise<{ status: string; latency: number | string }> { return new Promise(resolve => { const websocket = new WebSocket(id); const startTime = Date.now(); const onOpen = () => { const latency = Date.now() - startTime; websocket.close(); resolve({ status: "OK", latency }); }; const onMessage = () => { const latency = Date.now() - startTime; websocket.close(); resolve({ status: "OK", latency }); }; const onError = () => { websocket.close(); resolve({ status: "Fail", latency: "N/A" }); }; websocket.addEventListener("open", onOpen); websocket.addEventListener("message", onMessage); websocket.addEventListener("error", onError); setTimeout(() => { websocket.close(); resolve({ status: "Fail", latency: "N/A" }); }, 5000); }); } interface WifiIconProps { connection: boolean; } const WifiIcon: React.FC = ({ connection }) => { const iconRef = useRef(null); const LoadMenu = () => window.dispatchEvent(new Event("open-net")); return ( { iconRef.current?.classList.remove("scale-90"); LoadMenu(); }} onMouseLeave={() => { iconRef.current?.classList.remove("scale-90"); }} onMouseOver={() => { iconRef.current?.classList.add("scale-90"); }} onClick={() => { iconRef.current?.classList.add("scale-90"); }} > {connection === true ? ( ) : ( <> )} ); }; export default function Wifi() { const [connection, setConnection] = useState(false); useEffect(() => { const getConnection = async () => { if ("onLine" in navigator) { setConnection(navigator.onLine); } }; const int = setInterval(getConnection, 1000); getConnection(); return () => clearInterval(int); }, []); return ; } interface WispMenuProps { isOpen: boolean; } export function WispMenu({ isOpen }: WispMenuProps) { const [servers, setServers] = useState([]); const [loading, setLoading] = useState(true); const menuRef = useRef(null); const [isinDiag, setisinDiag] = useState(false); const [isUpdating, setUpdating] = useState(false); useEffect(() => { const fetchServers = async (): Promise => { const exists = await fileExists("//apps/system/settings.tapp/wisp-servers.json"); if (!exists) { await window.tb.fs.promises.mkdir("//apps/system/settings.tapp/", { recursive: true } as any); const stockDat: Server[] = [ { id: `${location.protocol.replace("http", "ws")}//${location.hostname}:${location.port}/wisp/`, name: "Backend" }, { id: "wss://wisp.terbiumon.top/wisp/", name: "TB Wisp Instance" }, ]; await window.tb.fs.promises.writeFile("//apps/system/settings.tapp/wisp-servers.json", JSON.stringify(stockDat)); } const data: Server[] = JSON.parse(await window.tb.fs.promises.readFile("//apps/system/settings.tapp/wisp-servers.json")); const settings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`)); const servers = await Promise.all( data.map(async server => { const res = await ping(server.id); return { ...server, latency: res.latency === "N/A" ? res.latency : `${res.latency}ms`, connected: server.id === settings["wispServer"] ? true : false, isOnline: res.latency !== "N/A", }; }), ); setServers(servers); setLoading(false); }; if (isUpdating) fetchServers(); window.addEventListener("update-wispsrvs", fetchServers); return () => window.removeEventListener("update-wispsrvs", fetchServers); }, [isUpdating, isOpen]); useEffect(() => { setUpdating(true); }); useEffect(() => { const leave = (e: MouseEvent) => { const withinRadius = (e: MouseEvent) => { if (!menuRef.current) return false; const rect = menuRef.current.getBoundingClientRect(); const xBound = e.clientX >= rect.left - 5 && e.clientX <= rect.right + 5; const yBound = e.clientY >= rect.top - 5 && e.clientY <= rect.bottom + 5; return xBound && yBound; }; if (e.button === 0) { if (!menuRef.current?.contains(e.target as Node) && !withinRadius(e)) { setTimeout(() => { if (!isinDiag) { window.dispatchEvent(new Event("open-net")); setisinDiag(false); document.removeEventListener("mousedown", leave); } }, 150); } } }; if (!isOpen) { document.removeEventListener("mousedown", leave); } else { document.addEventListener("mousedown", leave); } return () => document.removeEventListener("mousedown", leave); }, [isOpen, isinDiag]); return (
{loading ? (

Loading...

) : // check if there are any servers servers.length === 0 ? ( No servers found ) : ( servers.map(server => (
{server.isOnline ? (
{ let settings = await window.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8"); let settdata = JSON.parse(settings); settdata.wispServer = server.id; window.tb.proxy.updateSWs(); const updSet = JSON.stringify(settdata, null, 2); await window.tb.fs.promises.writeFile(`/home/${await window.tb.user.username()}/settings.json`, updSet); setServers(prevServers => prevServers.map(server => ({ ...server, connected: server.name === server.id, })), ); window.dispatchEvent(new Event("update-wispsrvs")); setUpdating(true); }} >

{server.name}

{server.id}

{server.latency}

) : null}
)) )}
); } ================================================ FILE: src/sys/gui/WinSwitcher.tsx ================================================ import React, { useEffect, useMemo, useRef, useState } from "react"; import "./styles/win_switcher.css"; import { useWindowStore } from "../Store"; import { UserSettings, WindowConfig } from "../types"; import { domToCanvas } from "modern-screenshot"; type ThumbnailMap = Record; const titleText = (windowConfig: WindowConfig) => (typeof windowConfig.title === "string" ? windowConfig.title : windowConfig.title?.text); const getPreviewSourceElement = (wid?: string) => { if (!wid) return null; const windowElement = document.getElementById(wid); if (!windowElement) return null; const mainElement = windowElement.querySelector(".w-full.h-full") as HTMLElement | null; if (!mainElement) return null; const iframeElement = mainElement.querySelector("iframe") as HTMLElement | null; return iframeElement ?? mainElement; }; const nextFrame = () => new Promise(resolve => requestAnimationFrame(() => resolve())); const captureWindowPreview = async (wid?: string, attempt = 0): Promise => { const sourceEl = getPreviewSourceElement(wid); if (!sourceEl) return null; const rect = sourceEl.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) { if (attempt < 2) { await nextFrame(); return captureWindowPreview(wid, attempt + 1); } return null; } const filter = (node: Node) => { const tagName = (node as Element).tagName; if (tagName === "SCRIPT" || tagName === "NOSCRIPT" || tagName === "IFRAME") return false; return true; }; try { const fullCanvas = await domToCanvas(sourceEl, { scale: 1, backgroundColor: null, filter, features: { restoreScrollPosition: true, }, }); if (fullCanvas.width <= 0 || fullCanvas.height <= 0) { if (attempt < 2) { await nextFrame(); return captureWindowPreview(wid, attempt + 1); } return null; } return fullCanvas.toDataURL("image/png"); } catch { if (attempt < 2) { await nextFrame(); return captureWindowPreview(wid, attempt + 1); } return null; } }; const WinSwitcher: React.FC = () => { const [isVisible, setIsVisible] = useState(false); const [activeIndex, setActiveIndex] = useState(0); const [thumbnails, setThumbnails] = useState({}); const [thumbnailErrors, setThumbnailErrors] = useState>(new Set()); const [loadingThumbs, setLoadingThumbs] = useState>(new Set()); const [windowOptimizationsEnabled, setWindowOptimizationsEnabled] = useState(false); const isVisibleRef = useRef(false); const activeIndexRef = useRef(0); const windowsRef = useRef([]); const isCapturingRef = useRef>(new Set()); const triggerModifierRef = useRef<"alt" | "shift" | null>(null); const windows = useWindowStore(state => state.windows); const orderedWindows = useMemo(() => { return [...windows].sort((a, b) => (b.zIndex ?? 0) - (a.zIndex ?? 0)); }, [windows]); useEffect(() => { isVisibleRef.current = isVisible; }, [isVisible]); useEffect(() => { activeIndexRef.current = activeIndex; }, [activeIndex]); useEffect(() => { windowsRef.current = orderedWindows; const validIds = new Set(orderedWindows.map(win => win.wid).filter(Boolean) as string[]); setThumbnails(prev => Object.fromEntries(Object.entries(prev).filter(([wid]) => validIds.has(wid)))); setThumbnailErrors(prev => new Set([...prev].filter(wid => validIds.has(wid)))); setLoadingThumbs(prev => new Set([...prev].filter(wid => validIds.has(wid)))); isCapturingRef.current = new Set([...isCapturingRef.current].filter(wid => validIds.has(wid))); if (orderedWindows.length === 0) { setIsVisible(false); setActiveIndex(0); } if (activeIndex >= orderedWindows.length) { setActiveIndex(0); } }, [orderedWindows, activeIndex]); const cycleSelection = (direction: number) => { const currentWindows = windowsRef.current; if (currentWindows.length === 0) return; const nextIndex = (activeIndexRef.current + direction + currentWindows.length) % currentWindows.length; setActiveIndex(nextIndex); }; const commitSelection = () => { const currentWindows = windowsRef.current; if (currentWindows.length === 0) { setIsVisible(false); return; } const win = currentWindows[activeIndexRef.current]; if (win?.wid) { window.dispatchEvent(new CustomEvent("sel-win", { detail: win.wid })); window.dispatchEvent(new CustomEvent("currWID", { detail: win.wid })); } setIsVisible(false); }; const commitByWindow = (windowConfig: WindowConfig) => { if (windowConfig?.wid) { window.dispatchEvent(new CustomEvent("sel-win", { detail: windowConfig.wid })); window.dispatchEvent(new CustomEvent("currWID", { detail: windowConfig.wid })); } setIsVisible(false); }; const openSwitcher = (direction: number) => { const currentWindows = windowsRef.current; if (currentWindows.length === 0) return; const focusedIndex = currentWindows.findIndex(win => win.focused); const baseIndex = focusedIndex >= 0 ? focusedIndex : 0; const nextIndex = (baseIndex + direction + currentWindows.length) % currentWindows.length; setActiveIndex(nextIndex); setIsVisible(true); }; const captureForWindow = async (windowConfig: WindowConfig) => { if (!windowConfig.wid) return; if (thumbnails[windowConfig.wid] || thumbnailErrors.has(windowConfig.wid) || isCapturingRef.current.has(windowConfig.wid)) return; if (windowOptimizationsEnabled) { setThumbnailErrors(prev => { const next = new Set(prev); next.add(windowConfig.wid as string); return next; }); return; } isCapturingRef.current.add(windowConfig.wid); setLoadingThumbs(prev => { const next = new Set(prev); next.add(windowConfig.wid as string); return next; }); const thumbnail = await captureWindowPreview(windowConfig.wid); if (thumbnail) { setThumbnails(prev => ({ ...prev, [windowConfig.wid as string]: thumbnail })); } else { setThumbnailErrors(prev => { const next = new Set(prev); next.add(windowConfig.wid as string); return next; }); } setLoadingThumbs(prev => { const next = new Set(prev); next.delete(windowConfig.wid as string); return next; }); isCapturingRef.current.delete(windowConfig.wid); }; useEffect(() => { const updateWindowOptimizations = async () => { try { const settings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8")) as UserSettings; setWindowOptimizationsEnabled(settings.windowOptimizations ?? false); } catch { setWindowOptimizationsEnabled(false); } }; updateWindowOptimizations(); window.addEventListener("tfsready", updateWindowOptimizations); window.addEventListener("load", updateWindowOptimizations); window.addEventListener("updWallpaper", updateWindowOptimizations); return () => { window.removeEventListener("tfsready", updateWindowOptimizations); window.removeEventListener("load", updateWindowOptimizations); window.removeEventListener("updWallpaper", updateWindowOptimizations); }; }, []); useEffect(() => { if (!windowOptimizationsEnabled) { setThumbnailErrors(new Set()); } }, [windowOptimizationsEnabled]); useEffect(() => { const onDown = (e: KeyboardEvent) => { if (e.key === "Tab" && e.altKey) { e.preventDefault(); triggerModifierRef.current = "alt"; if (!isVisibleRef.current) { openSwitcher(e.shiftKey ? -1 : 1); } else { cycleSelection(e.shiftKey ? -1 : 1); } } if (e.key === "Tab" && e.shiftKey && !e.altKey) { e.preventDefault(); triggerModifierRef.current = "shift"; if (!isVisibleRef.current) { openSwitcher(-1); } else { cycleSelection(-1); } } if (isVisibleRef.current && e.key === "Escape") { e.preventDefault(); setIsVisible(false); triggerModifierRef.current = null; } }; const onUp = (e: KeyboardEvent) => { if (e.key === "Alt" && isVisibleRef.current && triggerModifierRef.current === "alt") { e.preventDefault(); commitSelection(); triggerModifierRef.current = null; } if (e.key === "Shift" && isVisibleRef.current && triggerModifierRef.current === "shift") { e.preventDefault(); commitSelection(); triggerModifierRef.current = null; } }; const onBlur = () => { if (isVisibleRef.current) { setIsVisible(false); triggerModifierRef.current = null; } }; window.addEventListener("keydown", onDown); window.addEventListener("keyup", onUp); window.addEventListener("blur", onBlur); return () => { window.removeEventListener("keydown", onDown); window.removeEventListener("keyup", onUp); window.removeEventListener("blur", onBlur); }; }, []); useEffect(() => { if (!isVisible) return; if (windowOptimizationsEnabled) return; orderedWindows.forEach(windowConfig => { captureForWindow(windowConfig); }); }, [isVisible, orderedWindows, windowOptimizationsEnabled]); if (orderedWindows.length === 0) return null; return (
{orderedWindows.map((windowConfig: WindowConfig, index: number) => { const text = titleText(windowConfig); const thumbSrc = windowConfig.wid ? thumbnails[windowConfig.wid] : null; const showFallback = windowOptimizationsEnabled || (windowConfig.wid && thumbnailErrors.has(windowConfig.wid)) || !thumbSrc; const isLoading = !!windowConfig.wid && loadingThumbs.has(windowConfig.wid); const spinnerId = `a12-switch-${windowConfig.wid ?? index}`; return ( ); })}
); }; export default WinSwitcher; ================================================ FILE: src/sys/gui/WindowArea.tsx ================================================ import { useState, useRef, useEffect, memo } from "react"; import { fileExists, UserSettings, WindowConfig } from "../types"; import { clearInfo, updateInfo } from "./AppIsland"; import { useWindowStore } from "../Store"; interface WindowProps { config: WindowConfig; className?: string; children?: React.ReactNode; onSnapPreview?: (pos: string) => void; onSnapDone?: () => void; } interface DesktopItem { name: string; icon: string; position: { custom: boolean; left: number | string; top: number | string; }; item: string; type: string; config: WindowConfig; } const WindowElement: React.FC = ({ className, config, onSnapDone, onSnapPreview }) => { const windowStore = useWindowStore(); const [optimizationsEnabled, setOptimizationsEnabled] = useState(true); const windowRef = useRef(null); const regionRef = useRef(null); const focuserRef = useRef(null); const srcRef = useRef(null); const miniRef = useRef(null); const minMaxRef = useRef(null); const closeRef = useRef(null); const contentRef = useRef(null); const titleRef = useRef(null); const thtmlref = useRef(null); const [zIndex, setZIndex] = useState(config.zIndex); const [isMouseDown, setIsMouseDown] = useState(false); const [isDragging, setIsDragging] = useState(false); const [x, setX] = useState("center"); const [y, setY] = useState("center"); const [width, setWidth] = useState(config.size?.width || 400); const [height, setHeight] = useState(config.size?.height || 400); const [titlebarhtml] = useState(typeof config.title === "object" ? config.title?.html : undefined); const [maximized, setMaximized] = useState(false); const [minimized, setMinimized] = useState(false); const [title] = useState(typeof config.title === "string" ? config.title : config.title?.text); const [snapRegion, setSnapRegion] = useState(null); const [isResizing, setIsResizing] = useState(false); const [controls, setControls] = useState(config.controls); const [src, setSrc] = useState(config.src); const originalSize = useRef<{ width: number; height: number } | null>(null); const [isSnapped, setIsSnapped] = useState(false); const [accent, setAccent] = useState("#ffffff18"); const [isFullscreen, setIsFullscreen] = useState(false); const mobileCheck = async () => { const platform = await window.tb.platform.getPlatform(); const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8")); if (platform === "mobile" || settings.window.alwaysMaximized === true) { setMaximized(true); } }; mobileCheck(); useEffect(() => { const loadOptimizationSettings = async () => { try { const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8")); setOptimizationsEnabled(settings.windowOptimizations ?? true); } catch (err) { console.error("Failed to load optimization settings:", err); setOptimizationsEnabled(true); // Default to enabled } }; loadOptimizationSettings(); }, []); useEffect(() => { updateInfo({ appname: typeof config.title === "string" ? config.title : config.title?.text }); }, [config]); useEffect(() => { if (windowRef.current) { if (x === "center") { setX(window.innerWidth / 2 - windowRef.current.offsetWidth / 2); } if (y === "center") { setY(window.innerHeight / 2 - windowRef.current.offsetHeight / 2); } windowRef.current.classList.remove("opacity-0", "translate-y-3"); setTimeout(() => { windowRef.current?.classList.remove("duration-150"); }, 150); } if (thtmlref.current && titlebarhtml) { thtmlref.current.innerHTML = titlebarhtml; } const prox = async () => { if (config.proxy === true) { const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8")); setSrc("about:blank"); console.log(settings.proxy); if (settings.proxy === "Ultraviolet") { setSrc(`${window.location.origin}/uv/service/${await window.tb.proxy.encode(config.src, "XOR")}`); } else { setSrc(`${window.location.origin}/service/${await window.tb.proxy.encode(config.src, "XOR")}`); } const instanceWin = (await window.anura.wm.getWeakRef(Number(config.pid))) || {}; Object.assign(srcRef.current?.contentWindow as typeof window, { tb: window.parent.tb, anura: window.parent.anura, AliceWM: window.parent.AliceWM, LocalFS: window.parent.LocalFS, ExternalApp: window.parent.ExternalApp, ExternalLib: window.parent.ExternalLib, Filer: window.parent.Filer, instanceWindow: instanceWin, }); } else { console.log(await window.anura.wm.getWeakRef(Number(config.pid))); const instanceWin = (await window.anura.wm.getWeakRef(Number(config.pid))) || {}; Object.assign(srcRef.current?.contentWindow as typeof window, { tb: window.parent.tb, anura: window.parent.anura, AliceWM: window.parent.AliceWM, LocalFS: window.parent.LocalFS, ExternalApp: window.parent.ExternalApp, ExternalLib: window.parent.ExternalLib, Filer: window.parent.Filer, instanceWindow: instanceWin, }); } }; prox(); }, [srcRef, src]); useEffect(() => { const reload = (e: CustomEvent) => { if (e.detail === config.pid) { if (srcRef.current?.contentWindow) { srcRef.current.contentWindow.location.reload(); srcRef.current.onload = () => { Object.assign(srcRef.current?.contentWindow!, { tb: window.parent.tb, anura: window.parent.anura, AliceWM: window.parent.AliceWM, LocalFS: window.parent.LocalFS, ExternalApp: window.parent.ExternalApp, ExternalLib: window.parent.ExternalLib, Filer: window.parent.Filer, instanceWindow: window.anura.wm.getWeakRef(Number(config.pid)) || {}, }); }; } } }; const max = (e: CustomEvent) => { if (e.detail === config.pid) { setMaximized(true); windowStore.arrange(config.wid); } }; const min = (e: CustomEvent) => { if (e.detail === config.pid) { setMinimized(true); } }; const returnCont = (e: CustomEvent) => { if (e.detail === config.pid) { window.dispatchEvent(new CustomEvent("curr-win-content", { detail: contentRef.current })); } }; const setCont = (e: CustomEvent) => { const msg = JSON.parse(e.detail); if (msg.currWin === config.pid) { if (contentRef.current) { contentRef.current.innerHTML = msg.content; } } }; const setBC = (e: CustomEvent) => { const msg = JSON.parse(e.detail); if (msg.currWin === config.pid) { if (titleRef.current) { titleRef.current.style.color = msg.color; } } }; const setBG = (e: CustomEvent) => { const msg = JSON.parse(e.detail); if (msg.currWin === config.pid) { if (titleRef.current) { titleRef.current.style.backgroundColor = msg.color; } } }; const settxt = (e: CustomEvent) => { const msg = JSON.parse(e.detail); if (msg.currWin === config.pid) { if (titleRef.current) { titleRef.current.innerText = msg.txt; } } }; const selWin = (e: CustomEvent) => { if (e.detail === config.wid) { windowStore.arrange(config.wid); // @ts-ignore setZIndex(windowStore.getWindow(config.wid)?.zIndex); setMinimized(false); setTimeout(() => { windowRef.current?.classList.remove("duration-150"); }, 150); if (focuserRef.current) focuserRef.current.click(); updateInfo({ appname: typeof config.title === "string" ? config.title : config.title?.text }); } }; const debugCTX = (e: MouseEvent) => { const rect = (e.target as HTMLElement).getBoundingClientRect(); window.tb.contextmenu.create({ x: rect.left + 100, y: rect.top + 40, options: [ { text: "Minimize", click: () => { setMinimized(true); }, }, { text: "Maximize", click: () => { setMaximized(true); }, }, { text: "Reload", click: () => { if (srcRef.current?.contentWindow) { srcRef.current.contentWindow.location.reload(); Object.assign(srcRef.current?.contentWindow!, { tb: window.parent.tb, anura: window.parent.anura, AliceWM: window.parent.AliceWM, LocalFS: window.parent.LocalFS, ExternalApp: window.parent.ExternalApp, ExternalLib: window.parent.ExternalLib, Filer: window.parent.Filer, instanceWindow: window.anura.wm.getWeakRef(Number(config.pid)) || {}, }); } }, }, { text: "Close", click: () => { windowStore.removeWindow(config.wid); clearInfo(); }, }, ], }); }; const changeURL = (e: CustomEvent) => { const det = JSON.parse(e.detail); if (det.pid === config.pid) { if (srcRef.current?.contentWindow) { setSrc(det.url); srcRef.current.onload = () => { Object.assign(srcRef.current?.contentWindow!, { tb: window.parent.tb, anura: window.parent.anura, AliceWM: window.parent.AliceWM, LocalFS: window.parent.LocalFS, ExternalApp: window.parent.ExternalApp, ExternalLib: window.parent.ExternalLib, Filer: window.parent.Filer, instanceWindow: window.anura.wm.getWeakRef(Number(config.pid)) || {}, }); }; } } }; const minall: any = () => { if (!minimized) setMinimized(true); }; const updAccent = async () => { const settings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8")) as UserSettings; setAccent(`${settings.window.winAccent}${settings.window.blurlevel}`); setIsFullscreen(settings.window.alwaysFullscreen); }; updAccent(); window.addEventListener("reload-win", reload as EventListener); window.addEventListener("max-win", max as EventListener); window.addEventListener("min-win", min as EventListener); window.addEventListener("get-content", returnCont as EventListener); window.addEventListener("upd-wincont", setCont as EventListener); window.addEventListener("upd-winbarcol", setBC as EventListener); window.addEventListener("upd-winbartxt", settxt as EventListener); window.addEventListener("upd-winbarbg", setBG as EventListener); window.addEventListener("upd-src", changeURL as EventListener); window.addEventListener("sel-win", selWin as EventListener); window.addEventListener("min-wins", minall); window.addEventListener("upd-accent", updAccent); if (regionRef.current) regionRef.current.addEventListener("contextmenu", debugCTX); return () => { window.removeEventListener("reload-win", reload as EventListener); window.removeEventListener("max-win", max as EventListener); window.removeEventListener("min-win", min as EventListener); window.removeEventListener("get-content", returnCont as EventListener); window.removeEventListener("upd-wincont", setCont as EventListener); window.removeEventListener("upd-winbarcol", setBC as EventListener); window.removeEventListener("upd-winbartxt", settxt as EventListener); window.removeEventListener("upd-winbarbg", setBG as EventListener); window.removeEventListener("upd-src", changeURL as EventListener); window.removeEventListener("sel-win", selWin as EventListener); window.removeEventListener("min-wins", minall); window.removeEventListener("upd-accent", updAccent); if (regionRef.current) regionRef.current.removeEventListener("contextmenu", debugCTX); }; }, []); const handleSnap = (newX: number, newY: number) => { if (config.snapable !== false) { if (!windowRef.current) return; originalSize.current = { width, height }; const windowWidth = windowRef.current.offsetWidth; const windowHeight = windowRef.current.offsetHeight; const SNAP_THRESHOLD = 7; const CORNER_THRESHOLD = 40; const atLeft = newX <= SNAP_THRESHOLD; const atRight = newX + windowWidth >= window.innerWidth - SNAP_THRESHOLD; const atTop = newY <= SNAP_THRESHOLD; const atBottom = newY + windowHeight >= window.innerHeight - SNAP_THRESHOLD; if (atLeft && atTop && newX <= CORNER_THRESHOLD && newY <= CORNER_THRESHOLD) { setX(0); setY(0); setSnapRegion("top-left"); onSnapPreview?.("top-left"); } else if (atRight && atTop && newX + windowWidth >= window.innerWidth - CORNER_THRESHOLD && newY <= CORNER_THRESHOLD) { setX(window.innerWidth - windowWidth); setY(0); setSnapRegion("top-right"); onSnapPreview?.("top-right"); } else if (atLeft && atBottom && newX <= CORNER_THRESHOLD && newY + windowHeight >= window.innerHeight - CORNER_THRESHOLD) { setX(0); setY(window.innerHeight - windowHeight); setSnapRegion("bottom-left"); onSnapPreview?.("bottom-left"); } else if (atRight && atBottom && newX + windowWidth >= window.innerWidth - CORNER_THRESHOLD && newY + windowHeight >= window.innerHeight - CORNER_THRESHOLD) { setX(window.innerWidth - windowWidth); setY(window.innerHeight - windowHeight); setSnapRegion("bottom-right"); onSnapPreview?.("bottom-right"); } else if (atLeft) { setX(0); setSnapRegion("left"); onSnapPreview?.("left"); } else if (atRight) { setX(window.innerWidth - windowWidth); setSnapRegion("right"); onSnapPreview?.("right"); } else if (atTop) { setY(0); setSnapRegion("top"); onSnapPreview?.("top"); } else { if (snapRegion && originalSize.current) { setWidth(originalSize.current.width); setHeight(originalSize.current.height); } setSnapRegion(null); onSnapDone?.(); } } }; useEffect(() => { if (isDragging && isSnapped && originalSize.current && windowRef.current) { windowRef.current.style.width = `${originalSize.current.width}px`; windowRef.current.style.height = `${originalSize.current.height}px`; setIsSnapped(false); setSnapRegion(null); onSnapDone?.(); } }, [isDragging, isSnapped]); // Disable pointer events on all iframes when dragging useEffect(() => { if (isDragging || isResizing) { const iframes = document.querySelectorAll("iframe"); iframes.forEach(iframe => { iframe.style.pointerEvents = "none"; }); } else { const iframes = document.querySelectorAll("iframe"); iframes.forEach(iframe => { iframe.style.pointerEvents = "auto"; }); } }, [isDragging, isResizing]); useEffect(() => { const snap = () => { setIsMouseDown(false); setIsDragging(false); if (windowRef.current) { if (snapRegion === "left") { windowRef.current.style.left = "0"; windowRef.current.style.width = "50%"; windowRef.current.style.height = "100%"; windowRef.current.style.top = "0"; setIsSnapped(true); } else if (snapRegion === "right") { windowRef.current.style.left = "50%"; windowRef.current.style.width = "50%"; windowRef.current.style.height = "100%"; windowRef.current.style.top = "0"; setIsSnapped(true); } else if (snapRegion === "top") { setMaximized(true); setIsSnapped(true); } else if (snapRegion === "top-left") { windowRef.current.style.left = "0"; windowRef.current.style.top = "0"; windowRef.current.style.width = "50%"; windowRef.current.style.height = "50%"; setIsSnapped(true); } else if (snapRegion === "top-right") { windowRef.current.style.left = "50%"; windowRef.current.style.top = "0"; windowRef.current.style.width = "50%"; windowRef.current.style.height = "50%"; setIsSnapped(true); } else if (snapRegion === "bottom-left") { windowRef.current.style.left = "0"; windowRef.current.style.top = "50%"; windowRef.current.style.width = "50%"; windowRef.current.style.height = "50%"; setIsSnapped(true); } else if (snapRegion === "bottom-right") { windowRef.current.style.left = "50%"; windowRef.current.style.top = "50%"; windowRef.current.style.width = "50%"; windowRef.current.style.height = "50%"; setIsSnapped(true); } } setSnapRegion(null); onSnapDone?.(); }; window.addEventListener("mouseup", snap); return () => window.removeEventListener("mouseup", snap); }, [snapRegion, isDragging, maximized, isResizing]); const handleMouseDown = (direction: "top" | "left" | "right" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right") => { let animationFrameId: number | null = null; let lastMouseEvent: MouseEvent | null = null; const onMove = (e: MouseEvent) => { if (!optimizationsEnabled) { // Without optimizations: update immediately setIsResizing(true); setMaximized(false); windowRef.current!.style.transform = ""; if (direction.includes("top")) { const offsetY = e.clientY - 65; const newY = Math.max(offsetY, 0); const newHeight = height + (typeof y === "number" ? y - newY : 0); if (newHeight >= (config.size?.minHeight ?? 224)) { setHeight(newHeight); setY(newY); } } if (direction.includes("left")) { const offsetX = e.clientX - 10; const newX = Math.max(offsetX, 0); const newWidth = width + (typeof x === "number" ? x - newX : 0); if (newWidth >= (config.size?.minWidth ?? 224)) { setWidth(newWidth); setX(newX); } } if (direction.includes("right")) { const offsetX = e.clientX - 5; const newX = typeof x === "number" ? x : 0; const newWidth = offsetX - newX; if (newWidth >= (config.size?.minWidth ?? 224)) { setWidth(newWidth); setX(newX); } } if (direction.includes("bottom")) { const offsetY = e.clientY - 55; const newY = typeof y === "number" ? y : 0; const newHeight = offsetY - newY; if (newHeight >= (config.size?.minHeight ?? 224)) { setHeight(newHeight); setY(newY); } } return; } // With optimizations: use requestAnimationFrame lastMouseEvent = e; if (!animationFrameId) { animationFrameId = requestAnimationFrame(() => { if (!lastMouseEvent) return; const e = lastMouseEvent; setIsResizing(true); setMaximized(false); windowRef.current!.style.transform = ""; if (direction.includes("top")) { const offsetY = e.clientY - 65; const newY = Math.max(offsetY, 0); const newHeight = height + (typeof y === "number" ? y - newY : 0); if (newHeight >= (config.size?.minHeight ?? 224)) { setHeight(newHeight); setY(newY); } } if (direction.includes("left")) { const offsetX = e.clientX - 10; const newX = Math.max(offsetX, 0); const newWidth = width + (typeof x === "number" ? x - newX : 0); if (newWidth >= (config.size?.minWidth ?? 224)) { setWidth(newWidth); setX(newX); } } if (direction.includes("right")) { const offsetX = e.clientX - 5; const newX = typeof x === "number" ? x : 0; const newWidth = offsetX - newX; if (newWidth >= (config.size?.minWidth ?? 224)) { setWidth(newWidth); setX(newX); } } if (direction.includes("bottom")) { const offsetY = e.clientY - 55; const newY = typeof y === "number" ? y : 0; const newHeight = offsetY - newY; if (newHeight >= (config.size?.minHeight ?? 224)) { setHeight(newHeight); setY(newY); } } animationFrameId = null; }); } originalSize.current = { width, height }; }; const onUp = () => { if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); window.removeEventListener("blur", onUp); setIsMouseDown(false); setIsResizing(false); }; window.onmouseleave = () => { setIsDragging(false); setIsMouseDown(false); window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); window.removeEventListener("blur", onUp); }; window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); window.addEventListener("blur", onUp); setIsMouseDown(true); }; return (
{ updateInfo({ appname: typeof config.title === "string" ? config.title : config.title?.text }); }} >
{isSnapped ? (
{ windowStore.arrange(config.wid); // @ts-ignore setZIndex(windowStore.getWindow(config.wid)?.zIndex); }} >
) : (
{ windowStore.arrange(config.wid); // @ts-ignore setZIndex(windowStore.getWindow(config.wid)?.zIndex); }} >
)}
handleMouseDown("top")} />
handleMouseDown("left")} />
handleMouseDown("right")} />
handleMouseDown("bottom")} />
handleMouseDown("top-left")} />
handleMouseDown("top-right")} />
handleMouseDown("bottom-left")} />
handleMouseDown("bottom-right")} />
{ windowStore.arrange(config.wid); // @ts-ignore setZIndex(windowStore.getWindow(config.wid)?.zIndex); if ((e.target as HTMLElement).classList.contains("no-drag")) return; const offsetX = e.clientX - windowRef.current!.offsetLeft; const offsetY = e.clientY - windowRef.current!.offsetTop; let animationFrameId: number | null = null; let lastMouseEvent: MouseEvent | null = null; const onMove = (e: MouseEvent) => { if (!optimizationsEnabled) { // Without optimizations: update immediately if (windowRef.current) windowRef.current.style.transform = ""; setIsDragging(true); setMaximized(false); const newX = e.clientX - offsetX; const newY = e.clientY - offsetY; handleSnap(newX, newY); if (newY > 0 && newY < window.innerHeight - windowRef.current!.offsetHeight) setY(newY); if (newX > 0 && newX < window.innerWidth - windowRef.current!.offsetWidth) setX(newX); return; } // With optimizations: use requestAnimationFrame lastMouseEvent = e; if (!animationFrameId) { animationFrameId = requestAnimationFrame(() => { if (!lastMouseEvent) return; const e = lastMouseEvent; if (windowRef.current) windowRef.current.style.transform = ""; setIsDragging(true); setMaximized(false); const newX = e.clientX - offsetX; const newY = e.clientY - offsetY; handleSnap(newX, newY); if (newY > 0 && newY < window.innerHeight - windowRef.current!.offsetHeight) setY(newY); if (newX > 0 && newX < window.innerWidth - windowRef.current!.offsetWidth) setX(newX); animationFrameId = null; }); } }; const onUp = () => { if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); window.removeEventListener("blur", onUp); setIsMouseDown(false); setIsDragging(false); }; window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); window.addEventListener("blur", onUp); window.onmouseleave = () => { setIsDragging(false); setIsMouseDown(false); window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); window.removeEventListener("blur", onUp); }; setIsMouseDown(true); }} onMouseUp={() => { setIsMouseDown(false); setIsDragging(false); }} onMouseLeave={() => { if (isMouseDown) { setIsDragging(false); } }} onMouseEnter={() => { if (isMouseDown) { setIsDragging(true); } }} onDoubleClick={() => { if (config.maximizable !== false) if (windowRef.current) { windowRef.current.style.transitionProperty = "width, height, left, top"; windowRef.current.style.transitionDuration = "150ms"; } setTimeout(() => { if (windowRef.current) { windowRef.current.style.transitionProperty = ""; windowRef.current.style.transitionDuration = ""; } }, 150); setMaximized(!maximized); }} >
icon {title} {titlebarhtml &&
}
{controls ? (
{controls?.map((control, index) => { if (control === "minimize") { return ( { if (config.minimizable === false) return; if (windowRef.current) { windowRef.current.style.transitionProperty = "transform, opacity"; windowRef.current.style.transitionDuration = "150ms"; } setTimeout(() => { if (windowRef.current) { windowRef.current.style.transitionProperty = ""; windowRef.current.style.transitionDuration = ""; } }, 150); setMinimized(true); }} > ); } if (control === "maximize") { return ( { if (config.maximizable === false) return; if (windowRef.current) { windowRef.current.style.transitionProperty = "width, height, left, top"; windowRef.current.style.transitionDuration = "150ms"; } setTimeout(() => { if (windowRef.current) { windowRef.current.style.transitionProperty = ""; windowRef.current.style.transitionDuration = ""; } }, 150); setMaximized(!maximized); }} > {maximized ? ( <> ) : ( )} ); } if (control === "close") { return ( { if (config.closable === false) return; if (windowRef.current) { windowRef.current.style.transitionProperty = "transform, opacity"; windowRef.current.style.transitionDuration = "150ms"; windowRef.current.classList.add("translate-y-3", "opacity-0"); } setTimeout(() => { clearInfo(); windowStore.removeWindow(config.wid); }, 150); }} > ); } })}
) : (
{ if (config.minimizable === false) return; if (windowRef.current) { windowRef.current.style.transitionProperty = "transform, opacity"; windowRef.current.style.transitionDuration = "150ms"; } setTimeout(() => { if (windowRef.current) { windowRef.current.style.transitionProperty = ""; windowRef.current.style.transitionDuration = ""; } }, 150); windowStore.minimize(config.wid); window.dispatchEvent(new CustomEvent("min-win", { detail: config.pid })); setMinimized(true); }} > { if (config.maximizable === false) return; if (windowRef.current) { windowRef.current.style.transitionProperty = "width, height, left, top"; windowRef.current.style.transitionDuration = "150ms"; } setTimeout(() => { if (windowRef.current) { windowRef.current.style.transitionProperty = ""; windowRef.current.style.transitionDuration = ""; } }, 150); setMaximized(!maximized); }} > {maximized ? ( <> ) : ( )} { if (config.closable === false) return; if (windowRef.current) { windowRef.current.style.transitionProperty = "transform, opacity"; windowRef.current.style.transitionDuration = "150ms"; windowRef.current.classList.add("translate-y-3", "opacity-0"); } setTimeout(() => { clearInfo(); windowStore.removeWindow(config.wid); }, 150); }} >
)}
); }; // Memoize WindowElement to prevent unnecessary re-renders const MemoizedWindowElement = memo(WindowElement, (prevProps, nextProps) => { // Only re-render if config changes in meaningful ways return prevProps.config.wid === nextProps.config.wid && prevProps.config.zIndex === nextProps.config.zIndex && prevProps.config.focused === nextProps.config.focused && prevProps.className === nextProps.className; }); const DesktopItems = () => { const [items, setItems] = useState([]); const [dragging, setDragging] = useState(false); const draggedItemIndex = useRef(null); const [offset, setOffset] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); const [dragradius, setDragradius] = useState(false); const [selected, setSelected] = useState(null); const selectedRef = useRef(null); const user = sessionStorage.getItem("currAcc"); useEffect(() => { const addDesktopListener = async () => { let desktopItems: string[] = await window.tb.fs.promises.readdir(`/home/${user}/desktop`); const handleDesktopChange = async () => { try { const updatedItems = await window.tb.fs.promises.readdir(`/home/${user}/desktop`); const addedItems = updatedItems.filter(item => !desktopItems.includes(item)); const removedItems = desktopItems.filter(item => !updatedItems.includes(item)); var desktopConfig = JSON.parse(await window.tb.fs.promises.readFile(`/home/${user}/desktop/.desktop.json`, "utf8")); if (addedItems.length > 0) { const findLastItem = () => { for (let i = desktopConfig.length - 1; i >= 0; i--) { if (!desktopConfig[i].position.custom) { return desktopConfig[i]; } } return null; }; const lastItem: any = findLastItem(); const highestLeft = Math.max(...desktopConfig.map((item: any) => item.position.left)); let topPos = 0; let leftPos = 0; if (lastItem && lastItem.position.top < 11) { topPos = Math.floor(lastItem.position.top + 1); leftPos = lastItem.position.left; } else { leftPos = Math.floor(highestLeft + 1); } for (const item of addedItems) { const itemExists = desktopConfig.some((config: any) => config.item === `/home/${user}/desktop/${item}`); if (!itemExists) { const type = (await window.tb.fs.promises.stat(`/home/${user}/desktop/${item}`))!.type.toLowerCase(); if (type === "symlink") { const isAppJson = (await window.tb.fs.promises.readFile(await window.tb.fs.promises.readlink(`/home/${user}/desktop/${item}`))).includes("config"); desktopConfig.push({ name: isAppJson ? JSON.parse(await window.tb.fs.promises.readFile(await window.tb.fs.promises.readlink(`/home/${user}/desktop/${item}`)))["config"].title : item, item: `/home/${user}/desktop/${item}`, position: { custom: false, top: topPos, left: leftPos, }, }); } else if (type === "file") { const ext = item.split(".").pop(); const icons = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/file-icons.json")); const iconName = ext ? icons["ext-to-name"][ext] : "Unknown"; const iconPath = iconName ? icons["name-to-path"][iconName] : "/system/etc/terbium/file-icons/Unknown.svg"; const iconData = await window.tb.fs.promises.readFile(iconPath, "utf8"); desktopConfig.push({ name: item, item: `/home/${user}/desktop/${item}`, position: { custom: false, top: topPos, left: leftPos, }, icon: iconData, }); } } } } if (removedItems.length > 0) { for (const item of removedItems) { const index = desktopConfig.findIndex((config: any) => config.item === `/home/${user}/desktop/${item}`); desktopConfig.splice(index, 1); } } desktopItems = updatedItems; await window.tb.fs.promises.writeFile(`/home/${user}/desktop/.desktop.json`, JSON.stringify(desktopConfig, null, 4)); } catch (error) { console.error("Error while reading directory:", error); } window.dispatchEvent(new Event("upd-desktop")); }; handleDesktopChange(); }; addDesktopListener(); }, []); useEffect(() => { const getItems = async () => { var allItems: any[] = []; const items = JSON.parse(await window.tb.fs.promises.readFile(`/home/${user}/desktop/.desktop.json`, "utf8")); for (const item of items) { const type = (await window.tb.fs.promises.stat(item.item))!.type.toLowerCase(); const position = item.position; if (type === "symlink") { allItems.push({ name: item.name, type: "symlink", item: item.item, position: { custom: position.custom, top: position.top, left: position.left, }, config: JSON.parse(await window.tb.fs.promises.readFile(await window.tb.fs.promises.readlink(item.item)))["config"], }); } else if (type === "file") { const ext = item.name.split(".").pop(); const icons = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/file-icons.json")); const iconName = ext ? icons["ext-to-name"][ext] : "Unknown"; const iconPath = iconName ? icons["name-to-path"][iconName] : "/system/etc/terbium/file-icons/Unknown.svg"; const iconData = await window.tb.fs.promises.readFile(iconPath, "utf8"); allItems.push({ name: item.name, type: "file", item: item.item, position: { custom: position.custom, top: position.top, left: position.left, }, icon: iconData, }); } else if (type === "directory") { allItems.push({ name: item.name, type: "directory", item: item.item, position: { custom: position.custom, top: position.top, left: position.left, }, icon: ``, }); } } setItems(allItems); }; getItems(); window.addEventListener("upd-desktop", getItems); return () => window.removeEventListener("upd-desktop", getItems); }, []); const onMouseDown = (e: React.MouseEvent, index: number) => { let holdTimeout: any | null = null; let renamingIndex: number | null = null; const startDragging = () => { setDragradius(true); setDragging(true); draggedItemIndex.current = index; }; const saveName = async () => { if (selectedRef.current && renamingIndex !== null) { const spanElement = selectedRef.current.querySelector("span"); if (spanElement) { const newName = spanElement.innerText; const oldName = items[renamingIndex].name; const itemPath = items[renamingIndex].item; const newPath = itemPath.replace(oldName, newName); if (selectedRef.current?.dataset.type === "shortcut") { const desktopItems = JSON.parse(await window.tb.fs.promises.readFile(`/home/${user}/desktop/.desktop.json`, "utf8")); const itemIndex = desktopItems.findIndex((item: any) => item.item === itemPath); if (itemIndex !== -1) { desktopItems[itemIndex].name = newName; desktopItems[itemIndex].item = newPath; await window.tb.fs.promises.writeFile(`/home/${user}/desktop/.desktop.json`, JSON.stringify(desktopItems, null, 4)); window.dispatchEvent(new Event("upd-desktop")); } } else { await window.tb.fs.promises.rename(itemPath, newPath); const desktopItems = JSON.parse(await window.tb.fs.promises.readFile(`/home/${user}/desktop/.desktop.json`, "utf8")); const itemIndex = desktopItems.findIndex((item: any) => item.item === itemPath); if (itemIndex !== -1) { desktopItems[itemIndex].name = newName; desktopItems[itemIndex].item = newPath; await window.tb.fs.promises.writeFile(`/home/${user}/desktop/.desktop.json`, JSON.stringify(desktopItems, null, 4)); window.dispatchEvent(new Event("upd-desktop")); selectedRef.current = null; } } } } renamingIndex = null; }; if (selectedRef.current && selectedRef.current === e.currentTarget) { if (selectedRef.current && selectedRef.current !== null) { const spanElement = selectedRef.current.querySelector("span"); if (spanElement) { spanElement.contentEditable = "true"; const range = document.createRange(); const selection = window.getSelection(); range.selectNodeContents(spanElement); range.collapse(false); selection?.removeAllRanges(); selection?.addRange(range); spanElement.addEventListener("keydown", async e => { if (e.key === "Enter") { e.preventDefault(); await saveName(); } }); spanElement.focus(); renamingIndex = index; } const mouseDownHandler = async (e: MouseEvent) => { if (selectedRef.current && !selectedRef.current.contains(e.target as Node)) { setSelected(null); const spanElement = selectedRef.current.querySelector("span"); if (spanElement) { await saveName(); spanElement.contentEditable = "false"; spanElement.blur(); selectedRef.current = null; } } }; document.addEventListener("mousedown", mouseDownHandler, { once: true }); } } else { selectedRef.current = e.currentTarget; renamingIndex = index; } holdTimeout = setTimeout(startDragging, 300); const clearHoldTimeout = () => { if (holdTimeout) { clearTimeout(holdTimeout); holdTimeout = null; } }; window.onmouseup = async (e: MouseEvent) => { setDragging(false); window.removeEventListener("mousemove", onMouseMove); if (draggedItemIndex.current !== null && !holdTimeout) { const draggedApp = items[draggedItemIndex.current]; const updatedApp = { ...draggedApp, leftPos: e.clientX - 44, topPos: e.clientY - 80, }; await savePos(draggedApp.item, updatedApp.leftPos, updatedApp.topPos); } draggedItemIndex.current = null; clearHoldTimeout(); setDragradius(false); }; window.onmouseleave = async () => { clearHoldTimeout(); setDragging(false); window.removeEventListener("mousemove", onMouseMove); if (draggedItemIndex.current !== null && dragging) { const draggedApp = items[draggedItemIndex.current]; const updatedApp = { ...draggedApp, leftPos: draggedApp.position.left, topPos: draggedApp.position.top, }; await savePos(draggedApp.item, updatedApp.leftPos, updatedApp.topPos); } draggedItemIndex.current = null; }; e.preventDefault(); e.target.addEventListener("mouseup", clearHoldTimeout, { once: true }); }; const onMouseMove = (e: MouseEvent) => { if (dragging && draggedItemIndex !== null) { let newX = e.clientX - offset.x - 44; let newY = e.clientY - offset.y - 80; setItems(prevApps => prevApps.map((app, index) => (index === draggedItemIndex.current ? { ...app, position: { ...app.position, left: newX, top: newY, custom: true } } : app))); } }; const savePos = async (item: string, left: number, top: number) => { try { const desktopConfig = JSON.parse(await window.tb.fs.promises.readFile(`/home/${user}/desktop/.desktop.json`, "utf8")); const itemIndex = desktopConfig.findIndex((config: any) => config.item === item); if (itemIndex !== -1) { const currentLeft = desktopConfig[itemIndex].position.left; const currentTop = desktopConfig[itemIndex].position.top; if ((Math.abs(Math.round(currentLeft) - Math.round(left)) > 67 || Math.abs(Math.round(currentTop) - Math.round(top)) > 67) && (Math.round(currentLeft) !== Math.round(left) || Math.round(currentTop) !== Math.round(top))) { desktopConfig[itemIndex].position.left = Math.round(left); desktopConfig[itemIndex].position.top = Math.round(top); desktopConfig[itemIndex].position.custom = true; await window.tb.fs.promises.writeFile(`/home/${user}/desktop/.desktop.json`, JSON.stringify(desktopConfig, null, 4)); console.log("Saved app position"); } } } catch (error) { console.error("Error saving app position:", error); } }; useEffect(() => { document.addEventListener("mousemove", onMouseMove); return () => { document.removeEventListener("mousemove", onMouseMove); }; }, [dragging]); return (
{items.map((item: DesktopItem, i: any) => { return item.type === "file" ? (
{ let handlers = JSON.parse(await window.tb.fs.promises.readFile("/system/etc/terbium/settings.json", "utf8"))["fileAssociatedApps"]; handlers = Object.entries(handlers).filter(([type, app]) => { return !(type === "text" && app === "text-editor") && !(type === "image" && app === "media-viewer") && !(type === "video" && app === "media-viewer") && !(type === "audio" && app === "media-viewer"); }); let hands = []; for (const [type, app] of handlers) { hands.push({ text: app, value: type }); } await window.tb.dialog.Select({ title: `Select a application to open: ${item.item.split("/").pop()}`, options: [ { text: "Text Editor", value: "text", }, { text: "Media Viewer", value: "media", }, { text: "Webview", value: "webview", }, ...hands, { text: "Other", value: "other", }, ], onOk: async (val: any) => { const data = await fetch(`/fs//system/etc/terbium/file-icons.json`).then(res => res.json()); const ext = item.name.split(".").pop(); switch (val) { case "text": parent.window.tb.file.handler.openFile(item.item, "text"); break; case "media": if (data["image"].includes(ext)) { parent.window.tb.file.handler.openFile(item.item, "image"); } else if (data["video"].includes(ext)) { parent.window.tb.file.handler.openFile(item.item, "video"); } else if (data["audio"].includes(ext)) { parent.window.tb.file.handler.openFile(item.item, "audio"); } break; case "webview": parent.window.tb.file.handler.openFile(item.item, "webpage"); break; case "other": window.tb.dialog.DirectoryBrowser({ title: "Select a application", filter: ".tapp", onOk: async (val: any) => { const app = JSON.parse(await window.tb.fs.promises.readFile(`${val}/.tbconfig`, "utf8")); createWindow({ ...app, message: { type: "process", path: item.item } }); }, }); break; default: if (hands.length === 0) { parent.window.tb.file.handler.openFile(item.item, "text"); } else { parent.window.tb.file.handler.openFile(item.item, val); } break; } }, }); }} onMouseDown={(e: React.MouseEvent) => onMouseDown(e, i)} onContextMenuCapture={(e: React.MouseEvent) => { setDragging(false); draggedItemIndex.current = null; setDragradius(false); e.preventDefault(); const { clientX, clientY } = e; window.tb.contextmenu.create({ x: clientX, y: clientY, options: [ { text: "Open", click: () => { sessionStorage.setItem("ldir", item.item); createWindow({ title: "Files", icon: "/fs/apps/system/files.tapp/icon.svg", src: "/fs/apps/system/files.tapp/index.html", size: { width: 600, height: 500, }, }); }, }, { text: "Delete Shortcut", click: async () => { let idx = JSON.parse(await window.tb.fs.promises.readFile(`/home/${user}/desktop/.desktop.json`, "utf8")); idx = idx.filter((entry: any) => entry.name !== item.name); await window.tb.fs.promises.writeFile(`/home/${user}/desktop/.desktop.json`, JSON.stringify(idx, null, 4)); window.dispatchEvent(new Event("upd-desktop")); }, }, ], }); }} style={{ position: "absolute", left: item.position.custom === true ? item.position.left : Math.floor(Number(item.position.left) * 80), top: item.position.custom === true ? item.position.top : Math.floor(Number(item.position.top) * 66), }} >
{
} {item.name.length > 12 ? `${item.name.slice(0, 10)}...` : item.name}
) : item.type === "directory" ? (
{ sessionStorage.setItem("ldir", item.item); createWindow({ title: "Files", icon: "/fs/apps/system/files.tapp/icon.svg", src: "/fs/apps/system/files.tapp/index.html", size: { width: 600, height: 500, }, }); }} onMouseDown={(e: React.MouseEvent) => onMouseDown(e, i)} onContextMenuCapture={(e: React.MouseEvent) => { setDragging(false); draggedItemIndex.current = null; setDragradius(false); e.preventDefault(); const { clientX, clientY } = e; window.tb.contextmenu.create({ x: clientX, y: clientY, options: [ { text: "Open", click: () => { sessionStorage.setItem("ldir", item.item); createWindow({ title: "Files", icon: "/fs/apps/system/files.tapp/icon.svg", src: "/fs/apps/system/files.tapp/index.html", size: { width: 600, height: 500, }, }); }, }, { text: "Delete Shortcut", click: async () => { let idx = JSON.parse(await window.tb.fs.promises.readFile(`/home/${user}/desktop/.desktop.json`, "utf8")); idx = idx.filter((entry: any) => entry.name !== item.name); await window.tb.fs.promises.writeFile(`/home/${user}/desktop/.desktop.json`, JSON.stringify(idx, null, 4)); window.dispatchEvent(new Event("upd-desktop")); }, }, ], }); }} style={{ position: "absolute", left: item.position.custom === true ? item.position.left : Math.floor(Number(item.position.left) * 80), top: item.position.custom === true ? item.position.top : Math.floor(Number(item.position.top) * 66), }} >
{item.name.length > 12 ? `${item.name.slice(0, 10)}...` : item.name}
) : (
{ createWindow({ ...item.config, wid: undefined }); }} style={{ position: "absolute", left: item.position.custom === true ? item.position.left : Math.floor(Number(item.position.left) * 80), top: item.position.custom === true ? item.position.top : Math.floor(Number(item.position.top) * 66), }} onMouseDown={(e: React.MouseEvent) => onMouseDown(e, i)} onContextMenuCapture={(e: React.MouseEvent) => { setDragging(false); draggedItemIndex.current = null; setDragradius(false); e.preventDefault(); window.tb.contextmenu.create({ x: e.clientX - 50, y: e.clientY, options: [ { text: "Open", click: () => { createWindow(item.config); }, }, { text: "Pin to Dock", click: () => { window.tb.desktop.dock.pin(item.config); }, }, { text: "Delete Shortcut", click: async () => { const stat = await window.tb.fs.promises.stat(`/home/${user}/desktop/${item.item}`); if (stat!.isDirectory()) { // @ts-expect-error await new window.tb.fs.Shell().promises.rm(`/home/${user}/desktop/${item.item}`, { recursive: true }); } else { await window.tb.fs.promises.unlink(`/home/${user}/desktop/${item.item}`); } window.dispatchEvent(new Event("upd-desktop")); }, }, ], }); }} >
{item.name} {item.name.length > 12 ? `${item.name.slice(0, 10)}...` : item.name}
); })}
); }; interface WindowAreaProps { className: string; } const WindowArea: React.FC = ({ className }) => { const windowStore = useWindowStore(); const [prevShowing, showPrev] = useState(false); const [direction, setDirection] = useState(null); const snapPrev = (pos: string) => { showPrev(true); setDirection(pos); }; const FinishSnap = () => { showPrev(false); }; const setClass = () => { switch (direction) { case "left": return ` left-0 w-6/12 h-full ${prevShowing ? "translate-x-0" : "-translate-x-4"} `; case "right": return ` right-0 w-6/12 h-full ${prevShowing ? "translate-x-0" : "translate-x-4"} `; case "top": return ` left-0 right-0 w-full h-full ${prevShowing ? "translate-y-0" : "-translate-y-4"} `; case "top-left": return ` left-0 top-0 w-6/12 h-6/12 ${prevShowing ? "translate-x-0 translate-y-0" : "-translate-x-4 -translate-y-4"} `; case "top-right": return ` right-0 top-0 w-6/12 h-6/12 ${prevShowing ? "translate-x-0 translate-y-0" : "translate-x-4 -translate-y-4"} `; case "bottom-left": return ` left-0 top-[50%] w-6/12 h-6/12 ${prevShowing ? "translate-x-0 translate-y-0" : "-translate-x-4 translate-y-4"} `; case "bottom-right": return ` right-0 top-[50%] w-6/12 h-6/12 ${prevShowing ? "translate-x-0 translate-y-0" : "translate-x-4 translate-y-4"} `; } }; return ( // @ts-ignore { const pos = { x: e.clientX, y: e.clientY }; window.tb.contextmenu.create({ options: [ { text: "Change Wallpaper", click: () => { window.tb.window.create({ title: "Settings", icon: "/fs/apps/system/settings.tapp/icon.svg", src: "/fs/apps/system/settings.tapp/index.html", }); }, }, { text: "New Folder", click: () => { window.tb.dialog.Message({ title: "Enter the new name of the folder", onOk: async (val: any) => { const user = sessionStorage.getItem("currAcc"); await window.tb.fs.promises.mkdir(`/home/${user}/desktop/${val}`); const desktopConfig = JSON.parse(await window.tb.fs.promises.readFile(`/home/${user}/desktop/.desktop.json`, "utf8")); const getLastItem = () => { for (let i = desktopConfig.length - 1; i >= 0; i--) { if (!desktopConfig[i].position.custom) { return desktopConfig[i]; } } return null; }; const lastItem = getLastItem(); const highestLeft = Math.max(...desktopConfig.map((item: any) => item.position.left)); let topPos = 0; let leftPos = 0; if (lastItem && lastItem.position.top < 11) { topPos = Math.floor(lastItem.position.top + 1); leftPos = lastItem.position.left; } else { leftPos = Math.floor(highestLeft + 1); } desktopConfig.push({ name: val, item: `/home/${user}/desktop/${val}`, position: { custom: false, top: topPos, left: leftPos, }, }); await window.tb.fs.promises.writeFile(`/home/${user}/desktop/.desktop.json`, JSON.stringify(desktopConfig, null, 4)); window.dispatchEvent(new Event("upd-desktop")); }, }); }, }, { text: "New File", click: () => { window.tb.dialog.Message({ title: "Enter the new name of the file", onOk: async (val: any) => { const user = sessionStorage.getItem("currAcc"); await window.tb.fs.promises.writeFile(`/home/${user}/desktop/${val}`, "", "utf8"); const desktopConfig = JSON.parse(await window.tb.fs.promises.readFile(`/home/${user}/desktop/.desktop.json`, "utf8")); const getLastItem = () => { for (let i = desktopConfig.length - 1; i >= 0; i--) { if (!desktopConfig[i].position.custom) { return desktopConfig[i]; } } return null; }; const lastItem = getLastItem(); const highestLeft = Math.max(...desktopConfig.map((item: any) => item.position.left)); let topPos = 0; let leftPos = 0; if (lastItem && lastItem.position.top < 11) { topPos = Math.floor(lastItem.position.top + 1); leftPos = lastItem.position.left; } else { leftPos = Math.floor(highestLeft + 1); } desktopConfig.push({ name: val, item: `/home/${user}/desktop/${val}`, position: { custom: false, top: topPos, left: leftPos, }, }); await window.tb.fs.promises.writeFile(`/home/${user}/desktop/.desktop.json`, JSON.stringify(desktopConfig, null, 4)); window.dispatchEvent(new Event("upd-desktop")); }, }); }, }, { text: "New Shortcut", click: async () => { const make = async (item: any) => { const user = sessionStorage.getItem("currAcc"); const desktopConfig = JSON.parse(await window.tb.fs.promises.readFile(`/home/${user}/desktop/.desktop.json`, "utf8")); const getLastItem = () => { for (let i = desktopConfig.length - 1; i >= 0; i--) { if (!desktopConfig[i].position.custom) { return desktopConfig[i]; } } return null; }; const lastItem = getLastItem(); const highestLeft = Math.max(...desktopConfig.map((item: any) => item.position.left)); let topPos = 0; let leftPos = 0; if (lastItem && lastItem.position.top < 11) { topPos = Math.floor(lastItem.position.top + 1); leftPos = lastItem.position.left; } else { leftPos = Math.floor(highestLeft + 1); } if (topPos * 66 > window.innerHeight - 130) { leftPos = 1.3; topPos = 0; } const aname = item.split("/").pop(); if (aname.includes(".tapp")) { let tconf: any; if (await fileExists(`${item}/.tbconfig`)) { tconf = JSON.parse(await window.tb.fs.promises.readFile(`${item}/.tbconfig`, "utf8")); } else { tconf = JSON.parse(await window.tb.fs.promises.readFile(`${item}/index.json`, "utf8")); } await window.tb.fs.promises.writeFile( `${item}/desktopcfg.json`, JSON.stringify({ name: aname.replace(".tapp", ""), config: { ...(tconf.wmArgs ? tconf.wmArgs : tconf.config), icon: `/fs/${item}/${tconf.wmArgs ? tconf.wmArgs.icon : tconf.config.icon}`, src: `/fs/${item}/${tconf.wmArgs ? tconf.wmArgs.src : tconf.config.src}`, }, icon: `/fs/${item}/${tconf.icon}`, }), ); await window.tb.fs.promises.symlink(`${item}/desktopcfg.json`, `/home/${user}/desktop/${aname.replace(".tapp", "")}.lnk`, "file"); desktopConfig.push({ name: aname.replace(".tapp", ""), item: `/home/${user}/desktop/${aname.replace(".tapp", "")}.lnk`, position: { custom: false, top: topPos, left: leftPos, }, }); } else { desktopConfig.push({ name: aname.replace(".tapp", ""), item: item, position: { custom: false, top: topPos, left: leftPos, }, }); } await window.tb.fs.promises.writeFile(`/home/${user}/desktop/.desktop.json`, JSON.stringify(desktopConfig, null, 4)); window.dispatchEvent(new Event("upd-desktop")); }; await window.tb.dialog.Select({ title: "Select the type of Shortcut", options: [ { text: "Application", value: "app", }, { text: "Folder", value: "dir", }, { text: "File", value: "file", }, ], onOk: async (val: any) => { switch (val) { case "app": window.tb.dialog.DirectoryBrowser({ title: "Select a application", filter: ".tapp", onOk: async (val: any) => { make(val); }, }); break; case "dir": window.tb.dialog.DirectoryBrowser({ title: "Select a application", onOk: async (val: any) => { make(val); }, }); break; case "file": window.tb.dialog.FileBrowser({ title: "Select a application", onOk: async (val: any) => { make(val); }, }); break; } }, }); }, }, ], x: pos.x, y: pos.y, }); }} > {windowStore.windows.map((window: any) => { return ; })}
); }; export const createWindow = async (config: WindowConfig) => { const windowStore = useWindowStore.getState(); if (config.single) { const eWindow = windowStore.windows.find(window => window.src === config.src); if (eWindow) { if (config.message) { window.postMessage(config.message, "*"); } return; } } const addWindow = useWindowStore.getState().addWindow; addWindow(config); return true; }; export const removeWindow = (wid: string) => { // Did this for adding windows via COM const removeWindow = useWindowStore.getState().removeWindow; removeWindow(wid); }; export const killWindow = (wid: string) => { // Did this for adding windows via COM const killWindow = useWindowStore.getState().killWindow; killWindow(wid); }; export default WindowArea; ================================================ FILE: src/sys/gui/styles/boot.css ================================================ /** * Copyright 2026 snoot * * Licensed under the AGPL License, Version 3.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.gnu.org/licenses/agpl-3.0.en.html * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ .opt .back { scale: 0.96; transition: 120ms ease-in-out; border: 1px solid transparent; } .opt { cursor: var(--cursor-pointer); } .opt:hover .back { scale: 0.97; background-color: #ffffff0a; border: 1px solid #ffffff21; } .opt:hover .text { color: #6f6f6f; } .version { margin-left: 4px; } ================================================ FILE: src/sys/gui/styles/contextmenu.css ================================================ @keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes fade-out { 0% { opacity: 1; } 100% { opacity: 0; } } .fade-in { animation: fade-in 150ms ease-in-out forwards; } .fade-out { animation: fade-out 150ms ease-in-out forwards; pointer-events: none; } .context-menu { position: absolute; z-index: 9999; display: flex; flex-direction: column; width: max-content; border-radius: 8px; overflow: hidden; background-color: #ffffff10; color: #ffffff; box-shadow: 0px 0px 6px 0px #00000052, inset 0 0 0 0.5px #ffffff38; } .context-menu-button { display: flex; font-size: 18px; font-weight: 700; line-height: 1; padding: 10px 14px; transition: 150ms ease-in-out; width: 100%; user-select: none; cursor: var(--cursor-pointer); } .context-menu-button:hover { background-color: #ffffff3c; } ================================================ FILE: src/sys/gui/styles/cropper.css ================================================ /* * Cropper.js v1.6.1 * https://fengyuanchen.github.io/cropperjs * * Copyright 2015-present Chen Fengyuan * Released under the MIT license * * Date: 2023-09-17T03:44:17.565Z */ .cropper-container { direction: ltr; font-size: 0; line-height: 0; position: relative; -ms-touch-action: none; touch-action: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .cropper-container img { backface-visibility: hidden; display: block; height: 100%; image-orientation: 0deg; max-height: none !important; max-width: none !important; min-height: 0 !important; min-width: 0 !important; width: 100%; } .cropper-wrap-box, .cropper-canvas, .cropper-drag-box, .cropper-crop-box, .cropper-modal { bottom: 0; left: 0; position: absolute; right: 0; top: 0; } .cropper-wrap-box, .cropper-canvas { overflow: hidden; } .cropper-drag-box { background-color: #fff; opacity: 0; } .cropper-modal { background-color: #000; opacity: 0.5; } .cropper-view-box { display: block; height: 100%; outline: 1px solid #39f; outline-color: rgba(51, 153, 255, 0.75); overflow: hidden; width: 100%; } .cropper-dashed { border: 0 dashed #eee; display: block; opacity: 0.5; position: absolute; } .cropper-dashed.dashed-h { border-bottom-width: 1px; border-top-width: 1px; height: calc(100% / 3); left: 0; top: calc(100% / 3); width: 100%; } .cropper-dashed.dashed-v { border-left-width: 1px; border-right-width: 1px; height: 100%; left: calc(100% / 3); top: 0; width: calc(100% / 3); } .cropper-center { display: block; height: 0; left: 50%; opacity: 0.75; position: absolute; top: 50%; width: 0; } .cropper-center::before, .cropper-center::after { background-color: #eee; content: " "; display: block; position: absolute; } .cropper-center::before { height: 1px; left: -3px; top: 0; width: 7px; } .cropper-center::after { height: 7px; left: 0; top: -3px; width: 1px; } .cropper-face, .cropper-line, .cropper-point { display: block; height: 100%; opacity: 0.1; position: absolute; width: 100%; } .cropper-face { background-color: #fff; left: 0; top: 0; } .cropper-line { background-color: #39f; } .cropper-line.line-e { cursor: var(--cursor-e-resize); right: -3px; top: 0; width: 5px; } .cropper-line.line-n { cursor: var(--cursor-n-resize); height: 5px; left: 0; top: -3px; } .cropper-line.line-w { cursor: var(--cursor-w-resize); left: -3px; top: 0; width: 5px; } .cropper-line.line-s { bottom: -3px; cursor: var(--cursor-s-resize); height: 5px; left: 0; } .cropper-point { background-color: #39f; height: 5px; opacity: 0.75; width: 5px; } .cropper-point.point-e { cursor: var(--cursor-e-resize); margin-top: -3px; right: -3px; top: 50%; } .cropper-point.point-n { cursor: var(--cursor-n-resize); left: 50%; margin-left: -3px; top: -3px; } .cropper-point.point-w { cursor: var(--cursor-w-resize); left: -3px; margin-top: -3px; top: 50%; } .cropper-point.point-s { bottom: -3px; cursor: var(--cursor-s-resize); left: 50%; margin-left: -3px; } .cropper-point.point-ne { cursor: var(--cursor-ne-resize); right: -3px; top: -3px; } .cropper-point.point-nw { cursor: var(--cursor-nw-resize); left: -3px; top: -3px; } .cropper-point.point-sw { bottom: -3px; cursor: var(--cursor-sw-resize); left: -3px; } .cropper-point.point-se { bottom: -3px; cursor: var(--cursor-se-resize); height: 20px; opacity: 1; right: -3px; width: 20px; } @media (min-width: 768px) { .cropper-point.point-se { height: 15px; width: 15px; } } @media (min-width: 992px) { .cropper-point.point-se { height: 10px; width: 10px; } } @media (min-width: 1200px) { .cropper-point.point-se { height: 5px; opacity: 0.75; width: 5px; } } .cropper-point.point-se::before { background-color: #39f; bottom: -50%; content: " "; display: block; height: 200%; opacity: 0; position: absolute; right: -50%; width: 200%; } .cropper-invisible { opacity: 0; } .cropper-bg { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC"); } .cropper-hide { display: block; height: 0; position: absolute; width: 0; } .cropper-hidden { display: none !important; } .cropper-move { cursor: var(--cursor-crosshair); } .cropper-crop { cursor: var(--cursor-crosshair); } .cropper-disabled .cropper-drag-box, .cropper-disabled .cropper-face, .cropper-disabled .cropper-line, .cropper-disabled .cropper-point { cursor: not-allowed; } ================================================ FILE: src/sys/gui/styles/dialog.css ================================================ @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes fade-out { from { opacity: 1; } to { opacity: 0; } } .dialog-container { position: fixed; inset: 0; background-color: #00000078; backdrop-filter: blur(6px); display: flex; flex-direction: column; align-items: center; justify-content: center; transition: 150ms ease-in-out; z-index: 9999999999999999999999; } .dialog-container.fade-in { animation: fade-in 150ms ease-in-out; } .dialog-container.fade-out { animation: fade-out 150ms ease-in-out; } @media screen and (max-width: 1920px) { .dialog-container .dialog { min-width: 600px; } } @media screen and (max-width: 800px) { .dialog-container .dialog { min-width: 400px; } } @media screen and (max-width: 645px) { .dialog-container .dialog { min-width: 340px; } } .dialog-container .dialog { display: flex; flex-direction: column; background-color: #ffffff28; color: #ffffff; padding: 10px; gap: 10px; backdrop-filter: blur(100px); border-radius: 12px; } .dialog-container .dialog .dialog-title { font-family: Inter; font-size: 20px; line-height: 20px; font-weight: 700; padding: 4px 0px 4px 4px; } .dialog-container .dialog .dialog-message { font-family: Inter; font-size: 20px; line-height: 20px; font-weight: 700; padding: 4px 0px 4px 4px; text-align: center; } .dialog-container .dialog .dialog-files { position: relative; min-height: 100px; max-height: 300px; background-color: #00000068; border: 2px solid #ffffff28; border-radius: 8px; } .dialog-container .dialog .dialog-buttons { display: flex; justify-content: space-between; } .dialog-container .dialog .dialog-buttons .disabled { background-color: #ffffff10; font-family: Inter; font-size: 16px; font-weight: 800; line-height: 16px; padding: 12px 12px; border-radius: 8px; border: 2px solid #ffffff28; transition: 150ms ease-in-out; cursor: var(--cursor-normal); z-index: -999; } .dialog-container .dialog .dialog-buttons .dialog-action-buttons { display: flex; gap: 10px; } .dialog-container .dialog .dialog-input { background-color: #ffffff38; border: none; outline: none; color: #ffffff; padding: 4px 8px; border-radius: 6px; transition: 150ms ease-in-out; cursor: var(--cursor-text); font-weight: 600; } .dialog-container .dialog .file-info { margin-top: 10px; background-color: #21212130; } .dialog-container .dialog .file-info:hover { margin-top: 10px; background-color: #58585830; } .dialog-container .dialog .file-name { margin-top: -34px; margin-left: 56px; } ================================================ FILE: src/sys/gui/styles/dock.css ================================================ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-thumb { background-color: #ffffff28; border-radius: 8px; } ::-webkit-scrollbar-track { background-color: #ffffff10; border-radius: 8px; } ================================================ FILE: src/sys/gui/styles/dropdown.css ================================================ .dropdown { position: relative; display: flex; flex-direction: column; } .dropdown .dropdown-title { display: flex; align-items: center; justify-content: space-between; padding: 8px 6px 8px 12px; border-radius: 8px; gap: 20px; color: #ffffff; backdrop-filter: blur(20px); box-shadow: 0px 0px 6px 0px #00000052, inset 0 0 0 0.5px #ffffff38; background-color: #ffffff10; font-weight: 700; cursor: var(--cursor-pointer); transition: all 150ms ease-in-out; } .dropdown .dropdown-title:hover { background-color: #ffffff28; } .dropdown .dropdown-title svg { stroke-width: 0.8px; stroke: #ffffff; } .dropdown .dropdown-options { display: flex; flex-direction: column; position: absolute; top: calc(100% + 6px); left: 0; width: 100%; opacity: 1; pointer-events: all; border-radius: 8px; color: #ffffff; backdrop-filter: blur(20px); box-shadow: 0px 0px 6px 0px #00000052, inset 0 0 0 0.5px #ffffff38; background-color: #ffffff10; transition: 150ms ease-in-out; overflow: hidden; } @keyframes not-active { 100% { height: 0px; } } .dropdown .dropdown-options:not(.active) { opacity: 0; pointer-events: none; animation: not-active 160ms ease-in-out forwards; } .dropdown .dropdown-options .dropdown-option { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 8px 12px; font-weight: 600; cursor: var(--cursor-pointer); transition: all 150ms ease-in-out; } .dropdown .dropdown-options .dropdown-option:hover { background-color: #ffffff28; } ================================================ FILE: src/sys/gui/styles/liquor.css ================================================ @keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes fade-out { 0% { opacity: 1; } 100% { opacity: 0; } } .fade-in { animation: fade-in 150ms ease-in-out forwards; } .fade-out { animation: fade-out 150ms ease-in-out forwards; } body .context-menu { background-color: #2020208c; color: #ffffff; } .context-menu { backdrop-filter: brightness(0.8) blur(100px); } .custom-menu { position: absolute; background-color: #ffffff1f; color: #fff; z-index: 1; display: flex; flex-direction: column; width: 200px; border-radius: 8px; overflow: hidden; z-index: 99999; } .custom-menu-item { background-color: transparent; display: flex; font-size: 15.5px; font-weight: 700; line-height: 18px; padding: 8px 8px; border-color: transparent; transition: 150ms ease-in-out; } body .custom-menu-item:hover { background-color: #ffffff1f; } ================================================ FILE: src/sys/gui/styles/loader.css ================================================ @keyframes breathe { 0%, 100% { transform: scale(1); opacity: 0.1; } 50% { transform: scale(1.1); opacity: 1; } } .breathe { animation: breathe 2s infinite; } ================================================ FILE: src/sys/gui/styles/login.css ================================================ .diffuse { filter: blur(10px); background: linear-gradient(120deg, #ff8686, #e09dff, #7ca1ff, #e09dff, #ff8686); } .broken_button { height: 42px; } ================================================ FILE: src/sys/gui/styles/mediaisland.css ================================================ .music-player { display: flex; flex-direction: column; justify-content: space-between; align-items: flex-start; padding: 20px; } .info { text-align: left; margin-top: -19px; } .playerctrl { display: flex; justify-content: flex-end; align-items: center; margin-bottom: 24px; margin-top: -30px; margin-left: 154px; } .playerctrl button { margin-left: 10px; width: 16px; } .seekbar { display: flex; justify-content: space-between; align-items: center; width: 100%; margin-top: -12px; } .bar { flex-grow: 1; height: 5px; background-color: #7d7d7d; margin: 0 10px; border-radius: 6px; } #currenttime, #endtime { margin: 0; font-size: 12px; color: white; } .track { color: white; font-size: 12px; font-weight: bolder; } .artist { color: #b8b8b8; font-weight: bolder; font-size: 10px; } ================================================ FILE: src/sys/gui/styles/notification.css ================================================ :root { font-family: Inter; color-scheme: light dark; color: #ffffffde; background-color: #000000; --cursor-normal: url("/cursors/dark/normal.svg") 6 0, default; --cursor-pointer: url("/cursors/dark/pointer.svg") 6 0, pointer; --cursor-text: url("/cursors/dark/text.svg") 10 0, text; --cursor-crosshair: url("/cursors/dark/crosshair.svg") 0 0, crosshair; --cursor-wait: url("/cursors/dark/wait.svg") 0 0, wait; --cursor-nw-resize: url("/cursors/dark/resize-l.svg") 0 0, nw-resize; --cursor-se-resize: url("/cursors/dark/resize-l.svg") 0 0, se-resize; --cursor-sw-resize: url("/cursors/dark/resize-r.svg") 0 0, sw-resize; --cursor-ne-resize: url("/cursors/dark/resize-r.svg") 0 0, ne-resize; --cursor-n-resize: url("/cursors/dark/resize-v.svg") 0 0, n-resize; --cursor-s-resize: url("/cursors/dark/resize-v.svg") 0 0, s-resize; --cursor-e-resize: url("/cursors/dark/resize-h.svg") 0 0, e-resize; --cursor-w-resize: url("/cursors/dark/resize-h.svg") 0 0, w-resize; } @keyframes anim0 { 0% { left: -35%; right: 100%; } 60% { left: 100%; right: -90%; } 100% { left: 100%; right: -90%; } } @keyframes anim1 { 0% { left: -200%; right: 100%; } 60% { left: 107%; right: -8%; } 100% { left: 107%; right: -8%; } } @keyframes fade-in { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } } @keyframes fade-out { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-20px); } } .notification-grid { position: fixed; top: 60px; right: 10px; width: 360px; height: max-content; display: grid; grid-template-columns: 300px; grid-template-rows: auto; gap: 10px; z-index: 9999; } .notification { height: 150px; width: inherit; font-family: Inter; background-color: #20202066; box-shadow: 0px 0px 6px 0px #00000052, inset 0 0 0 0.5px #ffffff38; padding: 10px; transition: 150ms ease-in-out; border-radius: 12px; color: white; z-index: 9999; background-image: url("/assets/img/notif_bg.svg"); background-repeat: no-repeat; background-position: center; background-size: cover; backdrop-filter: blur(100px); } .fade-in { animation: fade-in 150ms ease-in-out; } .fade-out { animation: fade-out 150ms ease-in-out; } .notification-info { display: flex; justify-content: space-between; } .notification .notification-buttons { display: flex; justify-content: space-between; position: absolute; bottom: 10px; left: 10px; right: 10px; } .notification .notification-buttons button { background-color: #ffffff48; font-family: Inter; font-size: 16px; font-weight: 800; line-height: 16px; padding: 8px 12px; border-radius: 8px; border: 2px solid #ffffff28; cursor: var(--cursor-pointer); transition: 150ms ease-in-out; } .notification .notification-buttons button:hover { background-color: #ffffff68; border: 2px solid #ffffff40; cursor: var(--cursor-pointer); } .notification .notification-buttons button.ok-button { background-color: #53f6748d; cursor: var(--cursor-pointer); } .notification .notification-buttons button.ok-button:hover { background-color: #53f674c5; cursor: var(--cursor-pointer); } .notification .notification-buttons .notification-action-buttons { display: flex; gap: 10px; } .notification .installNoti { background-color: #00000036; background-repeat: no-repeat; background-image: url("/assets/img/notif_ins.gif"); background-size: contain; border-radius: 13px; width: 99%; height: 22px; margin-top: 3.5px; } .notification .notification-input { background-color: #ffffff38; border: none; outline: none; color: #ffffff; padding: 4px 8px; border-radius: 6px; transition: 150ms ease-in-out; font-weight: 600; cursor: var(--cursor-text); } .notification .notification-img { width: 25px; height: 25px; } .notification .notification-application { display: flex; align-items: center; gap: 6px; font-weight: bolder; } .notification .notification-time { font-weight: bolder; color: #ffffff54; } .notification .notification-message { font-weight: bold; color: #ffffffb7; } .notification-center { position: fixed; top: 60px; right: 6px; width: 400px; height: calc(100% - calc(60px + 6px)); font-family: Inter; background-color: #20202066; box-shadow: 0px 0px 6px 0px #00000052, inset 0 0 0 0.5px #ffffff38; transition: 150ms ease-in-out; backdrop-filter: blur(100px); padding: 10px; border-radius: 12px; color: white; z-index: 9999; } .notification-center .notifications-list::-webkit-scrollbar { position: relative; width: 8px; height: calc(100% - 16px); display: flex; flex-direction: column; align-items: stretch; gap: 8px; padding: 8px; padding-right: 0; overflow: auto; scrollbar-width: thin; scrollbar-color: #ffffff30 transparent; } .notification-center .notifications-list::-webkit-scrollbar-track { background-color: transparent; } .notification-center .notifications-list::-webkit-scrollbar-thumb { background-color: #ffffff30; border-radius: 8px; border: 2px solid transparent; } .notification-center .notifications-list .notification-item .notification-application { position: relative; top: 0; left: 30px; margin-top: -25px; font-weight: bolder; } .notification-center .notifications-list .notification-item .notification-time { position: relative; top: 0; left: 200px; margin-top: -25px; } .notification-center .hidden { display: none; } .notification-center .notifications-list { overflow-y: auto; overflow-x: hidden; max-height: 100%; } .notification-center .notifications-list .notification-item { position: relative; } .notification-center .notifications-list .notification-item .select { position: absolute; top: 14px; right: 69px; background-color: #53f6748d; font-family: Inter; font-size: 10px; font-weight: 800; line-height: 10px; padding: 8px 8px; border-radius: 8px; border: 2px solid #ffffff28; transition: 150ms ease-in-out; cursor: var(--cursor-pointer); } .notification-center .notifications-list .notification-item .normal { position: absolute; top: 14px; right: 4px; background-color: #ffffff48; font-family: Inter; font-size: 10px; font-weight: 800; line-height: 10px; padding: 8px 8px; border-radius: 8px; border: 2px solid #ffffff28; transition: 150ms ease-in-out; cursor: var(--cursor-pointer); } ================================================ FILE: src/sys/gui/styles/oobe.css ================================================ .selector { position: relative; } .selector .title { display: flex; gap: 10px; justify-content: space-between; align-items: center; background-color: #ffffff28; color: #c2bebe; border-radius: 8px; padding: 10px 10px 10px 10px; cursor: var(--cursor-pointer); font-size: 18px; font-weight: 600; } .options { position: absolute; top: calc(100% + 10px); display: flex; flex-direction: column; width: 100%; background-color: #ffffff28; color: #ffffff; font-size: 18px; padding: 6px; font-weight: 600; border-radius: 8px; opacity: 0; pointer-events: none; transition: 150ms ease-in-out; } .options.open { opacity: 1; pointer-events: auto; } .option { padding: 8px 8px 6px 10px; background-color: transparent; border-radius: 8px; color: #c2bebe; cursor: var(--cursor-pointer); } .option:hover { background-color: #ffffff28; } .welcome { color: #ffffffa7; } .pag { box-shadow: inset 0 0 8px -2px transparent; } .pag:not(:disabled):hover { box-shadow: inset 0 0 8px -2px #00000075; background-color: #ffffff10; color: #ffffffcd; } .pag:disabled { background-color: transparent; border: 1px solid transparent; color: #ffffff21; } ================================================ FILE: src/sys/gui/styles/shell.css ================================================ .show_desk { background-color: #ffffff3e; } .island { position: relative; display: flex; flex-direction: row; align-items: center; justify-content: center; backdrop-filter: blur(8px); background-color: #2020208c; box-shadow: 0px 0px 6px 0px #00000052, inset 0 0 0 0.5px #ffffff38; color: #ffffff; box-shadow: 0px 0px 6px 0px #00000052, inset 0 0 0 0.5px #ffffff38; transition: 150ms ease-in-out; } /* .island.visible { min-height: 48px; border-radius: 8px; } */ /* .island.visible:not(.custom-padding):not(.text) { padding: 6px 10px; } */ .island.text { padding: 6px 16px !important; } .island .app .app_name { font-size: 20px; font-weight: 700; } .island .app .app_controls { display: flex; gap: 8px; align-items: center; } .island .app .app_controls .app_control { background-color: transparent; border: none; font-size: 14px; line-height: 14px; opacity: 0.8; transition: 150ms ease-in-out; } .island .app .app_controls .app_control:hover { opacity: 1; } .island .app .app_controls .app_control.hidden { display: none; } body.blurry .shell-menu { backdrop-filter: blur(8px); } .shell-menu { background-color: #00000028; color: #ffffff; } .shell-menu .shell-menu-item { color: #ffffff; } .shell-menu .shell-menu-item:hover { color: #ffffff; background-color: #ffffff28; } .shell-menu .shell-menu-item { transition: 150ms ease-in-out; cursor: var(--cursor-pointer); } ================================================ FILE: src/sys/gui/styles/wifi.css ================================================ @keyframes slideIn { 0% { opacity: 0; transform: translateY(-20px); } 100% { opacity: 1; transform: translateY(0); } } @keyframes slideOut { 0% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(-20px); } } .wifi-list { overflow-y: auto; overflow-x: hidden; max-height: 230px; display: none; } .wifi-list.open { display: block; position: fixed; top: 60px; right: 6px; width: 400px; height: 230px; font-family: Inter; background-color: #20202066; box-shadow: 0px 0px 6px 0px #00000052, inset 0 0 0 0.5px #ffffff38; transition: 150ms ease-in-out; -webkit-backdrop-filter: blur(100px); backdrop-filter: blur(100px); padding: 10px; border-radius: 12px; color: white; z-index: 9999; animation: slideIn 150ms ease-in-out forwards; } .wifi-list.open .btn.select { position: static; margin-top: 22px; margin-left: auto; width: 75px; height: 28px; background-color: #53f6748d; font-family: Inter; font-size: 10px; font-weight: 800; line-height: 10px; padding: 8px 8px; border-radius: 8px; border: 2px solid #ffffff28; transition: 150ms ease-in-out; } .wifi-list.open .btn { position: static; background-color: #ffffff48; font-family: Inter; font-size: 10px; font-weight: 800; line-height: 10px; padding: 8px 8px; margin-top: 22px; margin-left: auto; width: 75px; height: 28px; border-radius: 8px; border: 2px solid #ffffff28; transition: 150ms ease-in-out; } .wifi-list.open .btn.net { background-color: #ffffff48; font-family: Inter; font-size: 10px; font-weight: 800; line-height: 10px; padding: 8px 8px; margin-right: 37%; width: 100px; height: 28px; border-radius: 8px; border: 2px solid #ffffff28; transition: 150ms ease-in-out; } .wifi-list::-webkit-scrollbar { position: relative; width: 8px; height: calc(100% - 16px); display: flex; flex-direction: column; align-items: stretch; gap: 8px; padding: 8px; padding-right: 0; overflow: auto; scrollbar-width: thin; scrollbar-color: #ffffff30 transparent; } .wifi-list::-webkit-scrollbar-track { background-color: transparent; } .wifi-list::-webkit-scrollbar-thumb { background-color: #ffffff30; border-radius: 8px; border: 2px solid transparent; } ================================================ FILE: src/sys/gui/styles/win_switcher.css ================================================ .win-switcher-backdrop { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; padding: 2.25rem; opacity: 0; pointer-events: none; backdrop-filter: blur(8px); background: rgba(10, 10, 10, 0.18); transition: opacity 120ms ease-in; z-index: 999999999; } .win-switcher-backdrop.visible { opacity: 1; pointer-events: auto; } .win-switcher-panel { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 0.85rem; width: 45%; max-height: 100%; overflow-y: auto; padding: 0.95rem; border-radius: 0.95rem; background: #2020208c; background-image: url("/assets/img/grain.png"); border: 1px solid rgba(255, 255, 255, 0.08); box-shadow: 0 22px 62px rgba(0, 0, 0, 0.56), inset 0 1px 0 rgba(255, 255, 255, 0.12); transform: translateY(10px) scale(0.992); transition: transform 120ms ease-in; } .win-switcher-panel.visible { transform: translateY(0) scale(1); } .win-switcher-item { position: relative; display: flex; flex-direction: column; gap: 0.45rem; padding: 0.45rem; border-radius: 0.5rem; background: #0f0f0fa8; border: 1px solid rgba(255, 255, 255, 0.1); color: #ffffff; text-align: left; cursor: var(--cursor-pointer); transition: transform 120ms ease, box-shadow 120ms ease, background-color 120ms ease, border-color 120ms ease; } .win-switcher-item:hover { transform: translateY(-1px); background: color-mix(in srgb, #32ae62 22%, #0f0f0fa8); border-color: color-mix(in srgb, #32ae62 46%, rgba(255, 255, 255, 0.08)); } .win-switcher-item.active { background: color-mix(in srgb, #32ae62 30%, #0f0f0fa8); border-color: #32ae6294; box-shadow: 0 0 0 1px #32ae6232, 0 0 22px #32ae6248; transform: translateY(-2px); } .thumb-frame { position: relative; width: 100%; height: 128px; border-radius: 0.5rem; overflow: hidden; background: #1f1f1f; border: 1px solid rgba(255, 255, 255, 0.13); } .thumb-image, .thumb-fallback { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; object-fit: contain; } .thumb-fallback { background: #1f1f1f; } .thumb-fallback-icon { width: 58px; height: 58px; border-radius: 0.75rem; opacity: 0.94; filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.42)); } .thumb-loading-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: #111111b0; pointer-events: none; } .thumb-loading-spinner { width: 34px; height: 34px; } .item-meta { display: flex; align-items: center; gap: 0.45rem; min-width: 0; padding-inline: 0.15rem; padding-block: 0.15rem 0.1rem; } .item-icon { width: 16px; height: 16px; border-radius: 0.2rem; flex: none; } .item-title { font-size: 0.7rem; font-weight: 500; line-height: 1.2; color: rgba(255, 255, 255, 0.9); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .win-switcher-hint { position: absolute; bottom: 1.2rem; left: 50%; transform: translateX(-50%) translateY(4px); opacity: 0; pointer-events: none; padding: 0.45rem 0.7rem; font-size: 0.72rem; color: rgba(231, 244, 255, 0.95); background: rgba(7, 14, 20, 0.6); border: 1px solid rgba(192, 227, 255, 0.24); border-radius: 0.45rem; letter-spacing: 0.02em; transition: opacity 120ms ease, transform 120ms ease; } .win-switcher-hint.visible { opacity: 1; transform: translateX(-50%) translateY(0); } @media (max-width: 780px) { .win-switcher-backdrop { padding: 1.15rem; } .win-switcher-panel { grid-template-columns: repeat(auto-fit, minmax(165px, 1fr)); max-height: 78vh; } .thumb-frame { height: 112px; } } ================================================ FILE: src/sys/lemonade/app.ts ================================================ interface AppDetails { name: string; version: string; } export class App { private appDetails: AppDetails = { name: "Lemonade App", version: "1.0.0", }; getName(): string { return this.appDetails.name; } setName(name: string): void { this.appDetails.name = name; } getVersion(): string { return this.appDetails.version; } getPath(name: "home" | "appData" | "userData" | "temp" | "downloads" | "documents" | "desktop"): string { const username = sessionStorage.getItem("currAcc") || "user"; const pathMap: Record = { home: `/home/${username}`, appData: `/apps/user/${username}/`, userData: `/home/${username}/lemonade.conf`, temp: `/system/tmp/`, downloads: `/home/${username}/`, documents: `/home/${username}/Documents`, desktop: `/home/${username}/Desktop`, }; return pathMap[name] || `/home/${username}`; } getAppPath(): string { return "/"; } isReady(): boolean { return true; } whenReady(): Promise { return Promise.resolve(); } quit(): void { console.log("App quit requested - not implemented in web environment"); } exit(exitCode: number = 0): void { console.log(`App exit requested with code ${exitCode} - not implemented in web environment`); } relaunch(options?: { args?: string[]; execPath?: string }): void { console.log("App relaunch requested - not implemented in web environment", options); window.location.reload(); } focus(): void { window.focus(); } hide(): void { console.log("App hide requested - not implemented in web environment"); } show(): void { console.log("App show requested - not implemented in web environment"); } setAppLogsPath(path: string): void { console.log(`App logs path set to: ${path}`); } getLocale(): string { return navigator.language || "en-US"; } getSystemLocale(): string { return navigator.language || "en-US"; } isPackaged(): boolean { return false; } requestSingleInstanceLock(): boolean { return true; } hasSingleInstanceLock(): boolean { return true; } releaseSingleInstanceLock(): void { console.log("Single instance lock released"); } setAsDefaultProtocolClient(protocol: string, path?: string, args?: string[]): boolean { console.log(`Setting as default protocol client for ${protocol}`, path, args); return false; } isDefaultProtocolClient(protocol: string, path?: string, args?: string[]): boolean { console.log(`Checking if default protocol client for ${protocol}`, path, args); return false; } } export const app = new App(); ================================================ FILE: src/sys/lemonade/clipboard.ts ================================================ export class Clipboard { async readText(_type?: "selection" | "clipboard"): Promise { try { return await navigator.clipboard.readText(); } catch (error) { console.error("Failed to read clipboard:", error); return ""; } } async writeText(text: string, _type?: "selection" | "clipboard"): Promise { try { await navigator.clipboard.writeText(text); } catch (error) { console.error("Failed to write to clipboard:", error); } } async readHTML(_type?: "selection" | "clipboard"): Promise { try { const items = await navigator.clipboard.read(); for (const item of items) { if (item.types.includes("text/html")) { const blob = await item.getType("text/html"); return await blob.text(); } } return ""; } catch (error) { console.error("Failed to read HTML from clipboard:", error); return ""; } } async writeHTML(markup: string, _type?: "selection" | "clipboard"): Promise { try { const blob = new Blob([markup], { type: "text/html" }); await navigator.clipboard.write([ new ClipboardItem({ "text/html": blob, }), ]); } catch (error) { console.error("Failed to write HTML to clipboard:", error); } } async readImage(_type?: "selection" | "clipboard"): Promise { try { const items = await navigator.clipboard.read(); for (const item of items) { for (const mimeType of item.types) { if (mimeType.startsWith("image/")) { return await item.getType(mimeType); } } } return null; } catch (error) { console.error("Failed to read image from clipboard:", error); return null; } } async writeImage(image: any, _type?: "selection" | "clipboard"): Promise { console.log("writeImage not fully implemented", image); // Would need proper image handling } clear(_type?: "selection" | "clipboard"): void { this.writeText(""); } availableFormats(_type?: "selection" | "clipboard"): string[] { console.log("availableFormats called - limited in web environment"); return []; } has(format: string, _type?: "selection" | "clipboard"): boolean { console.log(`has(${format}) called - limited in web environment`); return false; } read(format: string): string { console.log(`read(${format}) called - limited in web environment`); return ""; } write(data: any, type?: "selection" | "clipboard"): void { console.log("write called with data:", data, type); if (data.text) { this.writeText(data.text, type); } if (data.html) { this.writeHTML(data.html, type); } } readFindText(): string { return ""; } writeFindText(text: string): void { console.log("writeFindText:", text); } readBookmark(): { title: string; url: string } { return { title: "", url: "" }; } writeBookmark(title: string, url: string, type?: "selection" | "clipboard"): void { console.log(`writeBookmark: ${title} - ${url}`, type); } } export const clipboard = new Clipboard(); ================================================ FILE: src/sys/lemonade/dialog.ts ================================================ interface diagArgs { title?: string; defaultPath?: string; properties?: ("openFile" | "openDirectory" | "multiSelections" | "showHiddenFiles")[]; buttonLabel?: string; filters?: { name: string; extensions: string[] }[]; message?: string; } interface MessageBoxOptions { message: string; title?: string; type?: "none" | "info" | "error" | "question" | "warning"; buttons?: string[]; defaultId?: number; cancelId?: number; noLink?: boolean; normalizeAccessKeys?: boolean; } export class Dialog { showOpenDialogSync(win: any, options: diagArgs) { console.log(`property: ${win} wont be used sorry`); return new Promise((resolve, reject) => { window.tb.dialog.FileBrowser({ title: options.title || "Open File", defualtDir: options.defaultPath || "/", onOk: (path: string) => { resolve(path); console.log(path); }, onCancel: () => reject("canceled"), }); }); } showOpenDialog(win: any, options: diagArgs) { return this.showOpenDialogSync(win, options); } showSaveDialogSync(win: any, options: diagArgs) { console.log(`property: ${win} wont be used sorry`); return new Promise((resolve, reject) => { window.tb.dialog.SaveFile({ title: options.title || "Save File", defualtDir: options.defaultPath || "/", onOk: (path: string) => { resolve(path); console.log(path); }, onCancel: () => reject("canceled"), }); }); } showSaveDialog(win: any, options: diagArgs) { return this.showSaveDialogSync(win, options); } showMessageBoxSync(_win: any, options: MessageBoxOptions) { console.log("Using Terbium dialog system"); return new Promise(resolve => { window.tb.dialog.Message({ title: options.title || "Message", defaultValue: options.message, onOk: () => { resolve({ response: options.defaultId || 0, checkboxChecked: false }); }, }); }); } showMessageBox(win: any, options: MessageBoxOptions) { return this.showMessageBoxSync(win, options); } showErrorBox(title: string, content: string) { window.tb.dialog.Alert({ title: title, message: content, onOk: () => {}, }); } showCertificateTrustDialog(_win: any, _options: { certificate: any; message: string }) { console.log("Certificate trust dialog not implemented in web environment"); return Promise.resolve(); } } ================================================ FILE: src/sys/lemonade/index.ts ================================================ import { Notification } from "./notification"; import { BrowserWindow } from "./window"; import { Dialog } from "./dialog"; import { Net } from "./net"; import { app, App } from "./app"; import { shell, Shell } from "./shell"; import { clipboard, Clipboard } from "./clipboard"; import { ipcRenderer, ipcMain, IpcRenderer, IpcMain } from "./ipc"; import { screen, Screen } from "./screen"; export class Lemonade { get version(): string { return "1.1.0"; } Notification = Notification; BrowserWindow = BrowserWindow; App = App; Net = Net; Dialog = Dialog; Shell = Shell; Clipboard = Clipboard; IpcRenderer = IpcRenderer; IpcMain = IpcMain; Screen = Screen; dialog = new Dialog(); net = new Net(); app = app; shell = shell; clipboard = clipboard; ipcRenderer = ipcRenderer; ipcMain = ipcMain; screen = screen; } export { Notification, BrowserWindow, Dialog, Net, app, shell, clipboard, ipcRenderer, ipcMain, screen }; ================================================ FILE: src/sys/lemonade/ipc.ts ================================================ type IpcHandler = (event: any, ...args: any[]) => any; export class IpcRenderer { private handlers: Map = new Map(); private onceHandlers: Map = new Map(); on(channel: string, listener: IpcHandler): this { if (!this.handlers.has(channel)) { this.handlers.set(channel, []); } this.handlers.get(channel)!.push(listener); return this; } once(channel: string, listener: IpcHandler): this { if (!this.onceHandlers.has(channel)) { this.onceHandlers.set(channel, []); } this.onceHandlers.get(channel)!.push(listener); return this; } off(channel: string, listener: IpcHandler): this { const handlers = this.handlers.get(channel); if (handlers) { const index = handlers.indexOf(listener); if (index > -1) { handlers.splice(index, 1); } } return this; } removeListener(channel: string, listener: IpcHandler): this { return this.off(channel, listener); } removeAllListeners(channel?: string): this { if (channel) { this.handlers.delete(channel); this.onceHandlers.delete(channel); } else { this.handlers.clear(); this.onceHandlers.clear(); } return this; } send(channel: string, ...args: any[]): void { console.log(`IPC Send: ${channel}`, args); window.postMessage( { type: "ipc-message", channel, args, }, "*", ); } sendSync(channel: string, ...args: any[]): any { console.log(`IPC SendSync: ${channel}`, args); return null; } invoke(channel: string, ...args: any[]): Promise { console.log(`IPC Invoke: ${channel}`, args); return new Promise((resolve, reject) => { const messageId = Math.random().toString(36).substring(7); const responseHandler = (event: MessageEvent) => { if (event.data?.type === "ipc-response" && event.data?.messageId === messageId) { window.removeEventListener("message", responseHandler); if (event.data.error) { reject(event.data.error); } else { resolve(event.data.result); } } }; window.addEventListener("message", responseHandler); window.postMessage( { type: "ipc-invoke", channel, args, messageId, }, "*", ); setTimeout(() => { window.removeEventListener("message", responseHandler); reject(new Error(`IPC invoke timeout: ${channel}`)); }, 30000); }); } sendToHost(channel: string, ...args: any[]): void { console.log(`IPC SendToHost: ${channel}`, args); this.send(channel, ...args); } _triggerEvent(channel: string, ...args: any[]): void { const event = { sender: this }; const handlers = this.handlers.get(channel); if (handlers) { handlers.forEach(handler => handler(event, ...args)); } const onceHandlers = this.onceHandlers.get(channel); if (onceHandlers) { onceHandlers.forEach(handler => handler(event, ...args)); this.onceHandlers.delete(channel); } } } export class IpcMain { private handlers: Map = new Map(); private handlersOnce: Map = new Map(); private invokeHandlers: Map = new Map(); on(channel: string, listener: IpcHandler): this { if (!this.handlers.has(channel)) { this.handlers.set(channel, []); } this.handlers.get(channel)!.push(listener); return this; } once(channel: string, listener: IpcHandler): this { if (!this.handlersOnce.has(channel)) { this.handlersOnce.set(channel, []); } this.handlersOnce.get(channel)!.push(listener); return this; } removeListener(channel: string, listener: IpcHandler): this { const handlers = this.handlers.get(channel); if (handlers) { const index = handlers.indexOf(listener); if (index > -1) { handlers.splice(index, 1); } } return this; } removeAllListeners(channel?: string): this { if (channel) { this.handlers.delete(channel); this.handlersOnce.delete(channel); this.invokeHandlers.delete(channel); } else { this.handlers.clear(); this.handlersOnce.clear(); this.invokeHandlers.clear(); } return this; } handle(channel: string, listener: IpcHandler): void { this.invokeHandlers.set(channel, listener); } handleOnce(channel: string, listener: IpcHandler): void { const wrapper: IpcHandler = (event, ...args) => { this.invokeHandlers.delete(channel); return listener(event, ...args); }; this.invokeHandlers.set(channel, wrapper); } removeHandler(channel: string): void { this.invokeHandlers.delete(channel); } } export const ipcRenderer = new IpcRenderer(); export const ipcMain = new IpcMain(); ================================================ FILE: src/sys/lemonade/net.ts ================================================ interface RequestOptions { method?: string; headers?: Record; body?: any; redirect?: "follow" | "error" | "manual"; signal?: AbortSignal; timeout?: number; } export class Net { async request(url: string, options: RequestOptions = {}): Promise { const controller = options.timeout ? new AbortController() : null; const timeoutId = options.timeout ? setTimeout(() => controller?.abort(), options.timeout) : null; try { const response = await window.tb.libcurl.fetch(url, { method: options.method || "GET", headers: options.headers || {}, body: options.body ? JSON.stringify(options.body) : undefined, signal: options.signal || controller?.signal, }); return response; } finally { if (timeoutId) clearTimeout(timeoutId); } } fetch(url: string, options: RequestOptions = {}): Promise { return this.request(url, options); } isOnline(): boolean { return navigator.onLine; } getOnlineStatus(): "online" | "offline" { return navigator.onLine ? "online" : "offline"; } createClientRequest(options: { url?: string; method?: string; protocol?: string; host?: string; port?: number; path?: string }): any { const url = options.url || `${options.protocol || "https:"}//${options.host}${options.port ? `:${options.port}` : ""}${options.path || "/"}`; const method = options.method || "GET"; const request = { url, method, headers: {} as Record, body: null as any, setHeader: (name: string, value: string) => { request.headers[name] = value; }, write: (chunk: any) => { request.body = request.body ? request.body + chunk : chunk; }, end: async () => { return await this.request(url, { method, headers: request.headers, body: request.body, }); }, abort: () => { console.log("Request aborted"); }, on: (event: string, _listener: Function) => { console.log(`Event listener added for: ${event}`); }, }; return request; } } ================================================ FILE: src/sys/lemonade/notification.ts ================================================ interface NotificationOptions { title: string; subtitle: string; body: string; silent: boolean; icon: string; } type NotificationEvent = "click"; export class Notification { private eventHandlers: { [K in NotificationEvent]?: ((...args: any[]) => void)[] } = {}; static isSupported(): boolean { return true; } constructor(options: NotificationOptions) { window.tb.notification.Toast({ message: options.body, iconSrc: options.icon || "/assets/img/logo.png", application: options.title || "Lemonade Communicator", onOk: (...args: any[]) => { this.emit("click", ...args); }, }); } on(event: NotificationEvent, handler: (...args: any[]) => void): void { if (!this.eventHandlers[event]) { this.eventHandlers[event] = []; } this.eventHandlers[event]!.push(handler); } off(event: NotificationEvent, handler: (...args: any[]) => void): void { const handlers = this.eventHandlers[event]; if (handlers) { this.eventHandlers[event] = handlers.filter(h => h !== handler); } } private emit(event: NotificationEvent, ...args: any[]): void { const handlers = this.eventHandlers[event]; if (handlers) { handlers.forEach(handler => handler(...args)); } } show(): string { return "API Stub"; } close(): string { return "API Stub"; } } ================================================ FILE: src/sys/lemonade/screen.ts ================================================ interface Display { id: number; bounds: { x: number; y: number; width: number; height: number }; workArea: { x: number; y: number; width: number; height: number }; size: { width: number; height: number }; workAreaSize: { width: number; height: number }; scaleFactor: number; rotation: number; internal: boolean; touchSupport: "available" | "unavailable" | "unknown"; } export class Screen { private eventHandlers: Map = new Map(); getCursorScreenPoint(): { x: number; y: number } { return { x: 0, y: 0 }; } getPrimaryDisplay(): Display { return { id: 1, bounds: { x: 0, y: 0, width: window.screen.width, height: window.screen.height, }, workArea: { x: 0, y: 0, width: window.screen.availWidth, height: window.screen.availHeight, }, size: { width: window.screen.width, height: window.screen.height, }, workAreaSize: { width: window.screen.availWidth, height: window.screen.availHeight, }, scaleFactor: window.devicePixelRatio, rotation: 0, internal: false, touchSupport: "ontouchstart" in window ? "available" : "unavailable", }; } getAllDisplays(): Display[] { return [this.getPrimaryDisplay()]; } getDisplayNearestPoint(_point: { x: number; y: number }): Display { return this.getPrimaryDisplay(); } getDisplayMatching(_rect: { x: number; y: number; width: number; height: number }): Display { return this.getPrimaryDisplay(); } on(event: "display-added" | "display-removed" | "display-metrics-changed", listener: Function): this { if (!this.eventHandlers.has(event)) { this.eventHandlers.set(event, []); } this.eventHandlers.get(event)!.push(listener); return this; } removeListener(event: string, listener: Function): this { const handlers = this.eventHandlers.get(event); if (handlers) { const index = handlers.indexOf(listener); if (index > -1) { handlers.splice(index, 1); } } return this; } } export const screen = new Screen(); ================================================ FILE: src/sys/lemonade/shell.ts ================================================ export class Shell { async openExternal(url: string, options?: { activate?: boolean }): Promise { console.log(`Opening external URL: ${url}`, options); window.open(url, "_blank"); } async openPath(path: string) { console.log(`Opening path: ${path}`); window.tb.system.openApp("Files"); } showItemInFolder(fullPath: string): void { console.log(`Showing item in folder: ${fullPath}`); const lastSlash = fullPath.lastIndexOf("/"); const directory = lastSlash > 0 ? fullPath.substring(0, lastSlash) : "/"; this.openPath(directory); } async moveItemToTrash(fullPath: string): Promise { console.log(`Moving item to trash: ${fullPath}`); try { const trashPath = `/system/trash`; const fileName = fullPath.substring(fullPath.lastIndexOf("/") + 1); const destPath = `${trashPath}/${fileName}`; try { await window.tb.fs.promises.mkdir(trashPath); } catch {} await window.tb.fs.promises.rename(fullPath, destPath); return true; } catch (error) { console.error("Failed to move item to trash:", error); return false; } } async trashItem(path: string): Promise { await this.moveItemToTrash(path); } beep(): void { const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.frequency.value = 800; oscillator.type = "sine"; gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.1); } writeShortcutLink(shortcutPath: string, operation: string, options: any): boolean { console.log(`Writing shortcut link: ${shortcutPath}`, operation, options); return false; } readShortcutLink(shortcutPath: string): any { console.log(`Reading shortcut link: ${shortcutPath}`); return null; } } export const shell = new Shell(); ================================================ FILE: src/sys/lemonade/window.ts ================================================ import { UserSettings } from "../types"; interface ElectronWinArgs { width?: number; height?: number; minWidth?: number; minHeight?: number; maxWidth?: number; maxHeight?: number; x?: number; y?: number; title?: string; icon?: string; resizable?: boolean; maximizable?: boolean; minimizable?: boolean; closable?: boolean; alwaysOnTop?: boolean; fullscreen?: boolean; skipTaskbar?: boolean; backgroundColor?: string; show?: boolean; frame?: boolean; parent?: BrowserWindow; modal?: boolean; webPreferences?: { nodeIntegration?: boolean; contextIsolation?: boolean; preload?: string; devTools?: boolean; webSecurity?: boolean; allowRunningInsecureContent?: boolean; sandbox?: boolean; }; } export class BrowserWindow { private eventHandlers: Map = new Map(); private _isDestroyed: boolean = false; constructor(args: ElectronWinArgs = {}) { window.tb.window.create({ title: args.title || "Lemonade Instance", size: { width: args.width || 500, height: args.height || 500, minWidth: args.minWidth || 300, minHeight: args.minHeight || 300, }, icon: args.icon || "/assets/img/logo.png", resizable: args.resizable !== false, maximizable: args.maximizable !== false, minimizable: args.minimizable !== false, src: "about:blank", }); } loadFile(path: string) { window.tb.window.changeSrc(`/fs/${path}`); } async loadURL(src: string) { const settings: UserSettings = JSON.parse(await window.tb.fs.promises.readFile(`/home/${sessionStorage.getItem("currAcc")}/settings.json`, "utf8")); window.tb.window.changeSrc(settings.proxy === "Ultraviolet" ? `/uv/service/${await window.tb.proxy.encode(src, "XOR")}` : `/service/${await window.tb.proxy.encode(src, "XOR")}`); } destroy() { if (this._isDestroyed) return; this._isDestroyed = true; this.emit("closed"); window.tb.window.close(); } close() { if (this._isDestroyed) return; this.emit("close"); this.destroy(); } show() { this.emit("show"); console.log("Window show"); } blur() { this.emit("blur"); window.blur(); } focus() { this.emit("focus"); window.focus(); } hide() { this.emit("hide"); window.tb.window.minimize(); } minimize() { this.emit("minimize"); window.tb.window.minimize(); } maximize() { this.emit("maximize"); window.tb.window.maximize?.(); } unmaximize() { this.emit("unmaximize"); console.log("API Stub"); } isMaximized(): boolean { return false; } isMinimized(): boolean { return false; } isVisible(): boolean { return !this.isMinimized(); } isDestroyed(): boolean { return this._isDestroyed; } setTitle(title: string) { window.tb.window.titlebar.setText(title); } getTitle(): string { return document.title; } setSize(width: number, height: number, _animate?: boolean) { console.log(`setSize called: ${width}x${height}`); } getSize(): [number, number] { return [window.innerWidth, window.innerHeight]; } setPosition(x: number, y: number, _animate?: boolean) { console.log(`setPosition called: ${x}, ${y}`); } getPosition(): [number, number] { return [window.screenX, window.screenY]; } setAlwaysOnTop(flag: boolean, level?: string, relativeLevel?: number) { console.log("setAlwaysOnTop:", flag, level, relativeLevel); } isAlwaysOnTop(): boolean { return false; } center() { const [width, height] = this.getSize(); const x = (window.screen.width - width) / 2; const y = (window.screen.height - height) / 2; this.setPosition(x, y); } setFullScreen(flag: boolean) { if (flag) { document.documentElement.requestFullscreen?.(); } else { document.exitFullscreen?.(); } } isFullScreen(): boolean { return !!document.fullscreenElement; } setResizable(resizable: boolean) { console.log("setResizable:", resizable); } isResizable(): boolean { return true; } setMovable(movable: boolean) { console.log("setMovable:", movable); } isMovable(): boolean { return true; } setMinimizable(minimizable: boolean) { console.log("setMinimizable:", minimizable); } isMinimizable(): boolean { return true; } setMaximizable(maximizable: boolean) { console.log("setMaximizable:", maximizable); } isMaximizable(): boolean { return true; } setClosable(closable: boolean) { console.log("setClosable:", closable); } isClosable(): boolean { return true; } flashFrame(flag: boolean) { console.log("flashFrame:", flag); } on(event: string, listener: Function): this { if (!this.eventHandlers.has(event)) { this.eventHandlers.set(event, []); } this.eventHandlers.get(event)!.push(listener); return this; } once(event: string, listener: Function): this { const wrapper = (...args: any[]) => { listener(...args); this.removeListener(event, wrapper); }; return this.on(event, wrapper); } removeListener(event: string, listener: Function): this { const handlers = this.eventHandlers.get(event); if (handlers) { const index = handlers.indexOf(listener); if (index > -1) { handlers.splice(index, 1); } } return this; } removeAllListeners(event?: string): this { if (event) { this.eventHandlers.delete(event); } else { this.eventHandlers.clear(); } return this; } private emit(event: string, ...args: any[]): void { const handlers = this.eventHandlers.get(event); if (handlers) { handlers.forEach(handler => handler(...args)); } } webContents = { send: (channel: string, ...args: any[]) => { console.log("webContents.send:", channel, args); }, openDevTools: () => { console.log("DevTools opened (if available)"); }, closeDevTools: () => { console.log("DevTools closed"); }, isDevToolsOpened: () => false, loadURL: (url: string) => this.loadURL(url), loadFile: (path: string) => this.loadFile(path), executeJavaScript: async (code: string) => { try { return eval(code); } catch (error) { console.error("executeJavaScript error:", error); throw error; } }, }; } ================================================ FILE: src/sys/libcurl.d.ts ================================================ declare module "libcurl.js" { export * from "libcurl.js/bundled"; export { default } from "libcurl.js/bundled"; } declare module "libcurl.js/bundled" { export type WebsocketUrl = `wss://${string}` | `ws://${string}`; export type ProxyUrl = `socks5h://${string}` | `socks4a://${string}` | `http://${string}`; export interface LibcurlVersion { lib: string; curl: string; ssl: string; brotli: string; nghttp2: string; protocols: string[]; wisp: string; } export interface HTTPSessionOptions { enable_cookies?: boolean; cookie_jar?: string; proxy?: ProxyUrl; } export interface SessionOptions { proxy?: ProxyUrl; verbose?: boolean; headers?: Record | Headers; } export interface WebSocketOptions extends SessionOptions { proxy?: ProxyUrl; } export interface TLSSocketOptions extends SessionOptions { verbose?: boolean; } export interface RequestCallbacks { end: (error: number) => void; data: (chunk: Uint8Array) => void; headers: (chunk: Uint8Array) => void; } export class HeadersDict { constructor(obj: Record); [key: string]: string; } export class CurlSession { constructor(options?: SessionOptions); session_ptr: number; active_requests: number; event_loop: null | any; requests_list: unknown[]; to_remove: unknown[]; end_callback_ptr: number; headers_callback_ptr: number; data_callback_ptr: number; request_callbacks: Record; last_request_id: number; options: SessionOptions; assert_ready(): void; set_connections(connections_limit: number, cache_limit: number, host_conn_limit?: number): void; end_callback(request_id: number, error: number): void; data_callback(request_id: number, chunk_ptr: number, chunk_size: number): void; headers_callback(request_id: number, chunk_ptr: number, chunk_size: number): void; create_request(url: string, js_data_callback: (chunk: Uint8Array) => void, js_end_callback: (error: number) => void, js_headers_callback: (chunk: Uint8Array) => void): number; remove_request_now(request_ptr: number): void; remove_request(request_ptr: number): void; start_request(request_ptr: number): void; close(): void; } export class HTTPSession extends CurlSession { constructor(options?: HTTPSessionOptions); base_url?: string; cookie_filename: string; import_cookies(): void; export_cookies(): string; close(): void; request_async(url: string, params: RequestInit, body?: Uint8Array): Promise; fetch(resource: string | URL | Request, params?: RequestInit): Promise; static create_response(response_data: Uint8Array | null, response_info: any): Response; static create_options(params: RequestInit): Promise; } export class CurlWebSocket extends CurlSession { constructor(url: WebsocketUrl, protocols?: string[], options?: WebSocketOptions); url: WebsocketUrl; protocols: string[]; connected: boolean; recv_loop: number | null; http_handle: number | null; recv_buffer: unknown[]; onopen: () => void; onerror: (error?: any) => void; onmessage: (data: string | Uint8Array) => void; onclose: () => void; connect(): void; send(data: string | Uint8Array): void; recv(): string | Uint8Array | null; close(): void; cleanup(error?: boolean | number): void; } export class FakeWebSocket extends EventTarget { constructor(url: WebsocketUrl, protocols?: string[], options?: WebSocketOptions); readonly CONNECTING: 0; readonly OPEN: 1; readonly CLOSING: 2; readonly CLOSED: 3; url: WebsocketUrl; protocols: string[]; options: WebSocketOptions; binaryType: "blob" | "arraybuffer"; status: 0 | 1 | 2 | 3; socket: CurlWebSocket | null; onopen: (event: Event) => void; onerror: (event: Event) => void; onmessage: (event: MessageEvent) => void; onclose: (event: CloseEvent) => void; connect(): void; send(data: string | Blob | ArrayBuffer | ArrayBufferView): void; close(): void; } export class TLSSocket extends CurlSession { constructor(hostname: string, port: number, options?: TLSSocketOptions); hostname: string; port: number; url: string; connected: boolean; recv_loop: number | null; onopen: () => void; onerror: (error?: any) => void; onmessage: (data: Uint8Array) => void; onclose: () => void; connect(): void; send(data: string | Uint8Array): void; recv(): Uint8Array | null; close(): void; cleanup(error?: boolean | number): void; } export interface WispConnection { [key: string]: unknown; } export class WispWebSocket { constructor(url: WebsocketUrl); } export function logger(type: "log" | "warn" | "error", text: string): void; export function log_msg(text: string): void; export function warn_msg(text: string): void; export function error_msg(text: string): void; export function data_to_array(data: string | ArrayBuffer | ArrayBufferView | Uint8Array): Uint8Array; export function merge_arrays(arrays: Uint8Array[]): Uint8Array; export function get_error_str(error_code: number): string; export function c_func(target: Function, args?: any[]): any; export function c_func_str(target: Function, args?: any[]): string; export const libcurl: { set_websocket: (url: WebsocketUrl) => void; fetch: (resource: string | URL | Request, params?: RequestInit) => Promise; load_wasm: (wasmPath: string) => Promise; get_cacert: () => string; get_error_string: (error_code: number) => string; ready: boolean; websocket_url: WebsocketUrl | null; transport: "wisp" | "wsproxy" | string | Function; copyright: string; version: LibcurlVersion | null; HTTPSession: typeof HTTPSession; WebSocket: typeof FakeWebSocket; CurlWebSocket: typeof CurlWebSocket; TLSSocket: typeof TLSSocket; wisp: { wisp_connections: Record; WispConnection: typeof WispConnection; WispWebSocket: typeof WispWebSocket; }; stdout: (text: string) => void; stderr: (text: string) => void; logger: (type: "log" | "warn" | "error", text: string) => void; events: EventTarget; onload: (callback?: () => void) => void; }; export default libcurl; } ================================================ FILE: src/sys/liquor/AliceWM.ts ================================================ import { createWindow } from "../gui/WindowArea"; import { ExternalApp } from "./coreapps/ExternalApp"; export interface WindowInformation { title: string; width: string | number; minwidth: number; height: string | number; minheight: number; allowMultipleInstance: boolean; icon?: string; src?: string | URL | any; /** * @description For Terbium Compatability only */ msg?: any; } export const AliceWM = { create: function (givenWinInfo?: string | WindowInformation) { console.trace(); // Default param let wininfo: WindowInformation = { title: "Generic Window", minheight: 40, minwidth: 40, width: "1000px", height: "500px", allowMultipleInstance: false, }; // Param given in argument if (typeof givenWinInfo == "object") wininfo = givenWinInfo; if (typeof givenWinInfo == "string") { // Only title given wininfo.title = givenWinInfo; } console.log(givenWinInfo); console.log(wininfo); console.log(wininfo); const parseSize = (v: string | number) => { if (typeof v === "number") return v; const num = parseFloat(String(v).replace(/[^0-9.\-]/g, "")); return Number.isFinite(num) ? num : 0; }; let resp: (val: any) => void = () => {}; let er: (err: any) => void = () => {}; const ready = new Promise((resolve, reject) => { resp = resolve; er = reject; }); const obj: any = { element: null, content: null, // @ts-expect-error keep same behavior — ExternalApp may be nonstandard app: new ExternalApp(wininfo), dragForceX: 0, dragForceY: 0, dragging: false, height: wininfo.height, width: wininfo.width, pid: null, state: null, maximized: false, minimizing: false, mouseLeft: null, mouseTop: null, onclose: () => null, onfocus: () => null, onresize: (_w: number, _h: number) => null, onsnap: (_side: string) => null, onunmaximize: () => null, restoreSvg: null, kill: () => { if (obj.pid != null) window.tb.process.kill(obj.pid); }, get alive() { return window.tb.window.getId() === obj.pid; }, maximizeImg: null, maximizeSvg: null, wininfo, title: wininfo.title, }; obj.then = (onFulfilled: (val: any) => any, onRejected?: (err: any) => any) => ready.then(onFulfilled, onRejected); obj.catch = (onRejected: (err: any) => any) => ready.catch(onRejected); obj.finally = (cb: () => void) => ready.finally(cb); createWindow({ title: wininfo.title, icon: wininfo.icon || "/assets/img/logo.png", src: wininfo.src || "about:blank", size: { width: parseSize(wininfo.width), height: parseSize(wininfo.height), }, single: false, resizable: true, message: wininfo.msg, }) .then(n => { setTimeout(() => { try { console.log(`created ${n}`); // Sorry I had to use DOM it was like the only way for actual cross compatability const currPID = window.tb.window.getId(); const elem = document.querySelector(`div[pid="${currPID}"]`); const winControls = elem?.querySelectorAll(".controls.flex.gap-1") ?? []; window.tb.window.content.set("
"); obj.element = elem; obj.content = elem?.querySelector(".w-full.h-full"); // @ts-expect-error types obj.app = new ExternalApp(wininfo); obj.dragForceX = 0; obj.dragForceY = 0; obj.dragging = false; obj.height = wininfo.height; obj.width = wininfo.width; obj.pid = currPID; obj.state = null; obj.maximized = false; obj.minimizing = false; obj.mouseLeft = null; obj.mouseTop = null; obj.onclose = () => null; obj.onfocus = () => null; obj.onresize = (_w: number, _h: number) => null; obj.onsnap = (_side: string) => null; obj.onunmaximize = () => null; obj.restoreSvg = winControls[0]?.childNodes[1] || null; obj.maximizeImg = winControls[0]?.childNodes[1] || null; obj.maximizeSvg = winControls[0]?.childNodes[1] || null; obj.wininfo = wininfo; obj.title = wininfo.title; resp(obj); } catch (err) { er(err); } }, 500); }) .catch(err => { er(err); }); return obj; }, }; ================================================ FILE: src/sys/liquor/Anura.ts ================================================ // @ts-expect-error import { WindowInformation, AliceWM } from "./AliceWM"; import { WMAPI } from "./api/WmApi"; import { ContextMenuAPI } from "./api/ContextMenuAPI"; import { FilesAPI } from "./api/Files"; import { NotificationService } from "./api/NotificationService"; import { Settings } from "./api/Settings"; import { App } from "./coreapps/App"; import { ExternalApp } from "./coreapps/ExternalApp"; import { Networking } from "./api/Networking"; import { URIHandlerAPI } from "./api/URIHandler"; import { ExternalLib } from "./libs/ExternalLib"; import { Lib } from "./libs/lib"; import { Processes } from "./api/Process"; import { Platform } from "./api/Platform"; import { Dialog } from "./api/Dialog"; import { Systray } from "./api/Systray"; import { AnuraFilesystem } from "./api/Filesystem"; import { TFSProvider } from "./api/TFS"; import { AnuraUI } from "./api/UI"; declare global { interface Window { anura: Anura; } } export class Anura { version = { semantic: { major: "2", minor: "1", patch: "0", }, buildstate: "Stable", codename: `Liquor "Starboy" Stable`, get pretty() { const semantic = this.semantic; return `${semantic.major}.${semantic.minor}.${semantic.patch} ${this.buildstate}`; }, }; initComplete = false; // x86: null | V86Backend; settings: Settings; fs: AnuraFilesystem; config: any; net: Networking; notifications: NotificationService; // x86hdd: FakeFile; processes: Processes; ui = new AnuraUI(); dialog: Dialog; platform: Platform; systray: Systray; private constructor( fs: AnuraFilesystem, settings: Settings, config: any, // hdd: FakeFile, ) { this.fs = fs; this.settings = settings; this.config = config; // this.x86hdd = hdd; this.net = new Networking(); this.notifications = new NotificationService(); this.processes = new Processes(); this.ui = new AnuraUI(); this.platform = new Platform(); this.dialog = new Dialog(); this.systray = new Systray(); // @ts-expect-error this.fs.readdir("/apps/anura", (err: Error, files: string[]) => { // Fixes a weird edgecase that I was facing where no user apps are installed, nothing breaks it just throws an error which I would like to mitigate. if (files == undefined) return; files.forEach(file => { try { this.registerExternalApp("/fs/apps/anura/" + file); } catch (e) { this.logger.error("Anura failed to load an app " + e); } }); }); try { // @ts-expect-error this.fs.readdir("/system/lib/anura/", (err: Error, files: string[]) => { // Fixes a weird edgecase that I was facing where no user apps are installed, nothing breaks it just throws an error which I would like to mitigate. if (files == undefined) return; files.forEach(file => { try { this.fs.readFile( "/system/lib/anura//" + file, // @ts-expect-error function (err: Error, data: Uint8Array) { if (err) throw "Failed to read file"; try { eval(new TextDecoder("utf-8").decode(data)); } catch (e) { console.error(e); } }, ); } catch (e) { this.logger.error("Anura failed to load an app " + e); } }); }); } catch (e) { this.logger.error(e); } this.registerExternalApp("/apps/fsapp.app"); if (import.meta.env.DEV) { this.registerExternalLib("/public/apps/libfileview.lib"); this.registerExternalLib("/public/apps/libfilepicker.lib"); this.registerExternalLib("/public/apps/libpersist.lib"); } else { this.registerExternalLib("/apps/libfileview.lib"); this.registerExternalLib("/apps/libfilepicker.lib"); this.registerExternalLib("/apps/libpersist.lib"); } } static async new(config: any): Promise { // File System Initialization // const filerProvider = new TFSProvider(window.tb.fs); // @ts-expect-error const fs = new AnuraFilesystem([filerProvider]); // @ts-expect-error const settings = await Settings.new(fs, config.defaultsettings); // const hdd = await InitV86Hdd(); const anuraPartial = new Anura(fs, settings, config); (window as any).tb.liquor = anuraPartial; return anuraPartial; } wm = new WMAPI(); apps: any = {}; libs: any = {}; logger = { log: console.log.bind(console, "anuraOS:"), debug: console.debug.bind(console, "anuraOS:"), warn: console.warn.bind(console, "anuraOS:"), error: console.error.bind(console, "anuraOS:"), }; // net = new Networking(); async registerApp(app: App) { if (app.package in this.apps) { throw "Application already installed"; } const apps: any = JSON.parse(await window.tb.fs.promises.readFile("/system/var/terbium/start.json", "utf8")); // @ts-expect-error if (apps.system_apps.some(existingApp => existingApp.title === app.name)) { console.log("Application already installed"); } else { console.log(app); apps.system_apps.push({ title: app.name, icon: app.icon, // @ts-expect-error src: `${app.source}/${app.manifest.index}`, }); await window.tb.fs.promises.writeFile("/system/var/terbium/start.json", JSON.stringify(apps, null, 2)); window.dispatchEvent(new Event("updApps")); await window.tb.fs.promises.writeFile(`/system/etc/anura/configs/${app.name}.json`, JSON.stringify(app, null, 2)); const installedApps = JSON.parse(await window.tb.fs.promises.readFile("/apps/installed.json", "utf8")); installedApps.push({ name: app.name, config: `/system/etc/anura/configs/${app.name}.json`, user: "System", }); await window.tb.fs.promises.writeFile("/apps/installed.json", JSON.stringify(installedApps)); } this.apps[app.package] = { title: app.name, icon: app.icon, id: app.package, }; return app; } async registerExternalApp(source: string): Promise { const resp = await fetch(`${source}/manifest.json`); const manifest = (await resp.json()) as AppManifest; if (manifest.type === "auto" || manifest.type === "manual") { const app = new ExternalApp(manifest, source); await this.registerApp(app); return app; } const handlers = this.settings.get("ExternalAppHandlers"); if (!handlers || !handlers[manifest.type]) { const error = `Could not register external app from source: "${source}" because no external handlers are registered for type "${manifest.type}"`; this.notifications.add({ title: "AnuraOS", description: error, }); throw error; } const handler = handlers[manifest.type]; const handlerModule = await this.import(handler); if (!handlerModule) { const error = `Failed to load external app handler ${handler}`; this.notifications.add({ title: "AnuraOS", description: error, }); throw error; } if (!handlerModule.createApp) { const error = `Handler ${handler} does not have a createApp function`; this.notifications.add({ title: "AnuraOS", description: error, }); throw error; } const app = handlerModule.createApp(manifest, source); await this.registerApp(app); // This will let us capture error messages return app; } registerExternalAppHandler(id: string, handler: string) { const handlers = this.settings.get("ExternalAppHandlers") || {}; handlers[handler] = id; this.settings.set("ExternalAppHandlers", handlers); } async registerLib(lib: Lib) { if (lib.package in this.libs) { throw "Library already installed"; } this.libs[lib.package] = lib; return lib; } async registerExternalLib(source: string): Promise { const resp = await fetch(`${source}/manifest.json`); const manifest = await resp.json(); const lib = new ExternalLib(manifest, source); await this.registerLib(lib); // This will let us capture error messages return lib; } ContextMenu = ContextMenuAPI; removeStaleApps() { for (const appName in this.apps) { const app = this.apps[appName]; app.windows.forEach((win: any) => { if (!win.element.parentElement) { app.windows.splice(app.windows.indexOf(win)); } }); } } async import(packageName: string, searchPath?: string) { if (searchPath) { // Using node-style module resolution let scope: string | null; let name: string; let filename: string; if (packageName.startsWith("@")) { const [_scope, _name, ...rest] = packageName.split("/"); scope = _scope!; name = _name!; filename = rest.join("/"); } else { const [_name, ...rest] = packageName.split("/"); scope = null; name = _name!; filename = rest.join("/"); } if (!filename || filename === "") { const data: any = await this.fs.promises.readFile(`${searchPath}/${scope}/${name}/package.json`); const pkg = JSON.parse(data); console.log("pkg", pkg); if (pkg.main) { filename = pkg.main; } else { filename = "index.js"; } } const file = await this.fs.promises.readFile(`${searchPath}/${scope}/${name}/${filename}`); // @ts-ignore const blob = new Blob([file], { type: "application/javascript" }); const url = URL.createObjectURL(blob); // @vite-ignore return await import(/* @vite-ignore */ url); } const splitName = packageName.split("@"); const pkg: string = splitName[0]!; const version = splitName[1] || null; if (this.libs[pkg]) { return await this.libs[pkg].getImport(version); } } uri = new URIHandlerAPI(); files = new FilesAPI(); get wsproxyURL() { return this.settings.get("wisp-url"); } } export interface AppManifest { name: string; type: "manual" | "auto" | "webview" | string; package: string; index?: string; icon: string; handler?: string; src?: string; hidden?: boolean; background?: string; wininfo: string; //| WindowInformation; useIdbWrapper?: boolean; } ================================================ FILE: src/sys/liquor/Boot.ts ================================================ import { Anura } from "./Anura"; const channel = new BroadcastChannel("tab"); // send message to all tabs, after a new tab channel.postMessage("newtab"); let activetab = true; channel.addEventListener("message", msg => { if (msg.data === "newtab" && activetab) { // if there's a previously registered tab that can read the message, tell the other tab to kill itself channel.postMessage("blackmanthunderstorm"); } if (msg.data === "blackmanthunderstorm") { activetab = false; //@ts-ignore for (const elm of [...document.children]) { elm.remove(); } document.open(); document.write("you already have an anura tab open"); document.close(); } }); // global window.addEventListener("load", async () => { await navigator.serviceWorker.register("/anura-sw.js"); let conf, milestone, instancemilestone; try { conf = await (await fetch("/config.json")).json(); milestone = await (await fetch("/MILESTONE")).text(); instancemilestone = conf.milestone; console.log("writing config??"); window.tb.fs.writeFile("/config_cached.json", JSON.stringify(conf)); } catch (e) { conf = JSON.parse(await new Promise(r => window.tb.fs.readFile("/config_cached.json", (_: any, b: Uint8Array) => r(new TextDecoder().decode(b))))); } window.anura = await Anura.new(conf); if (milestone) { const stored = window.anura.settings.get("milestone"); if (!stored) await window.anura.settings.set("milestone", milestone); else if (stored != milestone || window.anura.settings.get("instancemilestone") != instancemilestone) { await window.anura.settings.set("milestone", milestone); await window.anura.settings.set("instancemilestone", instancemilestone); navigator.serviceWorker.controller!.postMessage({ anura_target: "anura.cache.invalidate", }); console.log("invalidated cache"); window.location.reload(); } } if (!window.anura.settings.get("directories")) { const defaultDirectories = { apps: "/apps/anura/", libs: "/system/lib/anura/", init: "/system/etc/anura/init/", bin: "/system/bin/anura/", }; await window.anura.settings.set("directories", defaultDirectories); } if (!window.anura.settings.get("handler-migration-complete")) { // Convert legacy file handlers // This is a one-time migration const extHandlers = window.anura.settings.get("FileExts") || {}; console.log("migrating file handlers"); console.log(extHandlers); for (const ext in extHandlers) { const handler = extHandlers[ext]; if (handler.handler_type === "module") continue; if (handler.handler_type === "cjs") continue; if (typeof handler === "string") { if (handler === "/public/apps/libfileview.app/fileHandler.js") { extHandlers[ext] = { handler_type: "module", id: "anura.fileviewer", }; continue; } extHandlers[ext] = { handler_type: "cjs", path: handler, }; } } window.anura.settings.set("FileExts", extHandlers); window.anura.settings.set("handler-migration-complete", true); } setTimeout( () => { window.anura.logger.debug("boot completed"); document.dispatchEvent(new Event("anura-boot-completed")); }, window.anura.settings.get("oobe-complete") ? 1000 : 2000, ); }); document.addEventListener("anura-boot-completed", async () => { // Anura OOBE code used to be here window.anura.settings.set("handler-migration-complete", true); }); document.addEventListener("anura-login-completed", async () => { for (const app of window.anura.config.apps) { window.anura.registerExternalApp(app); } for (const lib of window.anura.config.libs) { window.anura.registerExternalLib(lib); } // Load all persistent sideloaded apps try { // @ts-expect-error window.anura.fs.readdir("/apps/anura", (err: Error, files: string[]) => { // Fixes a weird edgecase that I was facing where no user apps are installed, nothing breaks it just throws an error which I would like to mitigate. if (files == undefined) return; files.forEach(file => { try { window.anura.registerExternalApp("/fs/apps/anura/" + file); } catch (e) { window.anura.logger.error("Anura failed to load an app " + e); } }); }); } catch (e) { window.anura.logger.error(e); } // Load all user provided init scripts try { // @ts-expect-error window.anura.fs.readdir("/userInit", (err: Error, files: string[]) => { // Fixes a weird edgecase that I was facing where no user apps are installed, nothing breaks it just throws an error which I would like to mitigate. if (files == undefined) return; files.forEach(file => { try { window.anura.fs.readFile( "/userInit/" + file, // @ts-expect-error function (err: Error, data: Uint8Array) { if (err) throw "Failed to read file"; try { eval(new TextDecoder("utf-8").decode(data)); } catch (e) { console.error(e); } }, ); } catch (e) { window.anura.logger.error("Anura failed to load an app " + e); } }); }); } catch (e) { window.anura.logger.error(e); } if ((await await fetch("/fs/")).status === 404) { window.anura.notifications.add({ title: "Anura Error", description: "Anura has encountered an error with the Filesystem HTTP bridge, click this notification to restart", timeout: 50000, callback: () => window.location.reload(), }); } document.addEventListener("contextmenu", function (e) { if (e.shiftKey) return; e.preventDefault(); // const menu: any = document.querySelector(".custom-menu"); // menu.style.removeProperty("display"); // menu.style.top = `${e.clientY}px`; // menu.style.left = `${e.clientX}px`; }); // // document.addEventListener("click", (e) => { // if (e.button != 0) return; // ( // document.querySelector(".custom-menu")! as HTMLElement // ).style.setProperty("display", "none"); // }); window.anura.initComplete = true; }); ================================================ FILE: src/sys/liquor/api/ContextMenuAPI.tsx ================================================ import "../../gui/styles/liquor.css"; export class ContextMenuAPI { element: HTMLDivElement; item(text: string, callback: VoidFunction) { const menuItem = document.createElement("div"); menuItem.className = "custom-menu-item"; menuItem.onclick = () => { callback(); }; menuItem.innerText = text; return menuItem; } isShown = false; constructor() { this.element = document.createElement("div"); this.element.className = "custom-menu"; setTimeout( () => document.addEventListener("click", event => { const withinBoundaries = event.composedPath().includes(this.element); if (!withinBoundaries) { this.element.remove(); } }), 100, ); } removeAllItems() { this.element.innerHTML = ""; } addItem(text: string, callback: VoidFunction) { this.element.appendChild( this.item(text, () => { this.hide(); callback(); }), ); } show(x: number, y: number) { this.element.style.top = y.toString() + "px"; this.element.style.left = x.toString() + "px"; document.body.appendChild(this.element); this.isShown = true; this.element.focus(); return this.element; } hide() { if (this.isShown) { document.body.removeChild(this.element); this.isShown = false; } } } ================================================ FILE: src/sys/liquor/api/Dialog.ts ================================================ import { setDialogFn } from "../../apis/Dialogs"; export class Dialog { alert(message: string, title = "Alert") { // @ts-expect-error types setDialogFn("alert", { message: message, title: title, }); } async confirm(message: string, title = "Confirmation"): Promise { return new Promise(resolve => { setDialogFn("permissions", { message: message, title: title, onOk: () => { resolve(true); }, onCancel: () => { resolve(false); }, }); }); } async prompt(message: string, defaultValue?: any): Promise { return new Promise(resolve => { setDialogFn("message", { title: message, message: defaultValue, onOk: (val: any) => { resolve(val); }, }); }); } } ================================================ FILE: src/sys/liquor/api/FilerFS.ts ================================================ // @ts-nocheck import { FilerFS } from "../types/Filer"; import { Anura } from "../Anura"; import { AFSProvider } from "./Filesystem"; const AnuraFDSymbol = Symbol.for("AnuraFD"); type AnuraFD = { fd: number; [AnuraFDSymbol]: string; }; // @ts-expect-error const Filer = window.Filer; let anura: Anura; export class FilerAFSProvider extends AFSProvider { domain = "/"; name = "Filer"; version = "1.0.0"; fs: FilerFS; constructor(fs: FilerFS) { super(); this.fs = fs; } rename(oldPath: string, newPath: string, callback?: (err: Error | null) => void) { this.fs.rename(oldPath, newPath, callback); } ftruncate(fd: AnuraFD, len: number, callback?: (err: Error | null, fd: AnuraFD) => void) { this.fs.ftruncate(fd.fd, len, (err, fd) => callback!(err, { fd, [AnuraFDSymbol]: this.domain })); } truncate(path: string, len: number, callback?: (err: Error | null) => void) { this.fs.truncate(path, len, callback); } stat(path: string, callback?: (err: Error | null, stats: any) => void) { this.fs.stat(path, callback); } fstat(fd: AnuraFD, callback?: ((err: Error | null, stats: any) => void) | undefined): void { this.fs.fstat(fd.fd, callback); } lstat(path: string, callback?: (err: Error | null, stats: any) => void) { this.fs.lstat(path, callback); } /** @deprecated fs.exists() is an anachronism and exists only for historical reasons. */ exists(path: string, callback?: (exists: boolean) => void) { this.fs.exists(path, callback); } link(srcPath: string, dstPath: string, callback?: (err: Error | null) => void) { this.fs.link(srcPath, dstPath, callback); } symlink(path: string, ...rest: any[]) { // @ts-expect-error - Overloaded methods are scary this.fs.symlink(path, ...rest); } readlink(path: string, callback?: (err: Error | null, linkContents: string) => void) { this.fs.readlink(path, callback); } unlink(path: string, callback?: (err: Error | null) => void) { this.fs.unlink(path, callback); } mknod(path: string, mode: number, callback?: (err: Error | null) => void) { this.fs.mknod(path, mode, callback); } rmdir(path: string, callback?: (err: Error | null) => void) { this.fs.rmdir(path, callback); } mkdir(path: string, ...rest: any[]) { this.fs.mkdir(path, ...rest); } access(path: string, ...rest: any[]) { this.fs.access(path, ...rest); } mkdtemp(...args: any[]) { // Temp directories should remain in the root filesystem for now // @ts-expect-error - Overloaded methods are scary this.fs.mkdtemp(...args); } readdir(path: string, ...rest: any[]) { this.fs.readdir(path, ...rest); } close(fd: AnuraFD, callback?: ((err: Error | null) => void) | undefined): void { callback ||= () => {}; this.fs.close(fd.fd, callback); } open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode: number, callback?: ((err: Error | null, fd: AnuraFD) => void) | undefined): void; open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", callback?: ((err: Error | null, fd: AnuraFD) => void) | undefined): void; open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode?: unknown, callback?: unknown): void { if (typeof mode === "number") { this.fs.open(path, flags, mode, (err, fd) => (callback as (err: Error | null, fd: AnuraFD) => void)!(err, { fd, [AnuraFDSymbol]: this.domain, }), ); } else { this.fs.open(path, flags, (err, fd) => (mode as (err: Error | null, fd: AnuraFD) => void)!(err, { fd, [AnuraFDSymbol]: this.domain, }), ); } } utimes(path: string, atime: number | Date, mtime: number | Date, callback?: (err: Error | null) => void) { this.fs.utimes(path, atime, mtime, callback); } futimes(fd: AnuraFD, ...rest: any[]) { // @ts-expect-error - Overloaded methods are scary this.fs.futimes(fd.fd, ...rest); } chown(path: string, uid: number, gid: number, callback?: (err: Error | null) => void) { this.fs.chown(path, uid, gid, callback); } fchown(fd: AnuraFD, ...rest: any[]) { // @ts-expect-error - Overloaded methods are scary this.fs.fchown(fd.fd, ...rest); } chmod(path: string, mode: number, callback?: (err: Error | null) => void) { this.fs.chmod(path, mode, callback); } fchmod(fd: AnuraFD, ...rest: any[]) { // @ts-expect-error - Overloaded methods are scary this.fs.fchmod(fd.fd, ...rest); } fsync(fd: AnuraFD, ...rest: any[]) { // @ts-expect-error - Overloaded methods are scary this.fs.fsync(fd.fd, ...rest); } write(fd: AnuraFD, ...rest: any[]) { // @ts-expect-error - Overloaded methods are scary this.fs.write(fd.fd, ...rest); } read(fd: AnuraFD, ...rest: any[]) { // @ts-expect-error - Overloaded methods are scary this.fs.read(fd.fd, ...rest); } readFile(path: string, callback?: (err: Error | null, data: Uint8Array) => void) { this.fs.readFile(path, callback); } writeFile(path: string, ...rest: any[]) { // @ts-expect-error - Overloaded methods are scary this.fs.writeFile(path, ...rest); } appendFile(path: string, data: Uint8Array, callback?: (err: Error | null) => void) { this.fs.appendFile(path, data, callback); } setxattr(path: string, ...rest: any[]) { // @ts-expect-error - Overloaded methods are scary this.fs.setxattr(path, ...rest); } fsetxattr(fd: AnuraFD, ...rest: any[]) { // @ts-expect-error - Overloaded methods are scary this.fs.fsetxattr(fd.fd, ...rest); } getxattr(path: string, name: string, callback?: (err: Error | null, value: string | object) => void) { this.fs.getxattr(path, name, callback); } fgetxattr(fd: AnuraFD, name: string, callback?: (err: Error | null, value: string | object) => void) { this.fs.fgetxattr(fd.fd, name, callback); } removexattr(path: string, name: string, callback?: (err: Error | null) => void) { this.fs.removexattr(path, name, callback); } fremovexattr(fd: AnuraFD, ...rest: any[]) { // @ts-expect-error - Overloaded methods are scary this.fs.fremovexattr(fd.fd, ...rest); } promises = { appendFile: (path: string, data: Uint8Array, options: { encoding: string; mode: number; flag: string }) => this.fs.promises.appendFile(path, data, options), access: (path: string, mode?: number) => this.fs.promises.access(path, mode), chown: (path: string, uid: number, gid: number) => this.fs.promises.chown(path, uid, gid), chmod: (path: string, mode: number) => this.fs.promises.chmod(path, mode), getxattr: (path: string, name: string) => this.fs.promises.getxattr(path, name), link: (srcPath: string, dstPath: string) => this.fs.promises.link(srcPath, dstPath), lstat: (path: string) => this.fs.promises.lstat(path), mkdir: (path: string, mode?: number) => this.fs.promises.mkdir(path, mode), mkdtemp: (prefix: string, options?: { encoding: string }) => this.fs.promises.mkdtemp(prefix, options), mknod: (path: string, mode: number) => this.fs.promises.mknod(path, mode), open: async (path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode?: number) => ({ fd: await this.fs.promises.open(path, flags, mode), [AnuraFDSymbol]: this.domain, }), readdir: (path: string, options?: { encoding: string; withFileTypes: boolean }) => this.fs.promises.readdir(path, options), readFile: (path: string) => this.fs.promises.readFile(path), readlink: (path: string) => this.fs.promises.readlink(path), removexattr: (path: string, name: string) => this.fs.promises.removexattr(path, name), rename: (oldPath: string, newPath: string) => this.fs.promises.rename(oldPath, newPath), rmdir: (path: string) => this.fs.promises.rmdir(path), setxattr: (path: string, name: string, value: string | object, flag?: "CREATE" | "REPLACE") => this.fs.promises.setxattr(path, name, value, flag), stat: (path: string) => this.fs.promises.stat(path), symlink: (srcPath: string, dstPath: string, type?: string) => this.fs.promises.symlink(srcPath, dstPath, type), truncate: (path: string, len: number) => this.fs.promises.truncate(path, len), unlink: (path: string) => this.fs.promises.unlink(path), utimes: (path: string, atime: number | Date, mtime: number | Date) => this.fs.promises.utimes(path, atime, mtime), writeFile: (path: string, data: Uint8Array | string, options: { encoding: string; mode: number; flag: string }) => this.fs.promises.writeFile(path, data, options), }; } ================================================ FILE: src/sys/liquor/api/Files.ts ================================================ // Depends on Settings.ts, must be loaded AFTER export class FilesAPI { fallbackIcon = "/assets/img/missing_icon.svg"; folderIcon = "/assets/img/folder.svg"; open = async function (path: string): Promise { const ext = path.split("/").pop()!.split(".").pop(); const extHandlers = window.anura.settings.get("FileExts") || {}; if (extHandlers[ext!]) { const handler = extHandlers[ext!]; console.log(`Opening ${path} with ${handler}`); if (handler.handler_type === "module") { const handlerModule = await window.anura.import(handler.id); if (!handlerModule) { console.log(`Failed to load handler ${handler}`); // @ts-expect-error stfu await this.defaultOpen(path); return; } if (!handlerModule.openFile) { console.log(`Handler ${handler} does not have an openFile function`); // @ts-expect-error stfu await this.defaultOpen(path); return; } return handlerModule.openFile(path); } if (handler.handler_type === "cjs") { // Legacy handler, eval it eval((await (await fetch(handler.path)).text()) + `openFile(${JSON.stringify(path)})`); // here, JSON.stringify is used to properly escape the string return; } } // If no handler is found, try the default handler // @ts-expect-error stfu await this.defaultOpen(path); return; }; async defaultOpen(path: string): Promise { const extHandlers = window.anura.settings.get("FileExts") || {}; if (extHandlers["default"]) { const handler = extHandlers["default"]; console.log(`Opening ${path} with ${handler}`); if (handler.handler_type === "module") { const handlerModule = await window.anura.import(handler.id); if (!handlerModule) { console.log(`Failed to load handler ${handler}`); return; } if (!handlerModule.openFile) { console.log(`Handler ${handler} does not have an openFile function`); return; } handlerModule.openFile(path); return; } if (handler.handler_type === "cjs") { eval((await (await fetch(handler.path)).text()) + `openFile(${JSON.stringify(path)})`); return; } } } getIcon = async (path: string) => { const ext = path.split("/").pop()!.split(".").pop(); const extHandlers = window.anura.settings.get("FileExts") || {}; if (extHandlers[ext!]) { const handler = extHandlers[ext!]; if (handler.handler_type === "module") { const handlerModule = await window.anura.import(handler.id); if (!handlerModule) { console.log(`Failed to load handler ${handler}`); return await this.defaultIcon(path); } if (!handlerModule.getIcon) { console.log(`Handler ${handler} does not have an getIcon function`); return await this.defaultIcon(path); } return handlerModule.getIcon(path); } if (handler.handler_type === "cjs") { return eval( (await (await fetch(handler.path)).text()) + `if (getIcon) { getIcon(${JSON.stringify(path)}) } else { ${JSON.stringify(await this.defaultIcon(path))} }`, ); } } return await this.defaultIcon(path); }; async defaultIcon(path: string) { const extHandlers = window.anura.settings.get("FileExts") || {}; if (extHandlers["default"]) { const handler = extHandlers["default"]; if (handler.handler_type === "module") { const handlerModule = await window.anura.import(handler.id); if (!handlerModule) { console.log(`Failed to load handler ${handler}`); return this.fallbackIcon; } if (!handlerModule.getIcon) { console.log(`Handler ${handler} does not have an getIcon function`); return this.fallbackIcon; } return handlerModule.getIcon(path); } if (handler.handler_type === "cjs") { // Legacy handler, eval it return eval( (await (await fetch(handler.path)).text()) + `if (getIcon) { getIcon(${JSON.stringify(path)}) } else { ${JSON.stringify(this.fallbackIcon)} }`, ); // here, JSON.stringify is used to properly escape the string } } return this.fallbackIcon; } async getFileType(path: string) { const ext = path.split("/").pop()!.split(".").pop(); const extHandlers = window.anura.settings.get("FileExts") || {}; if (extHandlers[ext!]) { const handler = extHandlers[ext!]; console.log(handler); if (handler.handler_type === "module") { const handlerModule = await window.anura.import(handler.id); if (!handlerModule) { console.log(`Failed to load handler ${handler}`); return "Anura File"; } if (!handlerModule.getFileType) { console.log(`Handler ${handler} does not have an getFileType function`); return "Anura File"; } return handlerModule.getFileType(path); } if (handler.handler_type === "cjs") { // Legacy handler, eval it return eval( (await (await fetch(handler.path)).text()) + `if (getFileType) { getFileType(${JSON.stringify(path)}) } else { "Anura File" }`, ); // here, JSON.stringify is used to properly escape the string } } // If no handler is found, return "Anura File" return "Anura File"; } setFolderIcon(path: string) { this.folderIcon = path; } set(path: string, extension: string) { const extHandlers = window.anura.settings.get("FileExts") || {}; extHandlers[extension] = { handler_type: "cjs", path, }; window.anura.settings.set("FileExts", extHandlers); } setModule(id: string, extension: string) { const extHandlers = window.anura.settings.get("FileExts") || {}; extHandlers[extension] = { handler_type: "module", id, }; window.anura.settings.set("FileExts", extHandlers); } } ================================================ FILE: src/sys/liquor/api/Filesystem.ts ================================================ import { Anura } from "../Anura"; const AnuraFDSymbol = Symbol.for("AnuraFD"); const Filer = window.Filer; // @ts-expect-error const anura: Anura = window.Anura; export type AnuraFD = { fd: number; [AnuraFDSymbol]: string; }; export abstract class AnuraFSOperations { /* * Synchronous FS operations */ abstract rename(oldPath: string, newPath: string, callback?: (err: Error | null) => void): void; abstract ftruncate(fd: AnuraFD, len: number, callback?: (err: Error | null, fd: AnuraFD) => void): void; abstract truncate(path: string, len: number, callback?: (err: Error | null) => void): void; abstract stat(path: string, callback?: (err: Error | null, stats: TStats) => void): void; abstract fstat(fd: AnuraFD, callback?: (err: Error | null, stats: TStats) => void): void; abstract lstat(path: string, callback?: (err: Error | null, stats: TStats) => void): void; /** @deprecated fs.exists() is an anachronism and exists only for historical reasons. */ abstract exists(path: string, callback?: (exists: boolean) => void): void; abstract link(srcPath: string, dstPath: string, callback?: (err: Error | null) => void): void; abstract symlink(srcPath: string, dstPath: string, type: string, callback?: (err: Error | null) => void): void; abstract symlink(srcPath: string, dstPath: string, callback?: (err: Error | null) => void): void; abstract readlink(path: string, callback?: (err: Error | null, linkContents: string) => void): void; abstract unlink(path: string, callback?: (err: Error | null) => void): void; abstract rmdir(path: string, callback?: (err: Error | null) => void): void; abstract mkdir(path: string, mode: number, callback?: (err: Error | null) => void): void; abstract mkdir(path: string, callback?: (err: Error | null) => void): void; abstract access(path: string, mode: number, callback?: (err: Error | null) => void): void; abstract access(path: string, callback?: (err: Error | null) => void): void; abstract mkdtemp(prefix: string, options: { encoding: string } | string, callback?: (err: Error | null, path: string) => void): void; abstract mkdtemp(prefix: string, callback?: (err: Error | null, path: string) => void): void; abstract readdir(path: string, options: { encoding: string; withFileTypes: boolean } | string, callback?: (err: Error | null, files: string[]) => void): void; abstract readdir(path: string, callback?: (err: Error | null, files: string[]) => void): void; abstract close(fd: AnuraFD, callback?: (err: Error | null) => void): void; abstract open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode: number, callback?: (err: Error | null, fd: AnuraFD) => void): void; abstract open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", callback?: (err: Error | null, fd: AnuraFD) => void): void; abstract utimes(path: string, atime: number | Date, mtime: number | Date, callback?: (err: Error | null) => void): void; abstract futimes(fd: AnuraFD, atime: number | Date, mtime: number | Date, callback?: (err: Error | null) => void): void; abstract chown(path: string, uid: number, gid: number, callback?: (err: Error | null) => void): void; abstract fchown(fd: AnuraFD, uid: number, gid: number, callback?: (err: Error | null) => void): void; abstract chmod(path: string, mode: number, callback?: (err: Error | null) => void): void; abstract fchmod(fd: AnuraFD, mode: number, callback?: (err: Error | null) => void): void; abstract fsync(fd: AnuraFD, callback?: (err: Error | null) => void): void; abstract write(fd: AnuraFD, buffer: Uint8Array, offset: number, length: number, position: number | null, callback?: (err: Error | null, nbytes: number) => void): void; abstract read(fd: AnuraFD, buffer: Uint8Array, offset: number, length: number, position: number | null, callback?: (err: Error | null, nbytes: number, buffer: Uint8Array) => void): void; abstract readFile(path: string, callback?: (err: Error | null, data: Uint8Array) => void): void; abstract writeFile(path: string, data: Uint8Array | string, options: { encoding: string; flag: "r" | "r+" | "w" | "w+" | "a" | "a+" } | string, callback?: (err: Error | null) => void): void; abstract writeFile(path: string, data: Uint8Array | string, callback?: (err: Error | null) => void): void; abstract appendFile(path: string, data: Uint8Array, callback?: (err: Error | null) => void): void; /* * Asynchronous FS operations */ abstract promises: { appendFile(path: string, data: Uint8Array, options: { encoding: string; mode: number; flag: string }): Promise; access(path: string, mode?: number): Promise; chown(path: string, uid: number, gid: number): Promise; chmod(path: string, mode: number): Promise; link(srcPath: string, dstPath: string): Promise; lstat(path: string): Promise; mkdir(path: string, mode?: number): Promise; mkdtemp(prefix: string, options?: { encoding: string }): Promise; open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode?: number): Promise; readdir(path: string, options?: string | { encoding: string; withFileTypes: boolean }): Promise; readFile(path: string): Promise; readlink(path: string): Promise; rename(oldPath: string, newPath: string): Promise; rmdir(path: string): Promise; stat(path: string): Promise; symlink(srcPath: string, dstPath: string, type?: string): Promise; truncate(path: string, len: number): Promise; unlink(path: string): Promise; utimes(path: string, atime: number | Date, mtime: number | Date): Promise; writeFile(path: string, data: Uint8Array | string, options?: { encoding: string; mode: number; flag: string }): Promise; }; } /** * Generic class for a filesystem provider * This should be extended by the various filesystem providers */ export abstract class AFSProvider extends AnuraFSOperations { /** * This is the domain that the filesystem provider is responsible * for. The provider with the most specific domain * will be used to handle a given path. * * @example "/" If you want to handle the root filesystem * * @example "/tmp" If you want to handle everything under /tmp. * This will take precedence over the root filesystem. */ abstract domain: string; /** * The name of the filesystem provider */ abstract name: string; /** * The filesystem provider's version */ abstract version: string; } export class AFSShell { env = new Proxy({} as { [key: string]: string }, { get: (target: { [key: string]: string }, prop: string) => { if (prop === "set") { return (key: string, value: string) => { target[key] = value; }; } if (prop === "get") { return (key: string) => target[key]; } if (prop in target) { return target[prop]; } return undefined; }, set: (target: any, prop: string, value: string) => { if (prop === "set" || prop === "get") { return false; } target[prop] = value; return true; }, }); #relativeToAbsolute(path: string) { if (path.startsWith("/")) { return path; } return (this.env.PWD + "/" + path).replace(/\/+/g, "/"); } cat(files: string[], callback: (err: Error | null, contents: string) => void) { let contents = ""; let remaining = files.length; files.forEach(file => { anura.fs.readFile(this.#relativeToAbsolute(file), (err, data) => { if (err) { callback(err, contents); return; } contents += data.toString() + "\n"; remaining--; if (remaining === 0) { callback(null, contents.replace(/\n$/, "")); } }); }); } // This differs from the Filer version, because here we can use the anura.files API to open the file // instead of evaluating the contents as js. The behaviour of the Filer version can be replicated by // registering a file provider that evaluates the contents as js. exec(path: string) { anura.files.open(this.#relativeToAbsolute(path)); } find( path: string, options?: { /** * Regex to match file paths against */ regex?: RegExp; /** * Base name to search for (match patern) */ name?: string; /** * Folder to search in (match pattern) */ path?: string; /** * Callback to execute on each file. */ exec?: (path: string) => void; }, callback?: (err: Error | null, files: string[]) => void, ): void; find(path: string, callback?: (err: Error | null, files: string[]) => void): void; find(path: string, options?: any, callback?: (err: Error | null, files: string[]) => void) { if (typeof options === "function") { callback = options; options = {}; } callback ||= () => {}; options ||= {}; function walk(dir: string, done: (err: Error | null, files: string[]) => void) { const results: string[] = []; anura.fs.readdir(dir, (err: Error | null, list: string[]) => { if (err) { done(err, results); return; } let pending = list.length; if (!pending) { done(null, results); return; } list.forEach(file => { file = dir + "/" + file; anura.fs.stat(file, (err, stat) => { if (err) { done(err, results); return; } if (stat.isDirectory()) { walk(file, (err, res) => { results.push(...res); pending--; if (!pending) { done(null, results); } }); } else { results.push(file); pending--; if (!pending) { done(null, results); } } }); }); }); } walk(this.#relativeToAbsolute(path), (err, results) => { if (err) { callback!(err, []); return; } if (options.regex) { results = results.filter(file => options.regex!.test(file)); } if (options.name) { results = results.filter(file => file.includes(options.name!)); } if (options.path) { results = results.filter(file => file.includes(options.path!)); } if (options.exec) { results.forEach(file => options.exec!(file)); } else { callback!(null, results); } }); } ls( dir: string, options?: { recursive?: boolean; }, callback?: (err: Error | null, entries: any[]) => void, ): void; ls(dir: string, callback?: (err: Error | null, entries: any[]) => void): void; ls(dir: string, options?: any, callback?: (err: Error | null, entries: any[]) => void) { if (typeof options === "function") { callback = options; options = {}; } callback ||= () => {}; options ||= {}; const entries: any[] = []; if (options.recursive) { this.find(dir, (err, files) => { if (err) { callback!(err, []); return; } callback!(null, files); }); } else { anura.fs.readdir(this.#relativeToAbsolute(dir), (err: Error | null, files: string[]) => { if (err) { callback!(err, []); return; } if (files.length === 0) { callback!(null, []); return; } let pending = files.length; files.forEach(file => { anura.fs.stat(this.#relativeToAbsolute(dir) + "/" + file, (err, stats: { isDirectory: () => boolean }) => { if (err) { callback!(err, []); return; } entries.push(stats); pending--; if (!pending) { callback!(null, entries); } }); }); }); } } mkdirp(path: string, callback: (err: Error | null) => void) { this.promises .mkdirp(path) .then(() => callback!(null)) .catch(err => { callback(err); }); } rm(path: string, options?: { recursive?: boolean }, callback?: (err: Error | null) => void): void; rm(path: string, callback?: (err: Error | null) => void): void; rm(path: string, options?: any, callback?: (err: Error | null) => void) { path = this.#relativeToAbsolute(path); if (typeof options === "function") { callback = options; options = {}; } callback ||= () => {}; options ||= {}; function walk(dir: string, done: (err: Error | null) => void) { anura.fs.readdir(dir, (err: Error | null, list: string[]) => { if (err) { done(err); return; } let pending = list.length; if (!pending) { anura.fs.rmdir(dir, done); return; } list.forEach((file: string) => { file = dir + "/" + file; anura.fs.stat( file, ( err, stats: { isDirectory: () => boolean; }, ) => { if (err) { done(err); return; } if (stats.isDirectory()) { walk(file, err => { if (err) { done(err); return; } pending--; if (!pending) { anura.fs.rmdir(dir, done); } }); } else { anura.fs.unlink(file, err => { if (err) { done(err); return; } pending--; if (!pending) { anura.fs.rmdir(dir, done); } }); } }, ); }); }); } anura.fs.stat(path, (err: Error | null, stats: { isDirectory: () => boolean }) => { if (err) { callback!(err); return; } if (!stats.isDirectory()) { anura.fs.unlink(path, callback); return; } if (options.recursive) { walk(path, callback!); } else { anura.fs.readdir(path, (err: Error | null, files: string[]) => { if (err) { callback!(err); return; } if (files.length > 0) { callback!(new Error("Directory not empty! Pass { recursive: true } instead to remove it and all its contents.")); return; } }); } }); } tempDir(callback?: (err: Error | null, path: string) => void) { callback ||= () => {}; const tmp = this.env.TMP; anura.fs.mkdir(tmp, () => { callback!(null, tmp); }); } touch(path: string, options?: { updateOnly?: boolean; date?: Date }, callback?: (err: Error | null) => void): void; touch(path: string, callback?: (err: Error | null) => void): void; touch(path: string, options?: any, callback?: (err: Error | null) => void) { path = this.#relativeToAbsolute(path); if (typeof options === "function") { callback = options; options = { updateOnly: false, date: Date.now(), }; } callback ||= () => {}; options ||= { updateOnly: false, date: Date.now(), }; function createFile() { anura.fs.writeFile(path, "", callback); } function updateTimes() { anura.fs.utimes(path, options.date, options.date, callback); } anura.fs.stat(path, (err: Error | null) => { if (err) { if (options.updateOnly) { callback!(new Error("File does not exist and updateOnly is true")); return; } else { createFile(); } } else { updateTimes(); } }); } cd(dir: string) { this.env.PWD = this.#relativeToAbsolute(dir); } pwd() { return this.env.PWD; } promises = { cat: async (files: string[]) => { let contents = ""; for (const file of files) { contents += (await anura.fs.promises.readFile(this.#relativeToAbsolute(file))).toString(); } return contents; }, exec: async (path: string) => { anura.files.open(this.#relativeToAbsolute(path)); }, find: ( path: string, options?: { regex?: RegExp; name?: string; path?: string; exec?: (path: string) => void; }, ) => { return new Promise((resolve, reject) => { this.find(path, options, (err, files) => { if (err) { reject(err); return; } resolve(files); }); }); }, ls: ( dir: string, options?: { recursive?: boolean; }, ) => { return new Promise((resolve, reject) => { this.ls(dir, options, (err, entries) => { if (err) { reject(err); return; } resolve(entries); }); }); }, cpr: async (src: string, dest: string, options?: any) => { try { const stat = await anura.fs.promises.stat(src); if (options?.createInnerFolder === true) { try { const destStat = await anura.fs.promises.stat(dest); if (destStat.type === "DIRECTORY") { dest = Filer.Path.join(dest, Filer.Path.basename(src)); } } catch { // Destination does not exist; continue as-is } } if (stat.type === "FILE") { // Make sure destination directory exists const destDir = Filer.Path.dirname(dest); await this.promises.mkdirp(destDir); await anura.fs.promises.writeFile(dest, await anura.fs.promises.readFile(src)); } else if (stat.type === "DIRECTORY") { await this.promises.mkdirp(dest); const items = await anura.fs.promises.readdir(src); for (const item of items) { const srcPath = Filer.Path.join(src, item); const destPath = Filer.Path.join(dest, item); await this.promises.cpr(srcPath, destPath); } } else { throw new Error(`Unsupported file type at path: ${src}`); } } catch (err) { console.error(`Error copying from ${src} to ${dest}:`, err); throw err; } }, mkdirp: async (path: string) => { const parts = this.#relativeToAbsolute(path).split("/"); let builder = ""; for (const part of parts) { if (part === "") continue; builder += "/" + part; try { await anura.fs.promises.mkdir(builder); } catch (e: any) { if (e.code !== "EEXIST") throw e; } } }, rm: ( path: string, options?: { recursive?: boolean; }, ) => { return new Promise((resolve, reject) => { this.rm(path, options, err => { if (err) { reject(err); return; } resolve(); }); }); }, touch: ( path: string, options?: { updateOnly?: boolean; date?: Date; }, ) => { return new Promise((resolve, reject) => { this.touch(path, options, err => { if (err) { reject(err); return; } resolve(); }); }); }, }; constructor(options?: { env?: { [key: string]: string } }) { options ||= { env: { PWD: "/", TMP: "/tmp", }, }; if (options?.env) { Object.entries(options.env).forEach(([key, value]) => { this.env.set(key, value); }); } } } /** * Anura File System API * * This is fully compatible with Filer's filesystem API and, * by extension, most of the Node.js filesystem API. This is * a drop-in replacement for the legacy Filer API and should * be used in place of the Filer API in all new code. * * This API has the added benefit of type safety and a the ability * to register multiple filesystem providers. This allows for the * creation of virtual filesystems and the ability to mount filesystems * at arbitrary paths. */ export class AnuraFilesystem implements AnuraFSOperations { providers: Map> = new Map(); providerCache: { [path: string]: AFSProvider } = {}; whatwgfs = { fs: undefined, getFolder: async () => { // @ts-expect-error const fs = await import(/* @vite-ignore */ "/public/apps/nfsadapter/nfsadapter.js"); // @ts-expect-error return await this.whatwgfs.fs.getOriginPrivateDirectory( // @ts-expect-error import(/* @vite-ignore */ "/public/apps/nfsadapter/adapters/anuraadapter.js"), ); }, fileOrDirectoryFromPath: async (path: string) => { try { return await this.whatwgfs.directoryHandleFromPath(path); } catch (e1) { try { return await this.whatwgfs.fileHandleFromPath(path); } catch (e2) { // @ts-expect-error throw e1 + e2; } } }, directoryHandleFromPath: async (path: string) => { const picker = await window.anura.import("anura.filepicker"); const selectedPath = (await picker.selectFolder()).split("/"); // prettier-ignore let workingPath = await window.anura.fs.whatwgfs.getFolder(); for (const dir of selectedPath) { if (dir !== "") workingPath = await workingPath.getDirectoryHandle(dir); } return workingPath; }, fileHandleFromPath: async (givenPath: string) => { let path: string | string[] = givenPath.split("/"); const file = path.pop(); path = path.join("/"); // prettier-ignore const workingPath = await anura.fs.whatwgfs.directoryHandleFromPath(path); return await workingPath.getFileHandle(file); }, async showDirectoryPicker(options: object) { const picker = await window.anura.import("anura.filepicker"); const path = (await picker.selectFolder()).split("/"); // prettier-ignore let workingPath = await window.anura.fs.whatwgfs.getFolder(); for (const dir of path) { if (dir !== "") workingPath = await workingPath.getDirectoryHandle(dir); } return workingPath; }, async showOpenFilePicker(options: object) { const picker = await window.anura.import("anura.filepicker"); const path = (await picker.selectFile()).split("/"); // prettier-ignore let workingPath = await window.anura.fs.whatwgfs.getFolder(); for (const dir of path) { if (dir !== "") workingPath = await workingPath.getFileHandle(dir); } return workingPath; }, }; // Note: Intentionally aliasing the property to a class instead of an instance static Shell = AFSShell; Shell = AFSShell; constructor(providers: AFSProvider[]) { providers.forEach(provider => { this.providers.set(provider.domain, provider); }); // These paths must be TS ignore'd since they are in build/ (async () => { // @ts-ignore const fs = await import("/public/apps/nfsadapter/nfsadapter.js"); // @ts-ignore this.whatwgfs.FileSystemDirectoryHandle = fs.FileSystemDirectoryHandle; // @ts-ignore this.whatwgfs.FileSystemFileHandle = fs.FileSystemFileHandle; // @ts-ignore this.whatwgfs.FileSystemHandle = fs.FileSystemHandle; this.whatwgfs.fs = fs; })(); } clearCache() { this.providerCache = {}; } installProvider(provider: AFSProvider) { this.providers.set(provider.domain, provider); this.clearCache(); } processPath(path: string): AFSProvider { if (!path.startsWith("/")) { throw new Error("Path must be absolute"); } path = path.replace(/^\/+/, "/"); let provider = this.providerCache[path]; if (provider) { return provider; } if (this.providers.has(path)) { path += "/"; } const parts = path.split("/"); parts.shift(); parts.pop(); while (!provider && parts.length > 0) { const checkPath = "/" + parts.join("/"); // @ts-expect-error provider = this.providers.get(checkPath); parts.pop(); } if (!provider) { // @ts-expect-error provider = this.providers.get("/"); } this.providerCache[path] = provider!; return provider!; } processFD(fd: AnuraFD): AFSProvider { return this.processPath(fd[AnuraFDSymbol]); } rename(oldPath: string, newPath: string, callback?: (err: Error | null) => void) { this.processPath(oldPath).rename(oldPath, newPath, callback); } ftruncate(fd: AnuraFD, len: number, callback?: (err: Error | null, fd: AnuraFD) => void) { this.processFD(fd).ftruncate(fd, len, callback); } truncate(path: string, len: number, callback?: (err: Error | null) => void) { this.processPath(path).truncate(path, len, callback); } stat(path: string, callback?: (err: Error | null, stats: any) => void) { this.processPath(path).stat(path, callback); } fstat(fd: AnuraFD, callback?: ((err: Error | null, stats: any) => void) | undefined): void { this.processFD(fd).fstat(fd, callback); } lstat(path: string, callback?: (err: Error | null, stats: any) => void) { this.processPath(path).lstat(path, callback); } /** @deprecated fs.exists() is an anachronism and exists only for historical reasons. */ exists(path: string, callback?: (exists: boolean) => void) { this.processPath(path).exists(path, callback); } link(srcPath: string, dstPath: string, callback?: (err: Error | null) => void) { this.processPath(srcPath).link(srcPath, dstPath, callback); } symlink(path: string, ...rest: any[]) { // @ts-ignore - Overloaded methods are scary this.processPath(rest[0]).symlink(path, ...rest); } readlink(path: string, callback?: (err: Error | null, linkContents: string) => void) { this.processPath(path).readlink(path, callback); } unlink(path: string, callback?: (err: Error | null) => void) { this.processPath(path).unlink(path, callback); } rmdir(path: string, callback?: (err: Error | null) => void) { this.processPath(path).rmdir(path, callback); } mkdir(path: string, ...rest: any[]) { this.processPath(path).mkdir(path, ...rest); } access(path: string, ...rest: any[]) { this.processPath(path).access(path, ...rest); } mkdtemp(...args: any[]) { // Temp directories should remain in the root filesystem for now // @ts-ignore - Overloaded methods are scary this.processPath(path).mkdtemp(...args); } readdir(path: string, ...rest: any[]) { this.processPath(path).readdir(path, ...rest); } close(fd: AnuraFD, callback?: ((err: Error | null) => void) | undefined): void { this.processFD(fd).close(fd, callback); } open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode: number, callback?: ((err: Error | null, fd: AnuraFD) => void) | undefined): void; open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", callback?: ((err: Error | null, fd: AnuraFD) => void) | undefined): void; open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode?: unknown, callback?: unknown): void { if (typeof mode === "number") { this.processPath(path as string).open(path, flags, mode as number, callback as (err: Error | null, fd: AnuraFD) => void); } else { this.processPath(path as string).open(path, flags, mode as (err: Error | null, fd: AnuraFD) => void); } } utimes(path: string, atime: number | Date, mtime: number | Date, callback?: (err: Error | null) => void) { this.processPath(path).utimes(path, atime, mtime, callback); } futimes(fd: AnuraFD, ...rest: any[]) { // @ts-ignore - Overloaded methods are scary this.processFD(fd).futimes(fd, ...rest); } chown(path: string, uid: number, gid: number, callback?: (err: Error | null) => void) { this.processPath(path).chown(path, uid, gid, callback); } fchown(fd: AnuraFD, ...rest: any[]) { // @ts-ignore - Overloaded methods are scary this.processFD(fd).fchown(fd, ...rest); } chmod(path: string, mode: number, callback?: (err: Error | null) => void) { this.processPath(path).chmod(path, mode, callback); } fchmod(fd: AnuraFD, ...rest: any[]) { // @ts-ignore - Overloaded methods are scary this.processFD(fd).fchmod(fd, ...rest); } fsync(fd: AnuraFD, ...rest: any[]) { // @ts-ignore - Overloaded methods are scary this.processFD(fd).fsync(fd, ...rest); } write(fd: AnuraFD, ...rest: any[]) { // @ts-ignore - Overloaded methods are scary this.processFD(fd).write(fd, ...rest); } read(fd: AnuraFD, ...rest: any[]) { // @ts-ignore - Overloaded methods are scary this.processFD(fd).read(fd, ...rest); } readFile(path: string, callback?: (err: Error | null, data: Uint8Array) => void) { this.processPath(path).readFile(path, callback); } writeFile(path: string, data: Uint8Array | string, ...rest: any[]) { if (data instanceof Uint8Array && !(data instanceof Filer.Buffer)) { data = Filer.Buffer.from(data); } this.processPath(path).writeFile(path, data, ...rest); } appendFile(path: string, data: Uint8Array, callback?: (err: Error | null) => void) { if (data instanceof Uint8Array && !(data instanceof Filer.Buffer)) { data = Filer.Buffer.from(data); } this.processPath(path).appendFile(path, data, callback); } promises = { appendFile: (path: string, data: Uint8Array, options: { encoding: string; mode: number; flag: string }) => { if (data instanceof Uint8Array && !(data instanceof Filer.Buffer)) { data = Filer.Buffer.from(data); } return this.processPath(path).promises.appendFile(path, data, options); }, access: (path: string, mode?: number) => this.processPath(path).promises.access(path, mode), chown: (path: string, uid: number, gid: number) => this.processPath(path).promises.chown(path, uid, gid), chmod: (path: string, mode: number) => this.processPath(path).promises.chmod(path, mode), link: (srcPath: string, dstPath: string) => this.processPath(srcPath).promises.link(srcPath, dstPath), lstat: (path: string) => this.processPath(path).promises.lstat(path), mkdir: (path: string, mode?: number) => this.processPath(path).promises.mkdir(path, mode), mkdtemp: (prefix: string, options?: { encoding: string }) => this.processPath(prefix).promises.mkdtemp(prefix, options), open: async (path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode?: number) => this.processPath(path).promises.open(path, flags, mode), readdir: (path: string, options?: string | { encoding: string; withFileTypes: boolean }) => this.processPath(path).promises.readdir(path, options), readFile: (path: string) => this.processPath(path).promises.readFile(path), readlink: (path: string) => this.processPath(path).promises.readlink(path), rename: (oldPath: string, newPath: string) => this.processPath(oldPath).promises.rename(oldPath, newPath), rmdir: (path: string) => this.processPath(path).promises.rmdir(path), stat: (path: string) => this.processPath(path).promises.stat(path), symlink: (srcPath: string, dstPath: string, type?: string) => this.processPath(dstPath).promises.symlink(srcPath, dstPath, type), truncate: (path: string, len: number) => this.processPath(path).promises.truncate(path, len), unlink: (path: string) => this.processPath(path).promises.unlink(path), utimes: (path: string, atime: number | Date, mtime: number | Date) => this.processPath(path).promises.utimes(path, atime, mtime), writeFile: (path: string, data: Uint8Array | string, options?: { encoding: string; mode: number; flag: string }) => { if (data instanceof Uint8Array && !(data instanceof Filer.Buffer)) { data = Filer.Buffer.from(data); } return this.processPath(path).promises.writeFile(path, data, options); }, }; } ================================================ FILE: src/sys/liquor/api/LocalFS.ts ================================================ import { AFSProvider, AnuraFD } from "./Filesystem"; const AnuraFDSymbol = Symbol.for("AnuraFD"); const Filer = window.Filer; export class LocalFSStats { name: string; size: number; atime: Date; mtime: Date; ctime: Date; atimeMs: number; mtimeMs: number; ctimeMs: number; node: string; nlinks: number; mode: number; type: "FILE" | "DIRECTORY"; uid: number; gid: number; dev: string; isFile() { return this.type === "FILE"; } isDirectory() { return this.type === "DIRECTORY"; } isSymbolicLink() { return (this.mode & 0o170000) === 0o120000; } constructor(data: Partial) { this.name = data.name!; this.size = data.size || 0; this.atimeMs = data.atimeMs || Date.now(); this.mtimeMs = data.mtimeMs || Date.now(); this.ctimeMs = data.ctimeMs || Date.now(); this.atime = new Date(this.atimeMs); this.mtime = new Date(this.mtimeMs); this.ctime = new Date(this.ctimeMs); this.node = data.node || crypto.randomUUID(); this.nlinks = data.nlinks || 1; this.mode = data.mode || 0o100777; this.type = data.type || "FILE"; this.uid = data.uid || 0; this.gid = data.gid || 0; this.dev = data.dev || "localfs"; } } export class LocalFS extends AFSProvider { dirHandle: FileSystemDirectoryHandle; domain: string; name = "LocalFS"; version = "1.0.0"; path: any = Filer.Path; // replace with another polyfill stats: Map = new Map(); fds: FileSystemHandle[] = []; cursors: number[] = []; constructor(dirHandle: FileSystemDirectoryHandle, domain: string) { super(); this.dirHandle = dirHandle; this.domain = domain; this.name += ` (${domain})`; } relativizePath(path: string) { return path.replace(this.domain, "").replace(/^\/+/, ""); } async getChildDirHandle(path: string, recurseCounter = 0): Promise<[FileSystemDirectoryHandle, string]> { if (recurseCounter > 20) { throw { name: "ELOOP", code: "ELOOP", errno: -40, message: "no such file or directory", path: (this.domain + "/" + path).replace("//", "/"), }; } if (path === "") { return [this.dirHandle, path]; } if (path.endsWith("/")) { path = path.substring(0, path.length - 1); } let acc = this.dirHandle; let curr = ""; for await (const part of path.split("/")) { if (part === "" || part === ".") continue; curr += "/" + part; if ((this.stats.get(curr)?.mode & 0o170000) === 0o120000) { // We ran into a path symlink, we're storing symlinks of all types as files who's content is the target. const newPart = await (await (await acc.getFileHandle(path)).getFile()).text(); if (newPart.startsWith("/")) { // absolute return this.getChildDirHandle(newPart, recurseCounter + 1); } else { // relative return this.getChildDirHandle(this.path.resolve(curr, newPart), recurseCounter + 1); } } acc = await acc.getDirectoryHandle(part); } return [acc, curr]; } async getFileHandle(path: string, options?: FileSystemGetFileOptions, recurseCounter = 0): Promise<[FileSystemFileHandle, string]> { if (!path.includes("/")) { path = "/" + path; } const parentFolder = this.path.dirname(path); // eslint-disable-next-line prefer-const let [parentHandle, realPath] = await this.getChildDirHandle(parentFolder); const fileName = this.path.basename(path); if (realPath[0] === "/") { realPath = realPath.slice(1); } if (this.stats.has(realPath + "/" + fileName) && (this.stats.get(realPath + "/" + fileName).mode & 0o170000) === 0o120000) { // is symlink let realPath = await (await (await parentHandle.getFileHandle(fileName)).getFile()).text(); if (realPath.startsWith("/")) { if (realPath.startsWith(this.domain)) { realPath = this.relativizePath(realPath); // absolute return this.getFileHandle(realPath, options, recurseCounter + 1); } else { // Okay so, this goes over the mount boundary, and is slightly problematic // for us since we need to handle this as an event OUTSIDE of LocalFS itself // so this is a bit of a cheat using the compatibility layer for FileSystemAccess API let handle = await window.anura.fs.whatwgfs.getFolder(); for (const part in realPath.split("/").slice(1, -1)) { handle = await handle.getDirectoryHandle(part); } return [await handle.getFileHandle(this.path.basename(realPath)), "foreign:" + realPath]; } } else { // relative return this.getFileHandle(this.path.resolve(parentFolder, realPath), options, recurseCounter + 1); } } return [await parentHandle.getFileHandle(fileName, options), path]; } static async newOPFS(anuraPath: string) { const dirHandle = await navigator.storage.getDirectory(); try { await window.anura.fs.promises.mkdir(anuraPath); } catch (e) { // Ignore, the directory already exists so we don't need to create it } const fs = new LocalFS(dirHandle, anuraPath); // @ts-expect-error window.anura.fs.installProvider(fs); const textde = new TextDecoder(); try { fs.stats = new Map(JSON.parse(textde.decode(await fs.promises.readFile(anuraPath + "/.anura_stats")))); } catch (e: any) { console.log("Error on mount, probably first mount ", e); } return fs; } static async newSwOPFS() { const anuraPath = "/"; const dirHandle = await navigator.storage.getDirectory(); const fs = new LocalFS(dirHandle, anuraPath); const textde = new TextDecoder(); try { fs.stats = new Map(JSON.parse(textde.decode(await fs.promises.readFile(anuraPath + "/.anura_stats")))); } catch (e: any) { console.log("Error on mount, probably first mount ", e); } return fs; } static async new(anuraPath: string) { let dirHandle; try { // @ts-expect-error dirHandle = await window.showDirectoryPicker({ id: `anura-${anuraPath.replace(/\/|\s|\./g, "-")}`, }); } catch (e: any) { if (e.name !== "TypeError") { throw e; } // The path may not be a valid id, fallback to less specific id // @ts-expect-error dirHandle = await window.showDirectoryPicker({ id: "anura-localfs", }); } dirHandle.requestPermission({ mode: "readwrite" }); try { await window.anura.fs.promises.mkdir(anuraPath); } catch (e) { // Ignore, the directory already exists so we don't need to create it } const fs = new LocalFS(dirHandle, anuraPath); // @ts-ignore window.anura.fs.installProvider(fs); return fs; } readdir(path: string, _options?: any, callback?: (err: Error | null, files: string[]) => void) { if (typeof _options === "function") { callback = _options; } callback ||= () => {}; this.promises .readdir(path) .then(files => callback!(null, files)) .catch(e => callback!(e, [])); } stat(path: string, callback?: (err: Error | null, stats: any) => void): void { callback ||= () => {}; this.promises .stat(path) .then(stats => callback!(null, stats)) .catch(e => callback!(e, null)); } readFile(path: string, callback?: (err: Error | null, data: typeof Filer.Buffer) => void) { callback ||= () => {}; this.promises .readFile(path) .then(data => callback!(null, data)) .catch(e => callback!(e, new Filer.Buffer(0))); } writeFile(path: string, data: Uint8Array | string, _options?: any, callback?: (err: Error | null) => void) { if (typeof data === "string") { data = new TextEncoder().encode(data); } if (typeof _options === "function") { callback = _options; } callback ||= () => {}; this.promises .writeFile(path, data) .then(() => callback!(null)) .catch(callback); } appendFile(path: string, data: Uint8Array, callback?: (err: Error | null) => void) { this.promises .appendFile(path, data) .then(() => callback!(null)) .catch(callback); } unlink(path: string, callback?: (err: Error | null) => void) { callback ||= () => {}; this.promises .unlink(path) .then(() => callback!(null)) .catch(callback); } mkdir(path: string, _mode?: any, callback?: (err: Error | null) => void) { if (typeof _mode === "function") { callback = _mode; } callback ||= () => {}; this.promises .mkdir(path) .then(() => callback!(null)) .catch(callback); } rmdir(path: string, callback?: (err: Error | null) => void) { callback ||= () => {}; this.promises .rmdir(path) .then(() => callback!(null)) .catch(callback); } rename(srcPath: string, dstPath: string, callback?: (err: Error | null) => void) { callback ||= () => {}; this.promises .rename(srcPath, dstPath) .then(() => callback!(null)) .catch(callback); } truncate(path: string, len: number, callback?: (err: Error | null) => void) { this.promises .truncate(path, len) .then(() => callback!(null)) .catch(callback); } /** @deprecated — fs.exists() is an anachronism and exists only for historical reasons. */ exists(path: string, callback?: (exists: boolean) => void) { this.stat(path, (err, stats) => { if (err) { callback!(false); return; } callback!(true); }); } // @ts-ignore promises = { saveStats: async () => { const jsonStats = JSON.stringify(Array.from(this.stats.entries())); await this.promises.writeFile(this.domain + "/.anura_stats", jsonStats); }, writeFile: async (path: string, data: Uint8Array | string, options?: any) => { if (typeof data === "string") { data = new TextEncoder().encode(data); } path = this.relativizePath(path); // eslint-disable-next-line prefer-const let [handle, realPath] = await this.getFileHandle(path, { create: true, }); const writer = await handle.createWritable(); if (realPath.startsWith("/")) { realPath = realPath.slice(1); } const fileStats = this.stats.get(realPath) || {}; if (fileStats && !realPath.startsWith("foreign:")) { fileStats.mtimeMs = Date.now(); fileStats.ctimeMs = Date.now(); this.stats.set(realPath, fileStats); } // @ts-expect-error writer.write(data); writer.close(); }, readFile: async (path: string) => { path = this.relativizePath(path); const [handle, realPath] = await this.getFileHandle(path); const fileStats = this.stats.get(realPath) || {}; if (fileStats && !realPath.startsWith("foreign:")) { fileStats.atimeMs = Date.now(); this.stats.set(path, fileStats); } return new Filer.Buffer(await (await handle.getFile()).arrayBuffer()); }, readdir: async (path: string) => { let dirHandle, realPath; try { [dirHandle, realPath] = await this.getChildDirHandle(this.relativizePath(path)); } catch (e) { throw { name: "ENOENT", code: "ENOENT", errno: 34, message: "no such file or directory", path: (this.domain + "/" + path).replace("//", "/"), stack: e, }; } const nodes: string[] = []; // @ts-ignore for await (const entry of dirHandle.values()) { if (entry.name !== ".anura_stats") // internal file shouldn't appear on fs methods nodes.push(entry.name); } return nodes; }, appendFile: async (path: string, data: Uint8Array) => { const existingData = await this.promises.readFile(path); await this.promises.writeFile(path, new Uint8Array([...existingData, ...data])); }, unlink: async (path: string) => { let parentHandle = this.dirHandle; path = this.relativizePath(path); if (path.includes("/")) { const parts = path.split("/"); const finalFile = parts.pop(); parentHandle = (await this.getChildDirHandle(parts.join("/")))[0]; path = finalFile!; } await parentHandle.removeEntry(path); }, mkdir: async (path: string) => { if (path.endsWith("/")) path = path.slice(0, -1); let parentHandle = this.dirHandle; let realParentPath = ""; path = this.relativizePath(path); if (path.includes("/")) { const parts = path.split("/"); const finalDir = parts.pop(); [parentHandle, realParentPath] = await this.getChildDirHandle(parts.join("/")); path = finalDir!; } if (realParentPath.startsWith("/")) { realParentPath = realParentPath.slice(1); } const fullPath = realParentPath + "/" + path; const fileStats = this.stats.get(fullPath) || {}; if (fileStats) { fileStats.ctimeMs = Date.now(); this.stats.set(fullPath, fileStats); } await parentHandle.getDirectoryHandle(path, { create: true }); }, rmdir: async (path: string) => { let parentHandle = this.dirHandle; path = this.relativizePath(path); if (path.includes("/")) { const parts = path.split("/"); const finalDir = parts.pop(); parentHandle = (await this.getChildDirHandle(parts.join("/")))[0]; path = finalDir!; } await parentHandle.removeEntry(path); }, rename: async (oldPath: string, newPath: string) => { const data = await this.promises.readFile(oldPath); await this.promises.writeFile(newPath, data); await this.promises.unlink(oldPath); }, stat: async (path: string) => { path = this.relativizePath(path); const requestPath = path; let statPath = path; // when accessing this.stats dont have a trailing slash if (statPath.endsWith("/")) statPath = statPath.slice(0, -1); const currStats = this.stats.get(statPath) || {}; let handle; try { if (path === "") { handle = await this.dirHandle.getFileHandle(path); } else { [handle, path] = await this.getFileHandle(path); } } catch (e) { try { const handleAndPath = await this.getChildDirHandle(path); const handle = handleAndPath[0]; path = handleAndPath[1]; let rootName; if (!path) rootName = this.domain.split("/").pop(); return new LocalFSStats({ name: rootName || handle.name, mode: currStats.mode || 0o40777, type: "DIRECTORY", atimeMs: currStats.atimeMs || Date.now(), mtimeMs: currStats.mtimeMs || Date.now(), ctimeMs: currStats.ctimeMs || Date.now(), uid: currStats.uid || 0, gid: currStats.gid || 0, }); } catch (e) { throw { name: "ENOENT", code: "ENOENT", errno: 34, message: "no such file or directory", path: (this.domain + "/" + path).replace("//", "/"), stack: e, }; } } const file = await handle.getFile(); return new LocalFSStats({ name: this.path.basename(requestPath), size: file.size, type: "FILE", mode: currStats.mode || 0o100777, atimeMs: currStats.atimeMs || Date.now(), mtimeMs: currStats.mtimeMs || Date.now(), ctimeMs: currStats.ctimeMs || Date.now(), uid: currStats.uid || 0, gid: currStats.gid || 0, }); }, truncate: async (path: string, len: number) => { const data = await this.promises.readFile(path); await this.promises.writeFile(path, data.slice(0, len)); }, access(path: string, mode: number): Promise { // @ts-ignore path = this.relativizePath(path); return new Promise((resolve, reject) => { // @ts-ignore this.promises .stat(path) .then(() => resolve()) // File exists .catch(() => reject({ name: "ENOENT", code: "ENOENT", errno: 34, message: `No such file or directory`, path, stack: "Error: No such file or directory", } as Error), ); // File doesn't exist }); }, chown(path: string, uid: number, gid: number): Promise { // @ts-ignore path = this.relativizePath(path); return new Promise(async (resolve, reject) => { // @ts-ignore const type = (await this.promises.lstat(path)).type; // Check if the file exists // @ts-ignore const stats = this.stats.get(path); if (!stats) { return reject({ name: "ENOENT", code: "ENOENT", errno: 34, message: `No such file or directory`, path, stack: "Error: No such file or directory", } as Error); } if (path.endsWith("/")) path = path.slice(0, -1); if (type === "DIRECTORY") { // @ts-ignore path = (await this.getChildDirHandle(path))[1]; } else { const pathDir = // @ts-ignore (await this.getChildDirHandle(this.path.dirname(path)))[1]; // @ts-ignore path = pathDir + "/" + this.path.basename(path); } if (path.startsWith("/")) { path = path.slice(1); } // Update ownership in stats stats.uid = uid; stats.gid = gid; // Save updated stats // @ts-ignore this.stats.set(path, stats); // @ts-ignore this.promises .saveStats() .then(() => resolve()) .catch(reject); }); }, chmod: async (fullPath: string, mode: number) => { const stats = await this.promises.lstat(fullPath); const type = stats.type; const sym = (stats.mode & 0o170000) === 0o120000; let path = this.relativizePath(fullPath); if (path.endsWith("/")) path = path.slice(0, -1); if (type === "DIRECTORY") { path = (await this.getChildDirHandle(path))[1]; } else { const pathDir = (await this.getChildDirHandle(this.path.dirname(path)))[1]; path = pathDir + "/" + this.path.basename(path); } if (path.startsWith("/")) { path = path.slice(1); } const currStats = this.stats.get(path) || {}; if (mode > 0o777) { // Needed for v86 mode -= mode & 0o170000; } if (!sym) { if (type === "FILE") { currStats.mode = 0o100000 + mode; } if (type === "DIRECTORY") { currStats.mode = 0o40000 + mode; } } else { currStats.mode = 0o120000 + mode; } this.stats.set(path, currStats); await this.promises.saveStats(); }, link: (existingPath: string, newPath: string): Promise => { return this.promises.symlink(existingPath, newPath); }, lstat: async (path: string) => { path = this.relativizePath(path); let statPath = path; // when accessing this.stats dont have a trailing slash if (statPath.endsWith("/")) statPath = statPath.slice(0, -1); const currStats = this.stats.get(statPath) || {}; let handle; try { if (path === "") { handle = await this.dirHandle.getFileHandle(path); } else { const parent = await this.getChildDirHandle(this.path.dirname(path)); handle = parent[0]; const parentPath = parent[1]; handle = await handle.getFileHandle(this.path.basename(path)); path += path.lastIndexOf("/"); path = parentPath + this.path.basename(path); // [handle, path] = await this.getFileHandle(path); } } catch (e) { try { const handleAndPath = await this.getChildDirHandle(path); const handle = handleAndPath[0]; path = handleAndPath[1]; let rootName; if (!path) rootName = this.domain.split("/").pop(); return new LocalFSStats({ name: rootName || handle.name, mode: currStats.mode || 0o40777, type: "DIRECTORY", atimeMs: currStats.atimeMs || Date.now(), mtimeMs: currStats.mtimeMs || Date.now(), ctimeMs: currStats.ctimeMs || Date.now(), uid: currStats.uid || 0, gid: currStats.gid || 0, }); } catch (e) { throw { name: "ENOENT", code: "ENOENT", errno: 34, message: "no such file or directory", path: (this.domain + "/" + path).replace("//", "/"), stack: e, }; } } const file = await handle.getFile(); return new LocalFSStats({ name: file.name, size: file.size, type: "FILE", mode: currStats.mode || 0o100777, atimeMs: currStats.atimeMs || Date.now(), mtimeMs: currStats.mtimeMs || Date.now(), ctimeMs: currStats.ctimeMs || Date.now(), uid: currStats.uid || 0, gid: currStats.gid || 0, }); }, mkdtemp: (template: string): Promise => { return new Promise((resolve, reject) => { // Check if template has 'XXXXXX' if (!template.includes("XXXXXX")) { return reject({ name: "EINVAL", code: "EINVAL", errno: 342, message: "Invalid template, must contain 'XXXXXX'.", stack: "Error: Invalid template", } as Error); } // Generate a random suffix const randomSuffix = Math.random().toString(36).slice(2, 8); // 6-character random string // Replace 'XXXXXX' in the template with the random suffix const newDir = template.replace("XXXXXX", randomSuffix); this.promises.mkdir(newDir); // Save the new stats this.promises .saveStats() .then(() => resolve(newDir)) // Return the new directory path .catch(reject); }); }, open: async (path: string, _flags: "r" | "r+" | "w" | "w+" | "a" | "a+", _mode?: any) => { path = this.relativizePath(path); const stats = this.stats.get(path); const parentHandle = this.dirHandle; const [handle] = await this.getFileHandle(path, { create: true }); (handle as any).path = path; // Hack this.fds.push(handle); return { fd: this.fds.length - 1, [AnuraFDSymbol]: this.domain, }; }, readlink: async (path: string) => { // Check if the path exists in stats path = this.relativizePath(path); const fileName = this.path.basename(path); // eslint-disable-next-line prefer-const let [parentHandle, realParent] = await this.getChildDirHandle(this.path.dirname(path)); if (realParent.startsWith("/")) { realParent = realParent.slice(1); } const stats = this.stats.get(realParent + "/" + fileName); if (!stats) { throw { name: "ENOENT", code: "ENOENT", errno: 34, message: `No such file or directory`, path, stack: "Error: No such file", } as Error; } if (!((stats.mode & 0o170000) === 0o120000)) { throw { // I think this is the wrong error type name: "EINVAL", code: "EINVAL", errno: 342, message: `Is not a symbolic link: ${path}`, stack: "Error: Is not a symbolic link", } as Error; } // Return the target path return await (await (await parentHandle.getFileHandle(fileName)).getFile()).text(); }, symlink: async (target: string, path: string) => { // await this.promises.stat(path); // Save stats and resolve the promise path = this.relativizePath(path); // if (target.startsWith("/")) { // target = this.relativizePath(target); // } const fileName = this.path.basename(path); // eslint-disable-next-line prefer-const let [parentHandle, realParent] = await this.getChildDirHandle(this.path.dirname(path)); const fileHandleWritable = await (await parentHandle.getFileHandle(fileName, { create: true })).createWritable(); fileHandleWritable.write(target); fileHandleWritable.close(); if (realParent.startsWith("/")) realParent = realParent.slice(1); const fullPath = realParent + "/" + fileName; const fileStats = this.stats.get(fullPath) || {}; if (fullPath) { fileStats.mode = 41380; fileStats.ctimeMs = Date.now(); fileStats.mtimeMs = Date.now(); this.stats.set(fullPath, fileStats); } await this.promises.saveStats(); return; }, utimes: async (path: string, atime: Date | number, mtime: Date | number) => { const type = (await this.promises.lstat(path)).type; // Ensure path is relative path = this.relativizePath(path); // If the times are provided as numbers (timestamps), convert them to dates const accessTime = typeof atime === "number" ? new Date(atime) : atime; const modifiedTime = typeof mtime === "number" ? new Date(mtime) : mtime; if (type === "DIRECTORY") { path = (await this.getChildDirHandle(path))[1]; } else { const pathDir = (await this.getChildDirHandle(this.path.dirname(path)))[1]; path = pathDir + "/" + this.path.basename(path); } if (path.startsWith("/")) { path = path.slice(1); } // Fetch the current stats for the file, or initialize them if not present let fileStats = this.stats.get(path); if (!fileStats) { // Try to stat the file if not present in stats map fileStats = await this.promises.stat(path); } // Update the times in the file stats fileStats.atimeMs = accessTime.getTime(); fileStats.mtimeMs = modifiedTime.getTime(); // Save the updated stats back into the stats map this.stats.set(path, fileStats); await this.promises.saveStats(); }, }; ftruncate(fd: AnuraFD, len: number, callback?: (err: Error | null, fd: AnuraFD) => void) { callback ||= () => {}; const handle = this.fds[fd.fd]; if (!handle) { callback( { name: "EBADF", code: "EBADF", errno: 9, message: "bad file descriptor", stack: "Error: bad file descriptor", } as Error, fd, ); return; } const path = (handle as any).path; this.promises .truncate(path, len) .then(() => callback!(null, fd)) .catch(err => { callback(err, fd); }); } fstat(fd: AnuraFD, callback: (err: Error | null, stats: any) => void) { callback ||= () => {}; const handle = this.fds[fd.fd]; if (handle === undefined) { callback( { name: "EBADF", code: "EBADF", errno: 9, message: "bad file descriptor", stack: "Error: bad file descriptor", } as Error, null, ); return; } if (handle.kind === "file") { (handle as FileSystemFileHandle).getFile().then(file => { callback(null, new LocalFSStats({ name: file.name, size: file.size })); }); } else { callback( { name: "EISDIR", code: "EISDIR", errno: 28, message: "Is a directory", stack: "Error: Is a directory", } as Error, null, ); } } lstat(path: string, callback?: (err: Error | null, stats: any) => void): void { callback ||= () => {}; this.promises .lstat(path) .then(stats => callback!(null, stats)) .catch(e => callback!(e, null)); } link(existingPath: string, newPath: string, callback?: (err: Error | null) => void) { callback ||= () => {}; this.promises .link(existingPath, newPath) .then(() => callback(null)) .catch(callback); } symlink(target: string, path: string, type: any, callback?: (err: Error | null) => void) { callback ||= () => {}; this.promises .symlink(target, path) .then(() => callback(null)) .catch(callback); } readlink(path: any, callback?: any) { callback ||= () => {}; this.promises .readlink(path) .then(linkString => callback(null, linkString)) .catch(callback); } access(path: string, mode: any, callback?: (err: Error | null) => void) { callback ||= () => {}; this.promises .access(path, mode) .then(() => callback(null)) .catch(callback); } mkdtemp(prefix: string, options: any, callback?: (err: Error | null, path: string) => void): void { callback ||= () => {}; this.promises .mkdtemp(prefix) .then(folder => callback(null, folder)) .catch(err => { callback(err, null!); }); } fchown(fd: AnuraFD, uid: number, gid: number, callback?: (err: Error | null) => void) { callback ||= () => {}; const handle = this.fds[fd.fd]; if (!handle) { callback({ name: "EBADF", code: "EBADF", errno: 9, message: "bad file descriptor", stack: "Error: bad file descriptor", } as Error); return; } const path = (handle as any).path; // Retrieve the file path // Reuse the chown logic to update ownership by path this.chown(path, uid, gid, callback); } chmod(path: string, mode: number, callback?: (err: Error | null) => void) { this.promises .chmod(path, mode) .then(() => callback!(null)) .catch(callback); } fchmod(fd: AnuraFD, mode: number, callback?: (err: Error | null) => void) { callback ||= () => {}; const handle = this.fds[fd.fd]; if (!handle) { callback({ name: "EBADF", code: "EBADF", errno: 9, message: "bad file descriptor", stack: "Error: bad file descriptor", } as Error); return; } const path = (handle as any).path; // Retrieve the file path this.promises .chmod(path, mode) .then(() => callback!(null)) .catch(callback); } fsync(fd: AnuraFD, callback?: (err: Error | null) => void) { callback ||= () => {}; const handle = this.fds[fd.fd]; if (!handle) { callback({ name: "EBADF", code: "EBADF", errno: 9, message: "bad file descriptor", stack: "Error: bad file descriptor", } as Error); return; } // In the OPFS API, data is automatically flushed to disk, so fsync can be a no-op. callback(null); } write(fd: AnuraFD, buffer: Uint8Array, offset: number, length: number, position: number | null, callback?: (err: Error | null, nbytes: number) => void) { callback ||= () => {}; const handle = this.fds[fd.fd]; if (position !== null) { position += this.cursors[fd.fd] || 0; } else { position = this.cursors[fd.fd] || 0; } if (handle === undefined) { callback( { name: "EBADF", code: "EBADF", errno: 9, message: "bad file descriptor", stack: "Error: bad file descriptor", } as Error, 0, ); return; } if (handle.kind === "directory") { callback( { name: "EISDIR", code: "EISDIR", errno: 28, message: "Is a directory", stack: "Error: Is a directory", } as Error, 0, ); return; } const bufferSlice = buffer.slice(offset, offset + length); (handle as FileSystemFileHandle).createWritable().then(writer => { writer.seek(position || 0); writer.write(bufferSlice); writer.close(); this.cursors[fd.fd] = (position || 0) + bufferSlice.length; callback!(null, bufferSlice.length); }); } read(fd: AnuraFD, buffer: Uint8Array, offset: number, length: number, position: number | null, callback?: (err: Error | null, bytesRead: number, buffer: Uint8Array) => void) { callback ||= () => {}; const handle = this.fds[fd.fd]; if (handle === undefined) { callback( { name: "EBADF", code: "EBADF", errno: 9, message: "bad file descriptor", stack: "Error: bad file descriptor", } as Error, 0, null!, ); return; } if (handle.kind === "directory") { callback( { name: "EISDIR", code: "EISDIR", errno: 28, message: "Is a directory", stack: "Error: Is a directory", } as Error, 0, null!, ); return; } // Resolve symbolic link, if necessary const fileHandle = handle as FileSystemFileHandle; fileHandle.getFile().then(file => { const reader = new FileReader(); reader.onload = () => { const data = new Uint8Array(reader.result as ArrayBuffer); buffer.set(data.slice(offset, offset + length)); callback(null, data.length, buffer); }; reader.onerror = e => { callback(new Error("Failed to read file"), 0, null!); }; reader.readAsArrayBuffer(file.slice(position || 0, (position || 0) + length)); }); } utimes(path: string, atime: Date | number, mtime: Date | number, callback?: (err: Error | null) => void) { callback ||= () => {}; this.promises .utimes(path, atime, mtime) .then(() => callback!(null)) .catch(callback); } futimes(fd: AnuraFD, atime: Date, mtime: Date, callback?: (err: Error | null) => void) { callback ||= () => {}; const handle = this.fds[fd.fd]; if (!handle) { callback({ name: "EBADF", code: "EBADF", errno: 9, message: "bad file descriptor", stack: "Error: bad file descriptor", } as Error); return; } const path = (handle as any).path; // Retrieve the file path this.utimes(path, atime, mtime, callback); } chown(path: string, uid: number, gid: number, callback?: (err: Error | null) => void) { callback ||= () => {}; this.promises .chown(path, uid, gid) .then(() => callback(null)) .catch(callback); } close(fd: AnuraFD, callback: (err: Error | null) => void) { callback ||= () => {}; const handle = this.fds[fd.fd]; if (handle === undefined) { callback({ name: "EBADF", code: "EBADF", errno: 9, message: "bad file descriptor", stack: "Error: bad file descriptor", } as Error); return; } delete this.fds[fd.fd]; callback(null); } open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode?: any, callback?: ((err: Error | null, fd: AnuraFD) => void) | undefined): void { if (typeof mode === "function") { callback = mode; } callback ||= () => {}; this.promises .open(path, flags, mode) .then(fd => { // @ts-ignore callback!(null, fd); }) // @ts-ignore .catch(e => callback!(e, { fd: -1, [AnuraFDSymbol]: this.domain })); } } ================================================ FILE: src/sys/liquor/api/Networking.ts ================================================ export class Networking { libcurl: any; // @ts-expect-error WebSocket: typeof WebSocket; Socket: any; TLSSocket: any; external = { fetch: window.fetch, }; constructor() { if (window.tb.libcurl) { this.libcurl = window.tb.libcurl; this.initLibcurl(); } else { console.warn("Anura Networking failed to connect to the TB instance"); } } private initLibcurl = async () => { try { this.WebSocket = this.libcurl.WebSocket; // @ts-ignore this.external.fetch = (...args) => { return this.libcurl.fetch(...args); }; this.Socket = this.libcurl.WispConnection; this.TLSSocket = this.libcurl.TLSSocket; const wisp_server = JSON.parse(await window.tb.fs.promises.readFile(`/home/${await window.tb.user.username()}/settings.json`, "utf8")).wispServer; this.setWispServer(wisp_server); console.log("libcurl.js ready!"); } catch (error) { console.warn("Anura Networking Error:", error); } }; loopback = { addressMap: new Map(), call: async (port: number, request: Request) => { return await this.loopback.addressMap.get(port)(request); }, set: async (port: number, handler: () => Response) => { this.loopback.addressMap.set(port, handler); }, deregister: async (port: number) => { this.loopback.addressMap.delete(port); }, }; fetch = async (url: any, methods: any) => { console.log(this.libcurl.ready); let requestObj: Request; if (url instanceof Request) { requestObj = url; } else { if (methods) requestObj = new Request(url, methods); else requestObj = new Request(url); } const urlObj = new URL(requestObj.url); if (urlObj.hostname === "localhost") { const port = Number(urlObj.port) || 80; if (this.loopback.addressMap.has(port)) return this.loopback.call(port, requestObj); else { window.anura.notifications.add({ title: "Anura Networking Error", description: "fetch requested to non binded localhost port", timeout: 5000, }); return new Response(); } } else { return this.external.fetch(url, methods); } }; setWispServer = (wisp_server: string) => { this.libcurl.set_websocket(wisp_server); }; } ================================================ FILE: src/sys/liquor/api/Notification.ts ================================================ //TODO ================================================ FILE: src/sys/liquor/api/NotificationService.tsx ================================================ interface NotifParams { title: string; description: string; timeout?: number; callback?: () => void; closeIndicator?: boolean; icon?: string; buttons?: Array<{ text: string; callback: Function }>; } export class NotificationService { element: HTMLDivElement | null = null; constructor() { console.log("Loading notifications API"); } add(params: NotifParams) { // API STUB console.log(params); window.parent.tb.notification.Toast({ application: params.title, iconSrc: "/assets/img/logo.png", message: params.description, time: params.timeout ? params.timeout : 10000, onOk: params.callback, }); } remove(_notification: any) { // API STUB } } ================================================ FILE: src/sys/liquor/api/Platform.ts ================================================ export class Platform { type: string; touchInput: boolean; constructor() { this.type = "desktop"; this.touchInput = false; const mobileRE = /(android|bb\d+|meego).+mobile|armv7l|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series[46]0|samsungbrowser.*mobile|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i; const notMobileRE = /CrOS/; const tabletRE = /android|ipad|playbook|silk/i; const ua = navigator.userAgent; if (typeof ua === "string") { if (mobileRE.test(ua) && !notMobileRE.test(ua)) { console.log("Mobile detected"); this.type = "mobile"; this.touchInput = true; } else if (tabletRE.test(ua)) { console.log("Tablet detected"); this.type = "tablet"; this.touchInput = true; } if (!mobileRE.test(ua) && navigator && navigator.maxTouchPoints > 1 && ua.indexOf("Macintosh") !== -1 && ua.indexOf("Safari") !== -1) { console.log("Mobile detected"); this.type = "mobile"; this.touchInput = true; } } } } ================================================ FILE: src/sys/liquor/api/Process.ts ================================================ export class Processes { processesDiv: HTMLDivElement | null; constructor() { this.processesDiv = document.querySelector("window-area"); } get procs() { const wins = window.anura.wm.windows; const arr: WeakRef[] = wins.reduce((out: WeakRef[], w: any) => { if (!w) return out; out.push(typeof w?.deref === "function" ? (w as WeakRef) : new WeakRef(w)); return out; }, []); const s1 = Symbol(); const s2 = Symbol(); (arr as any)[s1] = []; (arr as any)[s2] = Array.from(arr); return new Proxy(arr, {}); } set procs(value) { console.log(`API Stub, ${value} will not be used`); window.tb.process.create(); } remove(pid: number) { window.tb.process.kill(String(pid)); } register(proc: Process) { console.log(`API Stub, ${proc} will not be used`); window.tb.process.create(); } create(proc: any) { console.log(`API Stub, ${proc} will not be used`); window.tb.process.create(); } } abstract class Process { abstract pid: number; abstract title: string; // @ts-expect-error stdout: ReadableStream; // @ts-expect-error stderr: ReadableStream; // @ts-expect-error stdin: WritableStream; kill() { window.tb.process.kill(String(this.pid)); } abstract get alive(): boolean; } ================================================ FILE: src/sys/liquor/api/Settings.ts ================================================ export class Settings { private cache: { [key: string]: any } = {}; fs: FilerFS; private constructor(fs: FilerFS, inital: { [key: string]: any }) { this.fs = fs; this.cache = inital; navigator.serviceWorker.ready.then(isReady => { isReady.active!.postMessage({ anura_target: "anura.cache", value: this.cache["use-sw-cache"], }); isReady.active!.postMessage({ anura_target: "anura.bareurl", value: this.cache["bare-url"], }); console.debug("ANURA-SW: For this boot, cache will be " + (this.cache["use-sw-cache"] ? "enabled" : "disabled")); this.cache["FileExts"] = { txt: { handler_type: "module", id: "anura.fileviewer" }, mp3: { handler_type: "module", id: "anura.fileviewer" }, flac: { handler_type: "module", id: "anura.fileviewer" }, wav: { handler_type: "module", id: "anura.fileviewer" }, ogg: { handler_type: "module", id: "anura.fileviewer" }, mp4: { handler_type: "module", id: "anura.fileviewer" }, mov: { handler_type: "module", id: "anura.fileviewer" }, webm: { handler_type: "module", id: "anura.fileviewer" }, gif: { handler_type: "module", id: "anura.fileviewer" }, png: { handler_type: "module", id: "anura.fileviewer" }, jpg: { handler_type: "module", id: "anura.fileviewer" }, jpeg: { handler_type: "module", id: "anura.fileviewer" }, svg: { handler_type: "module", id: "anura.fileviewer" }, pdf: { handler_type: "module", id: "anura.fileviewer" }, py: { handler_type: "module", id: "anura.fileviewer" }, js: { handler_type: "module", id: "anura.fileviewer" }, mjs: { handler_type: "module", id: "anura.fileviewer" }, cjs: { handler_type: "module", id: "anura.fileviewer" }, json: { handler_type: "module", id: "anura.fileviewer" }, html: { handler_type: "module", id: "anura.fileviewer" }, css: { handler_type: "module", id: "anura.fileviewer" }, default: { handler_type: "module", id: "anura.fileviewer" }, }; }); } static async new(fs: FilerFS, defaultsettings: { [key: string]: any }) { const initial = defaultsettings; if (!initial["wisp-url"]) { let url = ""; if (location.protocol == "https:") { url += "wss://"; } else { url += "ws://"; } url += window.location.origin.split("://")[1]; url += "/"; initial["wisp-url"] = url; } try { const raw = await fs.promises.readFile("/system/etc/anura/anura_settings.json"); // This Uint8Array is actuallly a buffer, so JSON.parse can handle it Object.assign(initial, JSON.parse(raw as any)); } catch (e) { fs.mkdir("/system/etc/anura/"); fs.mkdir("/system/etc/anura/configs/"); fs.mkdir("/system/etc/anura/init/"); fs.mkdir("/system/bin/anura/"); fs.writeFile( "/system/etc/anura/theme.json", JSON.stringify({ foreground: "#ffffff", secondaryForeground: "#ffffff38", border: "#ffffff28", darkBorder: "#333333", background: "#0e0e0e", secondaryBackground: "#383838", darkBackground: "#161616", accent: "#32ae62", }), ); fs.writeFile("/system/etc/anura/anura_settings.json", JSON.stringify(initial)); } return new Settings(fs, initial); } get(prop: string): any { return this.cache[prop]; } has(prop: string): boolean { return prop in this.cache; } async set(prop: string, val: any, subprop?: string) { console.debug("Setting " + prop + " to " + val); if (subprop) { this.cache[prop][subprop] = val; } else { this.cache[prop] = val; } this.save(); } async save() { console.debug("Saving settings to fs", this.cache); await this.fs.promises.writeFile("/system/etc/anura/anura_settings.json", JSON.stringify(this.cache)); } async remove(prop: string, subprop?: string) { console.warn("anura.settings.remove() is a debug feature, and should not be used outside of development."); if (subprop) { delete this.cache[prop][subprop]; } else { delete this.cache[prop]; } this.save(); } } ================================================ FILE: src/sys/liquor/api/Systray.ts ================================================ import { useWindowStore } from "../../Store"; export class SystrayIcon { onclick = () => {}; onrightclick = () => {}; get icon() { return "This API is not supported."; } get tooltip() { return "This API is not supported."; } destroy = () => { window.tb.window.island.removeControl("com.anura.genericsystray"); }; } export class Systray { icon: SystrayIcon[] = []; create = (args: any) => { const win = useWindowStore().windows.find((win: any) => win.pid === window.tb.window.getId()); const title = win ? win.title : "Anura File Manager"; window.tb.window.island.addControl({ text: `${args.tooltip}`, // @ts-expect-error click: () => this.icon.onclick, appname: title, id: "com.anura.genericsystray", }); }; } ================================================ FILE: src/sys/liquor/api/TFS.ts ================================================ // @ts-nocheck import { AFSProvider } from "./Filesystem"; const AnuraFDSymbol = Symbol.for("AnuraFD"); import { FSType } from "@terbiumos/tfs"; type AnuraFD = { fd: number; [AnuraFDSymbol]: string; }; export class TFSProvider extends AFSProvider { domain = "/"; name = "TFS Anura Provider"; version = window.tfs.version; fs: FSType; constructor(fs: FSType) { super(); this.fs = fs; } rename(oldPath: string, newPath: string, callback?: (err: Error | null) => void) { const fs = window.tb.vfs.whatFS(oldPath); fs.rename(oldPath, newPath, callback); } ftruncate(fd: AnuraFD, len: number, callback?: (err: Error | null, fd: AnuraFD) => void) { throw new Error("Method not implemented."); } truncate(path: string, len: number, callback?: (err: Error | null) => void) { throw new Error("Method not implemented."); } stat(path: string, callback?: (err: Error | null, stats: any) => void) { const fs = window.tb.vfs.whatFS(path); fs.stat(path, callback); } fstat(fd: AnuraFD, callback?: ((err: Error | null, stats: any) => void) | undefined): void { throw new Error("Method not implemented."); } lstat(path: string, callback?: (err: Error | null, stats: any) => void) { const fs = window.tb.vfs.whatFS(path); fs.lstat(path, callback); } exists(path: string, callback?: (exists: boolean) => void) { const fs = window.tb.vfs.whatFS(path); this.fs.exists(path, callback); } link(srcPath: string, dstPath: string, callback?: (err: Error | null) => void) { const fs = window.tb.vfs.whatFS(srcPath); if (!fs.link) throw new Error("Linking not supported on this filesystem."); fs.link(srcPath, dstPath, callback); } symlink(path: string, callback?: (err: Error | null) => void, ...rest: any[]) { const fs = window.tb.vfs.whatFS(path); if (!fs.symlink) throw new Error("Symlinking not supported on this filesystem."); // @ts-expect-error - Overloaded methods are scary fs.symlink(path, callback, ...rest); } readlink(path: string, callback?: (err: Error | null, linkContents: string) => void) { const fs = window.tb.vfs.whatFS(path); if (!fs.readlink) throw new Error("Reading links not supported on this filesystem."); fs.readlink(path, callback); } unlink(path: string, callback?: (err: Error | null) => void) { const fs = window.tb.vfs.whatFS(path); if (!fs.unlink) throw new Error("Unlinking not supported on this filesystem."); fs.unlink(path, callback); } mknod(path: string, mode: number, callback?: (err: Error | null) => void) { throw new Error("Method not implemented."); } rmdir(path: string, callback?: (err: Error | null) => void) { const fs = window.tb.vfs.whatFS(path); fs.rmdir(path, callback); } mkdir(path: string, callback?: (err: Error | null) => void, ...rest: any[]) { const fs = window.tb.vfs.whatFS(path); fs.mkdir(path, callback, ...rest); } access(path: string, callback?: (err: Error | null) => void, ...rest: any[]) { const fs = window.tb.vfs.whatFS(path); fs.access(path, callback, ...rest); } mkdtemp(...args: any[]) { window.tfs.shell.tempDir(...args); } readdir(path: string, callback?: (err: Error | null, files?: string[] | any[]) => void, ...rest: any[]) { const fs = window.tb.vfs.whatFS(path); fs.readdir(path, callback, ...rest); } close(fd: AnuraFD, callback?: ((err: Error | null) => void) | undefined): void { throw new Error("Method not implemented."); } open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode: number, callback?: ((err: Error | null, fd: AnuraFD) => void) | undefined): void; open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", callback?: ((err: Error | null, fd: AnuraFD) => void) | undefined): void; open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode?: unknown, callback?: unknown): void { throw new Error("Method not implemented."); } utimes(path: string, atime: number | Date, mtime: number | Date, callback?: (err: Error | null) => void) { throw new Error("Method not implemented."); } futimes(fd: AnuraFD, ...rest: any[]) { throw new Error("Method not implemented."); } chown(path: string, uid: number, gid: number, callback?: (err: Error | null) => void) { const fs = window.tb.vfs.whatFS(path); if (!fs.chown) throw new Error("Chowning not supported on this filesystem."); fs.chown(path, uid, gid, callback); } fchown(fd: AnuraFD, ...rest: any[]) { throw new Error("Method not implemented."); } chmod(path: string, mode: number, callback?: (err: Error | null) => void) { const fs = window.tb.vfs.whatFS(path); if (!fs.chmod) throw new Error("Chmod not supported on this filesystem."); fs.chmod(path, mode, callback); } fchmod(fd: AnuraFD, ...rest: any[]) { throw new Error("Method not implemented."); } fsync(fd: AnuraFD, ...rest: any[]) { // @ts-expect-error - Overloaded methods are scary this.fs.fsync(fd.fd, ...rest); } write(fd: AnuraFD, ...rest: any[]) { // @ts-expect-error - Overloaded methods are scary this.fs.write(fd.fd, ...rest); } read(fd: AnuraFD, ...rest: any[]) { // @ts-expect-error - Overloaded methods are scary this.fs.read(fd.fd, ...rest); } readFile(path: string, callback?: (err: Error | null, data: Uint8Array) => void) { const fs = window.tb.vfs.whatFS(path); fs.readFile(path, callback); } writeFile(path: string, ...rest: any[]) { const fs = window.tb.vfs.whatFS(path); // @ts-expect-error - Overloaded methods are scary fs.writeFile(path, ...rest); } appendFile(path: string, data: Uint8Array, callback?: (err: Error | null) => void) { const fs = window.tb.vfs.whatFS(path); fs.appendFile(path, data, callback); } setxattr(path: string, ...rest: any[]) { const fs = window.tb.vfs.whatFS(path); if (!fs.setxattr) throw new Error("Extended attributes not supported on this filesystem."); fs.setxattr(path, ...rest); } fsetxattr(fd: AnuraFD, ...rest: any[]) { throw new Error("Method not implemented."); } getxattr(path: string, name: string, callback?: (err: Error | null, value: string | object) => void) { const fs = window.tb.vfs.whatFS(path); if (!fs.getxattr) throw new Error("Extended attributes not supported on this filesystem."); fs.getxattr(path, name, callback); } fgetxattr(fd: AnuraFD, name: string, callback?: (err: Error | null, value: string | object) => void) { throw new Error("Method not implemented."); } removexattr(path: string, name: string, callback?: (err: Error | null) => void) { throw new Error("Method not implemented."); } fremovexattr(fd: AnuraFD, ...rest: any[]) { throw new Error("Method not implemented."); } promises = { appendFile: (path: string, data: Uint8Array, options: { encoding: string; mode: number; flag: string }) => { return new Promise((resolve, reject) => { this.appendFile(path, data, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); }, access: (path: string, mode?: number) => { return new Promise((resolve, reject) => { this.access(path, mode, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); }, chown: (path: string, uid: number, gid: number) => { return new Promise((resolve, reject) => { this.chown(path, uid, gid, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); }, chmod: (path: string, mode: number) => { return new Promise((resolve, reject) => { this.chmod(path, mode, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); }, getxattr: (path: string, name: string) => { return new Promise((resolve, reject) => { this.getxattr(path, name, (err: Error | null, value: string | object) => { if (err) reject(err); else resolve(value); }); }); }, link: (srcPath: string, dstPath: string) => { return new Promise((resolve, reject) => { this.link(srcPath, dstPath, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); }, lstat: (path: string) => { return new Promise((resolve, reject) => { this.lstat(path, (err: Error | null, stats: any) => { if (err) reject(err); else resolve(stats); }); }); }, mkdir: (path: string, mode?: number) => { return new Promise((resolve, reject) => { this.mkdir(path, mode, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); }, mkdtemp: (prefix: string, options?: { encoding: string }) => { return new Promise((resolve, reject) => { this.mkdtemp(prefix, options, (err: Error | null, folder: string) => { if (err) reject(err); else resolve(folder); }); }); }, mknod: (path: string, mode: number) => { return new Promise((resolve, reject) => { this.mknod(path, mode, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); }, open: async (path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode?: number) => ({ fd: await new Promise((resolve, reject) => { this.open(path, flags, mode as any, (err: Error | null, fd: AnuraFD) => { if (err) reject(err); else resolve(fd.fd); }); }), [AnuraFDSymbol]: this.domain, }), readdir: (path: string, options?: { encoding: string; withFileTypes: boolean }) => { return new Promise((resolve, reject) => { this.readdir(path, (err: Error | null, files: string[] | any[]) => { if (err) reject(err); else resolve(files); }); }); }, readFile: (path: string) => { return new Promise((resolve, reject) => { this.readFile(path, (err: Error | null, data: Uint8Array) => { if (err) reject(err); else resolve(data); }); }); }, readlink: (path: string) => { return new Promise((resolve, reject) => { this.readlink(path, (err: Error | null, linkString: string) => { if (err) reject(err); else resolve(linkString); }); }); }, removexattr: (path: string, name: string) => { return new Promise((resolve, reject) => { this.removexattr(path, name, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); }, rename: (oldPath: string, newPath: string) => { return new Promise((resolve, reject) => { this.rename(oldPath, newPath, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); }, rmdir: (path: string) => { return new Promise((resolve, reject) => { this.rmdir(path, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); }, setxattr: (path: string, name: string, value: string | object, flag?: "CREATE" | "REPLACE") => { return new Promise((resolve, reject) => { this.setxattr(path, name, value, flag, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); }, stat: (path: string) => { return new Promise((resolve, reject) => { this.stat(path, (err: Error | null, stats: any) => { if (err) reject(err); else resolve(stats); }); }); }, symlink: (srcPath: string, dstPath: string, type?: string) => { return new Promise((resolve, reject) => { this.symlink(srcPath, dstPath, type, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); }, truncate: (path: string, len: number) => { return new Promise((resolve, reject) => { this.truncate(path, len, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); }, unlink: (path: string) => { return new Promise((resolve, reject) => { this.unlink(path, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); }, utimes: (path: string, atime: number | Date, mtime: number | Date) => { return new Promise((resolve, reject) => { this.utimes(path, atime, mtime, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); }, writeFile: (path: string, data: Uint8Array | string, options: { encoding: string; mode: number; flag: string }) => { return new Promise((resolve, reject) => { this.writeFile(path, data, options, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); }, }; } ================================================ FILE: src/sys/liquor/api/Theme.ts ================================================ interface ThemeProps { foreground: string; secondaryForeground: string; border: string; darkBorder: string; background: string | any; secondaryBackground: string; darkBackground: string; accent: string | any; } export class Theme implements ThemeProps { // @ts-expect-error settings: ThemeProps = {}; constructor() { window.tb.fs.promises .readFile("/system/etc/anura/theme.json", "utf8") .then((data: string) => { this.settings = JSON.parse(data); }) .catch((err: any) => { if (localStorage.getItem("setup")) console.warn("Error reading theme settings:", err); }); } get foreground() { return this.settings.foreground; } set foreground(value) { window.tb.fs.readFile("/system/etc/anura/theme.json", (err: Error | null, data: Uint8Array) => { if (err) { console.error(err); return; } const settings: ThemeProps = JSON.parse(data.toString()); settings.foreground = value; window.tb.fs.writeFile("/system/etc/anura/theme.json", JSON.stringify(settings)); }); } get secondaryForeground() { return this.settings.secondaryForeground; } set secondaryForeground(value) { window.tb.fs.readFile("/system/etc/anura/theme.json", (err: Error | null, data: Uint8Array) => { if (err) { console.error(err); return; } const settings: ThemeProps = JSON.parse(data.toString()); settings.secondaryForeground = value; window.tb.fs.writeFile("/system/etc/anura/theme.json", JSON.stringify(settings)); }); } get border() { return this.settings.border; } set border(value) { window.tb.fs.readFile("/system/etc/anura/theme.json", (err: Error | null, data: Uint8Array) => { if (err) { console.error(err); return; } const settings: ThemeProps = JSON.parse(data.toString()); settings.border = value; window.tb.fs.writeFile("/system/etc/anura/theme.json", JSON.stringify(settings)); }); } get darkBorder() { return this.settings.darkBorder; } set darkBorder(value) { window.tb.fs.readFile("/system/etc/anura/theme.json", (err: Error | null, data: Uint8Array) => { if (err) { console.error(err); return; } const settings: ThemeProps = JSON.parse(data.toString()); settings.darkBorder = value; window.tb.fs.writeFile("/system/etc/anura/theme.json", JSON.stringify(settings)); }); } get background() { return this.settings.background; } set background(value) { window.tb.fs.readFile("/system/etc/anura/theme.json", (err: Error | null, data: Uint8Array) => { if (err) { console.error(err); return; } const settings: ThemeProps = JSON.parse(data.toString()); settings.background = value; window.tb.fs.writeFile("/system/etc/anura/theme.json", JSON.stringify(settings)); }); } get secondaryBackground() { return this.settings.secondaryBackground; } set secondaryBackground(value) { window.tb.fs.readFile("/system/etc/anura/theme.json", (err: Error | null, data: Uint8Array) => { if (err) { console.error(err); return; } const settings: ThemeProps = JSON.parse(data.toString()); settings.secondaryBackground = value; window.tb.fs.writeFile("/system/etc/anura/theme.json", JSON.stringify(settings)); }); } get darkBackground() { return this.settings.darkBackground; } set darkBackground(value) { window.tb.fs.readFile("/system/etc/anura/theme.json", (err: Error | null, data: Uint8Array) => { if (err) { console.error(err); return; } const settings: ThemeProps = JSON.parse(data.toString()); settings.darkBackground = value; window.tb.fs.writeFile("/system/etc/anura/theme.json", JSON.stringify(settings)); }); } get accent() { return this.settings.accent; } set accent(value) { window.tb.fs.readFile("/system/etc/anura/theme.json", (err: Error | null, data: Uint8Array) => { if (err) { console.error(err); return; } const settings: ThemeProps = JSON.parse(data.toString()); settings.accent = value; window.tb.fs.writeFile("/system/etc/anura/theme.json", JSON.stringify(settings)); }); } cssPropMap: Record = { background: ["--theme-bg", "--material-bg"], border: ["--theme-border", "--material-border"], darkBorder: ["--theme-dark-border"], foreground: ["--theme-fg"], secondaryBackground: ["--theme-secondary-bg"], secondaryForeground: ["--theme-secondary-fg"], darkBackground: ["--theme-dark-bg"], accent: ["--theme-accent", "--matter-helper-theme"], }; state: ThemeProps = this.settings; css(): string { const lines = []; lines.push(":root {"); for (const key in this.state) { for (const prop of this.cssPropMap[key as keyof ThemeProps]) { lines.push(` ${prop}: ${this.state[key as keyof ThemeProps]};`); } } lines.push("}"); return lines.join("\n"); } reset() { (this.foreground = "#ffffff"), (this.secondaryForeground = "#ffffff38"), (this.border = "#ffffff28"), (this.darkBorder = "#333333"), (this.background = "#0e0e0e"), (this.secondaryBackground = "#383838"), (this.darkBackground = "#161616"), (this.accent = "#32ae62"); } } ================================================ FILE: src/sys/liquor/api/UI.ts ================================================ import { Theme } from "./Theme"; export class AnuraUI { /** * This map contains all the built-in components that have been registered. */ builtins = new Map(); /** * This map contains all the components that have been registered from external libraries. */ components = new Map(); theme = new Theme(); /** * This function allows you to register a component to the built-in components registry. * @param component - The name of the component to register. * @param element - A function component that returns an HTMLElement. */ async registerComponent(component: string, element: HTMLDivElement): Promise { this.builtins.set(component, element); } /** * This function allows you to register a component from an external library. * @param lib - The name of the library to import the component from. * @param component - The name of the component to register. * @param version - (Optional) The version of the library to import the component from. */ async registerExternalComponent(lib: string, component: string, version?: string): Promise { if (version) { lib += "@" + version; } this.components.set(component, { lib, name: component, }); window.anura.settings.set("anura.ui.components", Array.from(this.components.entries())); } /** * This function allows you to import a component, whether it is a built-in component or a component from a library. * @param name - The name of the component to import. * @returns A promise that resolves to a function component that returns an HTMLElement. */ async get(name: string): Promise { const comp = this.components.get(name); if (!comp) { if (this.builtins.has(name)) { return this.builtins.get(name)!; } throw new Error("Component not registered"); } const [lib, scope_name] = [comp.lib, comp.name]; const library = await window.anura.import(lib); return library[scope_name]; } /** * This function allows you to check if a component is registered. * @param component - The name of the component to check. * @returns Whether the component is registered or not. */ exists(component: string): boolean { return this.components.has(component) || this.builtins.has(component); } async use(components: string[] | string | "*" = []): Promise<{ [key: string]: any }> { const result: { [key: string]: any; } = {}; if (components === "*") { components = Array.from(this.components.keys()).concat(Array.from(this.builtins.keys())); } if (typeof components === "string") { components = [components]; } for (const component of components) { result[component] = await this.get(component); } return result; } /** * Install internal components */ init() { const components = window.anura.settings.get("anura.ui.components"); if (components) { try { this.components = new Map(components); } catch (e) { this.components = new Map(); } } // API stub, Rest will not be implemented } } ================================================ FILE: src/sys/liquor/api/URIHandler.ts ================================================ interface LibURIHandler { tag: "lib"; pkg: string; version?: string; import: string; } type SplitArgMethod = { tag: "split"; separator: RegExp | string; }; type SingleArgMethod = { tag: "single"; }; interface AppURIHandler { tag: "app"; pkg: string; method: SplitArgMethod | SingleArgMethod; } interface URIHandlerOptions { handler: LibURIHandler | AppURIHandler; prefix?: string; } export class URIHandlerAPI { // Handles a URI like "protocol:something/etc" by opening the appropriate app or library. async handle(uri: string): Promise { // const url = new URL(uri); // const protocol = url.protocol.slice(0, -1); const [protocol, ...path] = uri.split(":"); const pathname = path.join(":"); const handlers = window.anura.settings.get("URIHandlers") || {}; const handler = handlers[protocol as string]; if (!handler) { throw new Error(`No handler for URI protocol ${protocol}`); } if (handler.handler.tag === "lib") { let lib; if (handler.handler.version) { lib = await window.anura.import(handler.handler.pkg + "@" + handler.handler.version); } else { lib = await window.anura.import(handler.handler.pkg); } await lib[handler.handler.import]((handler.prefix || "") + pathname); } else if (handler.handler.tag === "app") { const app = handler.handler; if (app.method && app.method.tag !== undefined && app.method.tag === "split") { const args = pathname.split(app.method.separator); await window.anura.apps[app.pkg].open(handler.prefix ? [handler.prefix, ...args] : args); } else { window.tb.window.create({ title: "Terbium Webview", src: handler.prefix, size: { width: 460, height: 460, minWidth: 160, minHeight: 160, }, icon: "/apps/browser.tapp/icon.svg", }); } } } // Sets a handler for a URI protocol. set(protocol: string, options: URIHandlerOptions): void { const handlers = window.anura.settings.get("URIHandlers") || {}; handlers[protocol] = options; window.anura.settings.set("URIHandlers", handlers); } // Removes a handler for a URI protocol. remove(protocol: string): void { const handlers = window.anura.settings.get("URIHandlers") || {}; delete handlers[protocol]; window.anura.settings.set("URIHandlers", handlers); } // Determines if a handler is set for a URI protocol. has(protocol: string): boolean { const handlers = window.anura.settings.get("URIHandlers") || {}; return !!handlers[protocol]; } } ================================================ FILE: src/sys/liquor/api/WmApi.tsx ================================================ import { AnuraWMWeakRef } from "@/sys/types"; import { AliceWM, WindowInformation } from "../AliceWM"; import { App } from "../coreapps/App"; export class WMAPI { windows: WeakRef[] = []; constructor() { setInterval(() => { for (const proc of Object.values(window.tb.process.list())) { this.convertProc(proc.pid); } }, 1); } async create(ctx: App | any, _info: WindowInformation, _onfocus: (() => void) | null = null, _onresize: ((w: number, h: number) => void) | null = null) { const win = await AliceWM.create(ctx); this.windows.push(new WeakRef(win)); return win; } async createGeneric(_ctx: App, info: object) { if (!info) { info = { title: "Generic Window", icon: "/assets/img/logo.png", minheight: 40, minwidth: 40, width: "1000px", height: "500px", allowMultipleInstance: false, }; } // @ts-expect-error const win = await AliceWM.create(info); //ctx.windows.push(win); This was causing problems this.windows.push(new WeakRef(win)); return win; } /** NON SPECED FOR TERBIUM COMPATABILITY ONLY */ convertProc(pid: number) { const winInf = window.tb.process.list()[pid]; if (!winInf) return; for (const ref of this.windows) { const r = ref?.deref?.() ?? null; if (!r) continue; if (r.pid === pid) return; if (typeof r.title === "string" && r.title === winInf.name) return; } const tbanuraproperties: WindowInformation = { title: winInf.name, icon: winInf.icon, minheight: 40, minwidth: 40, width: winInf.size?.width ? winInf.size.width : "0px", height: winInf.size?.height ? winInf.size.height : "0px", allowMultipleInstance: false, }; // Sorry, DOM has to be used here for cross compatability const elem = document.querySelector(`div[pid="${pid}"]`); const winControls = elem?.querySelectorAll(".controls.flex.gap-1") ?? []; const obj: AnuraWMWeakRef = { element: elem, content: elem?.querySelector(".w-full.h-full"), // @ts-expect-error keep same behavior — ExternalApp may be nonstandard app: new ExternalApp(winInf), dragForceX: 0, dragForceY: 0, dragging: false, height: winInf.size?.height ? winInf.size.height : 0, width: winInf.size?.width ? winInf.size.width : 0, pid: pid, state: null, maximized: false, minimizing: false, mouseLeft: null, mouseTop: null, onclose: () => null, onfocus: () => null, onresize: (_w: number, _h: number) => null, onsnap: (_side: string) => null, onunmaximize: () => null, restoreSvg: winControls[0]?.childNodes[1] || null, kill() { if (obj.pid != null) window.tb.process.kill(obj.pid); }, get alive() { return window.tb.process.list()[pid] != null; }, maximizeImg: winControls[0]?.childNodes[1] || null, maximizeSvg: winControls[0]?.childNodes[1] || null, wininfo: tbanuraproperties, title: winInf.name, }; this.windows.push(new WeakRef(obj)); } getWeakRef(pid: number) { for (const ref of this.windows) { const r = ref.deref(); if (!r) continue; if (Number(r.pid) === pid) return r; } } } ================================================ FILE: src/sys/liquor/bcc.ts ================================================ export class AnuraBareClient { ready = true; constructor() {} async init() { this.ready = true; } async meta() {} async request(remote: URL, method: string, body: BodyInit | null, headers: any, signal: AbortSignal | undefined): Promise { const payload = await window.anura.net.fetch(remote.href, { method, headers: headers, body, redirect: "manual", duplex: "half", }); const respheaders = {}; //@ts-ignore if (payload.raw_headers) for (const [key, value] of payload.raw_headers) { //@ts-ignore if (!respheaders[key]) { //@ts-ignore respheaders[key] = [value]; } else { //@ts-ignore respheaders[key].push(value); } } return { body: payload.body!, headers: respheaders, status: payload.status, statusText: payload.statusText, }; } connect( url: URL, origin: string, protocols: string[], requestHeaders: any, onopen: (protocol: string) => void, onmessage: (data: Blob | ArrayBuffer | string) => void, onclose: (code: number, reason: string) => void, onerror: (error: string) => void, ): [(data: Blob | ArrayBuffer | string) => void, (code: number, reason: string) => void] { //@ts-ignore const socket = new window.anura.net.WebSocket(url.toString(), protocols, { headers: requestHeaders, }); //bare client always expects an arraybuffer for some reason socket.binaryType = "arraybuffer"; socket.onopen = (event: Event) => { onopen(""); }; socket.onclose = (event: CloseEvent) => { onclose(event.code, event.reason); }; socket.onerror = (event: Event) => { onerror(""); }; socket.onmessage = (event: MessageEvent) => { onmessage(event.data); }; return [ data => { socket.send(data); }, (code, reason) => { socket.close(code, reason); }, ]; } } ================================================ FILE: src/sys/liquor/coreapps/App.tsx ================================================ // @ts-nocheck export class App { icon: string; package: string; name: string; hidden = false; windows: any[] = []; open(args: string[] = []): void {} } ================================================ FILE: src/sys/liquor/coreapps/ExternalApp.tsx ================================================ import { AliceWM, WindowInformation } from "../AliceWM"; import { AppManifest } from "../Anura"; import { App } from "./App"; import { LocalFS } from "../api/LocalFS"; export class ExternalApp extends App { manifest: AppManifest; source: string; icon = "/assets/icons/generic.png"; constructor(manifest: AppManifest, source: string) { super(); this.manifest = manifest; this.name = manifest.name; if (manifest.icon) { this.icon = source + "/" + manifest.icon; } this.source = source; this.package = manifest.package; this.hidden = manifest.hidden || false; } static serializeArgs(args: string[]): string { const encoder = new TextEncoder(); const encodedValues = args.map(value => { const bytes = encoder.encode(value); const binString = String.fromCodePoint(...bytes); return btoa(binString); }); return encodeURIComponent(encodedValues.join(",")); } static deserializeArgs(args: string): string[] { const decoder = new TextDecoder("utf-8"); return decodeURIComponent(args) .split(",") .map(value => { const binString = atob(value); return decoder.decode(Uint8Array.from(binString, c => c.charCodeAt(0))); }); } //@ts-expect-error manual apps exist async open(args: string[] = []): Promise { // TODO: have a "allowmultiinstance" option in manifest? it might confuse users, some windows open a second, some focus // if (this.windowinstance) return; if (this.manifest.type === "auto") { const win = await window.anura.wm.create(this, this.manifest.wininfo as unknown as WindowInformation); const iframe = document.createElement("iframe"); // CSS injection here but it's no big deal const bg = this.manifest.background || "#202124"; iframe.setAttribute("style", "top:0; left:0; bottom:0; right:0; width:100%; height:100%; " + `border: none; margin: 0; padding: 0; background-color: ${bg};`); iframe.setAttribute("src", `${this.source}/${this.manifest.index}${this.manifest.index?.includes("?") ? "&" : "?"}args=${ExternalApp.serializeArgs(args)}`); win.content.appendChild(iframe); if (this.manifest.useIdbWrapper) { const idbWrapper = new Proxy(iframe.contentWindow!.indexedDB, { get: (target, prop, receiver) => { switch (prop) { case "databases": return async () => { const dbs = await target.databases(); return dbs .filter((db: any) => db.name.startsWith(this.package + "-")) .map((db: any) => { db.name = db.name.slice(this.package.length + 1); return db; }); }; case "open": return (name: string, version: number) => { return target.open(name.startsWith(this.package + "-") ? name : `${this.package}-${name}`, version); }; case "deleteDatabase": return (name: string) => { return target.deleteDatabase(name.startsWith(this.package + "-") ? name : `${this.package}-${name}`); }; default: return Reflect.get(target, prop, receiver); } }, }); Object.defineProperty(iframe.contentWindow!, "indexedDB", { value: idbWrapper, writable: false, }); } Object.assign(iframe.contentWindow as any, { anura: window.anura, AliceWM, ExternalApp, LocalFS, instance: this, instanceWindow: win, print: (message: string) => { iframe.contentWindow!.window.postMessage({ type: "stdout", message, }); }, println: (message: string) => { iframe.contentWindow!.postMessage({ type: "stdout", message: message + "\n", }); }, printerr: (message: string) => { iframe.contentWindow!.postMessage({ type: "stderr", message, }); }, printlnerr: (message: string) => { iframe.contentWindow!.postMessage({ type: "stderr", message: message + "\n", }); }, read: () => { return new Promise(resolve => { iframe.contentWindow!.addEventListener( "message", e => { if (e.data.type === "stdin") { resolve(e.data.message); } }, { once: true }, ); }); }, readln: () => { return new Promise(resolve => { // Read until a newline let buffer = ""; const listener = (e: MessageEvent) => { if (e.data.type === "stdin") { buffer += e.data.message; if (buffer.includes("\n")) { resolve(buffer); iframe.contentWindow!.removeEventListener("message", listener); } } }; iframe.contentWindow!.addEventListener("message", listener); }); }, env: { process: win, }, open: async (url: string | URL) => { const browser = await window.anura.import("anura.libbrowser"); browser.openTab(url); }, }); win.stdin = new WritableStream({ write: message => { iframe.contentWindow!.postMessage({ type: "stdin", message, }); }, }); win.stderr = new ReadableStream({ start: controller => { iframe.contentWindow!.addEventListener("error", e => { controller.enqueue(e.error); }); iframe.contentWindow!.addEventListener("message", e => { if (e.data.type === "stderr") { controller.enqueue(e.data.message); } }); }, }); win.stdout = new ReadableStream({ start: controller => { iframe.contentWindow!.addEventListener("message", e => { if (e.data.type === "stdout") { controller.enqueue(e.data.message); } }); }, }); const matter = document.createElement("link"); matter.setAttribute("rel", "stylesheet"); matter.setAttribute("href", "/assets/matter.css"); iframe.contentWindow!.addEventListener("load", () => { iframe.contentDocument!.head.appendChild(matter); }); return win; } else if (this.manifest.type === "manual") { // This type of application is reserved only for scripts meant for hacking anura internals const req = await fetch(`${this.source}/${this.manifest.handler}`); const data = await req.text(); top!.window.eval(data); // @ts-expect-error loadingScript(this.source, this); return; } else if (this.manifest.type === "webview") { // FOR INTERNAL USE ONLY const win = await window.anura.wm.create(this, this.manifest.wininfo as unknown as WindowInformation); const iframe = document.createElement("iframe"); // CSS injection here but it's no big deal const bg = this.manifest.background || "var(--theme-bg)"; iframe.setAttribute("style", "top:0; left:0; bottom:0; right:0; width:100%; height:100%; " + `border: none; margin: 0; padding: 0; background-color: ${bg};`); let encoded = ""; for (let i = 0; i < this.manifest.src!.length; i++) { if (i % 2 === 0) { encoded += this.manifest.src![i]; } else { encoded += String.fromCharCode(this.manifest.src!.charCodeAt(i) ^ 2); } } iframe.setAttribute("src", `${"/service/" + encodeURIComponent(encoded)}`); win.content.appendChild(iframe); return win; } } } ================================================ FILE: src/sys/liquor/libs/ExternalLib.tsx ================================================ import { Lib } from "./lib"; // API Stub: TODO Later interface LibManifest { name: string; icon: string; package: string; versions: { [key: string]: string; }; installHook?: string; cache?: boolean; currentVersion: string; } export class ExternalLib extends Lib { source: string; manifest: LibManifest; // Import caching is optional cache: { [key: string]: any; } = {}; // The installed libs at the time of the last cache // If more libs are installed, the cache is invalidated // This is to prevent a race condition where a lib is installed // before the dependency is installed installedLibs: string[] = []; constructor(manifest: LibManifest, source: string) { super(); this.manifest = manifest; this.name = manifest.name; this.icon = source + "/" + manifest.icon; this.source = source; this.package = manifest.package; this.latestVersion = manifest.currentVersion; Object.keys(manifest.versions).forEach(version => { this.versions[version] = source + "/" + manifest.versions[version]; console.log(this.versions[version]); }); if (manifest.installHook) { import(/* @vite-ignore */ source + "/" + manifest.installHook).then(module => { try { module.default(window.anura, this); } catch (err) { console.warn(err); } }); } } async getImport(version?: string): Promise { if (!version) { version = this.latestVersion; } if (this.manifest.cache && this.cache[version] && this.installedLibs == Object.keys(window.anura.libs)) { return this.cache[version]; } if (this.versions[version]) { // @vite-ignore const mod = await import(/* @vite-ignore */ this.versions[version]); if (this.manifest.cache) { this.cache[version] = mod; this.installedLibs = Object.keys(window.anura.libs); } return mod; } else { throw new Error(`Library ${this.name} does not supply version ${version}`); } } } ================================================ FILE: src/sys/liquor/libs/lib.tsx ================================================ // @ts-nocheck export class Lib { icon: string; package: string; name: string; versions: { [key: string]: any } = {}; latestVersion: string; async getImport(version: string): Promise {} } ================================================ FILE: src/sys/liquor/types/Filer.d.ts ================================================ declare let Filer: FilerType; declare let $el: any; // Note: this is different from the Anura Filesystem type because file descriptors are internally stored as numbers rather than the AnuraFD type. // This should still be fully compatible as file descriptors are obtained from other methods and are not created directly. This will only be a // problem if someone for some reason tries to create a file descriptor manually or did some external logic based on the file descriptor being a // number. type FilerFS = { constants: { F_OK: number; R_OK: number; W_OK: number; X_OK: number; }; watch(filename: string, listener: (event: string, filename: string) => void, options?: { recursive: boolean }): void; Shell: { new (): { cd: (t: string, r?: any) => void; pwd: () => string; env: () => Record; fs: () => any; rm: (directory: string, options?: { recursive: boolean; force: boolean }) => void; promises: { cat: () => Promise; cd: (t: string, r?: any) => Promise; exec: (command: string) => Promise; find: ( path: string, options?: { name?: string; regex?: RegExp | string; exec?: boolean | Function; }, ) => Promise; ls: (dir: string) => Promise; mkdirp: (dir: string) => Promise; rm: (path: string) => Promise; tempDir: () => Promise; touch: (filePath: string) => Promise; }; }; }; rename(oldPath: string, newPath: string, callback?: (err: Error | null) => void): void; ftruncate(fd: number, len: number, callback?: (err: Error | null, fd: number) => void): void; truncate(path: string, len: number, callback?: (err: Error | null) => void): void; stat(path: string, callback?: (err: Error | null, stats: TStats) => void): void; fstat(fd: number, callback?: (err: Error | null, stats: TStats) => void): void; lstat(path: string, callback?: (err: Error | null, stats: TStats) => void): void; /** @deprecated fs.exists() is an anachronism and exists only for historical reasons. */ exists(path: string, callback?: (exists: boolean) => void): void; link(srcPath: string, dstPath: string, callback?: (err: Error | null) => void): void; symlink(srcPath: string, dstPath: string, type: string, callback?: (err: Error | null) => void): void; symlink(srcPath: string, dstPath: string, callback?: (err: Error | null) => void): void; readlink(path: string, callback?: (err: Error | null, linkContents: string) => void): void; unlink(path: string, callback?: (err: Error | null) => void): void; mknod(path: string, mode: number, callback?: (err: Error | null) => void): void; rmdir(path: string, callback?: (err: Error | null) => void): void; mkdir(path: string, mode: number, callback?: (err: Error | null) => void): void; mkdir(path: string, callback?: (err: Error | null) => void): void; access(path: string, mode: number, callback?: (err: Error | null) => void): void; access(path: string, callback?: (err: Error | null) => void): void; mkdtemp(prefix: string, options: { encoding: string } | string, callback?: (err: Error | null, path: string) => void): void; mkdtemp(prefix: string, callback?: (err: Error | null, path: string) => void): void; readdir(path: string, options: { encoding: string; withFileTypes: boolean } | string, callback?: (err: Error | null, files: string[]) => void): void; readdir(path: string, callback?: (err: Error | null, files: string[]) => void): void; close(fd: number, callback?: (err: Error | null) => void): void; open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode: number, callback?: (err: Error | null, fd: number) => void): void; open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", callback?: (err: Error | null, fd: number) => void): void; utimes(path: string, atime: number | Date, mtime: number | Date, callback?: (err: Error | null) => void): void; futimes(fd: number, atime: number | Date, mtime: number | Date, callback?: (err: Error | null) => void): void; chown(path: string, uid: number, gid: number, callback?: (err: Error | null) => void): void; fchown(fd: number, uid: number, gid: number, callback?: (err: Error | null) => void): void; chmod(path: string, mode: number, callback?: (err: Error | null) => void): void; fchmod(fd: number, mode: number, callback?: (err: Error | null) => void): void; fsync(fd: number, callback?: (err: Error | null) => void): void; write(fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null, callback?: (err: Error | null, nbytes: number) => void): void; read(fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null, callback?: (err: Error | null, nbytes: number, buffer: Uint8Array) => void): void; readFile(path: string, callback?: (err: Error | null, data: Uint8Array) => void): void; writeFile(path: string, data: Uint8Array | string, options: { encoding: string; flag: "r" | "r+" | "w" | "w+" | "a" | "a+" } | string, callback?: (err: Error | null) => void): void; writeFile(path: string, data: Uint8Array | string, callback?: (err: Error | null) => void): void; appendFile(path: string, data: Uint8Array, callback?: (err: Error | null) => void): void; setxattr(path: string, name: string, value: string | object, flag: "CREATE" | "REPLACE", callback?: (err: Error | null) => void): void; setxattr(path: string, name: string, value: string | object, callback?: (err: Error | null) => void): void; fsetxattr(fd: number, name: string, value: string | object, flag: "CREATE" | "REPLACE", callback?: (err: Error | null) => void): void; fsetxattr(fd: number, name: string, value: string | object, callback?: (err: Error | null) => void): void; getxattr(path: string, name: string, callback?: (err: Error | null, value: string | object) => void): void; fgetxattr(fd: number, name: string, callback?: (err: Error | null, value: string | object) => void): void; removexattr(path: string, name: string, callback?: (err: Error | null) => void): void; fremovexattr(fd: number, name: string, callback?: (err: Error | null) => void): void; /* * Asynchronous FS operations */ promises: { appendFile(path: string, data: Uint8Array, options: { encoding: string; mode: number; flag: string }): Promise; access(path: string, mode?: number): Promise; chown(path: string, uid: number, gid: number): Promise; chmod(path: string, mode: number): Promise; getxattr(path: string, name: string): Promise; link(srcPath: string, dstPath: string): Promise; lstat(path: string): Promise; mkdir(path: string, mode?: number): Promise; mkdtemp(prefix: string, options?: { encoding: string }): Promise; mknod(path: string, mode: number): Promise; open(path: string, flags: "r" | "r+" | "w" | "w+" | "a" | "a+", mode?: number): Promise; readdir(path: string, options?: string | { encoding: string; withFileTypes: boolean }): Promise; readFile(path: string, encoding?: string): Promise; readlink(path: string): Promise; removexattr(path: string, name: string): Promise; rename(oldPath: string, newPath: string): Promise; rmdir(path: string): Promise; setxattr(path: string, name: string, value: string | object, flag?: "CREATE" | "REPLACE"): Promise; stat(path: string, callback?: void | any): Promise; symlink(srcPath: string, dstPath: string, type?: string): Promise; truncate(path: string, len: number): Promise; unlink(path: string): Promise; utimes(path: string, atime: number | Date, mtime: number | Date): Promise; writeFile(path: string, data: any | string, encoding?: string, mode?: number, flag?: string): Promise; }; }; type FilerType = { fs: FilerFS; promises: FilerFS.promises; Buffer: any; Path: any; FileSystem: FilerFS.constructor; }; ================================================ FILE: src/sys/liquor/types/V86Starter.d.ts ================================================ declare let V86Starter: V86StarterType; // [todo] type V86StarterType = any; ================================================ FILE: src/sys/types.ts ================================================ /** * @file src/sys/types.ts * @description This file contains all the types and interfaces used in the Terbium system. */ import { TFSType, FSType, ShellType } from "@terbiumos/tfs"; import { System } from "./apis/System"; import { ServerInfo, vFS } from "./vFS"; import { ExternalApp } from "./liquor/coreapps/ExternalApp"; import { WindowInformation } from "./liquor/AliceWM"; import { createAuthClient } from "better-auth/client"; import type { HTTPSession, libcurl } from "libcurl.js"; import * as fflate from "fflate"; declare global { namespace React.JSX { interface IntrinsicElements { "window-area": React.DetailedHTMLProps, HTMLDivElement>; window: React.HTMLAttributes; region: React.DetailedHTMLProps, HTMLElement>; "window-body": React.DetailedHTMLProps, HTMLDivElement>; "dock-item": React.DetailedHTMLProps, HTMLDivElement>; } } interface Window { AliceWM: any; LocalFS: any; ExternalApp: any; ExternalLib: any; electron: any; tfs: TFSType; loadLock: boolean; libcurlLock: boolean; libcurlSession: HTTPSession; } } export const isURL = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; export const dirExists = async (path: string): Promise => { return new Promise(resolve => { if (!window.tb.fs) return resolve(false); window.tb.fs.exists(path, (exists: boolean) => { resolve(exists); }); }); }; export const fileExists = async (path: string): Promise => { return new Promise(resolve => { if (!window.tb.fs) return resolve(false); window.tb.fs.exists(path, (exists: boolean) => { resolve(exists); }); }); }; export async function unzip(path: string, target: string) { const response = await fetch("/fs/" + path); const zipFileContent = await response.arrayBuffer(); if (!(await dirExists(target))) { // @ts-expect-error types await window.tb.fs.promises.mkdir(target, { recursive: true }); } const compressedFiles = fflate.unzipSync(new Uint8Array(zipFileContent)); for (const [relativePath, content] of Object.entries(compressedFiles)) { const fullPath = `${target}/${relativePath}`; const pathParts = fullPath.split("/"); let currentPath = ""; 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), window.tb.buffer.from(content), "arraybuffer"); } else if (!(await dirExists(currentPath))) { await window.tb.fs.promises.mkdir(currentPath); } } if (relativePath.endsWith("/")) { await window.tb.fs.promises.mkdir(fullPath); } } return "Done!"; } /** * @interface User * @description The information about a user. * @property `id` The user ID. * @property `username` The username of the user. * @property `password` The password of the user. * @property `email` The email of the user. * @property `permissions` The permissions of the user. * @property `groups` The groups the user is in. */ export interface User { id: string; username: string; password: string | boolean; pfp: string; perm: Perm[]; securityQuestion?: { question: string; answer: string }; email?: string; groups?: string[]; } /** * @interface Group * @description The information about a group. * @property `id` The group ID. * @property `name` The name of the group. * @property `permissions` The permissions of the group. * @property `users` The users in the group. */ export interface Group { id: string; name: string; perm: Perm[]; users: string[]; } /** * @enum Perm * @description The permissions that can be assigned to a user or group. * @constant `sys` The system permission. This is the highest level of permission and can only be assigned to the system administrator. * @constant `usr` The user permission. This is the default permission level for a user. * @constant `grp` The group permission. This is the default permission level for a group. * @constant `pub` The public permission. This is the lowest level of permission and is assigned to all users by default. */ export enum Perm { sys, usr, grp, pub, } export enum Errors { ENOENT = "ENOENT", EEXIST = "EEXIST", EISDIR = "EISDIR", ENOTDIR = "ENOTDIR", EPERM = "EPERM", EACCES = "EACCES", ENOTEMPTY = "ENOTEMPTY", EBUSY = "EBUSY", EROFS = "EROFS", ENOTFOUND = "ENOTFOUND", EINVALID = "EINVALID", EUNKNOWN = "EUNKNOWN", ECONFLICT = "ECONFLICT", EINVALIDARGS = "EINVALIDARGS", EINVALIDTYPE = "EINVALIDTYPE", EINVALIDNAME = "EINVALIDNAME", EINVALIDVALUE = "EINVALIDVALUE", EINVALIDPATH = "EINVALIDPATH", EINVALIDDATA = "EINVALIDDATA", EINVALIDSTATE = "EINVALIDSTATE", EINVALIDFORMAT = "EINVALIDFORMAT", EINVALIDLENGTH = "EINVALIDLENGTH", EINVALIDINDEX = "EINVALIDINDEX", EINVALIDKEY = "EINVALIDKEY", EINVALIDID = "EINVALIDID", EINVALIDDATE = "EINVALIDDATE", EINVALIDTIME = "EINVALIDTIME", EINVALIDDATETIME = "EINVALIDDATETIME", EINVALIDCHAR = "EINVALIDCHAR", EINVALIDCHARSET = "EINVALIDCHARSET", EINVALIDENCODING = "EINVALIDENCODING", } export enum ExitCodes { SUCCESS = 0, FAILURE = 1, FORBIDDEN = 2, INVALID = 3, ERROR = 4, TIMEOUT = 5, INTERRUPT = 6, ABORTED = 7, } /** * @interface ProcessInfo * @description The information about a process. * @property `name` The name of the process. * @property `pid` The process ID. * @property `token` The token of the process. * @property `parent` The parent process ID. * @property `children` The child process IDs. * @property `status` The status of the process. * @property `memory` The memory usage of the process. * @property `cpu` The CPU usage of the process. * @property `uptime` The time the process has been running. * @property `startTime` The time the process started. * @property `exitCode` The exit code of the process. */ export interface ProcessInfo { name: string; pid: number; parent: number; children: number[]; status: "running" | "stopped"; memory: number; cpu: number; uptime: number; startTime: number; exitCode: ExitCodes; } /** * @interface WindowConfig * @description The configuration for a window. * @property `title` The title of the window. * @property `icon` The icon of the window. * @property `src` The source of the window. * @property `size` The size of the window. * @property `controls` The controls of the window. * @property `resizable` Whether the window is resizable. * @property `maximizable` Whether the window is maximizable. * @property `minimizable` Whether the window is minimizable. * @property `closable` Whether the window is closable. * @property `snapable` Whether the window is snapable. */ export interface WindowConfig { title: | string | { text: string; weight?: number; html?: string; }; src: string; icon?: string; size?: { width?: number; height?: number; minWidth?: number; minHeight?: number; }; single?: boolean; controls?: Array<"minimize" | "maximize" | "close">; resizable?: boolean; maximizable?: boolean; minimizable?: boolean; closable?: boolean; snapable?: boolean; message?: any; proxy?: boolean; // non window affecting properties pid?: string; wid?: string; zIndex?: number; focused?: boolean; } declare let props: any; export interface NotificationProps { message: string; application: string; iconSrc: string; time?: number; onOk?: void | any; onCancel?: void | any; txt?: string; } export interface launcherProps { name: string; icon: string; src: string; user?: string; } export interface dialogProps { title: string; options?: { text: string; value: string; }[]; message?: string; defaultValue?: string; filter?: string; defaultUsername?: string; defualtDir?: string; filename?: string; img?: string; onOk: void | any; onCancel?: void | any; sudo?: boolean; local?: boolean; } export interface cmprops { titlebar?: string | React.ReactNode; x: number; y: number; options: { text: string; color?: string; click: () => void; }[]; iframe?: boolean; } export interface AppData { wmArgs: { app_id: string; title: { text: string; weight: number; }; icon?: string; src: string; native: boolean; size: { width: number | string; height: number | string; minWidth?: number | string; minHeight?: number | string; }; single: boolean; resizable?: boolean; snappable?: boolean; }; icon: string; name: string; title: string; } export interface MediaProps { artist: string; track_name: string; creator: string; video_name: string; album?: string; time?: number; background?: string; endtime: number; onPausePlay: void; onSeek?: void; onNext?: void; onBack?: void; } export type websocketUrl = `wss://${string}` | `ws://${string}`; export interface UserSettings { wallpaper: string; wallpaperMode: "cover" | "contain" | "stretch"; animations: boolean; proxy: "Ultraviolet" | "Scramjet"; transport: string; wispServer: websocketUrl | string | any; "battery-percent": boolean; accent: string; windowOptimizations?: boolean; showFPS?: boolean; times: { format: "12h" | "24h"; internet: boolean; showSeconds: boolean; }; window: { winAccent: string; blurlevel: number; alwaysMaximized: boolean; alwaysFullscreen: boolean; }; } export interface SysSettings { theme: string; "system-blur": boolean; "dock-full": boolean; fileAssociatedApps: { text: string; image: string; video: string; audio: string; }; location: string; weather: { unit: string; }; "host-name": string; setup: boolean; defaultUser: string; } export interface ProcInf { name: string; size: { width: number | string; height: number | string }; icon: string; pid: number; src: string; type: "window" | "runtime"; onKill?: () => void; } export interface COM { registry: any; sh: ShellType; buffer: any; battery: { showPercentage(): void; hidePercentage(): void; canUse(): Promise; }; launcher: { addApp(props: launcherProps): Promise; removeApp(name: string): Promise; }; /** @deprecated API Stub for legacy applications */ theme: { get(): Promise; set(data: any): Promise; }; desktop: { preferences: { setTheme(color: string): Promise; theme(): void; setAccent(color: string): Promise; getAccent(): Promise; }; wallpaper: { set(path: string): Promise; contain(): Promise; stretch(): Promise; cover(): Promise; fillMode(): Promise; }; dock: { pin(app: any): void; unpin(app: any): void; }; }; window: { getId(): void; create(props: WindowConfig): void; content: { get(): void; set(html: string | HTMLElement): void; }; titlebar: { setColor(hex: string): void; setText(text: string): void; setBackgroundColor(hex: string): void; }; island: { addControl(args: any): void; removeControl(control_id: string): void; }; changeSrc(src: string): void; reload(): void; minimize(): void; maximize(): void; close(): void; }; contextmenu: { create(props: cmprops): void; close(): void; }; user: { username(): Promise; pfp(): Promise; }; proxy: { get(): Promise<"Ultraviolet" | "Scramjet">; set(proxy: string): Promise; updateSWs(): Promise; encode(url: string, encoder: string): Promise; decode(url: string, decoder: string): Promise; }; notification: { Message(props: NotificationProps): void; Toast(props: NotificationProps): void; Installing(props: NotificationProps, task?: Promise | (() => Promise), doneToast?: Partial | null, failToast?: Partial | null): Promise | void; }; dialog: { Alert(props: dialogProps): void; Message(props: dialogProps): void; Select(props: dialogProps): void; Auth(iprops: dialogProps, options: { sudo: boolean }): void; Permissions(props: dialogProps): void; FileBrowser(props: dialogProps): void; DirectoryBrowser(props: dialogProps): void; SaveFile(props: dialogProps): void; Cropper(props: dialogProps): void; WebAuth(props: dialogProps): void; }; system: { version(): string | number | unknown; instance: System["instance"]; openApp(pkg: string): Promise; download(url: string, location: string): Promise; exportfs(): void; users: { list(): Promise; add(user: User): Promise; remove(id: string): Promise; update(user: User): Promise; renameUser(olduser: string, newuser: string): Promise; }; bootmenu: { addEntry(name: string, file: string): void; removeEntry(name: string): void; }; startup: { addProc(apporname: string, target: "System" | "User", cmd?: string): Promise; removeProc(apporname: string, target: "System" | "User"): Promise; enable(apporname: string, target: "System" | "User"): Promise; disable(apporname: string, target: "System" | "User"): Promise; list(): Promise; }; }; libcurl: typeof libcurl; fflate: typeof fflate; fs: FSType; vfs: vFS; tauth: { client: ReturnType; signIn(email: string, password: string): Promise; signOut(): Promise; isTACC(username?: string): Promise; updateInfo(data: any): Promise; reauth(): Promise; sync: { retreive: () => Promise; upload: () => Promise; isSyncing: boolean; }; getInfo(username?: string): Promise; }; crypto(pass: string, file: string): Promise; platform: { getPlatform(): Promise<"desktop" | "mobile">; }; process: { procs: Record; kill(config: string | number | any): void; list(): Record; create(type: "window" | "runtime", config: any): void; parse: { build(src: string): void; }; }; screen: { captureScreen(): Promise; }; mediaplayer: { music(props: MediaProps): void; video(props: MediaProps): void; hide(): void; pauseplay(): void; isExisting(): void | boolean | Promise; }; file: { handler: { openFile(path: string, type: string): void; addHandler(app: string, ext: string): void; removeHandler(ext: string): void; }; icons: { get(ext: string): Promise; set(ext: string, iconPath: string): Promise; remove(ext: string): Promise; }; }; node: { webContainer: import("@webcontainer/api").WebContainer | {}; servers: Map; isReady: boolean; start: () => void; stop(): boolean; }; } export interface AnuraWMWeakRef { element: HTMLDivElement | Element | null; content: HTMLDivElement | undefined | null; app: ExternalApp; dragForceX: 0; dragForceY: 0; dragging: false; height: number | string; width: number | string; pid: number | null; state: null; maximized: false; minimizing: false; mouseLeft: null; mouseTop: null; onclose: () => null; onfocus: () => null; onresize: (_w: number, _h: number) => null; onsnap: (_side: string) => null; onunmaximize: () => null; restoreSvg: null | SVGElement | ChildNode; kill: () => void; alive: boolean; maximizeImg: null | SVGElement | ChildNode; maximizeSvg: null | SVGElement | ChildNode; wininfo: WindowInformation; title: string; } export interface TAuthReturnType { user: any; settings: [ { settings: UserSettings; apps: { repos: string[]; installed: string[]; }; davs: ServerInfo[]; }, ]; } export interface TAuthSSData { settings: { settings: UserSettings; apps: { repos: string[]; installed: string[]; }; davs: ServerInfo[]; }[]; } ================================================ FILE: src/sys/vFS.ts ================================================ // @ts-expect-error: No types import * as webdav from "../../public/apps/files.tapp/webdav.js"; import { FSType } from "@terbiumos/tfs"; export interface ServerInfo { name: string; url: string; username: string; password: string; } export interface ServerConnection { name: string; connected: boolean; connection: any | null; url: string; } export class vFS { servers: Map = new Map(); currentServer: ServerConnection | null = null; private constructor(servers: Map) { this.servers = servers; window.tb.fs.watch(`/apps/user/${sessionStorage.getItem("currAcc")}/files/davs.json`, { recursive: true }, async () => { const data: ServerInfo[] = JSON.parse(await window.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/davs.json`, "utf8")); this.servers.clear(); this.servers = new Map( data.map(info => [ info.name, { name: info.name, connected: false, connection: null, url: info.url, }, ]), ); await this.mountAll(); }); } static async create(): Promise { const data: ServerInfo[] = JSON.parse(await window.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/davs.json`, "utf8")); const servers = new Map(); for (const info of data) { servers.set(info.name, { name: info.name, connected: false, connection: null, url: info.url, }); } const vfs = new vFS(servers); window.addEventListener("libcurl_load", async () => { await vfs.mountAll(); }); return vfs; } async mount(serverName: string): Promise { const data: ServerInfo[] = JSON.parse(await window.tb.fs.promises.readFile(`/apps/user/${sessionStorage.getItem("currAcc")}/files/davs.json`, "utf8")); const { url, username, password } = data.find(s => s.name === serverName) || {}; if (!url) throw new Error(`Server "${serverName}" not found`); try { const client = webdav.createClient(url, { username, password }); await client.getDirectoryContents("/"); this.servers.set(serverName, { name: serverName, connected: true, connection: new vFSOperations(client), url, }); } catch (e) { this.servers.set(serverName, { name: serverName, connected: false, connection: null, url, }); console.warn(`Failed to connect to server "${serverName}":`, e); } } async mountAll(): Promise { for (const serverName of this.servers.keys()) { await this.mount(serverName); } } async addServer(info: ServerInfo): Promise { const davjson = JSON.parse(await window.tb.fs.promises.readFile(`/apps/user/${await window.tb.user.username()}/files/davs.json`, "utf8")); davjson.push({ name: info.name, url: info.url, username: info.username, password: info.password, }); await window.tb.fs.promises.writeFile(`/apps/user/${await window.tb.user.username()}/files/davs.json`, JSON.stringify(davjson, null, 2)); const config = JSON.parse(await window.tb.fs.promises.readFile(`/apps/user/${await window.tb.user.username()}/files/config.json`, "utf8")); config.drives[info.name] = `/mnt/${info.name}/`; await window.tb.fs.promises.writeFile(`/apps/user/${await window.tb.user.username()}/files/config.json`, JSON.stringify(config, null, 2)); window.tb.notification.Toast({ application: "System", iconSrc: "/fs/apps/system/about.tapp/icon.svg", message: "New Dav Device has been added", }); } async removeServer(serverName: string): Promise { const davjson = JSON.parse(await window.tb.fs.promises.readFile(`/apps/user/${await window.tb.user.username()}/files/davs.json`, "utf8")); const index = davjson.findIndex((entry: any) => entry.name.toLowerCase() === serverName.toLowerCase()); if (index !== -1) { davjson.splice(index, 1); await window.tb.fs.promises.writeFile(`/apps/user/${await window.tb.user.username()}/files/davs.json`, JSON.stringify(davjson, null, 2)); const config = JSON.parse(await window.tb.fs.promises.readFile(`/apps/user/${await window.tb.user.username()}/files/config.json`, "utf8")); delete config.drives[serverName.toLowerCase()]; await window.tb.fs.promises.writeFile(`/apps/user/${await window.tb.user.username()}/files/config.json`, JSON.stringify(config, null, 2)); window.tb.notification.Toast({ application: "System", iconSrc: "/fs/apps/system/about.tapp/icon.svg", message: "Dav Drive has been removed", }); } else { window.tb.notification.Toast({ application: "System", iconSrc: "/fs/apps/system/about.tapp/icon.svg", message: "Dav Drive not found", }); } } setServer(serverName: string): boolean { const server = this.servers.get(serverName); if (server && server.connected) { this.currentServer = server; return true; } return false; } whatFS(path: string): vFSOperations | FSType { if (path.startsWith("/mnt/")) { const parts = path.split("/"); if (parts.length > 2) { return this.servers.get(parts[2])?.connection; } } return window.tb.fs; } } export class vFSOperations { client: any; constructor(client: any) { this.client = client; } pathtourl(path: string): string { if (!path.startsWith("/mnt/")) return path; const parts = path.split("/").filter(Boolean); if (parts.length < 2) return path; const serverName = parts[1]; const servers: Map | undefined = window.tb.vfs.servers; const server = servers?.get(serverName); if (!server || !server.url) return path; const rest = parts.slice(2).join("/"); const base = server.url.replace(/\/+$/, ""); return rest ? `${base}/${rest}` : `${base}/`; } pathtoFSPath(path: string): string { if (!path.startsWith("/mnt/")) return path; const parts = path.split("/").filter(Boolean); if (parts.length < 2) return "/"; const rest = parts.slice(2).join("/"); return rest ? `/${rest}` : "/"; } readdir(path: string, callback: (err: any, files?: any[]) => void): void { this.client .getDirectoryContents(this.pathtoFSPath(path)) .then((files: any[]) => { const basenames = files.map((f: any) => { if (typeof f === "string") return f.split("/").filter(Boolean).pop() || ""; if (f && typeof f.basename === "string") return f.basename; if (f && typeof f.filename === "string") return f.filename.split("/").filter(Boolean).pop() || ""; return ""; }); callback(null, basenames); }) .catch((err: any) => callback(err)); } readFile(path: string, callback: (err: any, data?: string) => void): void { try { this.client .getFileContents(this.pathtoFSPath(path), { format: "binary" }) .then((data: any) => { let uint8: Uint8Array | null = null; if (data instanceof ArrayBuffer) { uint8 = new Uint8Array(data); } else if (ArrayBuffer.isView(data)) { uint8 = new Uint8Array((data as any).buffer, (data as any).byteOffset || 0, (data as any).byteLength || undefined); } else if (typeof data === "string") { return callback(null, data); } else if (data && data.buffer instanceof ArrayBuffer) { uint8 = new Uint8Array(data.buffer); } else { try { const asBuffer = Buffer.isBuffer(data) ? data : null; if (asBuffer) uint8 = new Uint8Array(asBuffer); } catch (e) { return callback(null, data); } } if (!uint8) return callback(null, data); const len = Math.min(uint8.length, 512); let nonText = 0; for (let i = 0; i < len; i++) { const ch = uint8[i]; if (ch === 0) { nonText = Infinity; break; } if ((ch >= 7 && ch <= 13) || (ch >= 32 && ch <= 126)) { } else { nonText++; } } if (nonText === Infinity || nonText / len > 0.3) { // @ts-expect-error return callback(null, uint8.buffer); } else { try { const text = new TextDecoder("utf-8", { fatal: false }).decode(uint8); return callback(null, text); } catch (e) { // @ts-expect-error return callback(null, uint8.buffer); } } }) .catch((err: any) => callback(err)); } catch (err) { callback(err); } } writeFile(path: string, data: string | ArrayBuffer, callback: (err: any) => void): void { this.client .putFileContents(this.pathtoFSPath(path), data) .then(() => callback(null)) .catch((err: any) => callback(err)); } delete(path: string, callback: (err: any) => void): void { this.client .deleteFile(this.pathtoFSPath(path)) .then(() => callback(null)) .catch((err: any) => callback(err)); } rename(oldPath: string, newPath: string, callback: (err: any) => void): void { this.client .moveFile(this.pathtoFSPath(oldPath), this.pathtoFSPath(newPath)) .then(() => callback(null)) .catch((err: any) => callback(err)); } mkdir(path: string, callback: (err: any) => void): void { this.client .createDirectory(this.pathtoFSPath(path)) .then(() => callback(null)) .catch((err: any) => callback(err)); } exists(path: string, callback: (err: any, exists?: boolean) => void): void { this.client .exists(this.pathtoFSPath(path)) .then((exists: boolean) => callback(null, exists)) .catch((err: any) => callback(err)); } stat(path: string, callback: (err: any, stat?: any) => void): void { this.client .stat(this.pathtoFSPath(path)) .then((stat: any) => callback(null, stat)) .catch((err: any) => callback(err)); } copyFile(source: string, destination: string, callback: (err: any) => void): void { this.client .copyFile(this.pathtoFSPath(source), this.pathtoFSPath(destination)) .then(() => callback(null)) .catch((err: any) => callback(err)); } unlink(path: string, callback: (err: any) => void): void { this.client .deleteFile(this.pathtoFSPath(path)) .then(() => callback(null)) .catch((err: any) => callback(err)); } move(source: string, destination: string, callback: (err: any) => void): void { this.client .moveFile(this.pathtoFSPath(source), this.pathtoFSPath(destination)) .then(() => callback(null)) .catch((err: any) => callback(err)); } appendFile(path: string, data: string | ArrayBuffer, callback: (err: any) => void): void { this.client .getFileContents(this.pathtoFSPath(path), { format: "text" }) .then((existingData: string) => { const newData = existingData + data; return this.client.putFileContents(this.pathtoFSPath(path), newData); }) .then(() => callback(null)) .catch((err: any) => callback(err)); } access(path: string, ...rest: any[]): void { this.exists(path, (err, exists) => { const callback = rest.pop(); if (err) return callback(err); if (!exists) return callback(new Error("File does not exist")); callback(null); }); } promises = { readdir: (path: string): Promise => new Promise((resolve, reject) => { this.readdir(path, (err, files) => (err ? reject(err) : resolve(files!))); }), readFile: (path: string): Promise => new Promise((resolve, reject) => { this.readFile(path, (err, data) => (err ? reject(err) : resolve(data!))); }), writeFile: (path: string, data: string | ArrayBuffer): Promise => new Promise((resolve, reject) => { this.writeFile(path, data, err => (err ? reject(err) : resolve())); }), delete: (path: string): Promise => new Promise((resolve, reject) => { this.delete(path, err => (err ? reject(err) : resolve())); }), rename: (oldPath: string, newPath: string): Promise => new Promise((resolve, reject) => { this.rename(oldPath, newPath, err => (err ? reject(err) : resolve())); }), mkdir: (path: string): Promise => new Promise((resolve, reject) => { this.mkdir(path, err => (err ? reject(err) : resolve())); }), exists: (path: string): Promise => new Promise((resolve, reject) => { this.exists(path, (err, exists) => (err ? reject(err) : resolve(exists!))); }), stat: (path: string): Promise => new Promise((resolve, reject) => { this.stat(path, (err, stat) => (err ? reject(err) : resolve(stat!))); }), copyFile: (source: string, destination: string): Promise => new Promise((resolve, reject) => { this.copyFile(source, destination, err => (err ? reject(err) : resolve())); }), unlink: (path: string): Promise => new Promise((resolve, reject) => { this.unlink(path, err => (err ? reject(err) : resolve())); }), move: (source: string, destination: string): Promise => new Promise((resolve, reject) => { this.move(source, destination, err => (err ? reject(err) : resolve())); }), appendFile: (path: string, data: string | ArrayBuffer): Promise => new Promise((resolve, reject) => { this.appendFile(path, data, err => (err ? reject(err) : resolve())); }), access: (path: string, ...rest: any[]): Promise => new Promise((resolve, reject) => { this.access(path, ...rest, (err: any) => (err ? reject(err) : resolve())); }), }; } ================================================ FILE: src/vite-env.d.ts ================================================ /// ================================================ FILE: tsconfig.app.json ================================================ { "compilerOptions": { "target": "ES2024", "useDefineForClassFields": true, "lib": ["ES2024", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", "paths": { "@/*": ["./src/*"], "@sys/*": ["./sys/*"], "@assets/*": ["./src/assets/*"], "@structs/*": ["./src/structs/*"] }, /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "typeRoots": ["./node_modules/@types", "./node_modules/@mercuryworkshop/scramjet/dist/types"] }, "include": ["src", "node_modules/@mercuryworkshop/scramjet/dist/types/types.d.ts"] } ================================================ FILE: tsconfig.json ================================================ { "files": [], "compilerOptions": { "target": "ESNext", "noUnusedLocals": false, "noUnusedParameters": false }, "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] } ================================================ FILE: tsconfig.node.json ================================================ { "compilerOptions": { "target": "ES2024", "lib": ["ES2024"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["vite.config.ts", "env.d.ts"] } ================================================ FILE: vite.config.ts ================================================ import { baremuxPath } from "@mercuryworkshop/bare-mux/node"; // @ts-expect-error no types import { epoxyPath } from "@mercuryworkshop/epoxy-transport"; import { libcurlPath } from "@mercuryworkshop/libcurl-transport"; import { scramjetPath } from "@mercuryworkshop/scramjet/path"; // @ts-expect-error no types import { server as wisp } from "@mercuryworkshop/wisp-js/server"; import { uvPath } from "@titaniumnetwork-dev/ultraviolet"; import react from "@vitejs/plugin-react-swc"; import config from "dotenv"; import { defineConfig } from "vite"; import { viteStaticCopy } from "vite-plugin-static-copy"; import { tfsPath } from "@terbiumos/tfs"; config.config(); // https://vitejs.dev/config/ export default defineConfig({ plugins: [ react(), viteStaticCopy({ targets: [ // These are copied so that Terbium will still work statically { src: `${uvPath}/**/*`.replace(/\\/g, "/"), dest: "uv", overwrite: false, }, { src: `${scramjetPath}/**/*`.replace(/\\/g, "/"), dest: "scram", overwrite: false, }, { src: `${baremuxPath}/**/*`.replace(/\\/g, "/"), dest: "baremux", overwrite: false, }, { src: `${epoxyPath}/**/*`.replace(/\\/g, "/"), dest: "epoxy", overwrite: false, }, { src: `${libcurlPath}/**/*`.replace(/\\/g, "/"), dest: "libcurl", overwrite: false, }, { src: `${tfsPath}/**/*`.replace(/\\/g, "/"), dest: "tfs", overwrite: false, }, ], }), { name: "vite-wisp-server", configureServer(server) { server.httpServer?.on("upgrade", (req, socket, head) => (req.url?.startsWith("/wisp") ? wisp.routeRequest(req, socket, head) : undefined)); }, }, ], server: { port: process.env.port || 3001, watch: { ignored: ["**/public/apps/terminal.tapp/**"], }, }, });