5 次代码提交 99ce3bde19 ... 368c20cf57

作者 SHA1 备注 提交日期
  KernelDeimos 368c20cf57 dev: updates and URL collector 11 小时之前
  KernelDeimos 12e0cecf02 fix: wait no 16 小时之前
  KernelDeimos c8f913d710 fix: phoenix incorrect lookup order 16 小时之前
  ProgrammerIn-wonderland 891e799e5e WIP symlink, net_device virtio, move fixes, upload binary fixes 17 小时之前
  jelveh 13248a99bf feat: add support for `fadeIn` effect for `UIWindow` 1 天之前

+ 9 - 3
src/backend/src/api/APIError.js

@@ -51,6 +51,12 @@ module.exports = class APIError {
             status: 400,
             message: () => 'Invalid token'
         },
+        'unrecognized_offering': {
+            status: 400,
+            message: ({ name }) => {
+                return `offering ${quot(name)} was not recognized.`;
+            },
+        },
         // Things
         'disallowed_thing': {
             status: 400,
@@ -65,7 +71,7 @@ module.exports = class APIError {
                         : ''
                 ) + '.'
         },
-        
+
         // Unorganized
         'item_with_same_name_exists': {
             status: 409,
@@ -590,11 +596,11 @@ module.exports = class APIError {
             status: this.status,
         };
     }
-    
+
     querystringize (extra) {
         return new URLSearchParams(this.querystringize_(extra));
     }
-    
+
     querystringize_ (extra) {
         const fields = {};
         for ( const k in this.fields ) {

+ 4 - 2
src/emulator/src/main.js

@@ -326,8 +326,10 @@ window.onload = async function()
         },
         // bzimage_initrd_from_filesystem: true,
         autostart: true,
-
-        network_relay_url: emu_config.network_relay ?? "wisp://127.0.0.1:4000",
+        net_device: {
+            relay_url: emu_config.network_relay ?? "wisp://127.0.0.1:4000",
+            type: "virtio"
+        },
         virtio_console: true,
     });
 

+ 19 - 0
src/gui/src/UI/Components/JustID.js

@@ -0,0 +1,19 @@
+const Component = use('util.Component');
+
+export default def(class JustID extends Component {
+    static ID = 'ui.component.JustID';
+    static RENDER_MODE = Component.NO_SHADOW;
+
+    static PROPERTIES = {
+        id: { value: undefined },
+    }
+
+    create_template ({ template }) {
+        const size = 24;
+        $(template).html(/*html*/`
+            <div
+                style="height: 358px"
+                id="${this.get('id')}"></div>
+        `);
+    }
+})

+ 19 - 0
src/gui/src/UI/Components/StepView.js

@@ -71,6 +71,21 @@ export default def(class StepView extends Component {
         // now that we're ready, show the wrapper
         $(this.dom_).find('#wrapper').show();
     }
+    
+    add_child (child) {
+        const children = this.get('children');
+        let pos = children.length;
+        child.setAttribute('slot', 'inside');
+        $(child).hide();
+        child.attach(this);
+        
+        return pos;
+    }
+    
+    display (child) {
+        const pos = this.add_child(child);
+        this.goto(pos);
+    }
 
     back () {
         if ( this.get('position') === 0 ) return;
@@ -84,4 +99,8 @@ export default def(class StepView extends Component {
         }
         this.set('position', this.get('position') + 1);
     }
+    
+    goto (pos) {
+        this.set('position', pos);
+    }
 });

+ 49 - 19
src/gui/src/UI/UIWindow.js

