Compare commits

..
97 Commits
Author SHA1 Message Date
Eric Dubé a2de5e3da2 Merge pull request #328 from HeyPuter/release-please--branches--main--components--puter.com
chore(main): release 2.2.0
2024-04-22 22:05:35 -04:00
github-actions[bot] 458f2eae4c chore(main): release 2.2.0 2024-04-23 02:03:34 +00:00
KernelDeimos c166560ff4 feat: add /healthcheck endpoint 2024-04-22 22:02:40 -04:00
Eric DubéandNariman Jelveh 331d9e7542 feat: allow apps to add a menubar via puter.js
* Begin work on menubar and dropdowns

* Improve menubar

* Fix pointer event behavior

* Fix labels

* Fix active button

* Eliminate flicker

* Update _default.js

---------

Co-authored-by: Nariman Jelveh <[email protected]>
2024-04-22 20:38:16 -04:00
Eric Dubé ec31007c4b Update main.yml 2024-04-22 19:23:03 -04:00
Eric Dubé e615c7ee7f Update main.yml 2024-04-22 19:15:59 -04:00
Eric Dubé 35d4d4f755 Update main.yml 2024-04-22 19:14:21 -04:00
Eric Dubé 195468c4e1 Update main.yml 2024-04-22 19:11:51 -04:00
Eric Dubé 15f274e662 Create main.yml 2024-04-22 19:06:59 -04:00
Eric Dubé 7cb898457c Merge pull request #323 from HeyPuter/release-please--branches--main--components--puter.com
chore(main): release 2.1.1
2024-04-22 18:50:43 -04:00
github-actions[bot] aa4050ee32 chore(main): release 2.1.1 2024-04-22 22:42:20 +00:00
KernelDeimos f392de722a fix: test release-please action #4 2024-04-22 18:40:49 -04:00
Eric Dubé 07d95d5d3c Update release-please.yml 2024-04-22 18:34:13 -04:00
Eric Dubé 22c61d8221 Update release-please.yml 2024-04-22 18:18:35 -04:00
KernelDeimos 8fb0a66ef2 fix: test release-please action #3 2024-04-22 18:16:08 -04:00
KernelDeimos d5cd828cba chore: test release-please action #2 2024-04-22 18:12:35 -04:00
KernelDeimos 0119ae36a8 chore: test release-please action #1 2024-04-22 17:51:03 -04:00
Eric Dubé 87852d125f Only deploy docker image on release branch 2024-04-22 17:47:47 -04:00
Eric Dubé d3969b0122 Create release-please.yml 2024-04-22 17:46:40 -04:00
Nariman Jelveh 3cba4cab1e Disable iframing of the main domain using meta tags as well 2024-04-22 14:14:20 -07:00
Nariman Jelveh ef35a04c4a Disable iframing of the main domain 2024-04-22 14:09:32 -07:00
Eric Dubé 874928e845 Update bug_report.md 2024-04-22 16:39:47 -04:00
Eric Dubé c3d1574f8c Merge pull request #321 from HeyPuter/eric/batch-error-handling
Make errors in /batch more preemptive
2024-04-22 16:37:31 -04:00
KernelDeimos eee5c9e48b Cleanup 2024-04-22 16:29:12 -04:00
KernelDeimos 2f5780d1d1 Cleanup 2024-04-22 16:28:22 -04:00
Nariman Jelveh fa60f66057 Update helpers.js 2024-04-22 13:13:49 -07:00
KernelDeimos a741bd5419 Make errors in /batch more preemptive 2024-04-22 04:35:01 -04:00
KernelDeimos 4d2d4a7ca3 Fix 86888h53w 2024-04-21 22:06:36 -04:00
KernelDeimos 5c1e6ab16b Fix false-positive password recovery response 2024-04-21 18:28:58 -04:00
Nariman Jelveh f5f75dbef8 Add link to Bug Bounty program 2024-04-21 15:04:49 -07:00
Nariman Jelveh 6577051d64 Add Steam Deck to DiRP 2024-04-21 14:46:45 -07:00
KernelDeimos b2f5cc11b5 Fix dbrr reference 2024-04-21 17:10:18 -04:00
Nariman Jelveh a1252f66cc Update README.md 2024-04-21 12:50:58 -07:00
Nariman Jelveh e8d9e8b42b Update README.md 2024-04-20 20:34:52 -07:00
Nariman Jelveh ced7265eb0 Merge pull request #306 from zinreq/main
Do not show the "Change Password" option when user is temporary
2024-04-20 19:46:54 -07:00
zinreq 14098b2ff2 Do not show the "Change Password" option when user is temporary 2024-04-20 19:42:30 -07:00
Eric Dubé fb93a0863b Merge pull request #305 from HeyPuter/eric/support-levels
Document scope for fixes
2024-04-20 20:02:52 -04:00
KernelDeimos 4088bff216 Document scope for fixes 2024-04-20 19:57:40 -04:00
Nariman Jelveh 915152d3b9 Merge pull request #296 from Xiayucheng1212/keep_params
Keep the query params in the url started with '/app/*'
2024-04-20 16:45:05 -07:00
Nariman Jelveh 2ecea9a7b0 Prevent XSS by escaping user_set_url_params 2024-04-20 16:34:15 -07:00
Eric Dubé dc2a620b4e Merge pull request #304 from HeyPuter/eric/stdio-bridge/2
stdio-bridge 2
2024-04-20 19:29:15 -04:00
Nariman Jelveh 8648a303f1 use puter.js to retrieve server info 2024-04-20 15:29:33 -07:00
Nariman Jelveh 6a65ca4ebd Merge pull request #303 from Eric-Lighthall/login-server-info
Add server info to the login page for issue #257
2024-04-20 10:12:31 -07:00
KernelDeimos 58b83c27f2 Remove large log 2024-04-20 07:23:32 -04:00
Eric Lighthall 338d925bcc Fetch server info from versionservice.js module
Instead of directly fetching for the version info in UITabAbout.js, the module is now used for it
2024-04-20 02:24:45 -07:00
Eric Lighthall 7e2c4a0a1c Add server/version info on login page
The server/version info is under the forgot password section of the login page
2024-04-20 02:22:13 -07:00
Eric Lighthall 9822be5df4 Add version service module for fetching server information
this centralizes the handling of requests to get the server information
2024-04-20 01:58:12 -07:00
Nariman Jelveh cf5c62bcfc Update README.md to add more DiRPs 2024-04-19 23:52:41 -07:00
KernelDeimos 27553ef926 Cleanup 2024-04-19 23:35:56 -04:00
KernelDeimos 3f249fcc89 Fix pipes 2024-04-19 23:31:23 -04:00
KernelDeimos c3654ab148 Add more streaming fixes 2024-04-19 23:24:32 -04:00
KernelDeimos 0e9e8d41d9 Fix authorizer not defined 2024-04-19 17:12:15 -04:00
Sam Atkins 2f49c1c9b0 Remove a whole load of noisy log messages
These can be added back if and when we need them, but right now, it's
hard to follow console output with all this chatter. 😅
2024-04-19 17:29:26 +01:00
Sam AtkinsandKernelDeimos 072dbe8db5 Make BetterReader buffer and cancel, to fix stdin data loss
BetterReader.read_with_cancel() returns both the read promise, and a
function that can be used to cancel the read. A cancelled read is
placed back into the BetterReader's chunk buffer, to be consumed by the
next user that requests a read.

This is used by Coupler so that when the coupler is closed, its pending
read() call does not consume the next batch of input.

This fixes the problem we were having with child applications consuming
one chunk of stdin after they are closed, meaning the first key you
press after an app exits would disappear.

Co-authored-by: KernelDeimos <[email protected]>
2024-04-19 15:52:59 +01:00
KernelDeimos da208e23f5 Add a valve and internal pipe to commands 2024-04-19 15:52:59 +01:00
Sam Atkins 222a617c44 Phoenix: Use regular code path to run built-in apps
Now launchApp() can always be awaited, we can run built-in apps using
the same code path for other apps, and eventually have SIGINT close
them.
2024-04-19 15:52:59 +01:00
Sam Atkins e355c77a4a Phoenix: Wait for apps to finish executing, and connect stdio to them
After launching an app, if successful, we connect stdio streams to it,
and wait for it to exit before we return to the prompt.

stdio is implemented as regular AppConnection messages:
- stdin:  `{ $: 'stdin',  data: Uint8Array }` from phoenix -> child
- stdout: `{ $: 'stdout', data: Uint8Array }` from child -> phoenix

Terminal and Phoenix now communicate with each other using the same
style, instead of 'input' and 'output' messages. This will help with
eventually running subshells.

SIGINT currently is not sent. We also suffer from the same "one more
read from stdin happens after app exits" bug that's in
PathCommandProvider where I copied the stdin code from.
2024-04-19 15:52:59 +01:00
Sam Atkins 639653dac2 Report when a non-SDK app closes
puter.ui.launchApp() returns a Promise that needs to resolve whether the
app uses the Puter SDK or not. Non-SDK apps are tricky because they
don't send a READY message on startup, and we don't know in advance
whether an app will use the SDK or not.

This is a workaround to ensure that launchApp() always resolves. When an
app is closed, if it wasn't using the SDK, we send an artificial
notification that it launched, followed by an extra notification that
it has closed (because the original close notification was sent before
this point). This means any users of launchApp() can await it, and get
an AppConnection, and listen to the close event. They can't otherwise
interact with a non-SDK app because it will have closed already, but we
can improve this in the future without breaking the API.
2024-04-19 15:52:59 +01:00
Sam Atkins 0aa5543397 Let AppConnection know if its target app uses the Puter SDK
Apps are not required to use the Puter SDK. If they don't, then we can
still launch them, close them, and listen to their close event, but are
unable to send messages to them.
2024-04-19 15:52:59 +01:00
Sam Atkins dfdda4f3b9 Extract some helper functions for accessing a window/iframe by uuid 2024-04-19 15:52:59 +01:00
Eric Dubé 8cf58d7fc6 Merge pull request #299 from Eric-Lighthall/reduce-console-flicker
Optimize console redraw by tracking widget changes
2024-04-19 07:47:41 -04:00
Eric Lighthall 4783e3eae4 Optimize console redraw by tracking widget changes
Instead of redrawing the widget area every 2 seconds, only auto redraw when the widget area has changed, reducing unecessary redraw operations.
2024-04-19 00:48:36 -07:00
Eric Dubé 4931ad3960 Merge pull request #297 from AtkinsSJ/test-minimum-version
Make tests work in Node 16.x
2024-04-18 14:41:20 -04:00
Sam Atkins 5ad78a5ebb Expand supported versions that we test in CI
Phoenix now only requires 16.x or above, and so does Puter itself, so
let's include that, and also 18.x as the other LTS version.
2024-04-18 14:41:06 +01:00
Sam Atkins dc95f2e065 Phoenix: Support older Node versions in test harness
This brings Phoenix's minimum required version from 20.x down to 16.x.

ReadableStream.from() is deemed experimental, and requires Node 20.x
(or at least, something higher than 18.x). This was the only code that
made us require version 20.x.

ReadableStream and WritableStream are available from Node 16.5, but
require that they be explicitly imported.
2024-04-18 14:41:06 +01:00
KernelDeimos 0361ceba6c Normalize session objects to have both user_uid and user_id 2024-04-17 23:22:10 -04:00
KernelDeimos 4ecc7372f9 Fix issue with use_bundled_gui flag 2024-04-17 22:59:22 -04:00
KernelDeimos dbcd627815 Apply various small fixes 2024-04-17 20:57:59 -04:00
Nariman Jelveh 6061c81c36 Update get-launch-apps.js 2024-04-17 17:58:11 -07:00
Nariman Jelveh c1aee61da1 Update README.md 2024-04-17 10:14:10 -07:00
KernelDeimos f0d3346ca7 Update session.last_touch always 2024-04-17 13:03:32 -04:00
KernelDeimos e8ca6376be Avoid logging sensitive query params 2024-04-17 12:51:30 -04:00
KernelDeimos c48c134869 Remove verbose log 2024-04-17 12:42:12 -04:00
KernelDeimos 49e334521d Update timers 2024-04-17 12:41:43 -04:00
Eric Dubé 04a641adc2 Merge pull request #291 from AtkinsSJ/test-submodules
Re-enable Phoenix tests
2024-04-17 10:08:43 -04:00
Eric Dubé 2210652643 Merge pull request #295 from AtkinsSJ/remove-phoenix-css
Phoenix: Remove unwanted CSS includes
2024-04-17 10:07:33 -04:00
Xiayucheng1212 8713db3d55 Fix params lost 2024-04-17 19:45:01 +08:00
Sam Atkins f2e8b5ee3e Phoenix: Remove unwanted CSS includes 2024-04-17 11:05:03 +01:00
KernelDeimos 7e0c6c6470 Fix last_activity_ts 2024-04-16 19:30:30 -04:00
KernelDeimos 19c49db538 Improve sessions 2024-04-16 18:57:02 -04:00
Sam Atkins ea40ac1e53 Re-enable Phoenix tests
This also runs the tests for contextlink, which I think we previously
never ran. I don't see any other packages that define an `npm test`
command. Having to manually list all the sub-package test directories in
the top-level package.json is a bit unfortunate, but it works.

