mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-13 20:12:57 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec31007c4b | ||
|
|
e615c7ee7f | ||
|
|
35d4d4f755 | ||
|
|
195468c4e1 | ||
|
|
15f274e662 | ||
|
|
7cb898457c | ||
|
|
aa4050ee32 | ||
|
|
f392de722a | ||
|
|
07d95d5d3c | ||
|
|
22c61d8221 | ||
|
|
8fb0a66ef2 | ||
|
|
d5cd828cba | ||
|
|
0119ae36a8 | ||
|
|
87852d125f | ||
|
|
d3969b0122 | ||
|
|
3cba4cab1e | ||
|
|
ef35a04c4a | ||
|
|
874928e845 | ||
|
|
c3d1574f8c | ||
|
|
eee5c9e48b | ||
|
|
2f5780d1d1 | ||
|
|
fa60f66057 | ||
|
|
a741bd5419 | ||
|
|
4d2d4a7ca3 | ||
|
|
5c1e6ab16b | ||
|
|
f5f75dbef8 | ||
|
|
6577051d64 | ||
|
|
b2f5cc11b5 | ||
|
|
a1252f66cc | ||
|
|
e8d9e8b42b | ||
|
|
ced7265eb0 | ||
|
|
14098b2ff2 | ||
|
|
fb93a0863b | ||
|
|
4088bff216 | ||
|
|
915152d3b9 | ||
|
|
2ecea9a7b0 | ||
|
|
dc2a620b4e | ||
|
|
8648a303f1 | ||
|
|
6a65ca4ebd | ||
|
|
58b83c27f2 | ||
|
|
338d925bcc | ||
|
|
7e2c4a0a1c | ||
|
|
9822be5df4 | ||
|
|
cf5c62bcfc | ||
|
|
27553ef926 | ||
|
|
3f249fcc89 | ||
|
|
c3654ab148 | ||
|
|
0e9e8d41d9 | ||
|
|
2f49c1c9b0 | ||
|
|
072dbe8db5 | ||
|
|
da208e23f5 | ||
|
|
222a617c44 | ||
|
|
e355c77a4a | ||
|
|
639653dac2 | ||
|
|
0aa5543397 | ||
|
|
dfdda4f3b9 | ||
|
|
8cf58d7fc6 | ||
|
|
4783e3eae4 | ||
|
|
4931ad3960 | ||
|
|
5ad78a5ebb | ||
|
|
dc95f2e065 | ||
|
|
0361ceba6c | ||
|
|
4ecc7372f9 | ||
|
|
dbcd627815 | ||
|
|
6061c81c36 | ||
|
|
c1aee61da1 | ||
|
|
f0d3346ca7 | ||
|
|
e8ca6376be | ||
|
|
c48c134869 | ||
|
|
49e334521d | ||
|
|
04a641adc2 | ||
|
|
2210652643 | ||
|
|
8713db3d55 | ||
|
|
f2e8b5ee3e | ||
|
|
7e0c6c6470 | ||
|
|
19c49db538 | ||
|
|
ea40ac1e53 | ||
|
|
653898b463 | ||
|
|
c07d0a2bc7 | ||
|
|
9bb221a0d7 | ||
|
|
e4f2c4a28b | ||
|
|
3a4858b83c | ||
|
|
826ea36502 | ||
|
|
397819d45e | ||
|
|
66fecf88ef | ||
|
|
868202ba41 | ||
|
|
c6b0ed32bf | ||
|
|
17adef11a6 | ||
|
|
6d1c807879 | ||
|
|
0ed5b20627 | ||
|
|
f5175941d9 | ||
|
|
e2f048ec2d | ||
|
|
5d8d25f370 |
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
node_modules
|
||||
puter
|
||||
/puter
|
||||
@@ -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`)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## [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
@@ -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"]
|
||||
@@ -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/>
|
||||
|
||||
|
||||
@@ -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.
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "puter.com",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "puter.com",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"workspaces": [
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "puter.com",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.1",
|
||||
"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",
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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])));
|
||||
};
|
||||
@@ -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 ]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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_ });
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -155,7 +169,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 +388,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{
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-19
@@ -74,14 +74,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 +90,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);
|
||||
|
||||
@@ -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} • Server: ${res.location} • 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();
|
||||
})
|
||||
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
@@ -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('');
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -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;"> (<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',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -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;"> (<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 + ' • ' + 'Server: ' + res.location + ' • ' + '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);
|
||||
});
|
||||
}
|
||||
|
||||
+11
-23
@@ -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) +'; ':''}
|
||||
@@ -2773,7 +2782,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 +2861,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 +3142,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');
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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} • Server: ${res.location} • Deployed: ${deployed_date}`);
|
||||
})
|
||||
.catch(() => {
|
||||
$("#version-placeholder").html("Failed to load version or server information.");
|
||||
});
|
||||
|
||||
const el_window = await UIWindow({
|
||||
title: null,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3601,6 +3601,11 @@ label {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.version#version-placeholder {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.version:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
+65
-6
@@ -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.
|
||||
};
|
||||
Reference in new issue
Block a user