@@ -589,25 +589,55 @@ async function UIWindow(options) {
     // window is actually appended and usable.
     // NOTE: there is another is_visible condition below
     if ( options.is_visible ) {
-        $(el_window).show(0, function(e){
-            // if SaveFileDialog, bring focus to the el_savefiledialog_filename and select all
-            if(options.is_saveFileDialog){
-                let item_name = el_savefiledialog_filename.value;
-                const extname = path.extname('/' + item_name);
-                if(extname !== '')
-                el_savefiledialog_filename.setSelectionRange(0, item_name.length - extname.length)
-                else
-                    $(el_savefiledialog_filename).select();
-        
-                $(el_savefiledialog_filename).get(0).focus({preventScroll:true});
-            }
-            //set custom window css
-            $(el_window).css(options.window_css);
-            // onAppend()
-            if(options.onAppend && typeof options.onAppend === 'function'){
-                options.onAppend(el_window);
-            }
-        });
+
+        if(options.fadeIn){
+            $(el_window).css('opacity', 0);
+
+            $(el_window).animate({ opacity: 1 }, options.fadeIn, function() {
+                // Move the onAppend callback here to ensure it's called after fade-in
+                if (options.is_visible) {
+                    $(el_window).show(0, function(e) {
+                        // if SaveFileDialog, bring focus to the el_savefiledialog_filename and select all
+                        if (options.is_saveFileDialog) {
+                            let item_name = el_savefiledialog_filename.value;
+                            const extname = path.extname('/' + item_name);
+                            if (extname !== '')
+                                el_savefiledialog_filename.setSelectionRange(0, item_name.length - extname.length);
+                            else
+                                $(el_savefiledialog_filename).select();
+                    
+                            $(el_savefiledialog_filename).get(0).focus({preventScroll:true});
+                        }
+                        //set custom window css
+                        $(el_window).css(options.window_css);
+                        // onAppend()
+                        if (options.onAppend && typeof options.onAppend === 'function') {
+                            options.onAppend(el_window);
+                        }
+                    });
+                }
+            });
+        }else{
+            $(el_window).show(0, function(e){
+                // if SaveFileDialog, bring focus to the el_savefiledialog_filename and select all
+                if(options.is_saveFileDialog){
+                    let item_name = el_savefiledialog_filename.value;
+                    const extname = path.extname('/' + item_name);
+                    if(extname !== '')
+                    el_savefiledialog_filename.setSelectionRange(0, item_name.length - extname.length)
+                    else
+                        $(el_savefiledialog_filename).select();
+            
+                    $(el_savefiledialog_filename).get(0).focus({preventScroll:true});
+                }
+                //set custom window css
+                $(el_window).css(options.window_css);
+                // onAppend()
+                if(options.onAppend && typeof options.onAppend === 'function'){
+                    options.onAppend(el_window);
+                }
+            });
+        }
     }
 
     if(options.is_saveFileDialog){

+ 1 - 0
src/gui/src/UI/UIWindowWelcome.js

@@ -67,6 +67,7 @@ async function UIWindowWelcome(options){
         dominant: true,
         show_in_taskbar: false,
         draggable_body: true,
+        fadeIn: 1000,
         onAppend: function(this_window){
         },
         window_class: 'window-welcome',

+ 7 - 1
src/gui/src/i18n/translations/en.js

@@ -322,7 +322,7 @@ const en = {
         zipping: "Zipping %strong%",
 
         // === 2FA Setup ===
-        setup2fa_1_step_heading: 'Open your authenticator app',      
+        setup2fa_1_step_heading: 'Open your authenticator app',
         setup2fa_1_instructions: `
             You can use any authenticator app that supports the Time-based One-Time Password (TOTP) protocol.
             There are many to choose from, but if you're unsure
@@ -349,6 +349,12 @@ const en = {
         login2fa_use_recovery_code: 'Use a recovery code',
         login2fa_recovery_back: 'Back',
         login2fa_recovery_placeholder: 'XXXXXXXX',
+
+        // Subscriptions
+        'offering.free': 'Use Puter',
+        'offering.pay-puter': 'Pay Puter',
+        'offering.pay-puter-more': 'Pay Puter More',
+        'offering.pay-puter-even-more': 'Pay Puter Even More',
     }
 };
 

文件差异内容过多而无法显示
+ 2 - 0
src/gui/src/icons/subscription.svg


+ 2 - 0
src/gui/src/init_async.js

@@ -21,12 +21,14 @@ logger.info('start -> async initialization');
 
 import './util/TeePromise.js';
 import './util/Component.js';
+import './util/Collector.js';
 import './UI/Components/Frame.js';
 import './UI/Components/Glyph.js';
 import './UI/Components/Spinner.js';
 import './UI/Components/ActionCard.js';
 import './UI/Components/NotifCard.js';
 import './UI/Components/TestView.js';
+import './UI/Components/JustID.js';
 
 logger.info('end -> async initialization');
 globalThis.init_promise.resolve();

+ 66 - 0
src/gui/src/util/Collector.js

@@ -0,0 +1,66 @@
+const CollectorHandle = (key, collector) => ({
+    async get (route) {
+        if ( collector.stored[key] ) return collector.stored[key];
+        return await collector.fetch({ key, method: 'get', route });
+    },
+    async post (route, body) {
+        if ( collector.stored[key] ) return collector.stored[key];
+        return await collector.fetch({ key, method: 'post', route, body });
+    }
+})
+
+// TODO: link this with kv.js for expiration handling
+export default def(class Collector {
+    constructor ({ origin, authToken }) {
+        this.origin = origin;
+        this.authToken = authToken;
+        this.stored = {};
+    }
+
+    to (name) {
+        return CollectorHandle(name, this);
+    }
+
+    whats (key) {
+        return this.stored[key];
+    }
+
+    async get (route) {
+        return await this.fetch({ method: 'get', route });
+    }
+    async post (route, body) {
+        return await this.fetch({ method: 'post', route, body });
+    }
+
+    discard (key) {
+        if ( ! key ) this.stored = {};
+        delete this.stored[key];
+    }
+
+    async fetch (options) {
+        const fetchOptions = {
+            method: options.method,
+            headers: {
+                Authorization: `Bearer ${this.authToken}`,
+                'Content-Type': 'application/json',
+            },
+        };
+
+        if ( options.method === 'post' ) {
+            fetchOptions.body = JSON.stringify(
+                options.body ?? {});
+        }
+
+        const maybe_slash = options.route.startsWith('/')
+            ? '' : '/';
+
+        const resp = await fetch(
+            this.origin +maybe_slash+ options.route,
+            fetchOptions,
+        );
+        const asJSON = await resp.json();
+
+        if ( options.key ) this.stored[options.key] = asJSON;
+        return asJSON;
+    }
+}, 'util.Collector');

+ 6 - 2
src/gui/src/util/Component.js

@@ -183,7 +183,10 @@ export const Component = def(class Component extends HTMLElement {
             this.dom_.appendChild(style);
         }
         if ( this.create_template ) {
-            this.create_template({ template });
+            this.create_template({
+                template,
+                content: template.content,
+            });
         }
         const el = template.content.cloneNode(true);
         return el;
@@ -202,7 +205,8 @@ export const Component = def(class Component extends HTMLElement {
                 }
                 this.values_[name].sub(callback);
                 callback(this.values_[name].get(), {});
-            }
+            },
+            dom: this.dom_,
         };
     }
 });

+ 4 - 4
src/gui/src/util/Placeholder.js

@@ -27,13 +27,13 @@
 /**
  * Placeholder creates a simple element with a unique ID
  * as an HTML string.
- * 
+ *
  * This can be useful where string concatenation is used
  * to build element trees.
- * 
+ *
  * The `replaceWith` method can be used to replace the
  * placeholder with a real element.
- * 
+ *
  * @returns {PlaceholderReturn}
  */
 const Placeholder = def(() => {
@@ -49,7 +49,7 @@ const Placeholder = def(() => {
     };
 }, 'util.Placeholder');
 
-const anti_collision = `94d2cb6b85a1`; // Arbitrary random string
+const anti_collision = `a4d2cb6b85a1`; // Arbitrary random string
 Placeholder.next_id_ = 0;
 Placeholder.get_next_id_ = () => `${anti_collision}_${Placeholder.next_id_++}`;
 

+ 2 - 0
src/puter-js/src/modules/FileSystem/index.js

@@ -10,6 +10,7 @@ import read from "./operations/read.js";
 import move from "./operations/move.js";
 import write from "./operations/write.js";
 import sign from "./operations/sign.js";
+import symlink from './operations/symlink.js';
 // Why is this called deleteFSEntry instead of just delete? because delete is 
 // a reserved keyword in javascript
 import deleteFSEntry from "./operations/deleteFSEntry.js";
@@ -32,6 +33,7 @@ export class PuterJSFileSystemModule extends AdvancedBase {
     move = move;
     write = write;
     sign = sign;
+    symlink = symlink;
 
     static NARI_METHODS = {
         stat: {

+ 15 - 1
src/puter-js/src/modules/FileSystem/operations/move.js

@@ -1,9 +1,10 @@
 import * as utils from '../../../lib/utils.js';
 import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
+import stat from "./stat.js"
+import path from "../../../lib/path.js"
 
 const move = function (...args) {
     let options;
-
     // If first argument is an object, it's the options
     if (typeof args[0] === 'object' && args[0] !== null) {
         options = args[0];
@@ -36,6 +37,19 @@ const move = function (...args) {
         options.source = getAbsolutePathForApp(options.source);
         options.destination = getAbsolutePathForApp(options.destination);
 
+        if (!options.new_name) {
+            // Handler to check if dest is supposed to be a file or a folder
+            try {
+                const destStats = await stat.bind(this)(options.destination); // this is meant to error if it doesn't exist
+                if (!destStats.is_dir) {
+                    throw "is not directory" // just a wuick way to just to the catch
+                }
+            } catch (e) {
+                options.new_name = path.basename(options.destination);
+                options.destination = path.dirname(options.destination);
+            }
+        }
+
         // create xhr object
         const xhr = utils.initXhr('/move', this.APIOrigin, this.authToken);
 

+ 55 - 0
src/puter-js/src/modules/FileSystem/operations/symlink.js

@@ -0,0 +1,55 @@
+import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
+import pathLib from '../../../lib/path.js';
+
+// This only works for absolute symlinks for now
+const symlink = async function (target, linkPath) {
+
+
+    // If auth token is not provided and we are in the web environment, 
+    // try to authenticate with Puter
+    if(!puter.authToken && puter.env === 'web'){
+        try{
+            await puter.ui.authenticateWithPuter();
+        }catch(e){
+            // if authentication fails, throw an error
+            throw 'Authentication failed.';
+        }
+    }
+
+    // convert path to absolute path
+    linkPath = getAbsolutePathForApp(linkPath);
+    target = getAbsolutePathForApp(target);
+    const name = pathLib.basename(linkPath);
+    const linkDir = pathLib.dirname(linkPath)
+
+    const op =
+        {
+          op: 'symlink',
+          path: linkDir,
+          name: name,
+          target: target
+        };
+
+    const formData = new FormData();
+    formData.append('operation', JSON.stringify(op));
+
+    try {
+        const response = await fetch(this.APIOrigin + "/batch", {
+            method: 'POST',
+            headers: { 'Authorization': `Bearer ${puter.authToken}` },
+            body: formData
+        });
+        if (response.status !== 200) {
+            const error = await response.text();
+            console.error("[symlink] fetch error: ", error);
+            throw error;
+        }
+    } catch (e) {
+        console.error("[symlink] fetch error: ", e);
+        throw e;
+    }
+    
+
+}
+
+export default symlink;

+ 1 - 1
src/puter-js/src/modules/FileSystem/operations/upload.js

@@ -104,7 +104,7 @@ const upload = async function(items, dirPath, options = {}){
         // blob
         else if(items instanceof Blob){
             // create a File object from the blob
-            let file = new File([items], options.name, { type: "text/plain" });
+            let file = new File([items], options.name, { type: "application/octet-stream" });
             entries = [file];
             // add FullPath property to each entry
             for(let i=0; i<entries.length; i++){

+ 1 - 1
submodules/v86

@@ -1 +1 @@
-Subproject commit a4b34ab471e31014a56a7467bfcd51a066de37e5
+Subproject commit d9c36dfccface164a34e20a7f44658fc5b7a6cc0