For Puter itself we support Node 16.x IIRC, but Phoenix requires at
least 20.x currently.
2024-04-16 13:57:02 +01:00
KernelDeimos 653898b463 Report proper error when an operation is invalid 2024-04-15 22:27:03 -04:00
KernelDeimos c07d0a2bc7 Set NO_VAR_RUNTIME in Dockerfile 2024-04-15 20:47:27 -04:00
KernelDeimos 9bb221a0d7 Fix Dockerfile 2024-04-15 19:03:00 -04:00
Eric Dubé e4f2c4a28b Merge pull request #280 from vineeth-vk11/#206
fixing on conflict issue in sqlite kv write
2024-04-15 17:55:52 -04:00
Eric Dubé 3a4858b83c Merge pull request #286 from AtkinsSJ/background-settings
Settings: Organise code to be grouped by tab, and add desktop background picker
2024-04-15 17:55:09 -04:00
KernelDeimos 826ea36502 Add use_bundled_gui option 2024-04-15 17:50:44 -04:00
KernelDeimos 397819d45e Add missing endpoint /down 2024-04-15 16:38:03 -04:00
KernelDeimos 66fecf88ef Fix funny context menu on task manager 2024-04-15 15:58:07 -04:00
Sam Atkins 868202ba41 Settings: Add desktop-background setting to 'Personalization' tab 2024-04-15 17:41:19 +01:00
Sam Atkins c6b0ed32bf Settings: Move 'Clock' tab into its own file 2024-04-15 17:41:19 +01:00
Sam Atkins 17adef11a6 Settings: Move 'Language' tab into its own file
Also implement an optional `on_show` callback for settings tabs, to
support the "select and clear the search field" behaviour we had before.
2024-04-15 17:41:19 +01:00
Sam Atkins 6d1c807879 Settings: Move 'Personalization' tab into its own file 2024-04-15 17:41:19 +01:00
Sam Atkins 0ed5b20627 Settings: Move 'Account' tab into its own file 2024-04-15 17:41:19 +01:00
Sam Atkins f5175941d9 Settings: Move 'Usage' tab into its own file 2024-04-15 17:41:19 +01:00
Sam Atkins e2f048ec2d Settings: Move 'About'-specific logic and data into UITabAbout.js
Keeping all the logic and data for the tab together should make it
easier to modify.
2024-04-15 17:41:19 +01:00
vineethvk11 5d8d25f370 fixing on conflict issue in sqlite kv write 2024-04-14 16:39:29 +05:30
76 changed files with 2233 additions and 776 deletions

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
.dockerignore
Dockerfile
node_modules
puter
/puter
+2 -1
View File
@@ -15,7 +15,8 @@ assignees: ''
**Addition Information or Screenshots (if applicable)**
**Tooling**
**Deployment**
- [ ] Production (puter.com)
- [ ] Development (`npm run start`)
- [ ] Docker (via `docker run`)
- [ ] Docker (via `docker-compose`)
+31
View File
@@ -0,0 +1,31 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Node.js CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x, 20.x, 21.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build --if-present
- run: npm test
+1 -1
View File
@@ -5,7 +5,7 @@ name: Docker Image CI
# branch called `main`.
on:
push:
branches: ['main']
branches: ['release']
# Defines two custom environment variables for the workflow. These are used
+40
View File
@@ -0,0 +1,40 @@
name: Maintain Release Merge PR
on:
push:
branches:
- main
jobs:
update-release-pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Git
run: |
git config user.name github-actions
git config user.email [email protected]
- name: Check for existing PR
id: find-pr
uses: juliangruber/find-pull-request-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
branch: release
- uses: actions/checkout@v4
if: steps.find-pr.outputs.number == ''
with:
ref: release
- name: Reset release branch
if: steps.find-pr.outputs.number == ''
run: |
git fetch origin main:main
git reset --hard main
- name: Create/Update PR
if: steps.find-pr.outputs.number == ''
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Update release branch
title: Merge Release Auto-PR
body: Merging this PR will invoke release actions
branch: auto-update/release
+18
View File
@@ -0,0 +1,18 @@
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
name: release-please
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v4
with:
release-type: node
+17
View File
@@ -0,0 +1,17 @@
# Changelog
## [2.2.0](https://github.com/HeyPuter/puter/compare/v2.1.1...v2.2.0) (2024-04-23)
### Features
* add /healthcheck endpoint ([c166560](https://github.com/HeyPuter/puter/commit/c166560ff4ab5a453d3ec4f97326c995deb7f522))
* allow apps to add a menubar via puter.js ([331d9e7](https://github.com/HeyPuter/puter/commit/331d9e75428ec7609394f59b1755374c7340f83e))
## [2.1.1](https://github.com/HeyPuter/puter/compare/v2.1.0...v2.1.1) (2024-04-22)
### Bug Fixes
* test release-please action [#3](https://github.com/HeyPuter/puter/issues/3) ([8fb0a66](https://github.com/HeyPuter/puter/commit/8fb0a66ef21921990e564e5f61c0e80e7f929dc7))
* test release-please action [#4](https://github.com/HeyPuter/puter/issues/4) ([f392de7](https://github.com/HeyPuter/puter/commit/f392de722a5232b622ed91b656a31cdc443c2e84))
+16 -5
View File
@@ -1,3 +1,12 @@
# /!\ NOTICE /!\
# Many of the developers DO NOT USE the Dockerfile or image.
# While we do test new changes to Docker configuration, it's
# possible that future changes to the repo might break it.
# When changing this file, please try to make it as resiliant
# to such changes as possible; developers shouldn't need to
# worry about Docker unless the build/run process changes.
# Build stage
FROM node:21-alpine AS build
@@ -11,13 +20,13 @@ WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Copy the source files
COPY . .
# Install node modules
RUN npm cache clean --force \
&& npm ci
# Copy the rest of the source files
COPY . .
# Run the build command if necessary
RUN npm run build
@@ -39,7 +48,7 @@ WORKDIR /opt/puter/app
# Copy built artifacts and necessary files from the build stage
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY package*.json ./
COPY . .
# Set permissions
RUN chown -R node:node /opt/puter/app
@@ -50,4 +59,6 @@ EXPOSE 4100
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://puter.localhost:4100/test || exit 1
CMD ["npm", "start"]
ENV NO_VAR_RUNTUME=1
CMD ["npm", "start"]
+8 -3
View File
@@ -1,6 +1,6 @@
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: All your files, apps, and games in one place accessible from anywhere at any time." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">The Internet OS! Free, Open-Source, and Self-Hostable!</h3>
<h3 align="center">The Internet OS! Free, Open-Source, and Self-Hostable.</h3>
<p align="center">
<a href="https://puter.com/"><strong>« LIVE DEMO »</strong></a>
@@ -15,6 +15,8 @@
<a href="https://reddit.com/r/puter">Reddit</a>
·
<a href="https://twitter.com/HeyPuter">X (Twitter)</a>
·
<a href="https://hackerone.com/puter_h1b">Bug Bounty</a>
</p>
<h3 align="center"><img width="700" style="border-radius:5px;" alt="screenshot" src="https://assets.puter.site/puter.com-screenshot-2.webp"></h3>
@@ -117,7 +119,7 @@ your first step.
## License
This repository is licensed under AGPL3. However, our SDK (puter.js) is also available under Apache 2.0, as indicated by the license file in that section (packages/puter-dot-js) of this repository.
This repository is licensed under AGPL-3.0; However, our SDK (puter.js) is also available under Apache 2.0, as indicated by the license file in that section (packages/puter-js) of this repository.
<br/>
@@ -152,13 +154,16 @@ Puter interacts directly with the DOM and jQuery provides an elegant yet powerfu
## #DoesItRunPuter
- [Minecraft](https://twitter.com/HeyPuter/status/1771957571496092036) | [video](https://www.youtube.com/watch?v=GIowZUXkg5g)
- [Samsung Watch](https://twitter.com/CharmunkDev/status/1781501714543030554)
- [PlayStation 4](https://twitter.com/HeyPuter/status/1767978053014270059)
- [Skyworth TV](https://twitter.com/ericalexdube/status/1767983049277411564)
- [A Fridge!](https://twitter.com/HeyPuter/status/1778890003797745842)
- [Oculus Quest 2](https://twitter.com/HeyPuter/status/1768664081756754012)
- [Tesla Model 3](https://twitter.com/EricLighthall/status/1781479732997214501)
- [Tesla Model S](https://twitter.com/HeyPuter/status/1767971178864587057)
- [Tesla Model Y](https://twitter.com/HeyPuter/status/1772858333751636310)
- Tesla Model 3
- [Nintendo Switch](https://twitter.com/HeyPuter/status/1780645795240538518)
- [Steam Deck](https://twitter.com/everythingSung/status/1782162352403828793)
<br/>
+39
View File
@@ -0,0 +1,39 @@
## Puter Support Levels for Repository Updates
This document describes issues requiring repository changes;
which issues will be fixed by Puter's core team, and which ones
will be fixed if the community makes a contribution.
This document is not "law". It is provided only as a helpful guide
on what to expect.
### Level Glossary
| Name | Description |
| ---- | ----------- |
| Core | Core developers will fix this |
| Community | We will accept contributions to fix this |
| Mixed | Core developers will fix this if it's currently a priority |
### Issues and their Levels
| Issue | Priority |
| ----- | -------- |
| Security vulnerability | Core |
| Breaking change to SDK or API | Core |
| Bug in service in CoreModule | Core |
| Bug in a built-in app | Core |
| Login/init failure in Docker on `release` branch | Core |
| Login/init failure in Linux or OSX | Core |
| Login/init failure in Docker on `main` branch | Mixed |
| Login/init failure with specific configuration | Mixed |
| Login/init failure in Windows | Community |
## Puter Support for a Particular Deployment
If you experience issues on a self-hosted deployment we're here to
help. Some issues are related to configuration or environment, so
we may only be able to help in a limited capacity. Issues related
to data loss, data corruption, or security will have higher priority
over other issues with particular deployments.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "puter.com",
"version": "2.1.0",
"version": "2.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "puter.com",
"version": "2.1.0",
"version": "2.2.0",
"hasInstallScript": true,
"license": "AGPL-3.0-only",
"workspaces": [
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "puter.com",
"version": "2.1.0",
"version": "2.2.0",
"author": "Puter Technologies Inc.",
"license": "AGPL-3.0-only",
"description": "Desktop environment in the browser!",
@@ -22,7 +22,7 @@
"webpack-cli": "^5.1.1"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "mocha ./packages/phoenix/test ./packages/phoenix/packages/contextlink/test",
"start=gui": "nodemon --exec \"node dev-server.js\" ",
"start": "node run-selfhosted.js",
"build": "node ./build.js",
+3
View File
@@ -189,6 +189,9 @@ const install = async ({ services, app }) => {
const { PuterVersionService } = require('./services/PuterVersionService');
services.registerService('puter-version', PuterVersionService);
const { SessionService } = require('./services/SessionService');
services.registerService('session', SessionService);
}
const install_legacy = async ({ services }) => {
+4
View File
@@ -128,6 +128,10 @@ module.exports = class APIError {
return `Invalid id`;
}
},
'invalid_operation': {
status: 400,
message: ({ operation }) => `Invalid operation: ${quot(operation)}.`,
},
'field_invalid': {
status: 400,
message: ({ key, expected, got }) => {
@@ -85,6 +85,12 @@ class BatchExecutor extends AdvancedBase {
if ( ! x ) throw new Error('no context');
try {
if ( ! command_cls ) {
throw APIError.create('invalid_operation', null, {
operation: op.op,
});
}
if ( file ) workUnit.checkpoint(
'about to run << ' +
(file.originalname ?? file.name) +
@@ -24,7 +24,7 @@ const { HLFilesystemOperation } = require("./definitions");
class HLReadDir extends HLFilesystemOperation {
async _run () {
const { subject, user, no_thumbs, no_assocs } = this.values;
const { subject, user, no_thumbs, no_assocs, actor } = this.values;
if ( ! await subject.exists() ) {
throw APIError.create('subject_does_not_exist');
@@ -49,12 +49,12 @@ class HLReadDir extends HLFilesystemOperation {
await child.fetchSubdomains(user);
}
const fs = require('fs');
fs.appendFileSync('/tmp/children.log',
JSON.stringify({
no_thumbs,
no_assocs,
entry: child.entry,
}) + '\n');
// fs.appendFileSync('/tmp/children.log',
// JSON.stringify({
// no_thumbs,
// no_assocs,
// entry: child.entry,
// }) + '\n');
return await child.getSafeEntry({ thumbnail: ! no_thumbs });
}));
}
@@ -17,9 +17,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const APIError = require("../../api/APIError");
const { chkperm, get_descendants } = require("../../helpers");
const { TYPE_DIRECTORY } = require("../FSNodeContext");
const { NodeUIDSelector, NodeRawEntrySelector } = require("../node/selectors");
const { RootNodeSelector } = require("../node/selectors");
const { NodeUIDSelector, NodeChildSelector } = require("../node/selectors");
const { LLFilesystemOperation } = require("./definitions");
class LLReadDir extends LLFilesystemOperation {
@@ -42,27 +41,14 @@ class LLReadDir extends LLFilesystemOperation {
const svc_fsentry = svc.get('fsEntryService');
const svc_fs = svc.get('filesystem');
if (
subject.isRoot ||
(await subject.isUserDirectory() && subject.name !== user.username)
) {
this.checkpoint('before call to get_descendants')
const entries = await get_descendants(
await subject.get('path'),
user,
1, true,
)
this.checkpoint('after call to get_descendants')
const children = await Promise.all(entries.map(async entry => {
const node = await svc_fs.node(new NodeRawEntrySelector(entry));
node.found_thumbnail = false;
return node;
}))
this.checkpoint('after get children (2)');
return children;
if ( subject.isRoot ) {
if ( ! actor.type.user ) return [];
return [
await svc_fs.node(new NodeChildSelector(
new RootNodeSelector(),
actor.type.user.username,
))
];
}
this.checkpoint('before get direct descendants')
+1 -1
View File
@@ -1147,7 +1147,7 @@ async function jwt_auth(req){
else if(req.handshake && req.handshake.query && req.handshake.query.auth_token)
token = req.handshake.query.auth_token;
if(!token)
if(!token || token === 'null')
throw('No auth token found');
else if (typeof token !== 'string')
throw('token must be a string.')
+2 -1
View File
@@ -176,7 +176,7 @@ router.all('*', async function(req, res, next) {
const user = await get_user({uuid: req.query.user_uuid})
// more validation
if(user === undefined || user === null || user === false)
if(!user)
h += '<p style="text-align:center; color:red;">User not found.</p>';
else if(user.unsubscribed === 1)
h += '<p style="text-align:center; color:green;">You are already unsubscribed.</p>';
@@ -300,6 +300,7 @@ router.all('*', async function(req, res, next) {
app_origin: APP_ORIGIN,
api_origin: API_ORIGIN,
use_bundled_gui: config.use_bundled_gui,
manifest,
gui_path: config.assets.gui,
+1 -1
View File
@@ -38,7 +38,7 @@ router.get('/apps', auth, express.json({limit: '50mb'}), async (req, res, next)=
const db = req.services.get('database').get(DB_READ, 'apps');
let apps_res = await dbrr.read(
let apps_res = await db.read(
`SELECT * FROM apps WHERE owner_user_id = ? ORDER BY timestamp DESC`,
[req.user.id]
);
+72
View File
@@ -0,0 +1,72 @@
"use strict"
const express = require('express');
const router = express.Router();
const auth = require('../middleware/auth.js');
const config = require('../config.js');
const fs = require('../middleware/fs.js');
const { DB_WRITE } = require('../services/database/consts.js');
const { NodePathSelector } = require('../filesystem/node/selectors.js');
const { HLRead } = require('../filesystem/hl_operations/hl_read.js');
// -----------------------------------------------------------------------//
// GET /down
// -----------------------------------------------------------------------//
router.get('/down', auth, fs, express.json(), async (req, res, next)=>{
// check subdomain
if(require('../helpers').subdomain(req) !== 'api')
next();
// check if user is verified
if((config.strict_email_verification_required || req.user.requires_email_confirmation) && !req.user.email_confirmed)
return res.status(400).send({code: 'account_is_not_verified', message: 'Account is not verified'});
// validation
if(!req.query.path)
return res.status(400).send('path is required')
// path must be a string
else if (typeof req.query.path !== 'string')
return res.status(400).send('path must be a string.')
else if(req.query.path.trim() === '')
return res.status(400).send('path cannot be empty')
// modules
const db = req.services.get('database').get(DB_WRITE, 'filesystem');
const _path = require('path');
const {chkperm} = require('../helpers')
const path = _path.resolve('/', req.query.path);
const AWS = require('aws-sdk');
// cannot download the root, because it's a directory!
if(path === '/')
return res.status(400).send('Cannot download a directory.');
// resolve path to its FSEntry
const fsnode = await req.fs.node(new NodePathSelector(path));
// not found
if( ! fsnode.exists() ) {
return res.status(404).send('File not found');
}
// stream data from S3
try{
const hl_read = new HLRead();
const stream = await hl_read.run({
fsNode: fsnode,
user: req.user,
});
// let stream = await s3.getObject({
// Bucket: fsentry.bucket,
// Key: fsentry.uuid, // File name you want to save as in S3
// }).createReadStream().on('error', error => {
// console.log(error);
// });
res.attachment(await fsnode.get('name'));
return stream.pipe(res);
}catch(e){
console.log(e);
return res.type('application/json').status(500).send({message: 'There was an internal problem reading the file.'});
}
})
module.exports = router
@@ -136,9 +136,32 @@ module.exports = eggspress('/batch', {
let total_tbd = true;
const on_first_file = () => {
if ( request_error ) {
return;
}
// log fileinfos
console.log('HERE ARE THE FILEINFOS');
console.log(JSON.stringify(fileinfos, null, 2));
const indexes_to_remove = [];
for ( let i=0 ; i < pending_operations.length ; i++ ) {
const op_spec = pending_operations[i];
if ( ! operation_requires_file(op_spec) ) {
indexes_to_remove.push(i);
console.log(`EXEUCING OP ${op_spec.op}`)
response_promises.push(
batch_exe.exec_op(req, op_spec)
);
}
}
for ( let i=indexes_to_remove.length-1 ; i >= 0 ; i-- ) {
const index = indexes_to_remove[i];
pending_operations.splice(index, 1)[0];
response_promises.splice(index, 1);
}
}
@@ -150,48 +173,47 @@ module.exports = eggspress('/batch', {
});
const still_reading = new TeePromise();
let request_error = null;
busboy.on('field', (fieldname, value, details) => {
if ( details.fieldnameTruncated ) {
throw new Error('fieldnameTruncated');
}
if ( details.valueTruncated ) {
throw new Error('valueTruncated');
}
try {
if ( details.fieldnameTruncated ) {
throw new Error('fieldnameTruncated');
}
if ( details.valueTruncated ) {
throw new Error('valueTruncated');
}
if ( expected_metadata.hasOwnProperty(fieldname) ) {
expected_metadata[fieldname] = value;
req.body[fieldname] = value;
return;
}
if ( expected_metadata.hasOwnProperty(fieldname) ) {
expected_metadata[fieldname] = value;
req.body[fieldname] = value;
return;
}
if ( fieldname === 'fileinfo' ) {
fileinfos.push(JSON.parse(value));
return;
}
if ( fieldname === 'fileinfo' ) {
fileinfos.push(JSON.parse(value));
return;
}
if ( ! frame ) {
create_frame();
}
if ( ! frame ) {
create_frame();
}
if ( fieldname === 'operation' ) {
const op_spec = JSON.parse(value);
batch_exe.total++;
if ( operation_requires_file(op_spec) ) {
console.log(`WAITING FOR FILE ${op_spec.op}`)
if ( fieldname === 'operation' ) {
const op_spec = JSON.parse(value);
batch_exe.total++;
pending_operations.push(op_spec);
response_promises.push(null);
return;
}
console.log(`EXEUCING OP ${op_spec.op}`)
response_promises.push(
batch_exe.exec_op(req, op_spec)
);
return;
req.body[fieldname] = value;
} catch (e) {
request_error = e;
req.unpipe(busboy);
res.set("Connection", "close");
res.sendStatus(400);
}
req.body[fieldname] = value;
});
let i = 0;
@@ -274,6 +296,11 @@ module.exports = eggspress('/batch', {
// Awaiting responses
//-------------------------------------------------------------
await still_reading;
if ( request_error ) {
return;
}
log.noticeme('WAITING ON OPERATIONS')
let responsePromises = response_promises;
// let responsePromises = batch_exe.responsePromises;
@@ -98,6 +98,7 @@ module.exports = eggspress('/readdir', {
no_thumbs,
no_assocs,
user: req.user,
actor: req.actor,
});
// check for duplicate names
@@ -81,10 +81,11 @@ router.get('/get-launch-apps', auth, express.json(), async (req, res, next)=>{
'dev-center',
'code',
'editor',
'terminal',
'cloudtabs-browserbox',
'draw',
'camera',
'recorder',
'terminal',
'shell-shockers-outpan',
'krunker',
'rows',
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2024 Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter 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 <https://www.gnu.org/licenses/>.
*/
"use strict"
const express = require('express');
const router = new express.Router();
// -----------------------------------------------------------------------//
// GET /healthcheck
// -----------------------------------------------------------------------//
router.get('/healthcheck', async (req, res) => {
const svc_serverHealth = req.services.get('server-health');
const status = await svc_serverHealth.get_status();
res.json(status);
})
module.exports = router
+10
View File
@@ -31,6 +31,16 @@ router.post('/logout', auth, express.json(), async (req, res, next)=>{
next();
// delete cookie
res.clearCookie(config.cookie_name);
// delete session
(async () => {
if ( ! req.token ) return;
try {
const svc_auth = req.services.get('auth');
await svc_auth.remove_session_by_token(req.token);
} catch (e) {
console.log(e);
}
})()
//---------------------------------------------------------
// DANGER ZONE: delete temp user and all its data
//---------------------------------------------------------
@@ -53,10 +53,15 @@ router.post('/set-pass-using-token', express.json(), async (req, res, next)=>{
return res.status(400).send(`Password must be at least ${config.min_pass_length} characters long.`)
try{
await db.write(
const info = await db.write(
'UPDATE user SET password=?, pass_recovery_token=NULL WHERE `uuid` = ? AND pass_recovery_token = ?',
[await bcrypt.hash(req.body.password, 8), req.body.user_id, req.body.token]
);
if ( ! info?.anyRowsAffected ) {
return res.status(400).send('Invalid token or user_id.');
}
invalidate_cached_user_by_id(req.body.user_id);
return res.send('Password successfully updated.')
@@ -24,6 +24,7 @@ class DevConsoleService extends BaseService {
this.static_lines = [];
this.widgets = [];
this.identifiers = {};
this.has_updates = false;
}
turn_on_the_warning_lights () {
@@ -39,6 +40,7 @@ class DevConsoleService extends BaseService {
if ( opt_id ) {
this.identifiers[opt_id] = outputter;
}
this.mark_updated();
}
remove_widget (id_or_outputter) {
@@ -46,9 +48,11 @@ class DevConsoleService extends BaseService {
id_or_outputter = this.identifiers[id_or_outputter];
}
this.widgets = this.widgets.filter(w => w !== id_or_outputter);
this.mark_updated();
}
update_ () {
const initialOutput = [...this.static_lines];
this.static_lines = [];
// if a widget throws an error we MUST remove it;
// it's probably a stack overflow because it's printing.
@@ -64,11 +68,22 @@ class DevConsoleService extends BaseService {
output = Array.isArray(output) ? output : [output];
this.static_lines.push(...output);
}
if (!this.arrays_equal(initialOutput, this.static_lines)) {
this.mark_updated(); // Update only if outputs have changed
}
for ( const w of to_remove ) {
this.remove_widget(w);
}
}
arrays_equal (a, b) {
return a.length === b.length && a.every((val, index) => val === b[index]);
}
mark_updated () {
this.has_updates = true;
}
async _init () {
const services = this.services;
// await services.ready;
@@ -146,7 +161,10 @@ class DevConsoleService extends BaseService {
};
setInterval(() => {
this._redraw();
if (this.has_updates) {
this._redraw();
this.has_updates = false;
}
}, 2000);
consoleLogManager.decorate_all(({ replace }, ...args) => {
@@ -42,6 +42,7 @@ class PuterAPIService extends BaseService {
app.use(require('../routers/drivers/list-interfaces'))
app.use(require('../routers/drivers/usage'))
app.use(require('../routers/confirm-email'))
app.use(require('../routers/down'))
app.use(require('../routers/contactUs'))
app.use(require('../routers/delete-site'))
app.use(require('../routers/get-dev-profile'))
@@ -70,6 +71,7 @@ class PuterAPIService extends BaseService {
app.use(require('../routers/sites'))
// app.use(require('../routers/filesystem_api/stat'))
app.use(require('../routers/suggest_apps'))
app.use(require('../routers/healthcheck'))
app.use(require('../routers/test'))
app.use(require('../routers/update-taskbar-items'))
require('../routers/whoami')(app);
@@ -0,0 +1,167 @@
const { invalidate_cached_user, get_user } = require("../helpers");
const { asyncSafeSetInterval } = require("../util/promise");
const { MINUTE, SECOND } = require("../util/time");
const BaseService = require("./BaseService");
const { DB_WRITE } = require("./database/consts");
/**
* This service is responsible for updating session activity
* timestamps and maintaining the number of active sessions.
*/
class SessionService extends BaseService {
static MODULES = {
// uuidv5: require('uuid').v5,
uuidv4: require('uuid').v4,
}
_construct () {
this.sessions = {};
}
async _init () {
this.db = await this.services.get('database').get(DB_WRITE, 'session');
(async () => {
// TODO: change to 5 minutes or configured value
asyncSafeSetInterval(async () => {
await this._update_sessions();
}, 2 * MINUTE);
})();
}
async create_session (user, meta) {
const unix_ts = Math.floor(Date.now() / 1000);
meta = {
// clone
...(meta || {}),
};
meta.created = new Date().toISOString();
meta.created_unix = unix_ts;
const uuid = this.modules.uuidv4();
await this.db.write(
'INSERT INTO `sessions` ' +
'(`uuid`, `user_id`, `meta`, `last_activity`, `created_at`) ' +
'VALUES (?, ?, ?, ?, ?)',
[uuid, user.id, JSON.stringify(meta), unix_ts, unix_ts],
);
const session = {
last_touch: Date.now(),
last_store: Date.now(),
uuid,
user_uid: user.uuid,
user_id: user.id,
meta,
};
this.sessions[uuid] = session;
return session;
}
async get_session_ (uuid) {
let session = this.sessions[uuid];
if ( session ) {
session.last_touch = Date.now();
return session;
}
;[session] = await this.db.read(
"SELECT * FROM `sessions` WHERE `uuid` = ? LIMIT 1",
[uuid],
);
if ( ! session ) return;
session.last_store = Date.now();
session.meta = this.db.case({
mysql: () => session.meta,
otherwise: () => JSON.parse(session.meta ?? "{}")
})();
const user = await get_user(session.user_id);
session.user_uid = user?.uuid;
this.sessions[uuid] = session;
return session;
}
async get_session (uuid) {
const session = await this.get_session_(uuid);
if ( session ) {
session.last_touch = Date.now();
session.meta.last_activity = (new Date()).toISOString();
}
return this.remove_internal_values_(session);
}
remove_internal_values_ (session) {
const copy = {
...session,
};
delete copy.last_touch;
delete copy.last_store;
delete copy.user_id;
return copy;
}
get_user_sessions (user) {
const sessions = [];
for ( const session of Object.values(this.sessions) ) {
if ( session.user_id === user.id ) {
sessions.push(session);
}
}
return sessions.map(this.remove_internal_values_.bind(this));
}
remove_session (uuid) {
delete this.sessions[uuid];
return this.db.write(
'DELETE FROM `sessions` WHERE `uuid` = ?',
[uuid],
);
}
async _update_sessions () {
this.log.tick('UPDATING SESSIONS');
const now = Date.now();
const keys = Object.keys(this.sessions);
const user_updates = {};
for ( const key of keys ) {
const session = this.sessions[key];
if ( now - session.last_store > 5 * MINUTE ) {
this.log.debug('storing session meta: ' + session.uuid);
const unix_ts = Math.floor(now / 1000);
await this.db.write(
'UPDATE `sessions` ' +
'SET `meta` = ?, `last_activity` = ? ' +
'WHERE `uuid` = ?',
[JSON.stringify(session.meta), unix_ts, session.uuid],
);
session.last_store = now;
if (
! user_updates[session.user_id] ||
user_updates[session.user_id][1] < session.last_touch
) {
user_updates[session.user_id] = [session.user_id, session.last_touch];
}
}
}
for ( const [user_id, last_touch] of Object.values(user_updates) ) {
const sql_ts = (date =>
date.toISOString().split('T')[0] + ' '
+ date.toTimeString().split(' ')[0]
)(new Date(last_touch));
await this.db.write(
'UPDATE `user` ' +
'SET `last_activity_ts` = ? ' +
'WHERE `id` = ? LIMIT 1',
[sql_ts, user_id],
);
const user = kv.get('users:id:' + user_id);
if ( user ) {
user.last_activity_ts = sql_ts;
}
}
}
}
module.exports = { SessionService };
@@ -208,6 +208,24 @@ class WebServerService extends BaseService {
responseTime: parseFloat(responseTime),
};
if ( url.includes('android-icon') ) return;
// remove `puter.auth.*` query params
const safe_url = (u => {
// We need to prepend an arbitrary domain to the URL
const url = new URL('https://example.com' + u);
const search = url.searchParams;
for ( const key of search.keys() ) {
if ( key.startsWith('puter.auth.') ) search.delete(key);
}
return url.pathname + '?' + search.toString();
})(fields.url);
fields.url = safe_url;
// re-write message
message = [
fields.method, fields.url,
fields.status, fields.responseTime,
].join(' ');
const log = this.services.get('log-service').create('morgan');
log.info(message, fields);
}
@@ -318,6 +336,13 @@ class WebServerService extends BaseService {
// res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
// Pass to next layer of middleware
// disable iframes on the main domain
if ( req.hostname === config.domain ) {
// disable iframes
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
}
next();
});
@@ -36,6 +36,7 @@ class AuthService extends BaseService {
async _init () {
this.db = await this.services.get('database').get(DB_WRITE, 'auth');
this.svc_session = await this.services.get('session');
this.sessions = {};
}
@@ -214,40 +215,11 @@ class AuthService extends BaseService {
}
}
meta.created = new Date().toISOString();
meta.created_unix = Math.floor(Date.now() / 1000);
const uuid = this.modules.uuidv4();
await this.db.write(
'INSERT INTO `sessions` ' +
'(`uuid`, `user_id`, `meta`) ' +
'VALUES (?, ?, ?)',
[uuid, user.id, JSON.stringify(meta)],
);
const session = { uuid, user_uid: user.uuid, meta };
this.sessions[uuid] = session;
return session;
return await this.svc_session.create_session(user, meta);
}
async get_session_ (uuid) {
this.log.info(`USING SESSION`);
if ( this.sessions[uuid] ) {
return this.sessions[uuid];
}
const [session] = await this.db.read(
"SELECT * FROM `sessions` WHERE `uuid` = ? LIMIT 1",
[uuid],
);
if ( ! session ) return;
session.meta = this.db.case({
mysql: () => session.meta,
otherwise: () => JSON.parse(session.meta ?? "{}")
})();
return session;
return await this.svc_session.get_session(uuid);
}
async create_session_token (user, meta) {
@@ -314,6 +286,18 @@ class AuthService extends BaseService {
return { actor, user, token };
}
async remove_session_by_token (token) {
const decoded = this.modules.jwt.verify(
token, this.global_config.jwt_secret
);
if ( decoded.type !== 'session' ) {
return;
}
await this.svc_session.remove_session(decoded.uuid);
}
async create_access_token (authorizer, permissions) {
const jwt_obj = {};
const authorizer_obj = {};
@@ -372,14 +356,26 @@ class AuthService extends BaseService {
}
async list_sessions (actor) {
const seen = new Set();
const sessions = [];
const cache_sessions = this.svc_session.get_user_sessions(actor.type.user);
for ( const session of cache_sessions ) {
seen.add(session.uuid);
sessions.push(session);
}
// We won't take the cached sessions here because it's
// possible the user has sessions on other servers
const sessions = await this.db.read(
const db_sessions = await this.db.read(
'SELECT uuid, meta FROM `sessions` WHERE `user_id` = ?',
[actor.type.user.id],
);
sessions.forEach(session => {
for ( const session of db_sessions ) {
if ( seen.has(session.uuid) ) {
continue;
}
session.meta = this.db.case({
mysql: () => session.meta,
otherwise: () => JSON.parse(session.meta ?? "{}")
@@ -387,7 +383,8 @@ class AuthService extends BaseService {
if ( session.uuid === actor.type.session ) {
session.current = true;
}
});
sessions.push(session);
};
return sessions;
}
@@ -207,7 +207,7 @@ class PermissionService extends BaseService {
if ( actor.type instanceof AccessTokenActorType ) {
// Authorizer must have permission
const authorizer_permission = await this.check(authorizer, permission);
const authorizer_permission = await this.check(actor.type.authorizer, permission);
if ( ! authorizer_permission ) return false;
return await this.check_access_token_permission(
@@ -42,7 +42,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
this.db = new Database(this.config.path);
// Database upgrade logic
const TARGET_VERSION = 4;
const TARGET_VERSION = 5;
if ( do_setup ) {
this.log.noticeme(`SETUP: creating database at ${this.config.path}`);
@@ -53,6 +53,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
'0004_sessions.sql',
'0005_background-apps.sql',
'0006_update-apps.sql',
'0007_sessions.sql',
].map(p => path_.join(__dirname, 'sqlite_setup', p));
const fs = require('fs');
for ( const filename of sql_files ) {
@@ -85,6 +86,10 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
upgrade_files.push('0006_update-apps.sql');
}
if ( user_version <= 4 ) {
upgrade_files.push('0007_sessions.sql');
}
if ( upgrade_files.length > 0 ) {
this.log.noticeme(`Database out of date: ${this.config.path}`);
this.log.noticeme(`UPGRADING DATABASE: ${user_version} -> ${TARGET_VERSION}`);
@@ -121,6 +126,17 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
svc_devConsole.add_widget(this.database_update_notice);
})();
}
const svc_serverHealth = this.services.get('server-health');
svc_serverHealth.add_check('sqlite', async () => {
const [{ user_version }] = await this._requireRead('PRAGMA user_version');
if ( user_version !== TARGET_VERSION ) {
throw new Error(
`Database version mismatch: expected ${TARGET_VERSION}, ` +
`got ${user_version}`);
}
});
}
async _read (query, params = []) {
@@ -120,7 +120,10 @@ CREATE TABLE `kv` (
`value` text,
-- 0016
`migrated` tinyint(1) DEFAULT '0'
`migrated` tinyint(1) DEFAULT '0',
-- 0019
UNIQUE (user_id, app, kkey_hash)
);
CREATE TABLE `subdomains` (
@@ -0,0 +1,2 @@
ALTER TABLE `sessions` ADD COLUMN "created_at" INTEGER DEFAULT 0;
ALTER TABLE `sessions` ADD COLUMN "last_activity" INTEGER DEFAULT 0;
@@ -90,8 +90,7 @@ class DBKVStore extends BaseImplementation {
VALUES (?, ?, ?, ?, ?) ` +
db.case({
mysql: 'ON DUPLICATE KEY UPDATE value = ?',
sqlite: ' ',
// sqlite: 'ON CONFLICT(user_id, app, kkey_hash) DO UPDATE SET value = ?',
sqlite: 'ON CONFLICT(user_id, app, kkey_hash) DO UPDATE SET value = excluded.value',
}),
[
user.id, app?.uid ?? 'global', key_hash, key, value,
@@ -19,14 +19,18 @@
const BaseService = require("../BaseService");
const { SECOND } = require("../../util/time");
const { parse_meminfo } = require("../../util/linux");
const { asyncSafeSetInterval } = require("../../util/promise");
const { asyncSafeSetInterval, TeePromise } = require("../../util/promise");
class ServerHealthService extends BaseService {
static MODULES = {
fs: require('fs'),
}
_construct () {
this.checks_ = [];
this.failures_ = [];
}
async _init () {
const ram_poll_interval = 10 * SECOND;
this.init_service_checks_();
/*
There's an interesting thread here:
@@ -53,7 +57,7 @@ class ServerHealthService extends BaseService {
return;
}
asyncSafeSetInterval(async () => {
this.add_check('ram-usage', async () => {
const meminfo_text = await this.modules.fs.promises.readFile(
'/proc/meminfo', 'utf8'
);
@@ -69,11 +73,46 @@ class ServerHealthService extends BaseService {
if ( meminfo.MemAvailable < min_available_KiB ) {
svc_alarm.create('low-available-memory', 'Low available memory', alarm_fields);
}
}, ram_poll_interval, null,{
});
}
init_service_checks_ () {
const svc_alarm = this.services.get('alarm');
asyncSafeSetInterval(async () => {
const check_failures = [];
for ( const { name, fn } of this.checks_ ) {
const p_timeout = new TeePromise();
const timeout = setTimeout(() => {
p_timeout.reject(new Error('Health check timed out'));
}, 5 * SECOND);
try {
await Promise.race([
fn(),
p_timeout,
]);
clearTimeout(timeout);
} catch ( err ) {
// Trigger an alarm if this check isn't already in the failure list
if ( this.failures_.some(v => v.name === name) ) {
return;
}
svc_alarm.create(
'health-check-failure',
`Health check ${name} failed`,
{ error: err }
);
check_failures.push({ name });
}
}
this.failures_ = check_failures;
}, 10 * SECOND, null, {
onBehindSchedule: (drift) => {
svc_alarm.create(
'ram-usage-poll-behind-schedule',
'RAM usage poll is behind schedule',
'health-checks-behind-schedule',
'Health checks are behind schedule',
{ drift }
);
}
@@ -83,6 +122,18 @@ class ServerHealthService extends BaseService {
async get_stats () {
return { ...this.stats_ };
}
add_check (name, fn) {
this.checks_.push({ name, fn });
}
get_status () {
const failures = this.failures_.map(v => v.name);
return {
ok: failures.length === 0,
...(failures.length ? { failed: failures } : {}),
};
}
}
module.exports = { ServerHealthService };
+21 -6
View File
@@ -25,6 +25,7 @@ const generate_puter_page_html = ({
manifest,
gui_path,
use_bundled_gui,
app_origin,
api_origin,
@@ -55,6 +56,8 @@ const generate_puter_page_html = ({
? '/src' : '/dist' ;
// const asset_dir = '/dist';
const bundled = env != 'dev' || use_bundled_gui;
return `<!DOCTYPE html>
<html lang="en">
@@ -72,6 +75,9 @@ const generate_puter_page_html = ({
<meta property="og:description" content="${e((short_description).replace(/\n/g, " "))}">
<meta property="og:image" content="${asset_dir}/images/screenshot.png">
<!-- disable iframes -->
<meta http-equiv="X-Frame-Options" content="sameorigin">
<!-- Twitter meta tags -->
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="puter.com">
@@ -104,7 +110,7 @@ const generate_puter_page_html = ({
<!-- Files from JSON (may be empty) -->
${
((env == 'dev' && manifest?.css_paths)
((!bundled && manifest?.css_paths)
? manifest.css_paths.map(path => `<link rel="stylesheet" href="${path}">\n`)
: []).join('')
}
@@ -114,7 +120,12 @@ const generate_puter_page_html = ({
<body>
<script>window.puter_gui_enabled = true;</script>
${
((env == 'dev' && manifest?.lib_paths)
use_bundled_gui
? `<script>window.gui_env = 'prod';</script>`
: ''
}
${
((!bundled && manifest?.lib_paths)
? manifest.lib_paths.map(path => `<script type="text/javascript" src="${path}"></script>\n`)
: []).join('')
}
@@ -123,7 +134,7 @@ const generate_puter_page_html = ({
window.icons = {};
${(() => {
if ( !(env == 'dev' && manifest) ) return '';
if ( !(!bundled && manifest) ) return '';
const html = [];
fs_.readdirSync(path_.join(gui_path, 'src/icons')).forEach(file => {
// skip dotfiles
@@ -144,16 +155,20 @@ const generate_puter_page_html = ({
</script>
${
((env == 'dev' && manifest?.js_paths)
((!bundled && manifest?.js_paths)
? manifest.js_paths.map(path => `<script type="module" src="${path}"></script>\n`)
: []).join('')
}
<!-- Load the GUI script -->
<script ${ env == 'dev' ? ' type="module"' : ''} src="${(env == 'dev' && manifest?.index) || '/dist/gui.js'}"></script>
<script ${ !bundled ? ' type="module"' : ''} src="${(!bundled && manifest?.index) || '/dist/gui.js'}"></script>
<!-- Initialize GUI when document is loaded -->
<script>
window.addEventListener('load', function() {
gui(${JSON.stringify(gui_params)});
gui(${
// TODO: override JSON.stringify to ALWAYS to this...
// this should be an opt-OUT, not an opt-IN!
JSON.stringify(gui_params).replace(/</g, '\\u003c')
});
});
</script>
<div id="templates" style="display: none;"></div>
-3
View File
@@ -5,9 +5,6 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="xterm.css">
<link rel="stylesheet" href="normalize.css">
<link rel="stylesheet" href="style.css">
<script src="__SDK_URL__"></script>
<script src="config.js"></script>
<script src="bundle.js"></script>
+238 -38
View File
@@ -16,59 +16,233 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { TeePromise, raceCase } from '../../src/promise.js';
const encoder = new TextEncoder();
const CHAR_LF = '\n'.charCodeAt(0);
const CHAR_CR = '\r'.charCodeAt(0);
const DONE = Symbol('done');
class Channel {
constructor () {
this.chunks_ = [];
globalThis.chnl = this;
const events = ['write','consume','change'];
for ( const event of events ) {
this[`on_${event}_`] = [];
this[`emit_${event}_`] = () => {
for ( const listener of this[`on_${event}_`] ) {
listener();
}
};
}
this.on('write', () => { this.emit_change_(); });
this.on('consume', () => { this.emit_change_(); });
}
on (event, listener) {
this[`on_${event}_`].push(listener);
}
off (event, listener) {
const index = this[`on_${event}_`].indexOf(listener);
if ( index !== -1 ) {
this[`on_${event}_`].splice(index, 1);
}
}
get () {
const cancel = new TeePromise();
const data = new TeePromise();
const done = new TeePromise();
let called = 0;
const on_data = () => {
if ( this.chunks_.length > 0 ) {
if ( called > 0 ) {
throw new Error('called more than once');
}
called++;
const chunk = this.chunks_.shift();
( chunk === DONE ? done : data ).resolve(chunk);
this.off('write', on_data);
this.emit_consume_();
}
};
this.on('write', on_data);
on_data();
const to_return = {
cancel: () => {
this.off('write', on_data);
cancel.resolve();
},
promise: raceCase({
cancel,
data,
done,
}),
};
return to_return;
}
write (chunk) {
this.chunks_.push(chunk);
this.emit_write_();
}
pushback (...chunks) {
for ( let i = chunks.length - 1; i >= 0; i-- ) {
this.chunks_.unshift(chunks[i]);
}
this.emit_write_();
}
is_empty () {
return this.chunks_.length === 0;
}
}
export class BetterReader {
constructor ({ delegate }) {
this.delegate = delegate;
this.chunks_ = [];
this.channel_ = new Channel();
this._init();
}
_init () {
let working = Promise.resolve();
this.channel_.on('consume', async () => {
await working;
working = new TeePromise();
if ( this.channel_.is_empty() ) {
await this.intake_();
}
working.resolve();
});
this.intake_();
}
async intake_ () {
const { value, done } = await this.delegate.read();
if ( done ) {
this.channel_.write(DONE);
return;
}
this.channel_.write(value);
}
_create_cancel_response () {
return {
chunk: null,
n_read: 0,
debug_meta: {
source: 'delegate',
returning: 'cancelled',
this_value_should_not_be_used: true,
},
};
}
read_and_get_info (opt_buffer, cancel_state) {
if ( ! opt_buffer ) {
const { promise, cancel } = this.channel_.get();
return {
cancel,
promise: promise.then(([which, chunk]) => {
if ( which !== 'data' ) {
return { done: true, value: null };
}
return { value: chunk };
}),
};
}
const final_promise = new TeePromise();
let current_cancel_ = () => {};
(async () => {
let n_read = 0;
const chunks = [];
while ( n_read < opt_buffer.length ) {
const { promise, cancel } = this.channel_.get();
current_cancel_ = cancel;
let [which, chunk] = await promise;
if ( which === 'done' ) {
break;
}
if ( which === 'cancel' ) {
this.channel_.pushback(...chunks);
return
}
if ( n_read + chunk.length > opt_buffer.length ) {
const diff = opt_buffer.length - n_read;
this.channel_.pushback(chunk.subarray(diff));
chunk = chunk.subarray(0, diff);
}
chunks.push(chunk);
opt_buffer.set(chunk, n_read);
n_read += chunk.length;
}
final_promise.resolve({ n_read });
})();
return {
cancel: () => {
current_cancel_();
},
promise: final_promise,
};
}
read_with_cancel (opt_buffer) {
const o = this.read_and_get_info(opt_buffer);
const { cancel, promise } = o;
// const promise = (async () => {
// const { chunk, n_read } = await this.read_and_get_info(opt_buffer, cancel_state);
// return opt_buffer ? n_read : chunk;
// })();
return {
cancel,
promise,
};
}
async read (opt_buffer) {
if ( ! opt_buffer && this.chunks_.length === 0 ) {
return await this.delegate.read();
}
const chunk = await this.getChunk_();
if ( ! opt_buffer ) {
return chunk;
}
this.chunks_.push(chunk);
while ( this.getTotalBytesReady_() < opt_buffer.length ) {
this.chunks_.push(await this.getChunk_())
}
// TODO: need to handle EOT condition in this loop
let offset = 0;
for (;;) {
let item = this.chunks_.shift();
if ( item === undefined ) {
throw new Error('calculation is wrong')
}
if ( offset + item.length > opt_buffer.length ) {
const diff = opt_buffer.length - offset;
this.chunks_.unshift(item.subarray(diff));
item = item.subarray(0, diff);
}
opt_buffer.set(item, offset);
offset += item.length;
if ( offset == opt_buffer.length ) break;
}
// return opt_buffer.length;
const { chunk, n_read } = await this.read_and_get_info(opt_buffer).promise;
return opt_buffer ? n_read : chunk;
}
async getChunk_() {
if ( this.chunks_.length === 0 ) {
const { value } = await this.delegate.read();
return value;
// Wait for either a delegate read to happen, or for a chunk to be added to the buffer from a cancelled read.
const delegate_read = this.delegate.read();
const [which, result] = await raceCase({
delegate: delegate_read,
buffer_not_empty: this.waitUntilDataAvailable(),
});
if (which === 'delegate') {
return result;
}
// There's a chunk in the buffer now, so we can use the regular path.
// But first, make sure that once the delegate read completes, we save the chunk.
this.chunks_.push(result);
}
const len = this.getTotalBytesReady_();
@@ -85,7 +259,33 @@ export class BetterReader {
}
getTotalBytesReady_ () {
return this.chunks_.reduce((sum, chunk) => sum + chunk.length, 0);
return this.chunks_.reduce((sum, chunk) => {
return sum + chunk.value.length
}, 0);
}
canRead() {
return this.getTotalBytesReady_() > 0;
}
async waitUntilDataAvailable() {
let resolve_promise;
let reject_promise;
const promise = new Promise((resolve, reject) => {
resolve_promise = resolve;
reject_promise = reject;
});
const check = () => {
if (this.canRead()) {
resolve_promise();
} else {
setTimeout(check, 0);
}
};
setTimeout(check, 0);
await promise;
}
}
@@ -23,7 +23,6 @@ export default class StrUntilParserImpl {
parse (lexer) {
let text = '';
for ( ;; ) {
console.log('B')
let { done, value } = lexer.look();
if ( done ) break;
@@ -41,8 +40,6 @@ export default class StrUntilParserImpl {
if ( text.length === 0 ) return;
console.log('test?', text)
return { $: 'until', text };
}
}
@@ -22,7 +22,6 @@ export default class ContextSwitchingPStratumImpl {
constructor ({ contexts, entry }) {
this.contexts = { ...contexts };
for ( const key in this.contexts ) {
console.log('parsers?', this.contexts[key]);
const new_array = [];
for ( const parser of this.contexts[key] ) {
if ( parser.hasOwnProperty('transition') ) {
@@ -44,7 +43,6 @@ export default class ContextSwitchingPStratumImpl {
this.lastvalue = null;
}
get stack_top () {
console.log('stack top?', this.stack[this.stack.length - 1])
return this.stack[this.stack.length - 1];
}
get current_context () {
@@ -55,7 +53,6 @@ export default class ContextSwitchingPStratumImpl {
const lexer = api.delegate;
const context = this.current_context;
console.log('context?', context);
for ( const spec of context ) {
{
const { done, value } = lexer.look();
@@ -64,7 +61,6 @@ export default class ContextSwitchingPStratumImpl {
throw new Error('infinite loop');
}
this.lastvalue = value;
console.log('last value?', value, done);
if ( done ) return { done };
}
@@ -76,7 +72,6 @@ export default class ContextSwitchingPStratumImpl {
}
const subLexer = lexer.fork();
// console.log('spec?', spec);
const result = parser.parse(subLexer);
if ( result.status === ParseResult.UNRECOGNIZED ) {
continue;
@@ -84,11 +79,9 @@ export default class ContextSwitchingPStratumImpl {
if ( result.status === ParseResult.INVALID ) {
return { done: true, value: result };
}
console.log('RESULT', result, spec)
if ( ! peek ) lexer.join(subLexer);
if ( transition ) {
console.log('GOT A TRANSITION')
if ( transition.pop ) this.stack.pop();
if ( transition.to ) this.stack.push({
context_name: transition.to,
@@ -97,7 +90,6 @@ export default class ContextSwitchingPStratumImpl {
if ( result.value.$discard || peek ) return this.next(api);
console.log('PROVIDING VALUE', result.value);
return { done: false, value: result.value };
}
@@ -22,11 +22,6 @@ import { DEFAULT_OPTIONS } from '../../puter-shell/coreutils/coreutil_lib/help.j
export default {
name: 'simple-parser',
async process (ctx, spec) {
console.log({
...spec,
args: ctx.locals.args
});
// Insert standard options
spec.options = Object.assign(spec.options || {}, DEFAULT_OPTIONS);
@@ -48,7 +48,6 @@ export class SignalReader extends ProxyReader {
// show hex for debugging
// console.log(value.split('').map(c => c.charCodeAt(0).toString(16)).join(' '));
console.log('value??', value)
for ( const [key, signal] of mapping ) {
if ( tmp_value.includes(key) ) {
@@ -37,7 +37,6 @@ export class PuterShellParser {
if ( sp.error ) {
throw new Error(sp.error);
}
console.log('PARSER RESULT', result);
return result;
}
parseScript (input) {
@@ -54,7 +54,6 @@ class ReducePrimitivesPStratumImpl {
let text = '';
for ( const item of contents.results ) {
if ( item.$ === 'string.segment' ) {
// console.log('segment?', item.text)
text += item.text;
continue;
}
@@ -86,7 +85,6 @@ class ShellConstructsPStratumImpl {
node.commands = [];
},
exit ({ node }) {
console.log('!!!!!',this.stack_top.node)
if ( this.stack_top?.node?.$ === 'script' ) {
this.stack_top.node.statements.push(node);
}
@@ -96,7 +94,6 @@ class ShellConstructsPStratumImpl {
},
next ({ value, lexer }) {
if ( value.$ === 'op.line-terminator' ) {
console.log('the stack??', this.stack)
this.pop();
return;
}
@@ -189,7 +186,6 @@ class ShellConstructsPStratumImpl {
},
next ({ value, lexer }) {
if ( value.$ === 'op.line-terminator' ) {
console.log('well, got here')
this.pop();
return;
}
@@ -223,9 +219,7 @@ class ShellConstructsPStratumImpl {
this.stack_top.node.components.push(...node.components);
},
next ({ node, value, lexer }) {
console.log('WHAT THO', node)
if ( value.$ === 'op.line-terminator' && node.quote === null ) {
console.log('well, got here')
this.pop();
return;
}
@@ -292,7 +286,6 @@ class ShellConstructsPStratumImpl {
const lexer = api.delegate;
console.log('THE NODE', this.stack[0].node);
// return { done: true, value: { $: 'test' } };
for ( let i=0 ; i < 500 ; i++ ) {
@@ -306,15 +299,12 @@ class ShellConstructsPStratumImpl {
}
const { state, node } = this.stack_top;
console.log('value?', value, done)
console.log('state?', state.name);
state.next.call(this, { lexer, value, node, state });
// if ( done ) break;
}
console.log('THE NODE', this.stack[0]);
this.done_ = true;
return { done: false, value: this.stack[0].node };
@@ -433,7 +423,6 @@ export const buildParserSecondHalf = (sp, { multiline } = {}) => {
// sp.add(new ReducePrimitivesPStratumImpl());
if ( multiline ) {
console.log('USING MULTILINE');
sp.add(new MultilinePStratumImpl());
} else {
sp.add(new ShellConstructsPStratumImpl());
@@ -16,6 +16,8 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { TeePromise, raceCase } from "../../promise.js";
export class Coupler {
static description = `
Connects a read stream to a write stream.
@@ -26,6 +28,7 @@ export class Coupler {
this.source = source;
this.target = target;
this.on_ = true;
this.closed_ = new TeePromise();
this.isDone = new Promise(rslv => {
this.resolveIsDone = rslv;
})
@@ -35,11 +38,29 @@ export class Coupler {
off () { this.on_ = false; }
on () { this.on_ = true; }
close () {
this.closed_.resolve({
done: true,
});
}
async listenLoop_ () {
this.active = true;
for (;;) {
const { value, done } = await this.source.read();
let cancel = () => {};
let promise;
if ( this.source.read_with_cancel !== undefined ) {
({ cancel, promise } = this.source.read_with_cancel());
} else {
promise = this.source.read();
}
const [which, result] = await raceCase({
source: promise,
closed: this.closed_,
});
const { value, done } = result;
if ( done ) {
cancel();
this.source = null;
this.target = null;
this.active = false;
@@ -47,6 +68,7 @@ export class Coupler {
break;
}
if ( this.on_ ) {
if ( ! value ) debugger;
await this.target.write(value);
}
}
@@ -38,11 +38,6 @@ class Token {
throw new Error('expected token node');
}
console.log('ast has cst?',
ast,
ast.components?.[0]?.$cst
)
return new Token(ast);
}
constructor (ast) {
@@ -53,20 +48,15 @@ class Token {
// If the only components are of type 'symbol' and 'string.segment'
// then we can statically resolve the value of the token.
console.log('checking viability of static resolve', this.ast)
const isStatic = this.ast.components.every(c => {
return c.$ === 'symbol' || c.$ === 'string.segment';
});
if ( ! isStatic ) return;
console.log('doing static thing', this.ast)
// TODO: Variables can also be statically resolved, I think...
let value = '';
for ( const component of this.ast.components ) {
console.log('component', component);
value += component.text;
}
@@ -113,7 +103,6 @@ export class PreparedCommand {
// TODO: check that node for command name is of a
// supported type - maybe use adapt pattern
console.log('ast?', ast);
const cmd = command_token.maybeStaticallyResolve(ctx);
const { commands } = ctx.registries;
@@ -124,16 +113,13 @@ export class PreparedCommand {
: command_token;
if ( command === undefined ) {
console.log('command token?', command_token);
throw new ConcreteSyntaxError(
`no command: ${JSON.stringify(cmd)}`,
command_token.$cst,
);
throw new Error('no command: ' + JSON.stringify(cmd));
}
// TODO: test this
console.log('ast?', ast);
const inputRedirect = ast.inputRedirects.length > 0 ? (() => {
const token = Token.createFromAST(ctx, ast.inputRedirects[0]);
return token.maybeStaticallyResolve(ctx) ?? token;
@@ -172,7 +158,6 @@ export class PreparedCommand {
// command to run.
if ( command instanceof Token ) {
const cmd = await command.resolve(this.ctx);
console.log('RUNNING CMD?', cmd)
const { commandProvider } = this.ctx.externs;
command = await commandProvider.lookup(cmd, { ctx: this.ctx });
if ( command === undefined ) {
@@ -314,31 +299,23 @@ export class PreparedCommand {
);
ctx.locals.exit = -1;
}
if ( ! (e instanceof Exit) ) console.error(e);
}
// ctx.externs.in?.close?.();
// ctx.externs.out?.close?.();
await ctx.externs.out.close();
// TODO: need write command from puter-shell before this can be done
for ( let i=0 ; i < this.outputRedirects.length ; i++ ) {
console.log('output redirect??', this.outputRedirects[i]);
const { filesystem } = this.ctx.platform;
const outputRedirect = this.outputRedirects[i];
const dest_path = outputRedirect instanceof Token
? await outputRedirect.resolve(this.ctx)
: outputRedirect;
const path = resolveRelativePath(ctx.vars, dest_path);
console.log('it should work?', {
path,
outputMemWriters,
})
// TODO: error handling here
await filesystem.write(path, outputMemWriters[i].getAsBlob());
}
console.log('OUTPUT WRITERS', outputMemWriters);
}
}
@@ -366,6 +343,11 @@ export class Pipeline {
let nextIn = ctx.externs.in;
let lastPipe = null;
// Create valve to close input pipe when done
const pipeline_input_pipe = new Pipe();
const valve = new Coupler(nextIn, pipeline_input_pipe.in);
nextIn = pipeline_input_pipe.out;
// TOOD: this will eventually defer piping of certain
// sub-pipelines to the Puter Shell.
@@ -400,8 +382,8 @@ export class Pipeline {
commandPromises.push(command.execute());
}
await Promise.all(commandPromises);
console.log('PIPELINE DONE');
await coupler.isDone;
valve.close();
}
}
@@ -96,7 +96,6 @@ const ReadlineProcessorBuilder = builder => builder
externs.out.write(externs.prompt);
externs.out.write(vars.result);
const invCurPos = vars.result.length - vars.cursor;
console.log(invCurPos)
if ( invCurPos !== 0 ) {
externs.out.write(`\x1B[${invCurPos}D`);
}
@@ -111,8 +110,6 @@ const ReadlineProcessorBuilder = builder => builder
}
}));
// NEXT: get tab completer for input state
console.log('input state', inputState);
let completer = null;
if ( inputState.$ === 'redirect' ) {
completer = new FileCompleter();
@@ -141,7 +138,6 @@ const ReadlineProcessorBuilder = builder => builder
const applyCompletion = txt => {
const p1 = vars.result.slice(0, vars.cursor);
const p2 = vars.result.slice(vars.cursor);
console.log({ p1, p2 });
vars.result = p1 + txt + p2;
vars.cursor += txt.length;
externs.out.write(txt);
+57
View File
@@ -0,0 +1,57 @@
export class TeePromise {
static STATUS_PENDING = Symbol('pending');
static STATUS_RUNNING = {};
static STATUS_DONE = Symbol('done');
constructor () {
this.status_ = this.constructor.STATUS_PENDING;
this.donePromise = new Promise((resolve, reject) => {
this.doneResolve = resolve;
this.doneReject = reject;
});
}
get status () {
return this.status_;
}
set status (status) {
this.status_ = status;
if ( status === this.constructor.STATUS_DONE ) {
this.doneResolve();
}
}
resolve (value) {
this.status_ = this.constructor.STATUS_DONE;
this.doneResolve(value);
}
awaitDone () {
return this.donePromise;
}
then (fn, ...a) {
return this.donePromise.then(fn, ...a);
}
reject (err) {
this.status_ = this.constructor.STATUS_DONE;
this.doneReject(err);
}
/**
* @deprecated use then() instead
*/
onComplete(fn) {
return this.then(fn);
}
}
/**
* raceCase is like Promise.race except it takes an object instead of
* an array, and returns the key of the promise that resolves first
* as well as the value that it resolved to.
*
* @param {Object.<string, Promise>} promise_map
*
* @returns {Promise.<[string, any]>}
*/
export const raceCase = async (promise_map) => {
return Promise.race(Object.entries(promise_map).map(
([key, promise]) => promise.then(value => [key, value])));
};
+2 -2
View File
@@ -38,7 +38,7 @@ export class XDocumentPTT {
chunk = encoder.encode(chunk);
}
terminalConnection.postMessage({
$: 'output',
$: 'stdout',
data: chunk,
});
}
@@ -52,7 +52,7 @@ export class XDocumentPTT {
this.emit('ioctl.set', message);
return;
}
if (message.$ === 'input') {
if (message.$ === 'stdin') {
this.readController.enqueue(message.data);
return;
}
@@ -16,49 +16,92 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Exit } from '../coreutils/coreutil_lib/exit.js';
import { signals } from '../../ansi-shell/signals.js';
const BUILT_IN_APPS = [
'explorer',
];
const lookup_app = async (id) => {
if (BUILT_IN_APPS.includes(id)) {
return { success: true, path: null };
}
const request = await fetch(`${puter.APIOrigin}/drivers/call`, {
"headers": {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
"body": JSON.stringify({ interface: 'puter-apps', method: 'read', args: { id: { name: id } } }),
"method": "POST",
});
const { success, result } = await request.json();
return { success, path: result?.index_url };
};
export class PuterAppCommandProvider {
async lookup (id) {
// Built-in apps will not be returned by the fetch query below, so we handle them separately.
if (BUILT_IN_APPS.includes(id)) {
return {
name: id,
path: 'Built-in Puter app',
// TODO: Parameters and options?
async execute(ctx) {
const args = {}; // TODO: Passed-in parameters and options would go here
// NOTE: No await here, because launchApp() currently only resolves for Puter SDK apps.
puter.ui.launchApp(id, args);
}
};
}
const request = await fetch(`${puter.APIOrigin}/drivers/call`, {
"headers": {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
"body": JSON.stringify({ interface: 'puter-apps', method: 'read', args: { id: { name: id } } }),
"method": "POST",
});
const { success, result } = await request.json();
const { success, path } = await lookup_app(id);
if (!success) return;
const { name, index_url } = result;
return {
name,
path: index_url,
name: id,
path: path ?? 'Built-in Puter app',
// TODO: Parameters and options?
async execute(ctx) {
const args = {}; // TODO: Passed-in parameters and options would go here
// NOTE: No await here, yet, because launchApp() currently only resolves for Puter SDK apps.
puter.ui.launchApp(name, args);
const child = await puter.ui.launchApp(id, args);
// Wait for app to close.
const app_close_promise = new Promise((resolve, reject) => {
child.on('close', () => {
// TODO: Exit codes for apps
resolve({ done: true });
});
});
// Wait for SIGINT
const sigint_promise = new Promise((resolve, reject) => {
ctx.externs.sig.on((signal) => {
if (signal === signals.SIGINT) {
child.close();
reject(new Exit(130));
}
});
});
// We don't connect stdio to non-SDK apps, because they won't make use of it.
if (child.usesSDK) {
const decoder = new TextDecoder();
child.on('message', message => {
if (message.$ === 'stdout') {
ctx.externs.out.write(decoder.decode(message.data));
}
});
// Repeatedly copy data from stdin to the child, while it's running.
// DRY: Initially copied from PathCommandProvider
let data, done;
const next_data = async () => {
// FIXME: This waits for one more read() after we finish.
({ value: data, done } = await Promise.race([
app_close_promise, sigint_promise, ctx.externs.in_.read(),
]));
if (data) {
child.postMessage({
$: 'stdin',
data: data,
});
if (!done) setTimeout(next_data, 0);
}
};
setTimeout(next_data, 0);
}
return Promise.race([ app_close_promise, sigint_promise ]);
}
};
}
+18 -2
View File
@@ -19,8 +19,9 @@
import { Context } from "contextlink";
import { SyncLinesReader } from '../../src/ansi-shell/ioutil/SyncLinesReader.js';
import { CommandStdinDecorator } from '../../src/ansi-shell/pipeline/iowrappers.js';
import { ReadableStream, WritableStream } from 'stream/web'
export class WritableStringStream extends WritableStream {
class WritableStringStream extends WritableStream {
constructor() {
super({
write: (chunk) => {
@@ -42,8 +43,23 @@ export class WritableStringStream extends WritableStream {
// TODO: Flesh this out as needed.
export const MakeTestContext = (command, { positionals = [], values = {}, stdinInputs = [], env = {} }) => {
// This is a replacement to ReadableStream.from() in earlier Node versions
// Sourece: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream#convert_an_iterator_or_async_iterator_to_a_stream
function iteratorToStream(iterator) {
return new ReadableStream({
async pull(controller) {
const { value, done } = await iterator.next();
let in_ = ReadableStream.from(stdinInputs).getReader();
if (done) {
controller.close();
} else {
controller.enqueue(value);
}
},
});
}
let in_ = iteratorToStream(stdinInputs.values()).getReader();
if (command.input?.syncLines) {
in_ = new SyncLinesReader({ delegate: in_ });
}
+5 -1
View File
@@ -9,6 +9,7 @@ import Auth from './modules/Auth.js';
import FSItem from './modules/FSItem.js';
import * as utils from './lib/utils.js';
import path from './lib/path.js';
import Util from './modules/Util.js';
window.puter = (function() {
'use strict';
@@ -168,6 +169,9 @@ window.puter = (function() {
// Initialize submodules
initSubmodules = function(){
// Util
this.util = new Util();
// Auth
this.auth = new Auth(this.authToken, this.APIOrigin, this.appID, this.env);
// OS
@@ -175,7 +179,7 @@ window.puter = (function() {
// FileSystem
this.fs = new FileSystem(this.authToken, this.APIOrigin, this.appID, this.env);
// UI
this.ui = new UI(this.appInstanceID, this.parentInstanceID, this.appID, this.env);
this.ui = new UI(this.appInstanceID, this.parentInstanceID, this.appID, this.env, this.util);
// Hosting
this.hosting = new Hosting(this.authToken, this.APIOrigin, this.appID, this.env);
// Apps
+111
View File
@@ -0,0 +1,111 @@
/**
* This module provides a simple RPC mechanism for cross-document
* (iframe / window.postMessage) communication.
*/
// Since `Symbol` is not clonable, we use a UUID to identify RPCs.
const $SCOPE = '9a9c83a4-7897-43a0-93b9-53217b84fde6';
/**
* The CallbackManager is used to manage callbacks for RPCs.
* It is used by the dehydrator and hydrator to store and retrieve
* the functions that are being called remotely.
*/
export class CallbackManager {
#messageId = 0;
constructor () {
this.callbacks = new Map();
}
register_callback (callback) {
const id = this.#messageId++;
this.callbacks.set(id, callback);
return id;
}
attach_to_source (source) {
source.addEventListener('message', event => {
const { data } = event;
console.log(
'test-app got message from window',
data,
);
debugger;
if (data && typeof data === 'object' && data.$SCOPE === $SCOPE) {
const { id, args } = data;
const callback = this.callbacks.get(id);
if (callback) {
callback(...args);
}
}
});
}
}
/**
* The dehydrator replaces functions in an object with identifiers,
* so that hydrate() can be called on the other side of the frame
* to bind RPC stubs. The original functions are stored in a map
* so that they can be called when the RPC is invoked.
*/
export class Dehydrator {
constructor ({ callbackManager }) {
this.callbackManager = callbackManager;
}
dehydrate (value) {
return this.dehydrate_value_(value);
}
dehydrate_value_ (value) {
if (typeof value === 'function') {
const id = this.callbackManager.register_callback(value);
return { $SCOPE, id };
} else if (Array.isArray(value)) {
return value.map(this.dehydrate_value_.bind(this));
} else if (typeof value === 'object' && value !== null) {
const result = {};
for (const key in value) {
result[key] = this.dehydrate_value_(value[key]);
}
return result;
} else {
return value;
}
}
}
/**
* The hydrator binds RPC stubs to the functions that were
* previously dehydrated. This allows the RPC to be invoked
* on the other side of the frame.
*/
export class Hydrator {
constructor ({ target }) {
this.target = target;
}
hydrate (value) {
return this.hydrate_value_(value);
}
hydrate_value_ (value) {
if (
value && typeof value === 'object' &&
value.$SCOPE === $SCOPE
) {
const { id } = value;
return (...args) => {
console.log('sending message', { $SCOPE, id, args });
console.log('target', this.target);
this.target.postMessage({ $SCOPE, id, args }, '*');
};
} else if (Array.isArray(value)) {
return value.map(this.hydrate_value_.bind(this));
} else if (typeof value === 'object' && value !== null) {
const result = {};
for (const key in value) {
result[key] = this.hydrate_value_(value[key]);
}
return result;
}
return value;
}
}
+35 -4
View File
@@ -14,7 +14,11 @@ class AppConnection extends EventListener {
// Whether the target app is open
#isOpen;
constructor(messageTarget, appInstanceID, targetAppInstanceID) {
// Whether the target app uses the Puter SDK, and so accepts messages
// (Closing and close events will still function.)
#usesSDK;
constructor(messageTarget, appInstanceID, targetAppInstanceID, usesSDK) {
super([
'message', // The target sent us something with postMessage()
'close', // The target app was closed
@@ -23,6 +27,7 @@ class AppConnection extends EventListener {
this.appInstanceID = appInstanceID;
this.targetAppInstanceID = targetAppInstanceID;
this.#isOpen = true;
this.#usesSDK = usesSDK;
// TODO: Set this.#puterOrigin to the puter origin
@@ -54,12 +59,21 @@ class AppConnection extends EventListener {
});
}
// Does the target app use the Puter SDK? If not, certain features will be unavailable.
get usesSDK() { return this.#usesSDK; }
// Send a message to the target app. Requires the target to use the Puter SDK.
postMessage(message) {
if (!this.#isOpen) {
console.warn('Trying to post message on a closed AppConnection');
return;
}
if (!this.#usesSDK) {
console.warn('Trying to post message to a non-SDK app');
return;
}
this.messageTarget.postMessage({
msg: 'messageToApp',
appInstanceID: this.appInstanceID,
@@ -135,7 +149,19 @@ class UI extends EventListener {
this.#callbackFunctions[msg_id] = resolve;
}
constructor (appInstanceID, parentInstanceID, appID, env) {
#postMessageWithObject = function(name, value) {
const dehydrator = this.util.rpc.getDehydrator({
target: this.messageTarget
});
this.messageTarget?.postMessage({
msg: name,
env: this.env,
appInstanceID: this.appInstanceID,
value: dehydrator.dehydrate(value),
}, '*');
}
constructor (appInstanceID, parentInstanceID, appID, env, util) {
const eventNames = [
'localeChanged',
'themeChanged',
@@ -146,6 +172,7 @@ class UI extends EventListener {
this.parentInstanceID = parentInstanceID;
this.appID = appID;
this.env = env;
this.util = util;
if(this.env === 'app'){
this.messageTarget = window.parent;
@@ -155,7 +182,7 @@ class UI extends EventListener {
}
if (this.parentInstanceID) {
this.#parentAppConnection = new AppConnection(this.messageTarget, this.appInstanceID, this.parentInstanceID);
this.#parentAppConnection = new AppConnection(this.messageTarget, this.appInstanceID, this.parentInstanceID, true);
}
// Tell the host environment that this app is using the Puter SDK and is ready to receive messages,
@@ -374,7 +401,7 @@ class UI extends EventListener {
}
else if (e.data.msg === 'childAppLaunched') {
// execute callback with a new AppConnection to the child
const connection = new AppConnection(this.messageTarget, this.appInstanceID, e.data.child_instance_id);
const connection = new AppConnection(this.messageTarget, this.appInstanceID, e.data.child_instance_id, e.data.uses_sdk);
this.#callbackFunctions[e.data.original_msg_id](connection);
}
else{
@@ -627,6 +654,10 @@ class UI extends EventListener {
})
}
setMenubar = function(spec) {
this.#postMessageWithObject('setMenubar', spec);
}
/**
* Asynchronously extracts entries from DataTransferItems, like files and directories.
*
+30
View File
@@ -0,0 +1,30 @@
import { CallbackManager, Dehydrator, Hydrator } from "../lib/xdrpc";
/**
* The Util module exposes utilities within puter.js itself.
* These utilities may be used internally by other modules.
*/
export default class Util {
constructor () {
// This is in `puter.util.rpc` instead of `puter.rpc` because
// `puter.rpc` is reserved for an app-to-app RPC interface.
// This is a lower-level RPC interface used to communicate
// with iframes.
this.rpc = new UtilRPC();
}
}
class UtilRPC {
constructor () {
this.callbackManager = new CallbackManager();
this.callbackManager.attach_to_source(window);
}
getDehydrator () {
return new Dehydrator({ callbackManager: this.callbackManager });
}
getHydrator ({ target }) {
return new Hydrator({ target });
}
}
-13
View File
@@ -41,27 +41,14 @@ class XTermIO {
}
async handleKeyBeforeProcess (evt) {
console.log(
'right this event might be up or down so it\'s necessary to determine which',
evt,
);
if ( evt.key === 'V' && evt.ctrlKey && evt.shiftKey && evt.type === 'keydown' ) {
const clipboard = navigator.clipboard;
const text = await clipboard.readText();
console.log(
'this is the relevant text for this thing that is the thing that is the one that is here',
text,
);
this.pty.out.write(text);
}
}
handleKey ({ key, domEvent }) {
console.log(
'key event happened',
key,
domEvent,
);
const pty = this.pty;
const handlers = {
@@ -53,7 +53,7 @@ export class XDocumentANSIShell {
return;
}
if (message.$ === 'output') {
if (message.$ === 'stdout') {
ptt.out.write(message.data);
return;
}
@@ -69,7 +69,7 @@ export class XDocumentANSIShell {
for ( ;; ) {
const chunk = (await ptt.in.read()).value;
shell.postMessage({
$: 'input',
$: 'stdin',
data: chunk,
});
}
+115 -19
View File
@@ -27,6 +27,7 @@ import UIWindowColorPicker from './UI/UIWindowColorPicker.js';
import UIPrompt from './UI/UIPrompt.js';
import download from './helpers/download.js';
import path from "./lib/path.js";
import UIContextMenu from './UI/UIContextMenu.js';
/**
* In Puter, apps are loaded in iframes and communicate with the graphical user interface (GUI) aand each other using the postMessage API.
@@ -74,14 +75,6 @@ window.addEventListener('message', async (event) => {
return;
}
const window_for_app_instance = (instance_id) => {
return $(`.window[data-element_uuid="${instance_id}"]`).get(0);
};
const iframe_for_app_instance = (instance_id) => {
return $(window_for_app_instance(instance_id)).find('.window-app-iframe').get(0);
};
const $el_parent_window = $(window_for_app_instance(event.data.appInstanceID));
const parent_window_id = $el_parent_window.attr('data-id');
const $el_parent_disable_mask = $el_parent_window.find('.window-disable-mask');
@@ -98,17 +91,7 @@ window.addEventListener('message', async (event) => {
$(target_iframe).attr('data-appUsesSDK', 'true');
// If we were waiting to launch this as a child app, report to the parent that it succeeded.
const child_launch_callback = window.child_launch_callbacks[event.data.appInstanceID];
if (child_launch_callback) {
const parent_iframe = iframe_for_app_instance(child_launch_callback.parent_instance_id);
// send confirmation to requester window
parent_iframe.contentWindow.postMessage({
msg: 'childAppLaunched',
original_msg_id: child_launch_callback.launch_msg_id,
child_instance_id: event.data.appInstanceID,
}, '*');
delete window.child_launch_callbacks[event.data.appInstanceID];
}
window.report_app_launched(event.data.appInstanceID, { uses_sdk: true });
// Send any saved broadcasts to the new app
globalThis.services.get('broadcast').sendSavedBroadcastsTo(event.data.appInstanceID);
@@ -370,6 +353,119 @@ window.addEventListener('message', async (event) => {
}, '*');
}
//--------------------------------------------------------
// setMenubar
//--------------------------------------------------------
else if(event.data.msg === 'setMenubar') {
const el_window = window_for_app_instance(event.data.appInstanceID);
console.error(`EXPERIMENTAL: setMenubar is a work-in-progress`);
const hydrator = puter.util.rpc.getHydrator({
target: target_iframe.contentWindow,
});
const value = hydrator.hydrate(event.data.value);
console.log('hydrated value', value);
// Show menubar
const $menubar = $(el_window).find('.window-menubar')
$menubar.show();
const sanitize_items = items => {
return items.map(item => {
return {
html: item.label,
action: item.action,
items: item.items && sanitize_items(item.items),
};
});
};
// This array will store the menubar button elements
const menubar_buttons = [];
// Add menubar items
let current = null;
let current_i = null;
let state_open = false;
const open_menu = ({ i, pos, parent_element, items }) => {
let delay = true;
if ( state_open ) {
if ( current_i === i ) return;
delay = false;
current && current.cancel({ meta: 'menubar', fade: false });
}
// Set this menubar button as active
menubar_buttons.forEach(el => el.removeClass('active'));
menubar_buttons[i].addClass('active');
// Open the context menu
const ctxMenu = UIContextMenu({
delay,
parent_element,
position: {top: pos.top + 28, left: pos.left},
items: sanitize_items(items),
});
state_open = true;
current = ctxMenu;
current_i = i;
ctxMenu.onClose = (cancel_options) => {
if ( cancel_options?.meta === 'menubar' ) return;
menubar_buttons.forEach(el => el.removeClass('active'));
ctxMenu.onClose = null;
current_i = null;
current = null;
state_open = false;
}
};
const add_items = (parent, items) => {
for (let i=0; i < items.length; i++) {
const I = i;
const item = items[i];
const label = html_encode(item.label);
const el_item = $(`<div class="window-menubar-item"><span>${label}</span></div>`);
const parent_element = el_item.parent()[0];
el_item.on('click', () => {
if ( state_open ) {
state_open = false;
current && current.cancel({ meta: 'menubar' });
current_i = null;
current = null;
return;
}
if (item.action) {
item.action();
} else if (item.items) {
const pos = el_item[0].getBoundingClientRect();
open_menu({
i,
pos,
parent_element,
items: item.items,
});
}
});
el_item.on('mouseover', () => {
if ( ! state_open ) return;
if ( ! item.items ) return;
const pos = el_item[0].getBoundingClientRect();
open_menu({
i,
pos,
parent_element,
items: item.items,
});
});
$menubar.append(el_item);
menubar_buttons.push(el_item);
}
};
add_items($menubar, value.items);
}
//--------------------------------------------------------
// setWindowWidth
//--------------------------------------------------------
else if(event.data.msg === 'setWindowWidth' && event.data.width !== undefined){
+94 -73
View File
@@ -18,79 +18,100 @@
*/
// About
function UITabAbout(){
let h = ``;
h += `<div class="settings-content active" data-settings="about">`;
h += `<div class="about-container">`
h += `<div class="about">
<a href="https://puter.com" target="_blank" class="logo"><img src="/images/logo.png"></a>
<p class="description">${i18n('puter_description')}</p>
<p class="links">
<a href="mailto:[email protected]" target="_blank">[email protected]</a>
<span style="color: #CCC;">•</span>
<a href="https://docs.puter.com" target="_blank">${i18n('developers')}</a>
<span style="color: #CCC;">•</span>
<a href="https://status.puter.com" target="_blank">${i18n('status')}</a>
<span style="color: #CCC;">•</span>
<a href="https://puter.com/terms" target="_blank">${i18n('terms')}</a>
<span style="color: #CCC;">•</span>
<a href="https://puter.com/privacy" target="_blank">${i18n('privacy')}</a>
<span style="color: #CCC;">•</span>
<a href="#" class="show-credits">${i18n('credits')}</a>
</p>
<div class="social-links">
<a href="https://twitter.com/HeyPuter/" target="_blank">
<svg viewBox="0 0 24 24" aria-hidden="true" style="opacity: 0.7;"><g><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path></g></svg>
</a>
<a href="https://github.com/HeyPuter/" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48">
<g transform="translate(0, 0)">
<path fill-rule="evenodd" clip-rule="evenodd" fill="#5a606b" d="M24,0.6c-13.3,0-24,10.7-24,24c0,10.6,6.9,19.6,16.4,22.8 c1.2,0.2,1.6-0.5,1.6-1.2c0-0.6,0-2.1,0-4.1c-6.7,1.5-8.1-3.2-8.1-3.2c-1.1-2.8-2.7-3.5-2.7-3.5c-2.2-1.5,0.2-1.5,0.2-1.5 c2.4,0.2,3.7,2.5,3.7,2.5c2.1,3.7,5.6,2.6,7,2c0.2-1.6,0.8-2.6,1.5-3.2c-5.3-0.6-10.9-2.7-10.9-11.9c0-2.6,0.9-4.8,2.5-6.4 c-0.2-0.6-1.1-3,0.2-6.4c0,0,2-0.6,6.6,2.5c1.9-0.5,4-0.8,6-0.8c2,0,4.1,0.3,6,0.8c4.6-3.1,6.6-2.5,6.6-2.5c1.3,3.3,0.5,5.7,0.2,6.4 c1.5,1.7,2.5,3.8,2.5,6.4c0,9.2-5.6,11.2-11,11.8c0.9,0.7,1.6,2.2,1.6,4.4c0,3.2,0,5.8,0,6.6c0,0.6,0.4,1.4,1.7,1.2 C41.1,44.2,48,35.2,48,24.6C48,11.3,37.3,0.6,24,0.6z">
</path>
</g>
</svg>
</a>
<a href="https://discord.gg/PQcx7Teh8u" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48"><g transform="translate(0, 0)"><path d="M19.837,20.3a2.562,2.562,0,0,0,0,5.106,2.562,2.562,0,0,0,0-5.106Zm8.4,0a2.562,2.562,0,1,0,2.346,2.553A2.45,2.45,0,0,0,28.232,20.3Z" fill="#444444" data-color="color-2"></path> <path d="M39.41,1H8.59A4.854,4.854,0,0,0,4,6V37a4.482,4.482,0,0,0,4.59,4.572H34.672l-1.219-4.255L36.4,40.054,39.18,42.63,44,47V6A4.854,4.854,0,0,0,39.41,1ZM30.532,31.038s-.828-.989-1.518-1.863a7.258,7.258,0,0,0,4.163-2.737A13.162,13.162,0,0,1,30.532,27.8a15.138,15.138,0,0,1-3.335.989,16.112,16.112,0,0,1-5.957-.023,19.307,19.307,0,0,1-3.381-.989,13.112,13.112,0,0,1-2.622-1.357,7.153,7.153,0,0,0,4.025,2.714c-.69.874-1.541,1.909-1.541,1.909-5.083-.161-7.015-3.5-7.015-3.5a30.8,30.8,0,0,1,3.312-13.409,11.374,11.374,0,0,1,6.463-2.415l.23.276a15.517,15.517,0,0,0-6.049,3.013s.506-.276,1.357-.667a17.272,17.272,0,0,1,5.221-1.449,2.266,2.266,0,0,1,.391-.046,19.461,19.461,0,0,1,4.646-.046A18.749,18.749,0,0,1,33.2,15.007a15.307,15.307,0,0,0-5.727-2.921l.322-.368a11.374,11.374,0,0,1,6.463,2.415A30.8,30.8,0,0,1,37.57,27.542S35.615,30.877,30.532,31.038Z" fill="#444444"></path></g></svg> </a>
<a href="https://www.linkedin.com/company/puter/" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48">
<g transform="translate(0, 0)">
<path fill="#5a606b" d="M46,0H2C0.9,0,0,0.9,0,2v44c0,1.1,0.9,2,2,2h44c1.1,0,2-0.9,2-2V2C48,0.9,47.1,0,46,0z M14.2,40.9H7.1V18 h7.1V40.9z M10.7,14.9c-2.3,0-4.1-1.8-4.1-4.1c0-2.3,1.8-4.1,4.1-4.1c2.3,0,4.1,1.8,4.1,4.1C14.8,13,13,14.9,10.7,14.9z M40.9,40.9 h-7.1V29.8c0-2.7,0-6.1-3.7-6.1c-3.7,0-4.3,2.9-4.3,5.9v11.3h-7.1V18h6.8v3.1h0.1c0.9-1.8,3.3-3.7,6.7-3.7c7.2,0,8.5,4.7,8.5,10.9 V40.9z">
</path>
</g>
</svg>
</a>
</div>
</div>
<div class="version"></div>
<dialog class="credits">
<div class="credit-content">
<p style="margin: 0; font-size: 18px; text-align: center;">${i18n('oss_code_and_content')}</p>
<div style="max-height: 300px; overflow-y: scroll;">
<ul style="padding-left: 25px; padding-top:15px;">
<li>FileSaver.js <a target="_blank" href="https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md">${i18n('license')}</a></li>
<li>html-entities <a target="_blank" href="https://github.com/mdevils/html-entities/blob/master/LICENSE">${i18n('license')}</a></li>
<li>iro.js <a target="_blank" href="https://github.com/jaames/iro.js/blob/master/LICENSE.txt">${i18n('license')}</a></li>
<li>jQuery <a target="_blank" href="https://jquery.org/license/">${i18n('license')}</a></li>
<li>jQuery-dragster <a target="_blank" href="https://github.com/catmanjan/jquery-dragster/blob/master/LICENSE">${i18n('license')}</a></li>
<li>jQuery-menu-aim <a target="_blank" href="https://github.com/kamens/jQuery-menu-aim?tab=readme-ov-file#faq">${i18n('license')}</a></li>
<li>jQuery UI <a target="_blank" href="https://jquery.org/license/">${i18n('license')}</a></li>
<li>lodash <a target="_blank" href="https://lodash.com/license">${i18n('license')}</a></li>
<li>mime <a target="_blank" href="https://github.com/broofa/mime/blob/main/LICENSE">${i18n('license')}</a></li>
<li>qrcodejs <a target="_blank" href="https://github.com/davidshimjs/qrcodejs/blob/master/LICENSE">${i18n('license')}</a></li>
<li>Selection <a target="_blank" href="https://github.com/simonwep/selection/blob/master/LICENSE">${i18n('license')}</a></li>
<li>socket.io <a target="_blank" href="https://github.com/socketio/socket.io/blob/main/LICENSE">${i18n('license')}</a></li>
<li>Wallpaper by <a target="_blank" href="https://unsplash.com/@fakurian?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Milad Fakurian</a> on <a target="_blank" href="https://unsplash.com/photos/blue-orange-and-yellow-wallpaper-E8Ufcyxz514?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a></li>
</ul>
export default {
id: 'about',
title_i18n_key: 'about',
icon: 'logo-outline.svg',
html: () => {
return `
<div class="about-container">
<div class="about">
<a href="https://puter.com" target="_blank" class="logo"><img src="/images/logo.png"></a>
<p class="description">${i18n('puter_description')}</p>
<p class="links">
<a href="mailto:hey@puter.com" target="_blank">[email protected]</a>
<span style="color: #CCC;">•</span>
<a href="https://docs.puter.com" target="_blank">${i18n('developers')}</a>
<span style="color: #CCC;">•</span>
<a href="https://status.puter.com" target="_blank">${i18n('status')}</a>
<span style="color: #CCC;">•</span>
<a href="https://puter.com/terms" target="_blank">${i18n('terms')}</a>
<span style="color: #CCC;">•</span>
<a href="https://puter.com/privacy" target="_blank">${i18n('privacy')}</a>
<span style="color: #CCC;">•</span>
<a href="#" class="show-credits">${i18n('credits')}</a>
</p>
<div class="social-links">
<a href="https://twitter.com/HeyPuter/" target="_blank">
<svg viewBox="0 0 24 24" aria-hidden="true" style="opacity: 0.7;"><g><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path></g></svg>
</a>
<a href="https://github.com/HeyPuter/" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48">
<g transform="translate(0, 0)">
<path fill-rule="evenodd" clip-rule="evenodd" fill="#5a606b" d="M24,0.6c-13.3,0-24,10.7-24,24c0,10.6,6.9,19.6,16.4,22.8 c1.2,0.2,1.6-0.5,1.6-1.2c0-0.6,0-2.1,0-4.1c-6.7,1.5-8.1-3.2-8.1-3.2c-1.1-2.8-2.7-3.5-2.7-3.5c-2.2-1.5,0.2-1.5,0.2-1.5 c2.4,0.2,3.7,2.5,3.7,2.5c2.1,3.7,5.6,2.6,7,2c0.2-1.6,0.8-2.6,1.5-3.2c-5.3-0.6-10.9-2.7-10.9-11.9c0-2.6,0.9-4.8,2.5-6.4 c-0.2-0.6-1.1-3,0.2-6.4c0,0,2-0.6,6.6,2.5c1.9-0.5,4-0.8,6-0.8c2,0,4.1,0.3,6,0.8c4.6-3.1,6.6-2.5,6.6-2.5c1.3,3.3,0.5,5.7,0.2,6.4 c1.5,1.7,2.5,3.8,2.5,6.4c0,9.2-5.6,11.2-11,11.8c0.9,0.7,1.6,2.2,1.6,4.4c0,3.2,0,5.8,0,6.6c0,0.6,0.4,1.4,1.7,1.2 C41.1,44.2,48,35.2,48,24.6C48,11.3,37.3,0.6,24,0.6z">
</path>
</g>
</svg>
</a>
<a href="https://discord.gg/PQcx7Teh8u" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48"><g transform="translate(0, 0)"><path d="M19.837,20.3a2.562,2.562,0,0,0,0,5.106,2.562,2.562,0,0,0,0-5.106Zm8.4,0a2.562,2.562,0,1,0,2.346,2.553A2.45,2.45,0,0,0,28.232,20.3Z" fill="#444444" data-color="color-2"></path> <path d="M39.41,1H8.59A4.854,4.854,0,0,0,4,6V37a4.482,4.482,0,0,0,4.59,4.572H34.672l-1.219-4.255L36.4,40.054,39.18,42.63,44,47V6A4.854,4.854,0,0,0,39.41,1ZM30.532,31.038s-.828-.989-1.518-1.863a7.258,7.258,0,0,0,4.163-2.737A13.162,13.162,0,0,1,30.532,27.8a15.138,15.138,0,0,1-3.335.989,16.112,16.112,0,0,1-5.957-.023,19.307,19.307,0,0,1-3.381-.989,13.112,13.112,0,0,1-2.622-1.357,7.153,7.153,0,0,0,4.025,2.714c-.69.874-1.541,1.909-1.541,1.909-5.083-.161-7.015-3.5-7.015-3.5a30.8,30.8,0,0,1,3.312-13.409,11.374,11.374,0,0,1,6.463-2.415l.23.276a15.517,15.517,0,0,0-6.049,3.013s.506-.276,1.357-.667a17.272,17.272,0,0,1,5.221-1.449,2.266,2.266,0,0,1,.391-.046,19.461,19.461,0,0,1,4.646-.046A18.749,18.749,0,0,1,33.2,15.007a15.307,15.307,0,0,0-5.727-2.921l.322-.368a11.374,11.374,0,0,1,6.463,2.415A30.8,30.8,0,0,1,37.57,27.542S35.615,30.877,30.532,31.038Z" fill="#444444"></path></g></svg> </a>
<a href="https://www.linkedin.com/company/puter/" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48">
<g transform="translate(0, 0)">
<path fill="#5a606b" d="M46,0H2C0.9,0,0,0.9,0,2v44c0,1.1,0.9,2,2,2h44c1.1,0,2-0.9,2-2V2C48,0.9,47.1,0,46,0z M14.2,40.9H7.1V18 h7.1V40.9z M10.7,14.9c-2.3,0-4.1-1.8-4.1-4.1c0-2.3,1.8-4.1,4.1-4.1c2.3,0,4.1,1.8,4.1,4.1C14.8,13,13,14.9,10.7,14.9z M40.9,40.9 h-7.1V29.8c0-2.7,0-6.1-3.7-6.1c-3.7,0-4.3,2.9-4.3,5.9v11.3h-7.1V18h6.8v3.1h0.1c0.9-1.8,3.3-3.7,6.7-3.7c7.2,0,8.5,4.7,8.5,10.9 V40.9z">
</path>
</g>
</svg>
</a>
</div>
</div>
</dialog>
`;
h += `</div>`;
h += `</div>`;
<div class="version"></div>
<dialog class="credits">
<div class="credit-content">
<p style="margin: 0; font-size: 18px; text-align: center;">${i18n('oss_code_and_content')}</p>
<div style="max-height: 300px; overflow-y: scroll;">
<ul style="padding-left: 25px; padding-top:15px;">
<li>FileSaver.js <a target="_blank" href="https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md">${i18n('license')}</a></li>
<li>html-entities <a target="_blank" href="https://github.com/mdevils/html-entities/blob/master/LICENSE">${i18n('license')}</a></li>
<li>iro.js <a target="_blank" href="https://github.com/jaames/iro.js/blob/master/LICENSE.txt">${i18n('license')}</a></li>
<li>jQuery <a target="_blank" href="https://jquery.org/license/">${i18n('license')}</a></li>
<li>jQuery-dragster <a target="_blank" href="https://github.com/catmanjan/jquery-dragster/blob/master/LICENSE">${i18n('license')}</a></li>
<li>jQuery-menu-aim <a target="_blank" href="https://github.com/kamens/jQuery-menu-aim?tab=readme-ov-file#faq">${i18n('license')}</a></li>
<li>jQuery UI <a target="_blank" href="https://jquery.org/license/">${i18n('license')}</a></li>
<li>lodash <a target="_blank" href="https://lodash.com/license">${i18n('license')}</a></li>
<li>mime <a target="_blank" href="https://github.com/broofa/mime/blob/main/LICENSE">${i18n('license')}</a></li>
<li>qrcodejs <a target="_blank" href="https://github.com/davidshimjs/qrcodejs/blob/master/LICENSE">${i18n('license')}</a></li>
<li>Selection <a target="_blank" href="https://github.com/simonwep/selection/blob/master/LICENSE">${i18n('license')}</a></li>
<li>socket.io <a target="_blank" href="https://github.com/socketio/socket.io/blob/main/LICENSE">${i18n('license')}</a></li>
<li>Wallpaper by <a target="_blank" href="https://unsplash.com/@fakurian?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Milad Fakurian</a> on <a target="_blank" href="https://unsplash.com/photos/blue-orange-and-yellow-wallpaper-E8Ufcyxz514?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a></li>
</ul>
</div>
</div>
</dialog>
</div>`;
},
init: ($el_window) => {
// server and version infomration
puter.os.version()
.then(res => {
const deployed_date = new Date(res.deploy_timestamp).toLocaleString();
$el_window.find('.version').html(`Version: ${res.version} &bull; Server: ${res.location} &bull; Deployed: ${deployed_date}`);
})
.catch(error => {
console.error("Failed to fetch server info:", error);
$el_window.find('.version').html("Failed to load version information.");
});
return h;
}
export default UITabAbout;
$el_window.find('.credits').on('click', function (e) {
if($(e.target).hasClass('credits')){
$('.credits').get(0).close();
}
});
$el_window.find('.show-credits').on('click', function (e) {
$('.credits').get(0).showModal();
})
},
};
+137
View File
@@ -0,0 +1,137 @@
/**
* Copyright (C) 2024 Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter 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 <https://www.gnu.org/licenses/>.
*/
import UIWindowChangePassword from '../UIWindowChangePassword.js';
import UIWindowChangeEmail from './UIWindowChangeEmail.js';
import UIWindowChangeUsername from '../UIWindowChangeUsername.js';
import UIWindowConfirmUserDeletion from './UIWindowConfirmUserDeletion.js';
import UIWindowManageSessions from '../UIWindowManageSessions.js';
// About
export default {
id: 'account',
title_i18n_key: 'account',
icon: 'user.svg',
html: () => {
let h = `<h1>${i18n('account')}</h1>`;
// change password button
if(!user.is_temp){
h += `<div class="settings-card">`;
h += `<strong>${i18n('password')}</strong>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button change-password" style="float:right;">${i18n('change_password')}</button>`;
h += `</div>`;
h += `</div>`;
}
// change username button
h += `<div class="settings-card">`;
h += `<div>`;
h += `<strong style="display:block;">${i18n('username')}</strong>`;
h += `<span class="username" style="display:block; margin-top:5px;">${user.username}</span>`;
h += `</div>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button change-username" style="float:right;">${i18n('change_username')}</button>`;
h += `</div>`
h += `</div>`;
// change email button
if(user.email){
h += `<div class="settings-card">`;
h += `<div>`;
h += `<strong style="display:block;">${i18n('email')}</strong>`;
h += `<span class="user-email" style="display:block; margin-top:5px;">${user.email}</span>`;
h += `</div>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button change-email" style="margin-bottom: 10px; float:right;">${i18n('change_email')}</button>`;
h += `</div>`;
h += `</div>`;
}
// session manager
h += `<div class="settings-card">`;
h += `<strong>${i18n('sessions')}</strong>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button manage-sessions" style="float:right;">${i18n('manage_sessions')}</button>`;
h += `</div>`;
h += `</div>`;
// 'Delete Account' button
h += `<div class="settings-card settings-card-danger">`;
h += `<strong style="display: inline-block;">${i18n("delete_account")}</strong>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button button-danger delete-account" style="float:right;">${i18n("delete_account")}</button>`;
h += `</div>`;
h += `</div>`;
return h;
},
init: ($el_window) => {
$el_window.find('.change-password').on('click', function (e) {
UIWindowChangePassword({
window_options:{
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
});
});
$el_window.find('.change-username').on('click', function (e) {
UIWindowChangeUsername({
window_options:{
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
});
});
$el_window.find('.change-email').on('click', function (e) {
console.log('change email', $el_window.attr('data-element_uuid'));
UIWindowChangeEmail({
window_options:{
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
});
});
$el_window.find('.manage-sessions').on('click', function (e) {
UIWindowManageSessions({
window_options:{
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
});
});
$el_window.find('.delete-account').on('click', function (e) {
UIWindowConfirmUserDeletion({
window_options:{
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
});
});
},
};
+45
View File
@@ -0,0 +1,45 @@
/**
* Copyright (C) 2024 Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter 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 <https://www.gnu.org/licenses/>.
*/
import UIWindowThemeDialog from '../UIWindowThemeDialog.js';
// About
export default {
id: 'clock',
title_i18n_key: 'clock',
icon: 'clock.svg',
html: () => {
return `
<h1>${i18n('clock')}</h1>
<div style="display: flex;align-items: center">
<span>${i18n('visibility')}:</span>
<select class="change-clock-visible" style="margin-left: 10px;flex: 1">
<option value="auto">${i18n('clock_visible_auto')}</option>
<option value="hide">${i18n('clock_visible_hide')}</option>
<option value="show">${i18n('clock_visible_show')}</option>
</select>
</div>`;
},
init: ($el_window) => {
$el_window.on('change', 'select.change-clock-visible', function(e){
window.change_clock_visible(this.value);
});
window.change_clock_visible();
},
};
+83
View File
@@ -0,0 +1,83 @@
/**
* Copyright (C) 2024 Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter 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 <https://www.gnu.org/licenses/>.
*/
import UIWindowThemeDialog from '../UIWindowThemeDialog.js';
import changeLanguage from '../../i18n/i18nChangeLanguage.js';
// About
export default {
id: 'language',
title_i18n_key: 'language',
icon: 'language.svg',
html: () => {
let h = `<h1>${i18n('language')}</h1>`;
// search
h += `<div class="search-container" style="margin-bottom: 10px;">
<input type="text" class="search search-language" placeholder="Search">
</div>`;
// list of languages
const available_languages = listSupportedLanguages();
h += `<div class="language-list">`;
for (let lang of available_languages) {
h += `<div class="language-item ${window.locale === lang.code ? 'active': ''}" data-lang="${lang.code}" data-english-name="${html_encode(lang.english_name)}">${lang.name}</div>`;
}
h += `</div>`;
return h;
},
init: ($el_window) => {
$el_window.on('click', '.language-item', function(){
const $this = $(this);
const lang = $this.attr('data-lang');
changeLanguage(lang);
$this.siblings().removeClass('active');
$this.addClass('active');
// make sure all other language items are visible
$this.closest('.language-list').find('.language-item').show();
});
$el_window.on('input', '.search-language', function(){
const $this = $(this);
const search = $this.val().toLowerCase();
const $container = $this.closest('.settings').find('.settings-content-container');
const $content = $container.find('.settings-content.active');
const $list = $content.find('.language-list');
const $items = $list.find('.language-item');
$items.each(function(){
const $item = $(this);
const lang = $item.attr('data-lang');
const name = $item.text().toLowerCase();
const english_name = $item.attr('data-english-name').toLowerCase();
if(name.includes(search) || lang.includes(search) || english_name.includes(search)){
$item.show();
}else{
$item.hide();
}
})
});
},
on_show: ($content) => {
// Focus on search
$content.find('.search').first().focus();
// make sure all language items are visible
$content.find('.language-item').show();
// empty search
$content.find('.search').val('');
},
};
+63
View File
@@ -0,0 +1,63 @@
/**
* Copyright (C) 2024 Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter 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 <https://www.gnu.org/licenses/>.
*/
import UIWindowThemeDialog from '../UIWindowThemeDialog.js';
import UIWindowDesktopBGSettings from '../UIWindowDesktopBGSettings.js';
// About
export default {
id: 'personalization',
title_i18n_key: 'personalization',
icon: 'palette-outline.svg',
html: () => {
return `
<h1>${i18n('personalization')}</h1>
<div class="settings-card">
<strong>${i18n('ui_colors')}</strong>
<div style="flex-grow:1;">
<button class="button change-ui-colors" style="float:right;">${i18n('change_ui_colors')}</button>
</div>
</div>
<div class="settings-card">
<strong>${i18n('background')}</strong>
<div style="flex-grow:1;">
<button class="button change-background" style="float:right;">${i18n('change_desktop_background')}</button>
</div>
</div>`;
},
init: ($el_window) => {
$el_window.find('.change-ui-colors').on('click', function (e) {
UIWindowThemeDialog({
window_options:{
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
});
});
$el_window.find('.change-background').on('click', function (e) {
UIWindowDesktopBGSettings({
window_options:{
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
});
});
},
};
+141
View File
@@ -0,0 +1,141 @@
/**
* Copyright (C) 2024 Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter 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 <https://www.gnu.org/licenses/>.
*/
// About
export default {
id: 'usage',
title_i18n_key: 'usage',
icon: 'speedometer-outline.svg',
html: () => {
return `
<h1>Usage</h1>
<div class="driver-usage">
<h3 style="margin-bottom: 5px; font-size: 14px;">${i18n('storage_usage')}</h3>
<div style="font-size: 13px; margin-bottom: 3px;">
<span id="storage-used"></span>
<span> used of </span>
<span id="storage-capacity"></span>
<span id="storage-puter-used-w" style="display:none;">&nbsp;(<span id="storage-puter-used"></span> ${i18n('storage_puter_used')})</span>
</div>
<div id="storage-bar-wrapper">
<span id="storage-used-percent"></span>
<div id="storage-bar"></div>
<div id="storage-bar-host"></div>
</div>
</div>`;
},
init: ($el_window) => {
$.ajax({
url: api_origin + "/drivers/usage",
type: 'GET',
async: true,
contentType: "application/json",
headers: {
"Authorization": "Bearer " + auth_token
},
statusCode: {
401: function () {
logout();
},
},
success: function (res) {
let h = ''; // Initialize HTML string for driver usage bars
// Loop through user services
res.user.forEach(service => {
const { monthly_limit, monthly_usage } = service;
let usageDisplay = ``;
if (monthly_limit !== null) {
let usage_percentage = (monthly_usage / monthly_limit * 100).toFixed(0);
usage_percentage = usage_percentage > 100 ? 100 : usage_percentage; // Cap at 100%
usageDisplay = `
<div class="driver-usage" style="margin-bottom: 10px;">
<h3 style="margin-bottom: 5px; font-size: 14px;">${service.service['driver.interface']} (${service.service['driver.method']}):</h3>
<span style="font-size: 13px; margin-bottom: 3px;">${monthly_usage} used of ${monthly_limit}</span>
<div class="usage-progbar-wrapper" style="width: 100%;">
<div class="usage-progbar" style="width: ${usage_percentage}%;"><span class="usage-progbar-percent">${usage_percentage}%</span></div>
</div>
</div>
`;
}
else {
usageDisplay = `
<div class="driver-usage" style="margin-bottom: 10px;">
<h3 style="margin-bottom: 5px; font-size: 14px;">${service.service['driver.interface']} (${service.service['driver.method']}):</h3>
<span style="font-size: 13px; margin-bottom: 3px;">${i18n('usage')}: ${monthly_usage} (${i18n('unlimited')})</span>
</div>
`;
}
h += usageDisplay;
});
// Append driver usage bars to the container
$('.settings-content[data-settings="usage"]').append(`<div class="driver-usage-container">${h}</div>`);
}
});
// df
$.ajax({
url: api_origin + "/df",
type: 'GET',
async: true,
contentType: "application/json",
headers: {
"Authorization": "Bearer " + auth_token
},
statusCode: {
401: function () {
logout();
},
},
success: function (res) {
let usage_percentage = (res.used / res.capacity * 100).toFixed(0);
usage_percentage = usage_percentage > 100 ? 100 : usage_percentage;
let general_used = res.used;
let host_usage_percentage = 0;
if ( res.host_used ) {
$('#storage-puter-used').html(byte_format(res.used));
$('#storage-puter-used-w').show();
general_used = res.host_used;
host_usage_percentage = ((res.host_used - res.used) / res.capacity * 100).toFixed(0);
}
$('#storage-used').html(byte_format(general_used));
$('#storage-capacity').html(byte_format(res.capacity));
$('#storage-used-percent').html(
usage_percentage + '%' +
(host_usage_percentage > 0
? ' / ' + host_usage_percentage + '%' : '')
);
$('#storage-bar').css('width', usage_percentage + '%');
$('#storage-bar-host').css('width', host_usage_percentage + '%');
if (usage_percentage >= 100) {
$('#storage-bar').css({
'border-top-right-radius': '3px',
'border-bottom-right-radius': '3px',
});
}
}
});
},
};
+30 -356
View File
@@ -23,7 +23,12 @@ import UIWindowChangeEmail from './UIWindowChangeEmail.js'
import UIWindowChangeUsername from '../UIWindowChangeUsername.js'
import changeLanguage from "../../i18n/i18nChangeLanguage.js"
import UIWindowConfirmUserDeletion from './UIWindowConfirmUserDeletion.js';
import UITabAbout from './UITabAbout.js';
import AboutTab from './UITabAbout.js';
import UsageTab from './UITabUsage.js';
import AccountTab from './UITabAccount.js';
import PersonalizationTab from './UITabPersonalization.js';
import LanguageTab from './UITabLanguage.js';
import ClockTab from './UITabClock.js';
import UIWindowThemeDialog from '../UIWindowThemeDialog.js';
import UIWindowManageSessions from '../UIWindowManageSessions.js';
@@ -31,138 +36,34 @@ async function UIWindowSettings(options){
return new Promise(async (resolve) => {
options = options ?? {};
const tabs = [
AboutTab,
UsageTab,
AccountTab,
PersonalizationTab,
LanguageTab,
ClockTab,
];
let h = '';
h += `<div class="settings-container">`;
h += `<div class="settings">`;
// side bar
h += `<div class="settings-sidebar disable-user-select">`;
h += `<div class="settings-sidebar-item disable-user-select active" data-settings="about" style="background-image: url(${icons['logo-outline.svg']});">${i18n('about')}</div>`;
h += `<div class="settings-sidebar-item disable-user-select" data-settings="usage" style="background-image: url(${icons['speedometer-outline.svg']});">${i18n('usage')}</div>`;
h += `<div class="settings-sidebar-item disable-user-select" data-settings="account" style="background-image: url(${icons['user.svg']});">${i18n('account')}</div>`;
h += `<div class="settings-sidebar-item disable-user-select" data-settings="personalization" style="background-image: url(${icons['palette-outline.svg']});">${i18n('personalization')}</div>`;
h += `<div class="settings-sidebar-item disable-user-select" data-settings="language" style="background-image: url(${icons['language.svg']});">${i18n('language')}</div>`;
h += `<div class="settings-sidebar-item disable-user-select" data-settings="clock" style="background-image: url(${icons['clock.svg']});">${i18n('clock')}</div>`;
tabs.forEach((tab, i) => {
h += `<div class="settings-sidebar-item disable-user-select ${i === 0 ? 'active' : ''}" data-settings="${tab.id}" style="background-image: url(${icons[tab.icon]});">${i18n(tab.title_i18n_key)}</div>`;
});
h += `</div>`;
// content
h += `<div class="settings-content-container">`;
// About
h += UITabAbout();
// Usage
h += `<div class="settings-content" data-settings="usage">`;
h += `<h1>Usage</h1>`;
h += `<div class="driver-usage">
<h3 style="margin-bottom: 5px; font-size: 14px;">${i18n('storage_usage')}</h3>
<div style="font-size: 13px; margin-bottom: 3px;">
<span id="storage-used"></span>
<span> used of </span>
<span id="storage-capacity"></span>
<span id="storage-puter-used-w" style="display:none;">&nbsp;(<span id="storage-puter-used"></span> ${i18n('storage_puter_used')})</span>
</div>
<div id="storage-bar-wrapper">
<span id="storage-used-percent"></span>
<div id="storage-bar"></div>
<div id="storage-bar-host"></div>
</div>
</div>`
h += `</div>`;
// Account
h += `<div class="settings-content" data-settings="account">`;
h += `<h1>${i18n('account')}</h1>`;
// change password button
h += `<div class="settings-card">`;
h += `<strong>${i18n('password')}</strong>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button change-password" style="float:right;">${i18n('change_password')}</button>`;
h += `</div>`;
h += `</div>`;
// change username button
h += `<div class="settings-card">`;
h += `<div>`;
h += `<strong style="display:block;">${i18n('username')}</strong>`;
h += `<span class="username" style="display:block; margin-top:5px;">${user.username}</span>`;
h += `</div>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button change-username" style="float:right;">${i18n('change_username')}</button>`;
h += `</div>`
h += `</div>`;
// change email button
if(user.email){
h += `<div class="settings-card">`;
h += `<div>`;
h += `<strong style="display:block;">${i18n('email')}</strong>`;
h += `<span class="user-email" style="display:block; margin-top:5px;">${user.email}</span>`;
h += `</div>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button change-email" style="margin-bottom: 10px; float:right;">${i18n('change_email')}</button>`;
h += `</div>`;
h += `</div>`;
}
// session manager
h += `<div class="settings-card">`;
h += `<strong>${i18n('sessions')}</strong>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button manage-sessions" style="float:right;">${i18n('manage_sessions')}</button>`;
h += `</div>`;
h += `</div>`;
// 'Delete Account' button
h += `<div class="settings-card settings-card-danger">`;
h += `<strong style="display: inline-block;">${i18n("delete_account")}</strong>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button button-danger delete-account" style="float:right;">${i18n("delete_account")}</button>`;
h += `</div>`;
h += `</div>`;
h += `</div>`;
// Personalization
h += `<div class="settings-content" data-settings="personalization">`;
h += `<h1>${i18n('personalization')}</h1>`;
// change password button
h += `<div class="settings-card">`;
h += `<strong>${i18n('ui_colors')}</strong>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button change-ui-colors" style="float:right;">${i18n('change_ui_colors')}</button>`;
h += `</div>`;
h += `</div>`;
h += `</div>`;
// Language
h += `<div class="settings-content" data-settings="language">`;
h += `<h1>${i18n('language')}</h1>`;
// search
h += `<div class="search-container" style="margin-bottom: 10px;">`;
h += `<input type="text" class="search" placeholder="Search">`;
h += `</div>`;
// list of languages
const available_languages = listSupportedLanguages();
h += `<div class="language-list">`;
for (let lang of available_languages) {
h += `<div class="language-item ${window.locale === lang.code ? 'active': ''}" data-lang="${lang.code}" data-english-name="${html_encode(lang.english_name)}">${lang.name}</div>`;
}
h += `</div>`;
h += `</div>`;
// Clock
h += `<div class="settings-content" data-settings="clock">`;
h += `<h1>${i18n('clock')}</h1>`;
h += `<div style="display: flex;align-items: center">`
h += `<span>${i18n('visibility')}:</span>`
h += `<select class="change-clock-visible" style="margin-left: 10px;flex: 1">`
h += `<option value="auto">${i18n('clock_visible_auto')}</option>`
h += `<option value="hide">${i18n('clock_visible_hide')}</option>`
h += `<option value="show">${i18n('clock_visible_show')}</option>`
h += `</select>`
h += `</div>`
h += `</div>`;
tabs.forEach((tab, i) => {
h += `<div class="settings-content ${i === 0 ? 'active' : ''}" data-settings="${tab.id}">
${tab.html()}
</div>`;
});
h += `</div>`;
h += `</div>`;
@@ -202,194 +103,8 @@ async function UIWindowSettings(options){
overflow: 'auto'
}
});
$.ajax({
url: api_origin + "/drivers/usage",
type: 'GET',
async: true,
contentType: "application/json",
headers: {
"Authorization": "Bearer " + auth_token
},
statusCode: {
401: function () {
logout();
},
},
success: function (res) {
let h = ''; // Initialize HTML string for driver usage bars
// Loop through user services
res.user.forEach(service => {
const { monthly_limit, monthly_usage } = service;
let usageDisplay = ``;
if (monthly_limit !== null) {
let usage_percentage = (monthly_usage / monthly_limit * 100).toFixed(0);
usage_percentage = usage_percentage > 100 ? 100 : usage_percentage; // Cap at 100%
usageDisplay = `
<div class="driver-usage" style="margin-bottom: 10px;">
<h3 style="margin-bottom: 5px; font-size: 14px;">${service.service['driver.interface']} (${service.service['driver.method']}):</h3>
<span style="font-size: 13px; margin-bottom: 3px;">${monthly_usage} used of ${monthly_limit}</span>
<div class="usage-progbar-wrapper" style="width: 100%;">
<div class="usage-progbar" style="width: ${usage_percentage}%;"><span class="usage-progbar-percent">${usage_percentage}%</span></div>
</div>
</div>
`;
}
else {
usageDisplay = `
<div class="driver-usage" style="margin-bottom: 10px;">
<h3 style="margin-bottom: 5px; font-size: 14px;">${service.service['driver.interface']} (${service.service['driver.method']}):</h3>
<span style="font-size: 13px; margin-bottom: 3px;">${i18n('usage')}: ${monthly_usage} (${i18n('unlimited')})</span>
</div>
`;
}
h += usageDisplay;
});
// Append driver usage bars to the container
$('.settings-content[data-settings="usage"]').append(`<div class="driver-usage-container">${h}</div>`);
}
})
// df
$.ajax({
url: api_origin + "/df",
type: 'GET',
async: true,
contentType: "application/json",
headers: {
"Authorization": "Bearer " + auth_token
},
statusCode: {
401: function () {
logout();
},
},
success: function (res) {
let usage_percentage = (res.used / res.capacity * 100).toFixed(0);
usage_percentage = usage_percentage > 100 ? 100 : usage_percentage;
let general_used = res.used;
let host_usage_percentage = 0;
if ( res.host_used ) {
$('#storage-puter-used').html(byte_format(res.used));
$('#storage-puter-used-w').show();
general_used = res.host_used;
host_usage_percentage = ((res.host_used - res.used) / res.capacity * 100).toFixed(0);
}
$('#storage-used').html(byte_format(general_used));
$('#storage-capacity').html(byte_format(res.capacity));
$('#storage-used-percent').html(
usage_percentage + '%' +
(host_usage_percentage > 0
? ' / ' + host_usage_percentage + '%' : '')
);
$('#storage-bar').css('width', usage_percentage + '%');
$('#storage-bar-host').css('width', host_usage_percentage + '%');
if (usage_percentage >= 100) {
$('#storage-bar').css({
'border-top-right-radius': '3px',
'border-bottom-right-radius': '3px',
});
}
}
})
// version
$.ajax({
url: api_origin + "/version",
type: 'GET',
async: true,
contentType: "application/json",
headers: {
"Authorization": "Bearer " + auth_token
},
statusCode: {
401: function () {
logout();
},
},
success: function (res) {
var d = new Date(0);
$('.version').html('Version: ' + res.version + ' &bull; ' + 'Server: ' + res.location + ' &bull; ' + 'Deployed: ' + new Date(res.deploy_timestamp));
}
})
$(el_window).find('.credits').on('click', function (e) {
if($(e.target).hasClass('credits')){
$('.credits').get(0).close();
}
});
$(el_window).find('.show-credits').on('click', function (e) {
$('.credits').get(0).showModal();
})
$(el_window).find('.change-password').on('click', function (e) {
UIWindowChangePassword({
window_options:{
parent_uuid: $(el_window).attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
});
})
$(el_window).find('.change-email').on('click', function (e) {
console.log('change email', $(el_window).attr('data-element_uuid'));
UIWindowChangeEmail({
window_options:{
parent_uuid: $(el_window).attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
});
})
$(el_window).find('.delete-account').on('click', function (e) {
UIWindowConfirmUserDeletion({
window_options:{
parent_uuid: $(el_window).attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
});
})
$(el_window).find('.change-username').on('click', function (e) {
UIWindowChangeUsername({
window_options:{
parent_uuid: $(el_window).attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
});
})
$(el_window).find('.change-ui-colors').on('click', function (e) {
UIWindowThemeDialog({
window_options:{
parent_uuid: $(el_window).attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
});
})
$(el_window).find('.manage-sessions').on('click', function (e) {
UIWindowManageSessions({
window_options:{
parent_uuid: $(el_window).attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
});
})
const $el_window = $(el_window);
tabs.forEach(tab => tab.init($el_window));
$(el_window).on('click', '.settings-sidebar-item', function(){
const $this = $(this);
@@ -402,55 +117,14 @@ async function UIWindowSettings(options){
// add active class to content
$container.find('.settings-content').removeClass('active');
$content.addClass('active');
// if language, focus on search
if(settings === 'language'){
$content.find('.search').first().focus();
// make sure all language items are visible
$content.find('.language-item').show();
// empty search
$content.find('.search').val('');
// Run on_show handlers
const tab = tabs.find((tab) => tab.id === settings);
if (tab.on_show) {
tab.on_show($content);
}
})
$(el_window).on('click', '.language-item', function(){
const $this = $(this);
const lang = $this.attr('data-lang');
changeLanguage(lang);
$this.siblings().removeClass('active');
$this.addClass('active');
// make sure all other language items are visible
$this.closest('.language-list').find('.language-item').show();
})
$(el_window).on('input', '.search', function(){
const $this = $(this);
const search = $this.val().toLowerCase();
const $container = $this.closest('.settings').find('.settings-content-container');
const $content = $container.find('.settings-content.active');
const $list = $content.find('.language-list');
const $items = $list.find('.language-item');
$items.each(function(){
const $item = $(this);
const lang = $item.attr('data-lang');
const name = $item.text().toLowerCase();
const english_name = $item.attr('data-english-name').toLowerCase();
if(name.includes(search) || lang.includes(search) || english_name.includes(search)){
$item.show();
}else{
$item.hide();
}
})
});
$(el_window).on('change', 'select.change-clock-visible', function(e){
const $this = $(this);
const value = $this.val();
window.change_clock_visible(value);
})
window.change_clock_visible();
resolve(el_window);
});
}
+38 -7
View File
@@ -119,17 +119,31 @@ function UIContextMenu(options){
else
y_pos = start_y;
// Show ContextMenu
$(contextMenu).delay(100).show(0)
// In the right position (the mouse)
.css({
$(contextMenu).css({
top: y_pos + "px",
left: x_pos + "px"
});
// Show ContextMenu
if ( options?.delay === false ) {
$(contextMenu).show(0);
} else {
$(contextMenu).delay(100).show(0);
}
// mark other context menus as inactive
$('.context-menu').not(contextMenu).removeClass('context-menu-active');
let cancel_options_ = null;
const fade_remove = () => {
$(`#context-menu-${menu_id}, .context-menu[data-element-id="${$(this).closest('.context-menu').attr('data-parent-id')}"]`).fadeOut(200, function(){
$(contextMenu).remove();
});
};
const remove = () => {
$(contextMenu).remove();
};
// An item is clicked
$(`#context-menu-${menu_id} > li:not(.context-menu-item-disabled)`).on('click', function (e) {
@@ -139,11 +153,13 @@ function UIContextMenu(options){
event.value = options.items[$(this).attr("data-action")]['val'] ?? undefined;
options.items[$(this).attr("data-action")].onClick(event);
}
// "action" - onClick without un-clonable pointer event
else if(options.items[$(this).attr("data-action")].action && typeof options.items[$(this).attr("data-action")].action === 'function'){
options.items[$(this).attr("data-action")].action();
}
// close menu and, if exists, its parent
if(!$(this).hasClass('context-menu-item-submenu')){
$(`#context-menu-${menu_id}, .context-menu[data-element-id="${$(this).closest('.context-menu').attr('data-parent-id')}"]`).fadeOut(200, function(){
$(contextMenu).remove();
});
fade_remove();
}
return false;
});
@@ -233,6 +249,7 @@ function UIContextMenu(options){
}
$(contextMenu).on("remove", function () {
if ( options.onClose ) options.onClose(cancel_options_);
// when removing, make parent scrollable again
if(options.parent_element){
$(options.parent_element).parent().removeClass('children-have-open-contextmenu');
@@ -248,7 +265,21 @@ function UIContextMenu(options){
e.preventDefault();
e.stopPropagation();
return false;
})
})
return {
cancel: (cancel_options) => {
cancel_options_ = cancel_options;
if ( cancel_options.fade === false ) {
remove();
} else {
fade_remove();
}
},
set onClose (fn) {
options.onClose = fn;
}
};
}
window.select_ctxmenu_item = function ($ctxmenu_item){
+21 -23
View File
@@ -172,6 +172,14 @@ async function UIWindow(options) {
// Window
let zindex = options.stay_on_top ? (99999999 + last_window_zindex + 1 + ' !important') : last_window_zindex;
let user_set_url_params = [];
if (options.params !== undefined) {
for (let key in options.params) {
user_set_url_params.push(key + "=" + options.params[key]);
}
user_set_url_params = '?'+ user_set_url_params.join('&');
}
h += `<div class="window window-active
${options.cover_page ? 'window-cover-page' : ''}
${options.uid !== undefined ? 'window-'+options.uid : ''}
@@ -206,6 +214,7 @@ async function UIWindow(options) {
data-sort_order ="${options.sort_order ?? 'asc'}"
data-multiselectable = "${options.selectable_body}"
data-update_window_url = "${options.update_window_url}"
data-user_set_url_params = "${html_encode(user_set_url_params)}"
data-initial_zindex = "${zindex}"
style=" z-index: ${zindex};
${options.width !== undefined ? 'width: ' + html_encode(options.width) +'; ':''}
@@ -256,6 +265,13 @@ async function UIWindow(options) {
h += `<div draggable="false" title="Desktop" class="window-sidebar-item disable-user-select ${options.path === window.desktop_path ? 'window-sidebar-item-active' : ''}" data-path="${html_encode(window.desktop_path)}"><img draggable="false" class="window-sidebar-item-icon" src="${html_encode(window.icons['folder-desktop.svg'])}">Desktop</div>`;
h += `<div draggable="false" title="Videos" class="window-sidebar-item disable-user-select ${options.path === window.videos_path ? 'window-sidebar-item-active' : ''}" data-path="${html_encode(window.videos_path)}"><img draggable="false" class="window-sidebar-item-icon" src="${html_encode(window.icons['folder-videos.svg'])}">Videos</div>`;
h += `</div>`;
}
// Menubar
{
h += `<div class="window-menubar">`;
h += `</div>`;
}
// Navbar
@@ -453,6 +469,9 @@ async function UIWindow(options) {
const el_openfiledialog_open_btn = document.querySelector(`#window-${win_id} .openfiledialog-open-btn`);
const el_directorypicker_select_btn = document.querySelector(`#window-${win_id} .directorypicker-select-btn`);
// disable menubar by default
$(el_window).find('.window-menubar').hide();
if(options.is_maximized){
// save original size and position
$(el_window).attr({
@@ -2773,7 +2792,6 @@ window.sidebar_item_droppable = (el_window)=>{
// closes a window
$.fn.close = async function(options) {
options = options || {};
console.log(options);
$(this).each(async function() {
const el_iframe = $(this).find('.window-app-iframe');
const app_uses_sdk = el_iframe.length > 0 && el_iframe.attr('data-appUsesSDK') === 'true';
@@ -2853,27 +2871,7 @@ $.fn.close = async function(options) {
$(`.window[data-parent_uuid="${window_uuid}"]`).close();
// notify other apps that we're closing
if (app_uses_sdk) {
// notify parent app, if we have one, that we're closing
const parent_id = this.dataset['parent_instance_id'];
const parent = $(`.window[data-element_uuid="${parent_id}"] .window-app-iframe`).get(0);
if (parent) {
parent.contentWindow.postMessage({
msg: 'appClosed',
appInstanceID: window_uuid,
}, '*');
}
// notify child apps, if we have them, that we're closing
const children = $(`.window[data-parent_instance_id="${window_uuid}"] .window-app-iframe`);
children.each((_, child) => {
child.contentWindow.postMessage({
msg: 'appClosed',
appInstanceID: window_uuid,
}, '*');
});
// TODO: Once other AppConnections exist, those will need notifying too.
}
window.report_app_closed(window_uuid);
// remove backdrop
$(this).closest('.window-backdrop').remove();
@@ -3154,7 +3152,7 @@ $.fn.focusWindow = function(event) {
//change window URL
const update_window_url = $(this).attr('data-update_window_url');
if(update_window_url === 'true' || update_window_url === null){
window.history.replaceState({window_id: $(this).attr('data-id')}, '', '/app/'+$(this).attr('data-app'));
window.history.replaceState({window_id: $(this).attr('data-id')}, '', '/app/'+$(this).attr('data-app')+$(this).attr('data-user_set_url_params'));
document.title = $(this).attr('data-name');
}
$(`.taskbar .taskbar-item[data-app="${$(this).attr('data-app')}"]`).addClass('taskbar-item-active');
+5 -2
View File
@@ -19,7 +19,9 @@
import UIWindow from './UIWindow.js'
async function UIWindowDesktopBGSettings(){
async function UIWindowDesktopBGSettings(options){
options = options ?? {};
return new Promise(async (resolve) => {
let h = '';
const original_background_css = $('body').attr('style');
@@ -103,7 +105,8 @@ async function UIWindowDesktopBGSettings(){
height: '100%',
'background-color': 'rgb(245 247 249)',
'backdrop-filter': 'blur(3px)',
}
},
...options.window_options,
})
if(window.desktop_bg_url !== undefined && window.desktop_bg_url !== null){
+12
View File
@@ -59,6 +59,8 @@ async function UIWindowLogin(options){
h += `<button class="login-btn button button-primary button-block button-normal">${i18n('log_in')}</button>`;
// password recovery
h += `<p style="text-align:center; margin-bottom: 0;"><span class="forgot-password-link">${i18n('forgot_pass_c2a')}</span></p>`;
// server and version info
h += `<div id="version-placeholder" class="version" style="text-align:center;"></div>`;
h += `</form>`;
h += `</div>`;
// create account link
@@ -68,6 +70,16 @@ async function UIWindowLogin(options){
h += `</div>`;
}
h += `</div>`;
// server and version infomration
puter.os.version()
.then(res => {
const deployed_date = new Date(res.deploy_timestamp).toLocaleString();
$("#version-placeholder").html(`Version: ${res.version} &bull; Server: ${res.location} &bull; Deployed: ${deployed_date}`);
})
.catch(() => {
$("#version-placeholder").html("Failed to load version or server information.");
});
const el_window = await UIWindow({
title: null,
+1 -1
View File
@@ -20,7 +20,7 @@ const UIWindowTaskManager = async function UIWindowTaskManager () {
has_head: true,
selectable_body: true,
draggable_body: false,
allow_context_menu: true,
allow_context_menu: false,
// allow_native_ctxmenu: true,
show_in_taskbar: true,
dominant: true,
+39 -1
View File
@@ -46,6 +46,11 @@
--taskbar-lightness: var(--primary-lightness);
--taskbar-alpha: calc(0.73 * var(--primary-alpha));
--taskbar-color: var(--primary-color);
--select-hue: 213.05;
--select-saturation: 74.22%;
--select-lightness: 55.88%;
--select-color: hsl(var(--select-hue), var(--select-saturation), var(--select-lightness));
}
html, body {
@@ -739,6 +744,34 @@ span.header-sort-icon img {
text-shadow: none;
}
.window-menubar {
display: flex;
box-sizing: border-box;
overflow: hidden;
border-bottom: 1px solid #e3e3e3;
background-color: #fafafa;
--scale: 2pt;
padding: 0 2pt;
}
.window-menubar-item {
padding: calc(1.4 * var(--scale)) 0;
font-size: calc(5 * var(--scale));
}
.window-menubar-item span {
display: inline-block;
padding: calc(1.6 * var(--scale)) calc(4 * var(--scale));
font-size: calc(5 * var(--scale));
border-radius: calc(1.5 * var(--scale));
}
.window-menubar-item:hover > span,
.window-menubar-item.active > span {
background-color: var(--select-color);
color: white;
}
.explorer-empty-message {
text-align: center;
margin-top: 20px;
@@ -1489,7 +1522,7 @@ span.header-sort-icon img {
}
.context-menu .context-menu-item-active {
background-color: rgb(59 134 226);
background-color: var(--select-color);
color: white;
border-radius: 4px;
}
@@ -3601,6 +3634,11 @@ label {
height: 12px;
}
.version#version-placeholder {
margin-top: 10px;
margin-bottom: 0;
}
.version:hover {
opacity: 1;
}
+65 -6
View File
@@ -1836,15 +1836,14 @@ window.launch_app = async (options)=>{
// Add options.args to URL
iframe_url.searchParams.append('puter.args', JSON.stringify(options.args ?? {}));
// ...and finally append urm_source=puter.com to the URL
iframe_url.searchParams.append('urm_source', 'puter.com');
console.log('backgrounded??', app_info.background);
// ...and finally append utm_source=puter.com to the URL
iframe_url.searchParams.append('utm_source', 'puter.com');
el_win = UIWindow({
element_uuid: uuid,
title: title,
iframe_url: iframe_url.href,
params: options.params ?? undefined,
icon: icon,
window_class: 'window-app',
update_window_url: true,
@@ -1895,10 +1894,18 @@ window.launch_app = async (options)=>{
(async () => {
const el = await el_win;
console.log('RESOV', el);
$(el).on('remove', () => {
const svc_process = globalThis.services.get('process');
svc_process.unregister(process.uuid);
// If it's a non-sdk app, report that it launched and closed.
// FIXME: This is awkward. Really, we want some way of knowing when it's launched and reporting that immediately instead.
const $app_iframe = $(el).find('.window-app-iframe');
if ($app_iframe.attr('data-appUsesSdk') !== 'true') {
window.report_app_launched(process.uuid, { uses_sdk: false });
// We also have to report an extra close event because the real one was sent already
window.report_app_closed(process.uuid);
}
});
process.references.el_win = el;
@@ -3512,4 +3519,56 @@ window.change_clock_visible = (clock_visible) => {
}
$('select.change-clock-visible').val(window.user_preferences.clock_visible);
}
}
// Finds the `.window` element for the given app instance ID
window.window_for_app_instance = (instance_id) => {
return $(`.window[data-element_uuid="${instance_id}"]`).get(0);
};
// Finds the `iframe` element for the given app instance ID
window.iframe_for_app_instance = (instance_id) => {
return $(window_for_app_instance(instance_id)).find('.window-app-iframe').get(0);
};
// Run any callbacks to say that the app has launched
window.report_app_launched = (instance_id, { uses_sdk = true }) => {
const child_launch_callback = window.child_launch_callbacks[instance_id];
if (child_launch_callback) {
const parent_iframe = iframe_for_app_instance(child_launch_callback.parent_instance_id);
// send confirmation to requester window
parent_iframe.contentWindow.postMessage({
msg: 'childAppLaunched',
original_msg_id: child_launch_callback.launch_msg_id,
child_instance_id: instance_id,
uses_sdk: uses_sdk,
}, '*');
delete window.child_launch_callbacks[instance_id];
}
};
// Run any callbacks to say that the app has closed
window.report_app_closed = (instance_id) => {
const el_window = window_for_app_instance(instance_id);
// notify parent app, if we have one, that we're closing
const parent_id = el_window.dataset['parent_instance_id'];
const parent = $(`.window[data-element_uuid="${parent_id}"] .window-app-iframe`).get(0);
if (parent) {
parent.contentWindow.postMessage({
msg: 'appClosed',
appInstanceID: instance_id,
}, '*');
}
// notify child apps, if we have them, that we're closing
const children = $(`.window[data-parent_instance_id="${instance_id}"] .window-app-iframe`);
children.each((_, child) => {
child.contentWindow.postMessage({
msg: 'appClosed',
appInstanceID: instance_id,
}, '*');
});
// TODO: Once other AppConnections exist, those will need notifying too.
};
+4
View File
@@ -0,0 +1,4 @@
- first line
- second line
- third line
- fourth line