From b2cf01e57c507649fc4f1a1d733f59b546c096d4 Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Fri, 1 Aug 2025 17:05:07 -0700 Subject: [PATCH 01/46] Add 'My Workers' tab --- src/dev-center/css/style.css | 17 +++++++- src/dev-center/img/workers-placeholder.svg | 13 ++++++ src/dev-center/img/workers-white.svg | 3 ++ src/dev-center/img/workers.svg | 3 ++ src/dev-center/index.html | 16 ++++++++ src/dev-center/js/dev-center.js | 46 ++++++++++++++++++++++ 6 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 src/dev-center/img/workers-placeholder.svg create mode 100644 src/dev-center/img/workers-white.svg create mode 100644 src/dev-center/img/workers.svg diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index 4758a145e..0924b882f 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -568,7 +568,12 @@ label { .tab-btn[data-tab="apps"] { background-image: url(../img/apps-outline-black.svg); } - +.tab-btn[data-tab="workers"] { + background-image: url(../img/workers.svg); +} +.tab-btn.active[data-tab="workers"] { + background-image: url(../img/workers-white.svg); +} .tab-btn[data-tab="payout-method"] { background-image: url(../img/wallet.svg); } @@ -590,7 +595,7 @@ label { box-sizing: border-box; } -#no-apps-notice, #loading { +#no-apps-notice, #no-workers-notice, #loading { display: flex; flex-direction: column; justify-content: center; @@ -924,6 +929,14 @@ ol li:before { margin-bottom: -5px; margin-right: 10px; } +.create-a-worker-btn{ + float:right; margin-bottom: 10px; +} +.create-a-worker-btn img{ + width: 20px; + margin-bottom: -5px; + margin-right: 5px; +} .jip-submit-btn{ float: left; margin-top: 10px; diff --git a/src/dev-center/img/workers-placeholder.svg b/src/dev-center/img/workers-placeholder.svg new file mode 100644 index 000000000..4da6d5d3a --- /dev/null +++ b/src/dev-center/img/workers-placeholder.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/dev-center/img/workers-white.svg b/src/dev-center/img/workers-white.svg new file mode 100644 index 000000000..b664600ef --- /dev/null +++ b/src/dev-center/img/workers-white.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/dev-center/img/workers.svg b/src/dev-center/img/workers.svg new file mode 100644 index 000000000..d2faa0fbf --- /dev/null +++ b/src/dev-center/img/workers.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/dev-center/index.html b/src/dev-center/index.html index 02fb8d873..b3ec46ac3 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -79,6 +79,7 @@ + + + +
+

My Workers

+
+ diff --git a/src/dev-center/js/dev-center.js b/src/dev-center/js/dev-center.js index 772d80555..9ba704a6f 100644 --- a/src/dev-center/js/dev-center.js +++ b/src/dev-center/js/dev-center.js @@ -21,6 +21,7 @@ let URLParams = new URLSearchParams(window.location.search); let domain = 'puter.com', authUsername; let source_path let apps = []; +let workers = []; let sortBy = 'created_at'; let sortDirection = 'desc'; const dev_center_uid = puter.appID; @@ -210,6 +211,18 @@ function refresh_app_list(show_loading = false) { }, show_loading ? 1000 : 0); } +function refresh_worker_list(show_loading = false) { + if (show_loading) + puter.ui.showSpinner(); + // get workers + setTimeout(function () { + puter.workers.list().then((workers_res) => { + workers = workers_res; + puter.ui.hideSpinner(); + }) + }, 1000); +} + $(document).on('click', '.tab-btn', function (e) { puter.ui.showSpinner(); $('section:not(.sidebar)').hide(); @@ -225,6 +238,26 @@ $(document).on('click', '.tab-btn', function (e) { activeTab = 'apps'; } // --------------------------------------------------------------- + // Workers tab + // --------------------------------------------------------------- + else if ($(this).attr('data-tab') === 'workers') { + // Get workers + puter.workers.list().then((resp) => { + workers = resp || []; + if (activeTab === 'workers' && workers.length > 0) { + $('#no-workers-notice').hide(); + $('#worker-list').show(); + workers.forEach(worker => { + $('#worker-list-table > tbody').append(generate_worker_card(worker)); + }); + } else { + $('#no-workers-notice').show(); + $('#worker-list').hide(); + } + }) + activeTab = 'workers'; + } + // --------------------------------------------------------------- // Payout Method tab // --------------------------------------------------------------- else if ($(this).attr('data-tab') === 'payout-method') { @@ -263,6 +296,14 @@ $(document).on('click', '.create-an-app-btn', async function (e) { } }) +$(document).on('click', '.create-a-worker-btn', async function (e) { + let name = await puter.ui.prompt('Please enter a name for your worker:', 'My Awesome Worker'); + + if (name) { + create_worker(name); + } +}) + async function create_app(title, source_path = null, items = null) { // name let name = slugify(title, { @@ -375,6 +416,11 @@ async function create_app(title, source_path = null, items = null) { }) } +async function create_worker(name) { + let worker = await puter.workers.create({ + name: name, + }); +} $(document).on('click', '.deploy-btn', function (e) { deploy(currently_editing_app, dropped_items); From fab062b0fe5c03486b6560b190d7fae97d94aca4 Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sat, 2 Aug 2025 15:12:23 -0700 Subject: [PATCH 02/46] Add the workers and websites tabs --- src/dev-center/css/style.css | 30 +- src/dev-center/img/website-white.svg | 3 + src/dev-center/img/website.svg | 3 + src/dev-center/index.html | 57 +- src/dev-center/js/apps.js | 2718 ++++++++++++++++ src/dev-center/js/dev-center.js | 2853 +---------------- src/dev-center/js/{ => libs}/html-entities.js | 0 .../js/{ => libs}/jquery-3.6.0.min.js | 0 .../js/{ => libs}/jquery.dragster.js | 0 src/dev-center/js/{ => libs}/slugify.js | 0 src/dev-center/js/websites.js | 4 + src/dev-center/js/workers.js | 63 + 12 files changed, 2932 insertions(+), 2799 deletions(-) create mode 100644 src/dev-center/img/website-white.svg create mode 100644 src/dev-center/img/website.svg create mode 100644 src/dev-center/js/apps.js rename src/dev-center/js/{ => libs}/html-entities.js (100%) rename src/dev-center/js/{ => libs}/jquery-3.6.0.min.js (100%) rename src/dev-center/js/{ => libs}/jquery.dragster.js (100%) rename src/dev-center/js/{ => libs}/slugify.js (100%) create mode 100644 src/dev-center/js/websites.js create mode 100644 src/dev-center/js/workers.js diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index 0924b882f..73f461c90 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -227,14 +227,14 @@ section { padding-left: 20px; box-sizing: border-box; } -#app-list{ +#app-list, #website-list, #worker-list { display: none; } -#app-list-table > thead{ +#app-list-table > thead, #website-list-table > thead, #worker-list-table > thead{ font-size:14px; background-color: #f7fafc; border-top: 1px solid #DDD; text-transform: uppercase; color: #6d767d; cursor: default; } -.app-card { +.app-card, .website-card, .worker-card { padding: 12px; border-top: 1px solid #d8dce5; border-radius: 0; @@ -245,33 +245,33 @@ section { } -.app-card:hover { +.app-card:hover, .website-card:hover, .worker-card:hover { background-color: #e8eff66e; } -.app-card.active { +.app-card.active, .website-card.active, .worker-card.active { background-color: #e8eff6; } -.app-card:hover .app-row-toolbar { +.app-card:hover .app-row-toolbar, .website-card:hover .website-row-toolbar, .worker-card:hover .worker-row-toolbar { visibility: visible; opacity: 1; } -.app-toolbar-container { +.app-toolbar-container, .website-toolbar-container, .worker-toolbar-container { height: 16px; overflow: hidden; position: relative; } -.app-card h1 { +.app-card h1, .website-card h1, .worker-card h1 { margin-top: 0; color: #657188; font-weight: 400; font-size: 25px; } -#create-app-success { +#create-app-success, #create-website-success, #create-worker-success { height: calc(100vh - 40px); overflow: hidden; text-align: center; @@ -286,7 +286,7 @@ label, input[type="text"] { user-select: none; } -#delete-app { +#delete-app, #delete-website, #delete-worker { cursor: pointer; float: left; margin-top: 30px; @@ -294,7 +294,7 @@ label, input[type="text"] { font-size: 13px; } -#delete-app:hover { +#delete-app:hover, #delete-website:hover, #delete-worker:hover { text-decoration: underline; } @@ -568,6 +568,14 @@ label { .tab-btn[data-tab="apps"] { background-image: url(../img/apps-outline-black.svg); } + +.tab-btn[data-tab="websites"] { + background-image: url(../img/website.svg); +} +.tab-btn.active[data-tab="websites"] { + background-image: url(../img/website-white.svg); +} + .tab-btn[data-tab="workers"] { background-image: url(../img/workers.svg); } diff --git a/src/dev-center/img/website-white.svg b/src/dev-center/img/website-white.svg new file mode 100644 index 000000000..7ef28b1af --- /dev/null +++ b/src/dev-center/img/website-white.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/dev-center/img/website.svg b/src/dev-center/img/website.svg new file mode 100644 index 000000000..5dcc7b279 --- /dev/null +++ b/src/dev-center/img/website.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/dev-center/index.html b/src/dev-center/index.html index b3ec46ac3..c0ac90dc2 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -78,8 +78,9 @@ -
-

My Workers

-
+
+

My Workers

+ + +
- - - - - +
+ + +
+ + +
+ + + + + + + + + + + + +
Worker▼▲Deployed▼▲
+
+ + + + +
+

My Websites

+
+ + + + + + + + + + + diff --git a/src/dev-center/js/apps.js b/src/dev-center/js/apps.js new file mode 100644 index 000000000..3c86d521a --- /dev/null +++ b/src/dev-center/js/apps.js @@ -0,0 +1,2718 @@ +let source_path +let apps = []; +let sortBy = 'created_at'; +let sortDirection = 'desc'; +let currently_editing_app; +let dropped_items; +let search_query; +let originalValues = {}; + +const APP_CATEGORIES = [ + { id: 'games', label: 'Games' }, + { id: 'developer-tools', label: 'Developer Tools' }, + { id: 'photo-video', label: 'Photo & Video' }, + { id: 'productivity', label: 'Productivity' }, + { id: 'utilities', label: 'Utilities' }, + { id: 'education', label: 'Education' }, + { id: 'business', label: 'Business' }, + { id: 'social', label: 'Social' }, + { id: 'graphics-design', label: 'Graphics & Design' }, + { id: 'music-audio', label: 'Music & Audio' }, + { id: 'news', label: 'News' }, + { id: 'entertainment', label: 'Entertainment' }, + { id: 'finance', label: 'Finance' }, + { id: 'health-fitness', label: 'Health & Fitness' }, + { id: 'lifestyle', label: 'Lifestyle' }, +]; + +const deploying_spinner = ``; +const loading_spinner = ``; +const drop_area_placeholder = `

Drop your app folder and files here to deploy.

HTML, JS, CSS, ...

`; +const index_missing_error = `Please upload an 'index.html' file or if you're uploading a directory, make sure it contains an 'index.html' file at its root.`; +const lock_svg = ` `; +const lock_svg_tippy = ` `; + +const copy_svg = ` `; + +async function init_apps() { + setTimeout(async function () { + puter.ui.onLaunchedWithItems(async function (items) { + source_path = items[0].path; + // if source_path is provided, this means that the user is creating a new app/updating an existing app + // by deploying an existing Puter folder. So we create the app and deploy it. + if (source_path) { + // todo if there are no apps, go straight to creating a new app + $('.insta-deploy-modal').get(0).showModal(); + // set item name + $('.insta-deploy-item-name').html(html_encode(items[0].name)); + } + }) + + // Get dev profile. This is only for puter.com for now as we don't have dev profiles in self-hosted Puter + if(domain === 'puter.com'){ + puter.apps.getDeveloperProfile(async function (dev_profile) { + window.developer = dev_profile; + if (dev_profile.approved_for_incentive_program && !dev_profile.joined_incentive_program) { + $('#join-incentive-program').show(); + } + + // show earn money c2a only if dev is not approved for incentive program or has already joined + if (!dev_profile.approved_for_incentive_program || dev_profile.joined_incentive_program) { + puter.kv.get('earn-money-c2a-closed').then((value) => { + if (value?.result || value === true || value === "true") + return; + + $('#earn-money').get(0).showModal(); + }); + } + + // show payout method tab if dev has joined incentive program + if (dev_profile.joined_incentive_program) { + $('.tab-btn[data-tab="payout-method"]').show(); + $('#payout-method-email').html(dev_profile.paypal); + } + }) + } + // Get apps + puter.apps.list({ icon_size: 64 }).then((resp) => { + apps = resp; + + // hide loading + puter.ui.hideSpinner(); + + // set apps + if (apps.length > 0) { + if (window.activeTab === 'apps') { + $('#no-apps-notice').hide(); + $('#app-list').show(); + } + $('.app-card').remove(); + apps.forEach(app => { + $('#app-list-table > tbody').append(generate_app_card(app)); + }); + count_apps(); + sort_apps(); + activate_tippy(); + } else { + $('#no-apps-notice').show(); + } + }) + }, 1000); + +} + + +/** + * Refreshes the list of apps in the UI. + * + * @param {boolean} [show_loading=false] - Whether to show a loading indicator while refreshing. + * + */ + +window.refresh_app_list = (show_loading = false) => { + if (show_loading) + puter.ui.showSpinner(); + // get apps + setTimeout(function () { + // uncheck the select all checkbox + $('.select-all-apps').prop('checked', false); + + puter.apps.list({ icon_size: 64 }).then((apps_res) => { + puter.ui.hideSpinner(); + apps = apps_res; + if (apps.length > 0) { + if (window.activeTab === 'apps') { + $('#no-apps-notice').hide(); + $('#app-list').show(); + } + $('.app-card').remove(); + apps.forEach(app => { + $('#app-list-table > tbody').append(generate_app_card(app)); + }); + count_apps(); + sort_apps(); + } else { + $('#no-apps-notice').show(); + $('#app-list').hide() + } + activate_tippy(); + }) + }, show_loading ? 1000 : 0); +} + +$(document).on('click', '.create-an-app-btn', async function (e) { + let title = await puter.ui.prompt('Please enter a title for your app:', 'My Awesome App'); + + if (title.length > 60) { + puter.ui.alert(`Title cannot be longer than 60.`, [ + { + label: 'Ok', + }, + ]); + // todo go back to create an app prompt and prefill the title input with the title the user entered + return; + } + else if (title) { + create_app(title); + } +}) + +async function create_app(title, source_path = null, items = null) { + // name + let name = slugify(title, { + lower: true, + strict: true, + }); + + // icon + let icon = await getBase64ImageFromUrl('./img/app.svg'); + + // open the 'Creting new app...' modal + let start_ts = Date.now(); + + puter.ui.showSpinner(); + + //---------------------------------------------------- + // Create app + //---------------------------------------------------- + puter.apps.create({ + title: title, + name: name, + indexURL: 'https://dev-center.puter.com/coming-soon.html', + icon: icon, + description: ' ', + maximizeOnStart: false, + background: false, + dedupeName: true, + metadata: { + window_resizable: true, + fullpage_on_landing: true, + }, + + }) + .then(async (app) => { + let app_dir; + // ---------------------------------------------------- + // Create app directory in AppData + // ---------------------------------------------------- + app_dir = await puter.fs.mkdir( + `/${auth_username}/AppData/${dev_center_uid}/${app.uid}`, + { overwrite: true, recursive: true, rename: false } + ); + // ---------------------------------------------------- + // Create a router for the app with a fresh hostname + // ---------------------------------------------------- + let subdomain = name + '-' + Math.random().toString(36).substring(2) + await puter.hosting.create(subdomain, app_dir.path); + + // ---------------------------------------------------- + // Update the app with the new hostname + // ---------------------------------------------------- + puter.apps.update(app.name, { + title: title, + indexURL: source_path ? protocol + `://${subdomain}.` + static_hosting_domain : 'https://dev-center.puter.com/coming-soon.html', + icon: icon, + description: ' ', + maximizeOnStart: false, + background: false, + metadata: { + category: null, // default category on creation + window_resizable: true, + fullpage_on_landing: true, + } +}).then(async (app) => { + // refresh app list + puter.apps.list({ icon_size: 64 }).then(async (resp) => { + apps = resp; + // Close the 'Creting new app...' modal + // but make sure it was shown for at least 2 seconds + setTimeout(() => { + // open edit app section + edit_app_section(app.name); + refresh_app_list(); // ✅ Refreshes main app list + + // set drop area if source_path was provided or items were dropped + if (source_path || items) { + $('.drop-area').removeClass('drop-area-hover'); + $('.drop-area').addClass('drop-area-ready-to-deploy'); + } + puter.ui.hideSpinner(); + // deploy app if source_path was provided + if (source_path) { + deploy(app, source_path); + } else if (items) { + deploy(app, items); + } + activate_tippy(); + }, (Date.now() - start_ts) > 2000 ? 1 : 2000 - (Date.now() - start_ts)); + }) + }).catch(async (err) => { + console.log(err); + }) + // ---------------------------------------------------- + // Create a "shortcut" on the desktop + // ---------------------------------------------------- + puter.fs.upload(new File([], app.title), + `/${auth_username}/Desktop`, + { + name: app.title, + dedupeName: true, + overwrite: false, + appUID: app.uid, + } + ) + }).catch(async (err) => { + $('#create-app-error').show(); + $('#create-app-error').html(err.message); + // scroll to top so that user sees error message + document.body.scrollTop = document.documentElement.scrollTop = 0; + }) +} + +$(document).on('click', '.deploy-btn', function (e) { + deploy(currently_editing_app, dropped_items); +}) + +$(document).on('click', '.edit-app, .got-to-edit-app', function (e) { + const cur_app_name = $(this).attr('data-app-name') + edit_app_section(cur_app_name); +}) + +$(document).on('click', '.delete-app', async function (e) { + let app_uid = $(this).attr('data-app-uid'); + let app_title = $(this).attr('data-app-title'); + let app_name = $(this).attr('data-app-name'); + + // get app + const app_data = await puter.apps.get(app_name, { icon_size: 16 }); + + if(app_data.metadata?.locked){ + puter.ui.alert(`${app_data.title} is locked and cannot be deleted.`, [ + { + label: 'Ok', + }, + ], { + type: 'warning', + }); + return; + } + + // confirm delete + const alert_resp = await puter.ui.alert(`Are you sure you want to premanently delete ${html_encode(app_title)}?`, + [ + { + label: 'Yes, delete permanently', + value: 'delete', + type: 'danger', + }, + { + label: 'Cancel' + }, + ] + ); + + if (alert_resp === 'delete') { + let init_ts = Date.now(); + puter.ui.showSpinner(); + puter.apps.delete(app_name).then(async (app) => { + setTimeout(() => { + puter.ui.hideSpinner(); + $(`.app-card[data-uid="${app_uid}"]`).fadeOut(200, function name(params) { + $(this).remove(); + if ($(`.app-card`).length === 0) { + $('section:not(.sidebar)').hide(); + $('#no-apps-notice').show(); + } else { + $('section:not(.sidebar)').hide(); + $('#app-list').show(); + } + count_apps(); + }); + }, + // make sure the modal was shown for at least 2 seconds + (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); + + // get app directory + puter.fs.stat({ + path: `/${auth_username}/AppData/${dev_center_uid}/${app_uid}`, + returnSubdomains: true, + }).then(async (stat) => { + // delete subdomain associated with the app dir + puter.hosting.delete(stat.subdomains[0].subdomain) + // delete app directory + puter.fs.delete( + `/${auth_username}/AppData/${dev_center_uid}/${app_uid}`, + { recursive: true } + ) + }) + }).catch(async (err) => { + setTimeout(() => { + puter.ui.hideSpinner(); + puter.ui.alert(err?.message, [ + { + label: 'Ok', + }, + ]); + }, + // make sure the modal was shown for at least 2 seconds + (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); + }) + } +}) + +// generate app link +function applink(app) { + return protocol + `://${domain}${ port ? ':' + port : '' }/app/${app.name}`; +} + +/** + * Generates the HTML for the app editing section. + * + * @param {Object} app - The app object containing details of the app to be edited. + * * + * @returns {string} HTML string for the app editing section. + * + * @description + * This function creates the HTML for the app editing interface, including: + * - App icon and title display + * - Options to open, add to desktop, or delete the app + * - Tabs for deployment and settings + * - Form fields for editing various app properties + * - Display of app statistics + * + * The generated HTML includes interactive elements and placeholders for + * dynamic content to be filled or updated by other functions. + * + * @example + * const appEditHTML = generate_edit_app_section(myAppObject); + * $('#edit-app').html(appEditHTML); + */ + +function generate_edit_app_section(app) { + if(app.result) + app = app.result; + + let maximize_on_start = app.maximize_on_start ? 'checked' : ''; + + let h = ``; + h += ` +
+
+ +

${html_encode(app.title)}${app.metadata?.locked ? lock_svg_tippy : ''}

+
+ Open + • + Add Shortcut to Desktop + • + Delete +
+ ${html_encode(applink(app))} +
+ +
+ + + +
+
+ New version deployed successfully 🎉× +

Give it a try!

+
+
${drop_area_placeholder}
+ +
+ +
+
+
+
App has been successfully updated.× +

Give it a try!

+
+ + +

Basic

+ + + + + + + + + + +
+ ${copy_svg} +
+ + +
+
Change App Icon
+
+ Remove icon + + ${generateSocialImageSection(app)} + + + + + + + +

A list of file type specifiers. For example if you include .txt your apps could be opened when a user clicks on a TXT file.

+

You can paste multiple extensions at once (comma, space, or tab separated) or press comma to add each extension.

+ + +

Window

+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + +
+ +
+ + +
+ +

Misc

+
+ + +

When enabled, the app cannot be deleted. This is useful for preventing accidental deletion of important apps.

+
+ +
+ + +
+
+
+
+ + +
+
+

Users

+
+
+
+

Opens

+
+
+
+
+

Timezone: UTC

+

More analytics features coming soon...

+
+ ` + return h; +} + +/* This function keeps track of the original values of the app before it is edited*/ +function trackOriginalValues(){ + originalValues = { + title: $('#edit-app-title').val(), + name: $('#edit-app-name').val(), + indexURL: $('#edit-app-index-url').val(), + description: $('#edit-app-description').val(), + icon: $('#edit-app-icon').attr('data-base64'), + fileAssociations: $('#edit-app-filetype-associations').val(), + category: $('#edit-app-category').val(), + socialImage: $('#edit-app-social-image').attr('data-base64'), + windowSettings: { + width: $('#edit-app-window-width').val(), + height: $('#edit-app-window-height').val(), + top: $('#edit-app-window-top').val(), + left: $('#edit-app-window-left').val() + }, + checkboxes: { + maximizeOnStart: $('#edit-app-maximize-on-start').is(':checked'), + background: $('#edit-app-background').is(':checked'), + resizableWindow: $('#edit-app-window-resizable').is(':checked'), + hideTitleBar: $('#edit-app-hide-titlebar').is(':checked'), + locked: $('#edit-app-locked').is(':checked'), + fullPageOnLanding: $('#edit-app-fullpage-on-landing').is(':checked') + } + }; +} + +/* This function compares for all fields and checks if anything has changed from before editting*/ +function hasChanges() { + // is icon changed + if($('#edit-app-icon').attr('data-base64') !== originalValues.icon){ + return true; + } + + // if social image is changed + if($('#edit-app-social-image').attr('data-base64') !== originalValues.socialImage){ + return true; + } + + // if any of the fields have changed + return( + $('#edit-app-title').val() !== originalValues.title || + $('#edit-app-name').val() !== originalValues.name || + $('#edit-app-index-url').val() !== originalValues.indexURL || + $('#edit-app-description').val() !== originalValues.description || + $('#edit-app-icon').attr('data-base64') !== originalValues.icon || + $('#edit-app-filetype-associations').val() !== originalValues.fileAssociations || + $('#edit-app-category').val() !== originalValues.category || + $('#edit-app-social-image').attr('data-base64') !== originalValues.socialImage || + $('#edit-app-window-width').val() !== originalValues.windowSettings.width || + $('#edit-app-window-height').val() !== originalValues.windowSettings.height || + $('#edit-app-window-top').val() !== originalValues.windowSettings.top || + $('#edit-app-window-left').val() !== originalValues.windowSettings.left || + $('#edit-app-maximize-on-start').is(':checked') !== originalValues.checkboxes.maximizeOnStart || + $('#edit-app-background').is(':checked') !== originalValues.checkboxes.background || + $('#edit-app-window-resizable').is(':checked') !== originalValues.checkboxes.resizableWindow || + $('#edit-app-hide-titlebar').is(':checked') !== originalValues.checkboxes.hideTitleBar || + $('#edit-app-locked').is(':checked') !== originalValues.checkboxes.locked || + $('#edit-app-fullpage-on-landing').is(':checked') !== originalValues.checkboxes.fullPageOnLanding + ); +} + +/* This function enables or disables the save button if there are any changes made */ +function toggleSaveButton() { + if (hasChanges()) { + $('.edit-app-save-btn').prop('disabled', false); + } else { + $('.edit-app-save-btn').prop('disabled', true); + } +} + +/* This function enables or disables the reset button if there are any changes made */ +function toggleResetButton() { + if (hasChanges()) { + $('.edit-app-reset-btn').prop('disabled', false); + } else { + $('.edit-app-reset-btn').prop('disabled', true); + } +} + +window.reset_drop_area = () => { + dropped_items = null; + $('.drop-area').html(drop_area_placeholder); + $('.drop-area').removeClass('drop-area-ready-to-deploy'); + $('.deploy-btn').addClass('disabled'); +} + +/* This function revers the changes made back to the original values of the edit form */ +function resetToOriginalValues() { + $('#edit-app-title').val(originalValues.title); + $('#edit-app-name').val(originalValues.name); + $('#edit-app-index-url').val(originalValues.indexURL); + $('#edit-app-description').val(originalValues.description); + $('#edit-app-filetype-associations').val(originalValues.fileAssociations); + $('#edit-app-category').val(originalValues.category); + $('#edit-app-window-width').val(originalValues.windowSettings.width); + $('#edit-app-window-height').val(originalValues.windowSettings.height); + $('#edit-app-window-top').val(originalValues.windowSettings.top); + $('#edit-app-window-left').val(originalValues.windowSettings.left); + $('#edit-app-maximize-on-start').prop('checked', originalValues.checkboxes.maximizeOnStart); + $('#edit-app-background').prop('checked', originalValues.checkboxes.background); + $('#edit-app-window-resizable').prop('checked', originalValues.checkboxes.resizableWindow); + $('#edit-app-hide-titlebar').prop('checked', originalValues.checkboxes.hideTitleBar); + $('#edit-app-locked').prop('checked', originalValues.checkboxes.locked); + $('#edit-app-fullpage-on-landing').prop('checked', originalValues.checkboxes.fullPageOnLanding); + + if (originalValues.icon) { + $('#edit-app-icon').css('background-image', `url(${originalValues.icon})`); + $('#edit-app-icon').attr('data-url', originalValues.icon); + $('#edit-app-icon').attr('data-base64', originalValues.icon); + $('#edit-app-icon-delete').show(); + } else { + $('#edit-app-icon').css('background-image', ''); + $('#edit-app-icon').removeAttr('data-url'); + $('#edit-app-icon').removeAttr('data-base64'); + $('#edit-app-icon-delete').hide(); + } + + if (originalValues.socialImage) { + $('#edit-app-social-image').css('background-image', `url(${originalValues.socialImage})`); + $('#edit-app-social-image').attr('data-url', originalValues.socialImage); + $('#edit-app-social-image').attr('data-base64', originalValues.socialImage); + } else { + $('#edit-app-social-image').css('background-image', ''); + $('#edit-app-social-image').removeAttr('data-url'); + $('#edit-app-social-image').removeAttr('data-base64'); + } +} + +async function edit_app_section(cur_app_name, tab = 'deploy') { + $('section:not(.sidebar)').hide(); + $('.tab-btn').removeClass('active'); + $('.tab-btn[data-tab="apps"]').addClass('active'); + + let cur_app = await puter.apps.get(cur_app_name, {icon_size: 128, stats_period: 'today'}); + + currently_editing_app = cur_app; + + // generate edit app section + $('#edit-app').html(generate_edit_app_section(cur_app)); + trackOriginalValues(); // Track initial field values + toggleSaveButton(); // Ensure Save button is initially disabled + toggleResetButton(); // Ensure Reset button is initially disabled + $('#edit-app').show(); + + // analytics + $('#analytics-users .count').html(cur_app.stats.user_count); + $('#analytics-opens .count').html(cur_app.stats.open_count); + + render_analytics('today') + + // show the correct tab + $('.section-tab').hide(); + $(`.section-tab[data-tab="${tab}"]`).show(); + $('.section-tab-buttons .section-tab-btn').removeClass('active'); + $(`.section-tab-buttons .section-tab-btn[data-tab="${tab}"]`).addClass('active'); + + const filetype_association_input = document.querySelector('textarea[id=edit-app-filetype-associations]'); + let tagify = new Tagify(filetype_association_input, { + pattern: /\.(?:[a-z0-9]+)|(?:[a-z]+\/(?:[a-z0-9.-]+|\*))/, + delimiters: ",", // Use comma as delimiter + duplicates: false, // Prevent duplicate tags + enforceWhitelist: false, + dropdown : { + // show the dropdown immediately on focus (0 character typed) + enabled: 0, + }, + whitelist: [ + // MIME type patterns + "text/*", "image/*", "audio/*", "video/*", "application/*", + + // Documents + ".doc", ".docx", ".pdf", ".txt", ".odt", ".rtf", ".tex", ".md", ".pages", ".epub", ".mobi", ".azw", ".azw3", ".djvu", ".xps", ".oxps", ".fb2", ".textile", ".markdown", ".asciidoc", ".rst", ".wpd", ".wps", ".abw", ".zabw", + + // Spreadsheets + ".xls", ".xlsx", ".csv", ".ods", ".numbers", ".tsv", ".gnumeric", ".xlt", ".xltx", ".xlsm", ".xltm", ".xlam", ".xlsb", + + // Presentations + ".ppt", ".pptx", ".key", ".odp", ".pps", ".ppsx", ".pptm", ".potx", ".potm", ".ppam", + + // Images + ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", ".svg", ".webp", ".ico", ".psd", ".ai", ".eps", ".raw", ".cr2", ".nef", ".orf", ".sr2", ".heic", ".heif", ".avif", ".jxr", ".hdp", ".wdp", ".jng", ".xcf", ".pgm", ".pbm", ".ppm", ".pnm", + + // Video + ".mp4", ".avi", ".mov", ".wmv", ".mkv", ".flv", ".webm", ".m4v", ".mpeg", ".mpg", ".3gp", ".3g2", ".ogv", ".vob", ".drc", ".gifv", ".mng", ".qt", ".yuv", ".rm", ".rmvb", ".asf", ".amv", ".m2v", ".svi", + + // Audio + ".mp3", ".wav", ".aac", ".flac", ".ogg", ".m4a", ".wma", ".aiff", ".alac", ".ape", ".au", ".mid", ".midi", ".mka", ".pcm", ".ra", ".ram", ".snd", ".wv", ".opus", + + // Code/Development + ".js", ".ts", ".html", ".css", ".json", ".xml", ".php", ".py", ".java", ".cpp", ".c", ".cs", ".h", ".hpp", ".hxx", ".rs", ".go", ".rb", ".pl", ".swift", ".kt", ".kts", ".scala", ".coffee", ".sass", ".scss", ".less", ".jsx", ".tsx", ".vue", ".sh", ".bash", ".zsh", ".fish", ".ps1", ".bat", ".cmd", ".sql", ".r", ".dart", ".f", ".f90", ".for", ".lua", ".m", ".mm", ".clj", ".erl", ".ex", ".exs", ".elm", ".hs", ".lhs", ".lisp", ".ml", ".mli", ".nim", ".pl", ".rkt", ".v", ".vhd", + + // Archives + ".zip", ".rar", ".7z", ".tar", ".gz", ".bz2", ".xz", ".z", ".lz", ".lzma", ".tlz", ".txz", ".tgz", ".tbz2", ".bz", ".br", ".lzo", ".ar", ".cpio", ".shar", ".lrz", ".lz4", ".lz2", ".rz", ".sfark", ".sz", ".zoo", + + // Database + ".db", ".sql", ".sqlite", ".sqlite3", ".dbf", ".mdb", ".accdb", ".db3", ".s3db", ".dbx", + + // Fonts + ".ttf", ".otf", ".woff", ".woff2", ".eot", ".pfa", ".pfb", ".sfd", + + // CAD and 3D + ".dwg", ".dxf", ".stl", ".obj", ".fbx", ".dae", ".3ds", ".blend", ".max", ".ma", ".mb", ".c4d", ".skp", ".usd", ".usda", ".usdc", ".abc", + + // Scientific/Technical + ".mat", ".fig", ".nb", ".cdf", ".fits", ".fts", ".fit", ".gmsh", ".msh", ".fem", ".neu", ".hdf", ".h5", ".nx", ".unv", + + // System + ".exe", ".dll", ".so", ".dylib", ".app", ".dmg", ".iso", ".img", ".bin", ".msi", ".apk", ".ipa", ".deb", ".rpm", + + // Directory + ".directory" + ], + }) + + // -------------------------------------------------------- + // Dragster + // -------------------------------------------------------- + let drop_area_content = drop_area_placeholder; + + $('.drop-area').dragster({ + enter: function (dragsterEvent, event) { + drop_area_content = $('.drop-area').html(); + $('.drop-area').addClass('drop-area-hover'); + $('.drop-area').html(drop_area_placeholder); + }, + leave: function (dragsterEvent, event) { + $('.drop-area').html(drop_area_content); + $('.drop-area').removeClass('drop-area-hover'); + }, + drop: async function (dragsterEvent, event) { + const e = event.originalEvent; + e.stopPropagation(); + e.preventDefault(); + + // hide previous success message + $('.deploy-success-msg').fadeOut(); + + // remove hover class + $('.drop-area').removeClass('drop-area-hover'); + + //---------------------------------------------------- + // Puter items dropped + //---------------------------------------------------- + if (e.detail?.items?.length > 0) { + let items = e.detail.items; + + // ---------------------------------------------------- + // One Puter file dropped + // ---------------------------------------------------- + if (items.length === 1 && !items[0].isDirectory) { + if (items[0].name.toLowerCase() === 'index.html') { + dropped_items = items[0].path; + $('.drop-area').removeClass('drop-area-hover'); + $('.drop-area').addClass('drop-area-ready-to-deploy'); + drop_area_content = `

index.html

Ready to deploy 🚀

Cancel

`; + $('.drop-area').html(drop_area_content); + + // enable deploy button + $('.deploy-btn').removeClass('disabled'); + + } else { + puter.ui.alert(`You need to have an index.html file in your deployment.`, [ + { + label: 'Ok', + }, + ]); + $('.drop-area').removeClass('drop-area-ready-to-deploy'); + $('.deploy-btn').addClass('disabled'); + dropped_items = []; + } + return; + } + // ---------------------------------------------------- + // Multiple Puter files dropped + // ---------------------------------------------------- + else if (items.length > 1) { + let hasIndexHtml = false; + for (let item of items) { + if (item.name.toLowerCase() === 'index.html') { + hasIndexHtml = true; + break; + } + } + + if (hasIndexHtml) { + dropped_items = items; + $('.drop-area').removeClass('drop-area-hover'); + $('.drop-area').addClass('drop-area-ready-to-deploy'); + drop_area_content = `

${items.length} items

Ready to deploy 🚀

Cancel

`; + $('.drop-area').html(drop_area_content); + + // enable deploy button + $('.deploy-btn').removeClass('disabled'); + } else { + puter.ui.alert(`You need to have an index.html file in your deployment.`, [ + { + label: 'Ok', + }, + ]); + $('.drop-area').removeClass('drop-area-ready-to-deploy'); + $('.drop-area').removeClass('drop-area-hover'); + $('.deploy-btn').addClass('disabled'); + dropped_items = []; + } + return; + } + // ---------------------------------------------------- + // One Puter directory dropped + // ---------------------------------------------------- + else if (items.length === 1 && items[0].isDirectory) { + let children = await puter.fs.readdir(items[0].path); + // check if index.html exists, if found, deploy entire directory + for (let child of children) { + if (child.name === 'index.html') { + // deploy(currently_editing_app, items[0].path); + dropped_items = items[0].path; + let rootItems = ''; + + if (children.length === 1) + rootItems = children[0].name; + else if (children.length === 2) + rootItems = children[0].name + ', ' + children[1].name; + else if (children.length === 3) + rootItems = children[0].name + ', ' + children[1].name + ', and' + children[1].name; + else if (children.length > 3) + rootItems = children[0].name + ', ' + children[1].name + ', and ' + (children.length - 2) + ' more item' + (children.length - 2 > 1 ? 's' : ''); + + $('.drop-area').removeClass('drop-area-hover'); + $('.drop-area').addClass('drop-area-ready-to-deploy'); + drop_area_content = `

${rootItems}

Ready to deploy 🚀

Cancel

`; + $('.drop-area').html(drop_area_content); + + // enable deploy button + $('.deploy-btn').removeClass('disabled'); + return; + } + } + + // no index.html in directory + puter.ui.alert(index_missing_error, [ + { + label: 'Ok', + }, + ]); + $('.drop-area').removeClass('drop-area-ready-to-deploy'); + $('.deploy-btn').addClass('disabled'); + dropped_items = []; + } + + return false; + } + + //----------------------------------------------------------------------------- + // Local items dropped + //----------------------------------------------------------------------------- + if (!e.dataTransfer || !e.dataTransfer.items || e.dataTransfer.items.length === 0) + return; + + // get dropped items + dropped_items = await puter.ui.getEntriesFromDataTransferItems(e.dataTransfer.items); + + // generate a flat array of full paths from the dropped items + let paths = []; + for (let item of dropped_items) { + paths.push('/' + (item.fullPath ?? item.filepath)); + } + + // generate a directory tree from the paths + let tree = generateDirTree(paths); + + dropped_items = setRootDirTree(tree, dropped_items); + + // alert if no index.html in root + if (!hasRootIndexHtml(tree)) { + puter.ui.alert(index_missing_error, [ + { + label: 'Ok', + }, + ]); + $('.drop-area').removeClass('drop-area-ready-to-deploy'); + $('.deploy-btn').addClass('disabled'); + dropped_items = []; + return; + } + + // Get all keys (directories and files) in the root + const rootKeys = Object.keys(tree); + + // generate a list of items in the root in the form of a string (e.g. /index.html, /css/style.css) with maximum of 3 items + let rootItems = ''; + + if (rootKeys.length === 1) + rootItems = rootKeys[0]; + else if (rootKeys.length === 2) + rootItems = rootKeys[0] + ', ' + rootKeys[1]; + else if (rootKeys.length === 3) + rootItems = rootKeys[0] + ', ' + rootKeys[1] + ', and' + rootKeys[1]; + else if (rootKeys.length > 3) + rootItems = rootKeys[0] + ', ' + rootKeys[1] + ', and ' + (rootKeys.length - 2) + ' more item' + (rootKeys.length - 2 > 1 ? 's' : ''); + + rootItems = html_encode(rootItems); + $('.drop-area').removeClass('drop-area-hover'); + $('.drop-area').addClass('drop-area-ready-to-deploy'); + drop_area_content = `

${rootItems}

Ready to deploy 🚀

Cancel

`; + $('.drop-area').html(drop_area_content); + + // enable deploy button + $('.deploy-btn').removeClass('disabled'); + + return false; + } + }); + + // Focus on the first input + $('#edit-app-title').focus(); + + try { + activate_tippy(); + } catch (e) { + console.log('no tippy:', e); + } + + // Custom function to handle bulk pasting of file extensions + if (tagify) { + // Create a completely separate paste handler + const handleBulkPaste = function(e) { + const clipboardData = e.clipboardData || window.clipboardData; + if (!clipboardData) return; + + const pastedText = clipboardData.getData('text'); + if (!pastedText) return; + + // Check if the pasted text contains delimiters + if (/[,;\t\s]/.test(pastedText)) { + e.stopPropagation(); + e.preventDefault(); + + // Process the pasted text to extract extensions + const extensions = pastedText.split(/[,;\t\s]+/) + .map(ext => ext.trim()) + .filter(ext => ext && (ext.startsWith('.') || ext.includes('/'))); + + if (extensions.length > 0) { + // Get existing values to prevent duplicates + const existingValues = tagify.value.map(tag => tag.value); + + // Only add extensions that don't already exist + const newExtensions = extensions.filter(ext => !existingValues.includes(ext)); + + if (newExtensions.length > 0) { + // Add the new tags + tagify.addTags(newExtensions); + + // Update the UI + setTimeout(() => { + toggleSaveButton(); + toggleResetButton(); + }, 10); + } + } + + // Clear the input element to prevent any text concatenation + setTimeout(() => { + if (tagify.DOM.input) { + tagify.DOM.input.textContent = ''; + } + }, 10); + } + }; + + // Add the paste handler directly to the tagify wrapper element + const tagifyWrapper = tagify.DOM.scope; + if (tagifyWrapper) { + tagifyWrapper.addEventListener('paste', handleBulkPaste, true); + } + + // Also add it to the input element for better coverage + if (tagify.DOM.input) { + tagify.DOM.input.addEventListener('paste', handleBulkPaste, true); + } + + // Add a comma key handler to support adding tags with comma + tagify.DOM.input.addEventListener('keydown', function(e) { + if (e.key === ',' && tagify.DOM.input.textContent.trim()) { + e.preventDefault(); + + const text = tagify.DOM.input.textContent.trim(); + + // Only add valid extensions + if ((text.startsWith('.') || text.includes('/')) && + tagify.settings.pattern.test(text)) { + + // Check for duplicates + const existingValues = tagify.value.map(tag => tag.value); + + if (!existingValues.includes(text)) { + tagify.addTags([text]); + + // Update UI + setTimeout(() => { + toggleSaveButton(); + toggleResetButton(); + }, 10); + } + + // Always clear the input + tagify.DOM.input.textContent = ''; + } + } + }); + } +} + +$(document).on('click', '.edit-app-save-btn', async function (e) { + const title = $('#edit-app-title').val(); + const name = $('#edit-app-name').val(); + const index_url = $('#edit-app-index-url').val(); + const description = $('#edit-app-description').val(); + const uid = $('#edit-app-uid').val(); + const height = $('#edit-app-window-height').val(); + const width = $('#edit-app-window-width').val(); + const top = $('#edit-app-window-top').val(); + const left = $('#edit-app-window-left').val(); + const category = $('#edit-app-category').val(); + + let filetype_associations = $('#edit-app-filetype-associations').val(); + + let icon; + + let error; + + //validation + if (title === '') + error = `Title is required.`; + else if (title.length > 60) + error = `Title cannot be longer than ${60}.`; + else if (name === '') + error = `Name is required.`; + else if (name.length > 60) + error = `Name cannot be longer than ${60}.`; + else if (index_url === '') + error = `Index URL is required.`; + else if (!name.match(/^[a-zA-Z0-9-_-]+$/)) + error = `Name can only contain letters, numbers, dash (-) and underscore (_).`; + else if (!is_valid_url(index_url)) + error = `Index URL must be a valid url.`; + else if (!index_url.toLowerCase().startsWith('https://') && !index_url.toLowerCase().startsWith('http://')) + error = `Index URL must start with 'https://' or 'http://'.`; + // height must be a number + else if (isNaN(height)) + error = `Window Height must be a number.`; + // height must be greater than 0 + else if (height <= 0) + error = `Window Height must be greater than 0.`; + // width must be a number + else if (isNaN(width)) + error = `Window Width must be a number.`; + // width must be greater than 0 + else if (width <= 0) + error = `Window Width must be greater than 0.`; + // top must be a number + else if (top && isNaN(top)) + error = `Window Top must be a number.`; + // left must be a number + else if (left && isNaN(left)) + error = `Window Left must be a number.`; + + // download icon from URL + else { + let icon_url = $('#edit-app-icon').attr('data-url'); + let icon_base64 = $('#edit-app-icon').attr('data-base64'); + + if(icon_base64){ + icon = icon_base64; + }else if (icon_url) { + icon = await getBase64ImageFromUrl(icon_url); + let app_max_icon_size = 5 * 1024 * 1024; + if (icon.length > app_max_icon_size) + error = `Icon cannot be larger than ${byte_format(app_max_icon_size)}`; + // make sure icon is an image + else if (!icon.startsWith('data:image/') && !icon.startsWith('data:application/octet-stream')) + error = `Icon must be an image.`; + }else{ + icon = null; + } + } + + // parse filetype_associations + if(filetype_associations !== ''){ + filetype_associations = JSON.parse(filetype_associations); + filetype_associations = filetype_associations.map((type) => { + const fileType = type.value; + if ( + !fileType || + fileType === "." || + fileType === "/" + ) { + error = `File Association Type must be valid.`; + return null; // Return null for invalid cases + } + const lower = fileType.toLocaleLowerCase(); + + if (fileType.includes("/")) { + return lower; + } else if (fileType.includes(".")) { + return "." + lower.split(".")[1]; + } else { + return "." + lower; + } + }).filter(Boolean); + } + + // error? + if (error) { + $('#edit-app-error').show(); + $('#edit-app-error').html(error); + document.body.scrollTop = document.documentElement.scrollTop = 0; + return; + } + + // show working spinner + puter.ui.showSpinner(); + + // disable submit button + $('.edit-app-save-btn').prop('disabled', true); + + let socialImageUrl = null; + if ($('#edit-app-social-image').attr('data-base64')) { + socialImageUrl = await handleSocialImageUpload(name, $('#edit-app-social-image').attr('data-base64')); + } else if ($('#edit-app-social-image').attr('data-url')) { + socialImageUrl = $('#edit-app-social-image').attr('data-url'); + } + + puter.apps.update(currently_editing_app.name, { + title: title, + name: name, + indexURL: index_url, + icon: icon, + description: description, + maximizeOnStart: $('#edit-app-maximize-on-start').is(":checked"), + background: $('#edit-app-background').is(":checked"), + metadata: { + fullpage_on_landing: $('#edit-app-fullpage-on-landing').is(":checked"), + social_image: socialImageUrl, + category: category || null, + window_size: { + width: width ?? 800, + height: height ?? 600, + }, + window_position: { + top: top, + left: left, + }, + window_resizable: $('#edit-app-window-resizable').is(":checked"), + hide_titlebar: $('#edit-app-hide-titlebar').is(":checked"), + locked: $(`#edit-app-locked`).is(":checked") ?? false, + }, + filetypeAssociations: filetype_associations, + }).then(async (app) => { + refresh_app_list(); + + currently_editing_app = app; + trackOriginalValues(); // Update original values after save + toggleSaveButton(); //Disable Save Button after succesful save + toggleResetButton(); //DIsable Reset Button after succesful save + $('#edit-app-error').hide(); + $('#edit-app-success').show(); + document.body.scrollTop = document.documentElement.scrollTop = 0; + // Update open-app-btn + $(`.open-app-btn[data-app-uid="${uid}"]`).attr('data-app-name', app.name); + $(`.open-app[data-uid="${uid}"]`).attr('data-app-name', app.name); + // Update title + $(`.app-title[data-uid="${uid}"]`).html(html_encode(app.title)); + // Update app link + $(`.app-url[data-uid="${uid}"]`).html(applink(app)); + $(`.app-url[data-uid="${uid}"]`).attr('href', applink(app)); + // Update icons + $(`.app-icon[data-uid="${uid}"]`).attr('src', html_encode(app.icon ? app.icon : './img/app.svg')); + $(`[data-app-uid="${uid}"]`).attr('data-app-title', html_encode(app.title)); + $(`[data-app-name="${uid}"]`).attr('data-app-name', html_encode(app.name)); + }).catch((err) => { + $('#edit-app-success').hide(); + $('#edit-app-error').show(); + $('#edit-app-error').html(err.error?.message); + // scroll to top so that user sees error message + document.body.scrollTop = document.documentElement.scrollTop = 0; + // re-enable submit button + $('.edit-app-save-btn').prop('disabled', false); + }).finally(() => { + puter.ui.hideSpinner(); + }) +}) + +$(document).on('input change', '#edit-app input, #edit-app textarea, #edit-app select', () => { + toggleSaveButton(); + toggleResetButton(); +}); + +$(document).on('click', '.edit-app-reset-btn', function () { + resetToOriginalValues(); + toggleSaveButton(); // Disable Save button since values are reverted to original + toggleResetButton(); //Disable Reset button since values are reverted to original +}); + +$(document).on('click', '.open-app-btn', async function (e) { + puter.ui.launchApp($(this).attr('data-app-name')) +}) + +$(document).on('click', '.edit-app-open-app-btn', async function (e) { + puter.ui.launchApp($(this).attr('data-app-name')) +}) + +$(document).on('click', '.delete-app-settings', async function (e) { + let app_uid = $(this).attr('data-app-uid'); + let app_name = $(this).attr('data-app-name'); + let app_title = $(this).attr('data-app-title'); + + // check if app is locked + const app_data = await puter.apps.get(app_name, {icon_size: 16}); + + if(app_data.metadata?.locked){ + puter.ui.alert(`${app_data.title} is locked and cannot be deleted.`, [ + { + label: 'Ok', + }, + ], { + type: 'warning', + }); + return; + } + + // confirm delete + const alert_resp = await puter.ui.alert(`Are you sure you want to premanently delete ${html_encode(app_title)}?`, + [ + { + label: 'Yes, delete permanently', + value: 'delete', + type: 'danger', + }, + { + label: 'Cancel' + }, + ] + ); + + if (alert_resp === 'delete') { + let init_ts = Date.now(); + puter.ui.showSpinner(); + puter.apps.delete(app_name).then(async (app) => { + setTimeout(() => { + puter.ui.hideSpinner(); + $('.back-to-main-btn').trigger('click'); + }, + // make sure the modal was shown for at least 2 seconds + (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); + // get app directory + puter.fs.stat({ + path: `/${auth_username}/AppData/${dev_center_uid}/${app_uid}`, + returnSubdomains: true, + }).then(async (stat) => { + // delete subdomain associated with the app dir + puter.hosting.delete(stat.subdomains[0].subdomain) + // delete app directory + puter.fs.delete( + `/${auth_username}/AppData/${dev_center_uid}/${app_uid}`, + { recursive: true } + ) + }) + }).catch(async (err) => { + setTimeout(() => { + puter.ui.hideSpinner(); + puter.ui.alert(err?.message, [ + { + label: 'Ok', + }, + ]); + }, + (Date.now() - init_ts) > 2000 ? 1 : (2000 - (Date.now() - init_ts))); + }) + } +}) + +$(document).on('click', '.edit-app', async function (e) { + $('#edit-app-uid').val($(this).attr('data-app-uid')); +}) + +$(document).on('click', '.back-to-main-btn', function (e) { + $('section:not(.sidebar)').hide(); + $('.tab-btn').removeClass('active'); + $('.tab-btn[data-tab="apps"]').addClass('active'); + + // get apps + puter.ui.showSpinner(); + setTimeout(function () { + puter.apps.list({icon_size: 64}).then((apps_res) => { + // uncheck the select all checkbox + $('.select-all-apps').prop('checked', false); + + puter.ui.hideSpinner(); + apps = apps_res; + if (apps.length > 0) { + if (window.activeTab === 'apps') { + $('#no-apps-notice').hide(); + $('#app-list').show(); + } + $('.app-card').remove(); + apps.forEach(app => { + $('#app-list-table > tbody').append(generate_app_card(app)); + }); + count_apps(); + sort_apps(); + activate_tippy(); + } else + $('#no-apps-notice').show(); + }) + }, 1000); +}) + +function count_apps() { + let count = 0; + $('.app-card').each(function () { + count++; + }) + $('.app-count').html(count); + return count; +} + +$(document).on('click', '#edit-app-icon-delete', async function (e) { + $('#edit-app-icon').css('background-image', ``); + $('#edit-app-icon').removeAttr('data-url'); + $('#edit-app-icon').removeAttr('data-base64'); + $('#edit-app-icon-delete').hide(); + + toggleSaveButton(); + toggleResetButton(); +}) + +$(document).on('click', '#edit-app-icon', async function (e) { + const res2 = await puter.ui.showOpenFilePicker({ + accept: "image/*", + }); + + const icon = await puter.fs.read(res2.path); + // convert blob to base64 + const reader = new FileReader(); + reader.readAsDataURL(icon); + + reader.onloadend = function () { + let image = reader.result; + // Get file extension + let fileExtension = res2.name.split('.').pop(); + + // Get MIME type + let mimeType = getMimeType(fileExtension); + + // Replace MIME type in the data URL + image = image.replace('data:application/octet-stream;base64', `data:${mimeType};base64`); + + $('#edit-app-icon').css('background-image', `url(${image})`); + $('#edit-app-icon').attr('data-base64', image); + $('#edit-app-icon-delete').show(); + + toggleSaveButton(); + toggleResetButton(); + } +}) + + +/** + * Generates HTML for an individual app card in the app list. + * + * @param {Object} app - The app object containing details of the app. + * * + * @returns {string} HTML string representing the app card. + * + * @description + * This function creates an HTML string for an app card, which includes: + * - Checkbox for app selection + * - App icon and title + * - Links to open, edit, add to desktop, or delete the app + * - Display of app statistics (user count, open count) + * - Creation date + * - Incentive program status badge (if applicable) + * + * The generated HTML is designed to be inserted into the app list table. + * It includes data attributes for various interactive features and + * event handling. + * + * @example + * const appCardHTML = generate_app_card(myAppObject); + * $('#app-list-table > tbody').append(appCardHTML); + */ +function generate_app_card(app) { + let h = ``; +h += ``; + // check box + h += ``; + h += `
`; + h += ``; + h += `
`; + h += ``; + // App info (title, category, toolbar) +h += ``; + + // Wrapper for icon + content side by side +h += `
`; + + // Icon + h += `
`; + + // App info content + h += `
`; + + // Info block with fixed layout +h += `
`; + + // Title + h += `

+ ${html_encode(app.title)}${app.metadata?.locked ? lock_svg_tippy : ''} +

`; + + // Category (optional) + if (app.metadata?.category) { + const category = APP_CATEGORIES.find(c => c.id === app.metadata.category); + if (category) { + h += `${html_encode(category.label)}`; + } + } + + // Link + h += `${html_encode(applink(app))}`; + +h += `
`; + + + + // Toolbar + h += `
`; + h += `
`; // remove inline styles + + h += `Open`; + h += `Settings`; + h += `Add Shortcut to Desktop`; + h += `Delete`; + h += `
`; +h += `
`; + h += `
`; // end info column + h += `
`; // end row +h += ``; + + + // users count + h += ``; + h += `${number_format((app.stats.referral_count ?? 0) + app.stats.user_count)}`; + h += ``; + + // opens + h += ``; + h += `${number_format(app.stats.open_count)}`; + h += ``; + + // Created + h += ``; + h += `${moment(app.created_at).format('MMM Do, YYYY')}`; + h += ``; + + h += ``; + h += `
`; + // "Approved for listing" + h += ``; + + // "Approved for opening items" + h += ``; + + // "Approved for incentive program" + h += ``; + h += `
`; + h += ``; + h += ``; + return h; +} + +$('th.sort').on('click', function (e) { + // determine what column to sort by + const sortByColumn = $(this).attr('data-column'); + + // toggle sort direction + if (sortByColumn === sortBy) { + if (sortDirection === 'asc') + sortDirection = 'desc'; + else + sortDirection = 'asc'; + } + else { + sortBy = sortByColumn; + sortDirection = 'desc'; + } + + // update arrow + $('.sort-arrow').css('display', 'none'); + $('#app-list-table').find('th').removeClass('sorted'); + $(this).find('.sort-arrow-' + sortDirection).css('display', 'inline'); + $(this).addClass('sorted'); + + sort_apps(); +}); + + + +function sort_apps() { + let sorted_apps; + + // sort + if (sortDirection === 'asc'){ + sorted_apps = apps.sort((a, b) => { + if(sortBy === 'name'){ + return a[sortBy].localeCompare(b[sortBy]); + }else if(sortBy === 'created_at'){ + return new Date(a[sortBy]) - new Date(b[sortBy]); + } else if(sortBy === 'user_count' || sortBy === 'open_count'){ + return a.stats[sortBy] - b.stats[sortBy]; + }else{ + a[sortBy] > b[sortBy] ? 1 : -1 + } + }); + }else{ + sorted_apps = apps.sort((a, b) => { + if(sortBy === 'name'){ + return b[sortBy].localeCompare(a[sortBy]); + }else if(sortBy === 'created_at'){ + return new Date(b[sortBy]) - new Date(a[sortBy]); + } else if(sortBy === 'user_count' || sortBy === 'open_count'){ + return b.stats[sortBy] - a.stats[sortBy]; + }else{ + b[sortBy] > a[sortBy] ? 1 : -1 + } + }); + } + // refresh app list + $('.app-card').remove(); + sorted_apps.forEach(app => { + $('#app-list-table > tbody').append(generate_app_card(app)); + }); + + count_apps(); + + // show apps that match search_query and hide apps that don't + if (search_query) { + // show apps that match search_query and hide apps that don't + apps.forEach((app) => { + if (app.title.toLowerCase().includes(search_query.toLowerCase())) { + $(`.app-card[data-name="${html_encode(app.name)}"]`).show(); + } else { + $(`.app-card[data-name="${html_encode(app.name)}"]`).hide(); + } + }) + } +} + +/** + * Checks if the items being deployed contain a .git directory + * @param {Array|string} items - Items to check (can be path string or array of items) + * @returns {Promise} - True if .git directory is found + */ +async function hasGitDirectory(items) { + // Case 1: Single Puter path + if (typeof items === 'string' && (items.startsWith('/') || items.startsWith('~'))) { + const stat = await puter.fs.stat(items); + if (stat.is_dir) { + const files = await puter.fs.readdir(items); + return files.some(file => file.name === '.git' && file.is_dir); + } + return false; + } + + // Case 2: Array of Puter items + if (Array.isArray(items) && items[0]?.uid) { + return items.some(item => item.name === '.git' && item.is_dir); + } + + // Case 3: Local items (DataTransferItems) + if (Array.isArray(items)) { + for (let item of items) { + if (item.fullPath?.includes('/.git/') || + item.path?.includes('/.git/') || + item.filepath?.includes('/.git/')) { + return true; + } + } + } + + return false; +} + +/** + * Shows a warning dialog about .git directory deployment + * @returns {Promise} - True if the user wants to proceed with deployment + */ +async function showGitWarningDialog() { + try { + // Check if the user has chosen to skip the warning + const skipWarning = await puter.kv.get('skip-git-warning'); + + // Log retrieved value for debugging + console.log('Retrieved skip-git-warning:', skipWarning); + + // If the user opted to skip the warning, proceed without showing it + if (skipWarning === true) { + return true; + } + } catch (error) { + console.error('Error accessing KV store:', error); + // If KV store access fails, fall back to showing the dialog + } + + // Create the modal dialog + const modal = document.createElement('div'); + modal.innerHTML = ` +
+

Warning: Git Repository Detected

+

A .git directory was found in your deployment files. Deploying .git directories may:

+
    +
  • Expose sensitive information like commit history and configuration
  • +
  • Significantly increase deployment size
  • +
+
+ + +
+
+ + +
+
+
+ `; + document.body.appendChild(modal); + + return new Promise((resolve) => { + // Handle "Continue Deployment" + document.getElementById('continue-deployment').addEventListener('click', async () => { + try { + const skipChecked = document.getElementById('skip-git-warning')?.checked; + if (skipChecked) { + console.log("Saving 'skip-git-warning' preference as true"); + await puter.kv.set('skip-git-warning', true); + } + } catch (error) { + console.error('Error saving user preference to KV store:', error); + } finally { + document.body.removeChild(modal); + resolve(true); // Continue deployment + } + }); + + // Handle "Cancel Deployment" + document.getElementById('cancel-deployment').addEventListener('click', () => { + document.body.removeChild(modal); + resolve(false); // Cancel deployment + }); + }); +} + +window.deploy = async function (app, items) { + // Check for .git directory before proceeding + try { + if (await hasGitDirectory(items)) { + const shouldProceed = await showGitWarningDialog(); + if (!shouldProceed) { + reset_drop_area(); + return; + } + } + } catch (err) { + console.error('Error checking for .git directory:', err); + } + let appdata_dir, current_app_dir; + + // disable deploy button + $('.deploy-btn').addClass('disabled'); + + // change drop area text + $('.drop-area').html(deploying_spinner + '
Deploying (0%)
'); + + if (typeof items === 'string' && (items.startsWith('/') || items.startsWith('~'))) { + $('.drop-area').removeClass('drop-area-hover'); + $('.drop-area').addClass('drop-area-ready-to-deploy'); + } + + // -------------------------------------------------------------------- + // Get current directory, we need to delete the existing hostname + // later on + // -------------------------------------------------------------------- + try { + current_app_dir = await puter.fs.stat({ + path: `/${auth_username}/AppData/${dev_center_uid}/${app.uid ?? app.uuid}`, + returnSubdomains: true + }); + } catch (err) { + console.log(err); + } + + // -------------------------------------------------------------------- + // Delete existing hostnames attached to this app directory if they exist + // -------------------------------------------------------------------- + if (current_app_dir?.subdomains.length > 0) { + for (let subdomain of current_app_dir?.subdomains) { + puter.hosting.delete(subdomain.subdomain) + } + } + + // -------------------------------------------------------------------- + // Delete existing app directory + // -------------------------------------------------------------------- + try { + await puter.fs.delete(current_app_dir.path) + } catch (err) { + console.log(err); + } + + // -------------------------------------------------------------------- + // Make an app directory under AppData + // if the directory already exists, it should be overwritten + // -------------------------------------------------------------------- + try { + appdata_dir = await puter.fs.mkdir( + // path + `/${auth_username}/AppData/${dev_center_uid}/${app.uid ?? app.uuid}`, + // options + { overwrite: true, recursive: true, rename: false } + ) + } catch (err) { + console.log(err); + } + + // -------------------------------------------------------------------- + // (A) One Puter Item: If 'items' is a string and starts with /, it's a path to a Puter item + // -------------------------------------------------------------------- + if (typeof items === 'string' && (items.startsWith('/') || items.startsWith('~'))) { + // perform stat on 'items' + const stat = await puter.fs.stat(items); + + // -------------------------------------------------------------------- + // Puter Directory + // -------------------------------------------------------------------- + // Perform readdir on 'items' + // todo there is apparently a bug in Puter where sometimes path is literally missing from the items + // returned by readdir. This is the 'path' that readdit didn't return a path for: "~/Desktop/particle-clicker-master" + if (stat.is_dir) { + const files = await puter.fs.readdir(items); + // copy the 'files' to the app directory + if (files.length > 0) { + for (let file of files) { + // perform copy + await puter.fs.copy( + file.path, + appdata_dir.path, + { overwrite: true } + ); + // update progress + $('.deploy-percent').text(`(${Math.round((files.indexOf(file) / files.length) * 100)}%)`); + } + } + } + // -------------------------------------------------------------------- + // Puter File + // -------------------------------------------------------------------- + else { + // copy the 'files' to the app directory + await puter.fs.copy( + items, + appdata_dir.path, + { overwrite: true } + ); + } + + // generate new hostname with a random suffix + let hostname = `${currently_editing_app.name}-${(Math.random() + 1).toString(36).substring(7)}`; + + // -------------------------------------------------------------------- + // Create a router for the app with the fresh hostname + // we change hostname every time to prevent caching issues + // -------------------------------------------------------------------- + puter.hosting.create(hostname, appdata_dir.path).then(async (res) => { + // TODO this endpoint needs to be able to update only the specified fields + puter.apps.update(currently_editing_app.name, { + indexURL: protocol + `://${hostname}.` + static_hosting_domain, + title: currently_editing_app.title, + name: currently_editing_app.name, + icon: currently_editing_app.icon, + description: currently_editing_app.description, + maximizeOnStart: currently_editing_app.maximize_on_start, + background: currently_editing_app.background, + filetypeAssociations: currently_editing_app.filetype_associations, + }) + // set the 'Index URL' field for the 'Settings' tab + $('#edit-app-index-url').val(protocol + `://${hostname}.` + static_hosting_domain); + // show success message + $('.deploy-success-msg').show(); + // reset drop area + reset_drop_area(); + }) + } + // -------------------------------------------------------------------- + // (B) Multiple Puter Items: If `items` is an Array `items[0]` has `uid` + // then it's a Puter Item Array. + // -------------------------------------------------------------------- + else if (Array.isArray(items) && items[0].uid) { + // If there's no index.html in the root, return + if (!hasRootIndexHtml) + return; + + // copy the 'files' to the app directory + for (let item of items) { + // perform copy + await puter.fs.copy( + item.fullPath ? item.fullPath : item.path ? item.path : item.filepath, + appdata_dir.path, + { overwrite: true } + ); + // update progress + $('.deploy-percent').text(`(${Math.round((items.indexOf(item) / items.length) * 100)}%)`); + } + + // generate new hostname with a random suffix + let hostname = `${currently_editing_app.name}-${(Math.random() + 1).toString(36).substring(7)}`; + + // -------------------------------------------------------------------- + // Create a router for the app with the fresh hostname + // we change hostname every time to prevent caching issues + // -------------------------------------------------------------------- + puter.hosting.create(hostname, appdata_dir.path).then(async (res) => { + // TODO this endpoint needs to be able to update only the specified fields + puter.apps.update(currently_editing_app.name, { + indexURL: protocol + `://${hostname}.` + static_hosting_domain, + title: currently_editing_app.title, + name: currently_editing_app.name, + icon: currently_editing_app.icon, + description: currently_editing_app.description, + maximizeOnStart: currently_editing_app.maximize_on_start, + background: currently_editing_app.background, + filetypeAssociations: currently_editing_app.filetype_associations, + }) + // set the 'Index URL' field for the 'Settings' tab + $('#edit-app-index-url').val(protocol + `://${hostname}.` + static_hosting_domain); + // show success message + $('.deploy-success-msg').show(); + // reset drop area + reset_drop_area(); + }) + } + + // -------------------------------------------------------------------- + // (C) Local Items: Upload new deploy + // -------------------------------------------------------------------- + else { + puter.fs.upload( + items, + `/${auth_username}/AppData/${dev_center_uid}/${currently_editing_app.uid}`, + { + dedupeName: false, + overwrite: false, + parsedDataTransferItems: true, + createMissingAncestors: true, + progress: function (operation_id, op_progress) { + $('.deploy-percent').text(`(${op_progress}%)`); + }, + }).then(async (uploaded) => { + // new hostname + let hostname = `${currently_editing_app.name}-${(Math.random() + 1).toString(36).substring(7)}`; + + // ---------------------------------------- + // Create a router for the app with a fresh hostname + // we change hostname every time to prevent caching issues + // ---------------------------------------- + puter.hosting.create(hostname, appdata_dir.path).then(async (res) => { + // TODO this endpoint needs to be able to update only the specified fields + puter.apps.update(currently_editing_app.name, { + indexURL: protocol + `://${hostname}.` + static_hosting_domain, + title: currently_editing_app.title, + name: currently_editing_app.name, + icon: currently_editing_app.icon, + description: currently_editing_app.description, + maximizeOnStart: currently_editing_app.maximize_on_start, + background: currently_editing_app.background, + filetypeAssociations: currently_editing_app.filetype_associations, + }) + // set the 'Index URL' field for the 'Settings' tab + $('#edit-app-index-url').val(protocol + `://${hostname}.` + static_hosting_domain); + // show success message + $('.deploy-success-msg').show(); + // reset drop area + reset_drop_area() + }) + }) + } +} + + +function generateDirTree(paths) { + const root = {}; + + for (let path of paths) { + let parts = path.split('/'); + let currentNode = root; + for (let part of parts) { + if (!part) continue; // skip empty parts, especially leading one + if (!currentNode[part]) { + currentNode[part] = {}; + } + currentNode = currentNode[part]; + } + } + + return root; +} + +function setRootDirTree(tree, items) { + // Get all keys (directories and files) in the root + const rootKeys = Object.keys(tree); + + // If there's only one object in the root, check if it's non-empty and return it + if (rootKeys.length === 1 && typeof tree[rootKeys[0]] === 'object' && Object.keys(tree[rootKeys[0]]).length > 0) { + let newItems = []; + for (let item of items) { + if (item.fullPath) + item.finalPath = item.fullPath.replace(rootKeys[0], ''); + else if (item.path) + item.path = item.path.replace(rootKeys[0], ''); + else + item.filepath = item.filepath.replace(rootKeys[0], ''); + + newItems.push(item); + } + return newItems; + } else { + return items; + } +} + +function hasRootIndexHtml(tree) { + // Check if index.html exists in the root + if (tree['index.html']) { + return true; + } + + // Get all keys (directories and files) in the root + const rootKeys = Object.keys(tree); + + // If there's only one directory in the root, check if index.html exists in that directory + if (rootKeys.length === 1 && typeof tree[rootKeys[0]] === 'object' && tree[rootKeys[0]]['index.html']) { + return true; + } + + return false; +} + + +$(document).on('click', '.open-app', function (e) { + puter.ui.launchApp($(this).attr('data-app-name')); +}) + +$(document).on('click', '.insta-deploy-to-new-app', async function (e) { + $('.insta-deploy-modal').get(0).close(); + let title = await puter.ui.prompt('Please enter a title for your app:', 'My Awesome App'); + + if (title.length > 60) { + puter.ui.alert(`Title cannot be longer than 60.`, [ + { + label: 'Ok', + }, + ]); + // todo go back to create an app prompt and prefill the title input with the title the user entered + $('.insta-deploy-modal').get(0).showModal(); + } + else if (title) { + if (source_path) { + create_app(title, source_path); + source_path = null; + } else { + create_app(title, null, dropped_items); + dropped_items = null; + } + } else + $('.insta-deploy-modal').get(0).showModal(); + + return; + +}) + +$(document).on('click', '.insta-deploy-to-existing-app', function (e) { + $('.insta-deploy-modal').get(0).close(); + $('.insta-deploy-existing-app-select').get(0).showModal(); + $('.insta-deploy-existing-app-list').html(`
${loading_spinner}
`); + puter.apps.list({ icon_size: 64 }).then((apps) => { + setTimeout(() => { + $('.insta-deploy-existing-app-list').html(''); + if (apps.length === 0) + $('.insta-deploy-existing-app-list').html(` +
+ + You have no existing apps. +
+ `); + else { + for (let app of apps) { + $('.insta-deploy-existing-app-list').append( + `
+ + ${html_encode(app.title)} +
+ ${number_format((app.stats.referral_count ?? 0) + app.stats.user_count)} + ${number_format(app.stats.open_count)} +
+
` + ); + } + } + }, 500); + }) + + // todo reset .insta-deploy-existing-app-list on close +}) + +$(document).on('click', '.insta-deploy-app-selector', function (e) { + $('.insta-deploy-app-selector').removeClass('active'); + $(this).addClass('active'); + + // enable deploy button + $('.insta-deploy-existing-app-deploy-btn').removeClass('disabled'); +}) + +$(document).on('click', '.insta-deploy-existing-app-deploy-btn', function (e) { + $('.insta-deploy-existing-app-deploy-btn').addClass('disabled'); + $('.insta-deploy-existing-app-select')?.get(0)?.close(); + + const app_item = $('.insta-deploy-app-selector.active'); + + // load the 'App Settings' section + edit_app_section(app_item.attr('data-name')); + + $('.drop-area').removeClass('drop-area-hover'); + $('.drop-area').addClass('drop-area-ready-to-deploy'); + let drop_area_content = `

Ready to deploy 🚀

Cancel

`; + $('.drop-area').html(drop_area_content); + + // deploy + console.log('data uid is present?', $(e.target).attr('data-uid'), app_item.attr('data-uid')); + deploy({ uid: app_item.attr('data-uid') }, source_path ?? dropped_items); + $('.insta-deploy-existing-app-list').html(''); +}) + +$(document).on('click', '.insta-deploy-cancel', function (e) { + $(this).closest('dialog')?.get(0)?.close(); +}) +$(document).on('click', '.insta-deploy-existing-app-back', function (e) { + $('.insta-deploy-existing-app-select')?.get(0)?.close(); + $('.insta-deploy-modal')?.get(0)?.showModal(); + // disable deploy button + $('.insta-deploy-existing-app-deploy-btn').addClass('disabled'); + + // todo disable the 'an existing app' option if there are no existing apps +}) + + + +$(document).on('click', '.add-app-to-desktop', function (e) { + let app_title = $(this).attr('data-app-title'); + let app_uid = $(this).attr('data-app-uid'); + + puter.fs.upload( + new File([], app_title), + `/${auth_username}/Desktop`, + { + name: app_title, + dedupeName: true, + overwrite: false, + appUID: app_uid, + }).then(async (uploaded) => { + puter.ui.alert(`${app_title} shortcut has been added to your desktop.`, [ + { + label: 'Ok', + type: 'primary', + }, + ], { + type: 'success', + }); + }) + +}) + + +$('.insta-deploy-existing-app-select').on('close', function (e) { + $('.insta-deploy-existing-app-list').html(''); +}) + +$('.refresh-app-list').on('click', function (e) { + puter.ui.showSpinner(); + + puter.apps.list({ icon_size: 64 }).then((resp) => { + setTimeout(() => { + apps = resp; + + $('.app-card').remove(); + apps.forEach(app => { + $('#app-list-table > tbody').append(generate_app_card(app)); + }); + + count_apps(); + + // preserve search query + if (search_query) { + // show apps that match search_query and hide apps that don't + apps.forEach((app) => { + if (app.title.toLowerCase().includes(search_query.toLowerCase())) { + $(`.app-card[data-name="${app.name}"]`).show(); + } else { + $(`.app-card[data-name="${app.name}"]`).hide(); + } + }) + } + + // preserve sort + sort_apps(); + activate_tippy(); + + puter.ui.hideSpinner(); + }, 1000); + }) +}) + +$(document).on('click', '.search', function (e) { + e.stopPropagation(); + e.preventDefault(); + // don't let click bubble up to window + e.stopImmediatePropagation(); +}) + +$(document).on('input change keyup keypress keydown paste cut', '.search', function (e) { + // search apps for query + search_query = $(this).val().toLowerCase(); + if (search_query === '') { + // hide 'clear search' button + $('.search-clear').hide(); + // show all apps again + $(`.app-card`).show(); + } else { + // show 'clear search' button + $('.search-clear').show(); + // show apps that match search_query and hide apps that don't + apps.forEach((app) => { + if ( + app.title.toLowerCase().includes(search_query.toLowerCase()) + || app.name.toLowerCase().includes(search_query.toLowerCase()) + || app.description.toLowerCase().includes(search_query.toLowerCase()) + || app.uid.toLowerCase().includes(search_query.toLowerCase()) + ) + { + $(`.app-card[data-name="${app.name}"]`).show(); + } else { + $(`.app-card[data-name="${app.name}"]`).hide(); + } + }) + } +}) + +$(document).on('click', '.search-clear', function (e) { + $('.search').val(''); + $('.search').trigger('change'); + $('.search').focus(); + search_query = ''; +}) + +$(document).on('change', '.app-checkbox', function (e) { + // determine if select-all checkbox should be checked, indeterminate, or unchecked + if ($('.app-checkbox:checked').length === $('.app-checkbox').length) { + $('.select-all-apps').prop('indeterminate', false); + $('.select-all-apps').prop('checked', true); + } else if ($('.app-checkbox:checked').length > 0) { + $('.select-all-apps').prop('indeterminate', true); + $('.select-all-apps').prop('checked', false); + } + else { + $('.select-all-apps').prop('indeterminate', false); + $('.select-all-apps').prop('checked', false); + } + + // activate row + if ($(this).is(':checked')) + $(this).closest('tr').addClass('active'); + else + $(this).closest('tr').removeClass('active'); + + // enable delete button if at least one checkbox is checked + if ($('.app-checkbox:checked').length > 0) + $('.delete-apps-btn').removeClass('disabled'); + else + $('.delete-apps-btn').addClass('disabled'); + +}) + +$(document).on('click', '.delete-apps-btn', async function (e) { + // show confirmation alert + let resp = await puter.ui.alert(`Are you sure you want to delete the selected apps?`, [ + { + label: 'Delete', + type: 'danger', + value: 'delete', + }, + { + label: 'Cancel', + }, + ], { + type: 'warning', + }); + + if (resp === 'delete') { + // disable delete button + // $('.delete-apps-btn').addClass('disabled'); + + // show 'deleting' modal + puter.ui.showSpinner(); + + let start_ts = Date.now(); + const apps = $('.app-checkbox:checked').toArray(); + + // delete all checked apps + for (let app of apps) { + // get app uid + const app_uid = $(app).attr('data-app-uid'); + const app_name = $(app).attr('data-app-name'); + + // get app + const app_data = await puter.apps.get(app_name, {icon_size: 64 }); + + if(app_data.metadata?.locked){ + if(apps.length === 1){ + puter.ui.alert(`${app_data.title} is locked and cannot be deleted.`, [ + { + label: 'Ok', + }, + ], { + type: 'warning', + }); + + break; + } + + let resp = await puter.ui.alert(`${app_data.title} is locked and cannot be deleted.`, [ + { + label: 'Skip and Continue', + value: 'Continue', + type: 'primary' + }, + { + label: 'Cancel', + }, + ], { + type: 'warning', + }); + + if(resp === 'Cancel') + break; + else if(resp === 'Continue') + continue; + else + continue; + } + + // delete app + await puter.apps.delete(app_name) + + // remove app card + $(`.app-card[data-uid="${app_uid}"]`).fadeOut(200, function name(params) { + $(this).remove(); + if ($(`.app-card`).length === 0) { + $('section:not(.sidebar)').hide(); + $('#no-apps-notice').show(); + } else { + $('section:not(.sidebar)').hide(); + $('#app-list').show(); + } + count_apps(); + }); + + try{ + // get app directory + const stat = await puter.fs.stat({ + path: `/${auth_username}/AppData/${dev_center_uid}/${app_uid}`, + returnSubdomains: true + }); + // delete subdomain associated with the app directory + if(stat?.subdomains[0]?.subdomain){ + await puter.hosting.delete(stat.subdomains[0].subdomain) + } + // delete app directory + await puter.fs.delete( + `/${auth_username}/AppData/${dev_center_uid}/${app_uid}`, + { recursive: true } + ) + count_apps(); + } catch(err) { + console.log(err); + } + } + + // close 'deleting' modal + setTimeout(() => { + puter.ui.hideSpinner(); + if($('.app-checkbox:checked').length === 0){ + // disable delete button + $('.delete-apps-btn').addClass('disabled'); + // reset the 'select all' checkbox + $('.select-all-apps').prop('indeterminate', false); + $('.select-all-apps').prop('checked', false); + } + }, (start_ts - Date.now()) > 500 ? 0 : 500); + } +}) + +$(document).on('change', '.select-all-apps', function (e) { + if ($(this).is(':checked')) { + $('.app-checkbox').prop('checked', true); + $('.app-card').addClass('active'); + $('.delete-apps-btn').removeClass('disabled'); + } else { + $('.app-checkbox').prop('checked', false); + $('.app-card').removeClass('active'); + $('.delete-apps-btn').addClass('disabled'); + } +}) + + +// if edit-app-maximize-on-start is checked, disable window size and position fields +$(document).on('change', '#edit-app-maximize-on-start', function (e) { + if ($(this).is(':checked')) { + $('#edit-app-window-width, #edit-app-window-height').prop('disabled', true); + $('#edit-app-window-top, #edit-app-window-left').prop('disabled', true); + } else { + $('#edit-app-window-width, #edit-app-window-height').prop('disabled', false); + $('#edit-app-window-top, #edit-app-window-left').prop('disabled', false); + } +}) + +$(document).on('change', '#edit-app-background', function (e) { + if($('#edit-app-background').is(":checked")){ + disable_window_settings() + }else{ + enable_window_settings() + } +}) + +function disable_window_settings(){ + $('#edit-app-maximize-on-start').prop('disabled', true); + $('#edit-app-fullpage-on-landing').prop('disabled', true); + $('#edit-app-window-width, #edit-app-window-height').prop('disabled', true); + $('#edit-app-window-top, #edit-app-window-left').prop('disabled', true); + $('#edit-app-window-resizable').prop('disabled', true); + $('#edit-app-hide-titlebar').prop('disabled', true); +} + +function enable_window_settings(){ + $('#edit-app-maximize-on-start').prop('disabled', false); + $('#edit-app-fullpage-on-landing').prop('disabled', false); + $('#edit-app-window-width, #edit-app-window-height').prop('disabled', false); + $('#edit-app-window-top, #edit-app-window-left').prop('disabled', false); + $('#edit-app-window-resizable').prop('disabled', false); + $('#edit-app-hide-titlebar').prop('disabled', false); +} + +$(document).on('click', '.reset-deploy', function (e) { + reset_drop_area(); +}) + + +window.initializeAssetsDirectory = async () => { + try { + // Check if assets_url exists + const existingURL = await puter.kv.get('assets_url'); + if (!existingURL) { + // Create assets directory + const assetsDir = await puter.fs.mkdir( + `/${auth_username}/AppData/${dev_center_uid}/assets`, + { overwrite: false } + ); + + // Publish the directory + const hostname = `assets-${Math.random().toString(36).substring(2)}`; + const route = await puter.hosting.create(hostname, assetsDir.path); + + // Store the URL + await puter.kv.set('assets_url', `https://${hostname}.puter.site`); + } + } catch (err) { + console.error('Error initializing assets directory:', err); + } +} + +window.generateSocialImageSection = (app) => { + return ` + + + Remove social image + + `; +} + + +$(document).on('click', '#edit-app-social-image', async function(e) { + const res = await puter.ui.showOpenFilePicker({ + accept: "image/*", + }); + + const socialImage = await puter.fs.read(res.path); + // Convert blob to base64 for preview + const reader = new FileReader(); + reader.readAsDataURL(socialImage); + + reader.onloadend = function() { + let image = reader.result; + // Get file extension + let fileExtension = res.name.split('.').pop(); + // Get MIME type + let mimeType = getMimeType(fileExtension); + // Replace MIME type in the data URL + image = image.replace('data:application/octet-stream;base64', `data:image/${mimeType};base64`); + + $('#edit-app-social-image').css('background-image', `url(${image})`); + $('#edit-app-social-image').attr('data-base64', image); + $('#edit-app-social-image-delete').show(); + + toggleSaveButton(); + toggleResetButton(); + } +}); + +$(document).on('click', '#edit-app-social-image-delete', async function(e) { + $('#edit-app-social-image').css('background-image', ''); + $('#edit-app-social-image').removeAttr('data-url'); + $('#edit-app-social-image').removeAttr('data-base64'); + $('#edit-app-social-image-delete').hide(); +}); + +window.handleSocialImageUpload = async (app_name, socialImageData) => { + if (!socialImageData) return null; + + try { + const assets_url = await puter.kv.get('assets_url'); + if (!assets_url) throw new Error('Assets URL not found'); + + // Convert base64 to blob + const base64Response = await fetch(socialImageData); + const blob = await base64Response.blob(); + + // Get assets directory path + const assetsDir = `/${auth_username}/AppData/${dev_center_uid}/assets`; + + // Upload new image + await puter.fs.upload( + new File([blob], `${app_name}.png`, { type: 'image/png' }), + assetsDir, + { overwrite: true } + ); + + return `${assets_url}/${app_name}.png`; + } catch (err) { + console.error('Error uploading social image:', err); + throw err; + } +} + +$(document).on('click', '.copy-app-uid', function(e) { + const appUID = $('#edit-app-uid').val(); + navigator.clipboard.writeText(appUID); + // change to 'copied' + $(this).html('Copied'); + setTimeout(() => { + $(this).html(copy_svg); + }, 2000); +}); + +$(document).on('change', '#analytics-period', async function(e) { + let period = $(this).val(); + render_analytics(period); +}); + +async function render_analytics(period){ + puter.ui.showSpinner(); + + // set a sensible stats_grouping based on the selected period + let stats_grouping; + + if (period === 'today' || period === 'yesterday') { + stats_grouping = 'hour'; + } + else if (period === 'this_week' || period === 'last_week' || period === 'this_month' || period === 'last_month' || period === '7d' || period === '30d') { + stats_grouping = 'day'; + } + else if (period === 'this_year' || period === 'last_year' || period === '12m' || period === 'all') { + stats_grouping = 'month'; + } + + const app = await puter.apps.get( + currently_editing_app.name, + { + icon_size: 16, + stats_period: period, + stats_grouping: stats_grouping, + } + ); + + $('#analytics-users .count').html(number_format(app.stats.user_count)); + $('#analytics-opens .count').html(number_format(app.stats.open_count)); + + // Clear existing chart if any + $('#analytics-chart').remove(); + $('.analytics-container').remove(); + + // Create new canvas + const container = $('
'); + const canvas = $(''); + container.append(canvas); + $('#analytics-opens').parent().after(container); + + // Format the data + const labels = app.stats.grouped_stats.open_count.map(item => { + let date; + if (stats_grouping === 'month') { + // Handle YYYY-MM format explicitly + const [year, month] = item.period.split('-'); + date = new Date(parseInt(year), parseInt(month) - 1); // month is 0-based in JS + } else { + date = new Date(item.period); + } + + if (stats_grouping === 'hour') { + return date.toLocaleString('en-US', { hour: 'numeric', hour12: true }).toLowerCase(); + } else if (stats_grouping === 'day') { + return date.toLocaleString('en-US', { month: 'short', day: 'numeric' }); + } else { + return date.toLocaleString('en-US', { month: 'short', year: 'numeric' }); + } + }); + const openData = app.stats.grouped_stats.open_count.map(item => item.count); + const userData = app.stats.grouped_stats.user_count.map(item => item.count); + + // Create chart + const ctx = document.getElementById('analytics-chart').getContext('2d'); + new Chart(ctx, { + type: 'line', + data: { + labels: labels, + datasets: [ + { + label: 'Opens', + data: openData, + borderColor: '#346beb', + tension: 0, + fill: false + }, + { + label: 'Users', + data: userData, + borderColor: '#27cc32', + tension: 0, + fill: false + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + display: true, + title: { + display: true, + text: 'Period' + }, + ticks: { + maxRotation: 45, + minRotation: 45 + } + }, + y: { + display: true, + beginAtZero: true, + title: { + display: true, + text: 'Count' + }, + ticks: { + precision: 0, // Show whole numbers only + stepSize: 1 // Increment by 1 + } + } + }, + } + }); + + puter.ui.hideSpinner(); +} + +$(document).on('click', '.stats-cell', function(e) { + edit_app_section($(this).attr('data-app-name'), 'analytics'); +}) +export default init_apps; \ No newline at end of file diff --git a/src/dev-center/js/dev-center.js b/src/dev-center/js/dev-center.js index 9ba704a6f..bec57515a 100644 --- a/src/dev-center/js/dev-center.js +++ b/src/dev-center/js/dev-center.js @@ -17,213 +17,83 @@ * along with this program. If not, see . */ -let URLParams = new URLSearchParams(window.location.search); -let domain = 'puter.com', authUsername; -let source_path -let apps = []; -let workers = []; -let sortBy = 'created_at'; -let sortDirection = 'desc'; -const dev_center_uid = puter.appID; -let developer; -let activeTab = 'apps'; -let currently_editing_app; -let dropped_items; -let search_query; -let originalValues = {}; +import init_apps from './apps.js'; +import init_workers from './workers.js'; +import init_websites from './websites.js'; -const APP_CATEGORIES = [ - { id: 'games', label: 'Games' }, - { id: 'developer-tools', label: 'Developer Tools' }, - { id: 'photo-video', label: 'Photo & Video' }, - { id: 'productivity', label: 'Productivity' }, - { id: 'utilities', label: 'Utilities' }, - { id: 'education', label: 'Education' }, - { id: 'business', label: 'Business' }, - { id: 'social', label: 'Social' }, - { id: 'graphics-design', label: 'Graphics & Design' }, - { id: 'music-audio', label: 'Music & Audio' }, - { id: 'news', label: 'News' }, - { id: 'entertainment', label: 'Entertainment' }, - { id: 'finance', label: 'Finance' }, - { id: 'health-fitness', label: 'Health & Fitness' }, - { id: 'lifestyle', label: 'Lifestyle' }, -]; +window.url_params = new URLSearchParams(window.location.search); +window.domain = 'puter.com' +window.auth_username = null; +window.dev_center_uid = puter.appID; +window.developer; +window.activeTab = 'apps'; -const deploying_spinner = ``; -const loading_spinner = ``; -const drop_area_placeholder = `

Drop your app folder and files here to deploy.

HTML, JS, CSS, ...

`; -const index_missing_error = `Please upload an 'index.html' file or if you're uploading a directory, make sure it contains an 'index.html' file at its root.`; -const lock_svg = ` `; -const lock_svg_tippy = ` `; - -const copy_svg = ` `; - -// authUsername +// auth_username (async () => { let user = await puter.auth.getUser(); if (user?.username) { - authUsername = user.username; + window.auth_username = user.username; } })() -// source_path -if (URLParams.has('source_path')) { - source_path = URLParams.get('source_path'); -} else { - source_path = null; -} - // domain and APIOrigin -if (URLParams.has('puter.domain')) { - domain = URLParams.get('puter.domain') +if (window.url_params.has('puter.domain')) { + window.domain = window.url_params.get('puter.domain') } // static hosting domain -let static_hosting_domain = 'puter.site'; -if(domain === 'puter.localhost'){ - static_hosting_domain = 'site.puter.localhost'; +window.static_hosting_domain = 'puter.site'; +if(window.domain === 'puter.localhost'){ + window.static_hosting_domain = 'site.puter.localhost'; } // add port to static_hosting_domain if provided -if (URLParams.has('puter.port') && URLParams.get('puter.port')) { - static_hosting_domain = static_hosting_domain + `:` + html_encode(URLParams.get('puter.port')); +if (window.url_params.has('puter.port') && window.url_params.get('puter.port')) { + window.static_hosting_domain = window.static_hosting_domain + `:` + html_encode(window.url_params.get('puter.port')); } // protocol -let protocol = 'https'; -if (URLParams.has('puter.protocol') && URLParams.get('puter.protocol') === 'http') - protocol = 'http'; +window.protocol = 'https'; +if (window.url_params.has('puter.protocol') && window.url_params.get('puter.protocol') === 'http') + window.protocol = 'http'; // port -let port = ''; -if (URLParams.has('puter.port') && URLParams.get('puter.port')) { - port = html_encode(URLParams.get('puter.port')); +window.port = ''; +if (window.url_params.has('puter.port') && window.url_params.get('puter.port')) { + window.port = html_encode(window.url_params.get('puter.port')); } -$(document).ready(function () { +// source_path +if (window.url_params.has('source_path')) { + window.source_path = window.url_params.get('source_path'); +} else { + window.source_path = null; +} + +// --------------------------------------------------------------- +// Initialize +// --------------------------------------------------------------- +$(document).ready(async function () { // initialize assets directory - initializeAssetsDirectory(); + await initializeAssetsDirectory(); + + // create default worker file + await createDefaultWorkerFile(); puter.ui.showSpinner(); - setTimeout(async function () { - puter.ui.onLaunchedWithItems(async function (items) { - source_path = items[0].path; - // if source_path is provided, this means that the user is creating a new app/updating an existing app - // by deploying an existing Puter folder. So we create the app and deploy it. - if (source_path) { - // todo if there are no apps, go straight to creating a new app - $('.insta-deploy-modal').get(0).showModal(); - // set item name - $('.insta-deploy-item-name').html(html_encode(items[0].name)); - } - }) + init_apps(); + init_websites(); + init_workers(); - // Get dev profile. This is only for puter.com for now as we don't have dev profiles in self-hosted Puter - if(domain === 'puter.com'){ - puter.apps.getDeveloperProfile(async function (dev_profile) { - developer = dev_profile; - if (dev_profile.approved_for_incentive_program && !dev_profile.joined_incentive_program) { - $('#join-incentive-program').show(); - } - - // show earn money c2a only if dev is not approved for incentive program or has already joined - if (!dev_profile.approved_for_incentive_program || dev_profile.joined_incentive_program) { - puter.kv.get('earn-money-c2a-closed').then((value) => { - if (value?.result || value === true || value === "true") - return; - - $('#earn-money').get(0).showModal(); - }); - } - - // show payout method tab if dev has joined incentive program - if (dev_profile.joined_incentive_program) { - $('.tab-btn[data-tab="payout-method"]').show(); - $('#payout-method-email').html(dev_profile.paypal); - } - }) - } - // Get apps - puter.apps.list({ icon_size: 64 }).then((resp) => { - apps = resp; - - // hide loading - puter.ui.hideSpinner(); - - // set apps - if (apps.length > 0) { - if (activeTab === 'apps') { - $('#no-apps-notice').hide(); - $('#app-list').show(); - } - $('.app-card').remove(); - apps.forEach(app => { - $('#app-list-table > tbody').append(generate_app_card(app)); - }); - count_apps(); - sort_apps(); - activate_tippy(); - } else { - $('#no-apps-notice').show(); - } - }) - }, 1000); + puter.ui.hideSpinner(); }); -/** - * Refreshes the list of apps in the UI. - * - * @param {boolean} [show_loading=false] - Whether to show a loading indicator while refreshing. - * - */ - -function refresh_app_list(show_loading = false) { - if (show_loading) - puter.ui.showSpinner(); - // get apps - setTimeout(function () { - // uncheck the select all checkbox - $('.select-all-apps').prop('checked', false); - - puter.apps.list({ icon_size: 64 }).then((apps_res) => { - puter.ui.hideSpinner(); - apps = apps_res; - if (apps.length > 0) { - if (activeTab === 'apps') { - $('#no-apps-notice').hide(); - $('#app-list').show(); - } - $('.app-card').remove(); - apps.forEach(app => { - $('#app-list-table > tbody').append(generate_app_card(app)); - }); - count_apps(); - sort_apps(); - } else { - $('#no-apps-notice').show(); - $('#app-list').hide() - } - activate_tippy(); - }) - }, show_loading ? 1000 : 0); -} - -function refresh_worker_list(show_loading = false) { - if (show_loading) - puter.ui.showSpinner(); - // get workers - setTimeout(function () { - puter.workers.list().then((workers_res) => { - workers = workers_res; - puter.ui.hideSpinner(); - }) - }, 1000); -} - -$(document).on('click', '.tab-btn', function (e) { +// --------------------------------------------------------------- +// Tab Buttons +// --------------------------------------------------------------- +$(document).on('click', '.tab-btn', async function (e) { puter.ui.showSpinner(); $('section:not(.sidebar)').hide(); $('.tab-btn').removeClass('active'); @@ -241,21 +111,29 @@ $(document).on('click', '.tab-btn', function (e) { // Workers tab // --------------------------------------------------------------- else if ($(this).attr('data-tab') === 'workers') { - // Get workers - puter.workers.list().then((resp) => { - workers = resp || []; - if (activeTab === 'workers' && workers.length > 0) { - $('#no-workers-notice').hide(); - $('#worker-list').show(); - workers.forEach(worker => { - $('#worker-list-table > tbody').append(generate_worker_card(worker)); - }); - } else { - $('#no-workers-notice').show(); - $('#worker-list').hide(); - } - }) activeTab = 'workers'; + // puter.workers.list() returns an object with each worker as a key and the worker object as the value + // so we need to convert it to an array, key is the worker name and value is the worker object + window.workers = Object.entries(await puter.workers.list()); + // Get workers + if (activeTab === 'workers' && window.workers.length > 0) { + $('#no-workers-notice').hide(); + $('#worker-list').show(); + window.workers.forEach(([name, worker]) => { + // append row to worker-list-table + $('#worker-list-table > tbody').append(` + + ${name} + + + + + `); + }); + } else { + $('#no-workers-notice').show(); + $('#worker-list').hide(); + } } // --------------------------------------------------------------- // Payout Method tab @@ -279,952 +157,6 @@ $(document).on('click', '.tab-btn', function (e) { puter.ui.hideSpinner(); }) -$(document).on('click', '.create-an-app-btn', async function (e) { - let title = await puter.ui.prompt('Please enter a title for your app:', 'My Awesome App'); - - if (title.length > 60) { - puter.ui.alert(`Title cannot be longer than 60.`, [ - { - label: 'Ok', - }, - ]); - // todo go back to create an app prompt and prefill the title input with the title the user entered - return; - } - else if (title) { - create_app(title); - } -}) - -$(document).on('click', '.create-a-worker-btn', async function (e) { - let name = await puter.ui.prompt('Please enter a name for your worker:', 'My Awesome Worker'); - - if (name) { - create_worker(name); - } -}) - -async function create_app(title, source_path = null, items = null) { - // name - let name = slugify(title, { - lower: true, - strict: true, - }); - - // icon - let icon = await getBase64ImageFromUrl('./img/app.svg'); - - // open the 'Creting new app...' modal - let start_ts = Date.now(); - - puter.ui.showSpinner(); - - //---------------------------------------------------- - // Create app - //---------------------------------------------------- - puter.apps.create({ - title: title, - name: name, - indexURL: 'https://dev-center.puter.com/coming-soon.html', - icon: icon, - description: ' ', - maximizeOnStart: false, - background: false, - dedupeName: true, - metadata: { - window_resizable: true, - fullpage_on_landing: true, - }, - - }) - .then(async (app) => { - let app_dir; - // ---------------------------------------------------- - // Create app directory in AppData - // ---------------------------------------------------- - app_dir = await puter.fs.mkdir( - `/${authUsername}/AppData/${dev_center_uid}/${app.uid}`, - { overwrite: true, recursive: true, rename: false } - ); - // ---------------------------------------------------- - // Create a router for the app with a fresh hostname - // ---------------------------------------------------- - let subdomain = name + '-' + Math.random().toString(36).substring(2) - await puter.hosting.create(subdomain, app_dir.path); - - // ---------------------------------------------------- - // Update the app with the new hostname - // ---------------------------------------------------- - puter.apps.update(app.name, { - title: title, - indexURL: source_path ? protocol + `://${subdomain}.` + static_hosting_domain : 'https://dev-center.puter.com/coming-soon.html', - icon: icon, - description: ' ', - maximizeOnStart: false, - background: false, - metadata: { - category: null, // default category on creation - window_resizable: true, - fullpage_on_landing: true, - } -}).then(async (app) => { - // refresh app list - puter.apps.list({ icon_size: 64 }).then(async (resp) => { - apps = resp; - // Close the 'Creting new app...' modal - // but make sure it was shown for at least 2 seconds - setTimeout(() => { - // open edit app section - edit_app_section(app.name); - refresh_app_list(); // ✅ Refreshes main app list - - // set drop area if source_path was provided or items were dropped - if (source_path || items) { - $('.drop-area').removeClass('drop-area-hover'); - $('.drop-area').addClass('drop-area-ready-to-deploy'); - } - puter.ui.hideSpinner(); - // deploy app if source_path was provided - if (source_path) { - deploy(app, source_path); - } else if (items) { - deploy(app, items); - } - activate_tippy(); - }, (Date.now() - start_ts) > 2000 ? 1 : 2000 - (Date.now() - start_ts)); - }) - }).catch(async (err) => { - console.log(err); - }) - // ---------------------------------------------------- - // Create a "shortcut" on the desktop - // ---------------------------------------------------- - puter.fs.upload(new File([], app.title), - `/${authUsername}/Desktop`, - { - name: app.title, - dedupeName: true, - overwrite: false, - appUID: app.uid, - } - ) - }).catch(async (err) => { - $('#create-app-error').show(); - $('#create-app-error').html(err.message); - // scroll to top so that user sees error message - document.body.scrollTop = document.documentElement.scrollTop = 0; - }) -} - -async function create_worker(name) { - let worker = await puter.workers.create({ - name: name, - }); -} - -$(document).on('click', '.deploy-btn', function (e) { - deploy(currently_editing_app, dropped_items); -}) - -$(document).on('click', '.edit-app, .got-to-edit-app', function (e) { - const cur_app_name = $(this).attr('data-app-name') - edit_app_section(cur_app_name); -}) - -$(document).on('click', '.delete-app', async function (e) { - let app_uid = $(this).attr('data-app-uid'); - let app_title = $(this).attr('data-app-title'); - let app_name = $(this).attr('data-app-name'); - - // get app - const app_data = await puter.apps.get(app_name, { icon_size: 16 }); - - if(app_data.metadata?.locked){ - puter.ui.alert(`${app_data.title} is locked and cannot be deleted.`, [ - { - label: 'Ok', - }, - ], { - type: 'warning', - }); - return; - } - - // confirm delete - const alert_resp = await puter.ui.alert(`Are you sure you want to premanently delete ${html_encode(app_title)}?`, - [ - { - label: 'Yes, delete permanently', - value: 'delete', - type: 'danger', - }, - { - label: 'Cancel' - }, - ] - ); - - if (alert_resp === 'delete') { - let init_ts = Date.now(); - puter.ui.showSpinner(); - puter.apps.delete(app_name).then(async (app) => { - setTimeout(() => { - puter.ui.hideSpinner(); - $(`.app-card[data-uid="${app_uid}"]`).fadeOut(200, function name(params) { - $(this).remove(); - if ($(`.app-card`).length === 0) { - $('section:not(.sidebar)').hide(); - $('#no-apps-notice').show(); - } else { - $('section:not(.sidebar)').hide(); - $('#app-list').show(); - } - count_apps(); - }); - }, - // make sure the modal was shown for at least 2 seconds - (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); - - // get app directory - puter.fs.stat({ - path: `/${authUsername}/AppData/${dev_center_uid}/${app_uid}`, - returnSubdomains: true, - }).then(async (stat) => { - // delete subdomain associated with the app dir - puter.hosting.delete(stat.subdomains[0].subdomain) - // delete app directory - puter.fs.delete( - `/${authUsername}/AppData/${dev_center_uid}/${app_uid}`, - { recursive: true } - ) - }) - }).catch(async (err) => { - setTimeout(() => { - puter.ui.hideSpinner(); - puter.ui.alert(err?.message, [ - { - label: 'Ok', - }, - ]); - }, - // make sure the modal was shown for at least 2 seconds - (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); - }) - } -}) - -// generate app link -function applink(app) { - return protocol + `://${domain}${ port ? ':' + port : '' }/app/${app.name}`; -} - -/** - * Generates the HTML for the app editing section. - * - * @param {Object} app - The app object containing details of the app to be edited. - * * - * @returns {string} HTML string for the app editing section. - * - * @description - * This function creates the HTML for the app editing interface, including: - * - App icon and title display - * - Options to open, add to desktop, or delete the app - * - Tabs for deployment and settings - * - Form fields for editing various app properties - * - Display of app statistics - * - * The generated HTML includes interactive elements and placeholders for - * dynamic content to be filled or updated by other functions. - * - * @example - * const appEditHTML = generate_edit_app_section(myAppObject); - * $('#edit-app').html(appEditHTML); - */ - -function generate_edit_app_section(app) { - if(app.result) - app = app.result; - - let maximize_on_start = app.maximize_on_start ? 'checked' : ''; - - let h = ``; - h += ` -
-
- -

${html_encode(app.title)}${app.metadata?.locked ? lock_svg_tippy : ''}

-
- Open - • - Add Shortcut to Desktop - • - Delete -
- ${html_encode(applink(app))} -
- -
- -
    -
  • Deploy
  • -
  • Settings
  • -
  • Analytics
  • -
- -
-
- New version deployed successfully 🎉× -

Give it a try!

-
-
${drop_area_placeholder}
- -
- -
-
-
-
App has been successfully updated.× -

Give it a try!

-
- - -

Basic

- - - - - - - - - - -
- ${copy_svg} -
- - -
-
Change App Icon
-
- Remove icon - - ${generateSocialImageSection(app)} - - - - - - - -

A list of file type specifiers. For example if you include .txt your apps could be opened when a user clicks on a TXT file.

-

You can paste multiple extensions at once (comma, space, or tab separated) or press comma to add each extension.

- - -

Window

-
- - -
- -
- - -
- -
- - -
- -
- - - - -
- -
- - - - -
- -
- - -
- -
- - -
- -

Misc

-
- - -

When enabled, the app cannot be deleted. This is useful for preventing accidental deletion of important apps.

-
- -
- - -
-
-
-
- - -
-
-

Users

-
-
-
-

Opens

-
-
-
-
-

Timezone: UTC

-

More analytics features coming soon...

-
- ` - return h; -} - -/* This function keeps track of the original values of the app before it is edited*/ -function trackOriginalValues(){ - originalValues = { - title: $('#edit-app-title').val(), - name: $('#edit-app-name').val(), - indexURL: $('#edit-app-index-url').val(), - description: $('#edit-app-description').val(), - icon: $('#edit-app-icon').attr('data-base64'), - fileAssociations: $('#edit-app-filetype-associations').val(), - category: $('#edit-app-category').val(), - socialImage: $('#edit-app-social-image').attr('data-base64'), - windowSettings: { - width: $('#edit-app-window-width').val(), - height: $('#edit-app-window-height').val(), - top: $('#edit-app-window-top').val(), - left: $('#edit-app-window-left').val() - }, - checkboxes: { - maximizeOnStart: $('#edit-app-maximize-on-start').is(':checked'), - background: $('#edit-app-background').is(':checked'), - resizableWindow: $('#edit-app-window-resizable').is(':checked'), - hideTitleBar: $('#edit-app-hide-titlebar').is(':checked'), - locked: $('#edit-app-locked').is(':checked'), - fullPageOnLanding: $('#edit-app-fullpage-on-landing').is(':checked') - } - }; -} - -/* This function compares for all fields and checks if anything has changed from before editting*/ -function hasChanges() { - // is icon changed - if($('#edit-app-icon').attr('data-base64') !== originalValues.icon){ - return true; - } - - // if social image is changed - if($('#edit-app-social-image').attr('data-base64') !== originalValues.socialImage){ - return true; - } - - // if any of the fields have changed - return( - $('#edit-app-title').val() !== originalValues.title || - $('#edit-app-name').val() !== originalValues.name || - $('#edit-app-index-url').val() !== originalValues.indexURL || - $('#edit-app-description').val() !== originalValues.description || - $('#edit-app-icon').attr('data-base64') !== originalValues.icon || - $('#edit-app-filetype-associations').val() !== originalValues.fileAssociations || - $('#edit-app-category').val() !== originalValues.category || - $('#edit-app-social-image').attr('data-base64') !== originalValues.socialImage || - $('#edit-app-window-width').val() !== originalValues.windowSettings.width || - $('#edit-app-window-height').val() !== originalValues.windowSettings.height || - $('#edit-app-window-top').val() !== originalValues.windowSettings.top || - $('#edit-app-window-left').val() !== originalValues.windowSettings.left || - $('#edit-app-maximize-on-start').is(':checked') !== originalValues.checkboxes.maximizeOnStart || - $('#edit-app-background').is(':checked') !== originalValues.checkboxes.background || - $('#edit-app-window-resizable').is(':checked') !== originalValues.checkboxes.resizableWindow || - $('#edit-app-hide-titlebar').is(':checked') !== originalValues.checkboxes.hideTitleBar || - $('#edit-app-locked').is(':checked') !== originalValues.checkboxes.locked || - $('#edit-app-fullpage-on-landing').is(':checked') !== originalValues.checkboxes.fullPageOnLanding - ); -} - -/* This function enables or disables the save button if there are any changes made */ -function toggleSaveButton() { - if (hasChanges()) { - $('.edit-app-save-btn').prop('disabled', false); - } else { - $('.edit-app-save-btn').prop('disabled', true); - } -} - -/* This function enables or disables the reset button if there are any changes made */ -function toggleResetButton() { - if (hasChanges()) { - $('.edit-app-reset-btn').prop('disabled', false); - } else { - $('.edit-app-reset-btn').prop('disabled', true); - } -} - -/* This function revers the changes made back to the original values of the edit form */ -function resetToOriginalValues() { - $('#edit-app-title').val(originalValues.title); - $('#edit-app-name').val(originalValues.name); - $('#edit-app-index-url').val(originalValues.indexURL); - $('#edit-app-description').val(originalValues.description); - $('#edit-app-filetype-associations').val(originalValues.fileAssociations); - $('#edit-app-category').val(originalValues.category); - $('#edit-app-window-width').val(originalValues.windowSettings.width); - $('#edit-app-window-height').val(originalValues.windowSettings.height); - $('#edit-app-window-top').val(originalValues.windowSettings.top); - $('#edit-app-window-left').val(originalValues.windowSettings.left); - $('#edit-app-maximize-on-start').prop('checked', originalValues.checkboxes.maximizeOnStart); - $('#edit-app-background').prop('checked', originalValues.checkboxes.background); - $('#edit-app-window-resizable').prop('checked', originalValues.checkboxes.resizableWindow); - $('#edit-app-hide-titlebar').prop('checked', originalValues.checkboxes.hideTitleBar); - $('#edit-app-locked').prop('checked', originalValues.checkboxes.locked); - $('#edit-app-fullpage-on-landing').prop('checked', originalValues.checkboxes.fullPageOnLanding); - - if (originalValues.icon) { - $('#edit-app-icon').css('background-image', `url(${originalValues.icon})`); - $('#edit-app-icon').attr('data-url', originalValues.icon); - $('#edit-app-icon').attr('data-base64', originalValues.icon); - $('#edit-app-icon-delete').show(); - } else { - $('#edit-app-icon').css('background-image', ''); - $('#edit-app-icon').removeAttr('data-url'); - $('#edit-app-icon').removeAttr('data-base64'); - $('#edit-app-icon-delete').hide(); - } - - if (originalValues.socialImage) { - $('#edit-app-social-image').css('background-image', `url(${originalValues.socialImage})`); - $('#edit-app-social-image').attr('data-url', originalValues.socialImage); - $('#edit-app-social-image').attr('data-base64', originalValues.socialImage); - } else { - $('#edit-app-social-image').css('background-image', ''); - $('#edit-app-social-image').removeAttr('data-url'); - $('#edit-app-social-image').removeAttr('data-base64'); - } -} - -async function edit_app_section(cur_app_name, tab = 'deploy') { - $('section:not(.sidebar)').hide(); - $('.tab-btn').removeClass('active'); - $('.tab-btn[data-tab="apps"]').addClass('active'); - - let cur_app = await puter.apps.get(cur_app_name, {icon_size: 128, stats_period: 'today'}); - - currently_editing_app = cur_app; - - // generate edit app section - $('#edit-app').html(generate_edit_app_section(cur_app)); - trackOriginalValues(); // Track initial field values - toggleSaveButton(); // Ensure Save button is initially disabled - toggleResetButton(); // Ensure Reset button is initially disabled - $('#edit-app').show(); - - // analytics - $('#analytics-users .count').html(cur_app.stats.user_count); - $('#analytics-opens .count').html(cur_app.stats.open_count); - - render_analytics('today') - - // show the correct tab - $('.section-tab').hide(); - $(`.section-tab[data-tab="${tab}"]`).show(); - $('.section-tab-buttons .section-tab-btn').removeClass('active'); - $(`.section-tab-buttons .section-tab-btn[data-tab="${tab}"]`).addClass('active'); - - const filetype_association_input = document.querySelector('textarea[id=edit-app-filetype-associations]'); - let tagify = new Tagify(filetype_association_input, { - pattern: /\.(?:[a-z0-9]+)|(?:[a-z]+\/(?:[a-z0-9.-]+|\*))/, - delimiters: ",", // Use comma as delimiter - duplicates: false, // Prevent duplicate tags - enforceWhitelist: false, - dropdown : { - // show the dropdown immediately on focus (0 character typed) - enabled: 0, - }, - whitelist: [ - // MIME type patterns - "text/*", "image/*", "audio/*", "video/*", "application/*", - - // Documents - ".doc", ".docx", ".pdf", ".txt", ".odt", ".rtf", ".tex", ".md", ".pages", ".epub", ".mobi", ".azw", ".azw3", ".djvu", ".xps", ".oxps", ".fb2", ".textile", ".markdown", ".asciidoc", ".rst", ".wpd", ".wps", ".abw", ".zabw", - - // Spreadsheets - ".xls", ".xlsx", ".csv", ".ods", ".numbers", ".tsv", ".gnumeric", ".xlt", ".xltx", ".xlsm", ".xltm", ".xlam", ".xlsb", - - // Presentations - ".ppt", ".pptx", ".key", ".odp", ".pps", ".ppsx", ".pptm", ".potx", ".potm", ".ppam", - - // Images - ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", ".svg", ".webp", ".ico", ".psd", ".ai", ".eps", ".raw", ".cr2", ".nef", ".orf", ".sr2", ".heic", ".heif", ".avif", ".jxr", ".hdp", ".wdp", ".jng", ".xcf", ".pgm", ".pbm", ".ppm", ".pnm", - - // Video - ".mp4", ".avi", ".mov", ".wmv", ".mkv", ".flv", ".webm", ".m4v", ".mpeg", ".mpg", ".3gp", ".3g2", ".ogv", ".vob", ".drc", ".gifv", ".mng", ".qt", ".yuv", ".rm", ".rmvb", ".asf", ".amv", ".m2v", ".svi", - - // Audio - ".mp3", ".wav", ".aac", ".flac", ".ogg", ".m4a", ".wma", ".aiff", ".alac", ".ape", ".au", ".mid", ".midi", ".mka", ".pcm", ".ra", ".ram", ".snd", ".wv", ".opus", - - // Code/Development - ".js", ".ts", ".html", ".css", ".json", ".xml", ".php", ".py", ".java", ".cpp", ".c", ".cs", ".h", ".hpp", ".hxx", ".rs", ".go", ".rb", ".pl", ".swift", ".kt", ".kts", ".scala", ".coffee", ".sass", ".scss", ".less", ".jsx", ".tsx", ".vue", ".sh", ".bash", ".zsh", ".fish", ".ps1", ".bat", ".cmd", ".sql", ".r", ".dart", ".f", ".f90", ".for", ".lua", ".m", ".mm", ".clj", ".erl", ".ex", ".exs", ".elm", ".hs", ".lhs", ".lisp", ".ml", ".mli", ".nim", ".pl", ".rkt", ".v", ".vhd", - - // Archives - ".zip", ".rar", ".7z", ".tar", ".gz", ".bz2", ".xz", ".z", ".lz", ".lzma", ".tlz", ".txz", ".tgz", ".tbz2", ".bz", ".br", ".lzo", ".ar", ".cpio", ".shar", ".lrz", ".lz4", ".lz2", ".rz", ".sfark", ".sz", ".zoo", - - // Database - ".db", ".sql", ".sqlite", ".sqlite3", ".dbf", ".mdb", ".accdb", ".db3", ".s3db", ".dbx", - - // Fonts - ".ttf", ".otf", ".woff", ".woff2", ".eot", ".pfa", ".pfb", ".sfd", - - // CAD and 3D - ".dwg", ".dxf", ".stl", ".obj", ".fbx", ".dae", ".3ds", ".blend", ".max", ".ma", ".mb", ".c4d", ".skp", ".usd", ".usda", ".usdc", ".abc", - - // Scientific/Technical - ".mat", ".fig", ".nb", ".cdf", ".fits", ".fts", ".fit", ".gmsh", ".msh", ".fem", ".neu", ".hdf", ".h5", ".nx", ".unv", - - // System - ".exe", ".dll", ".so", ".dylib", ".app", ".dmg", ".iso", ".img", ".bin", ".msi", ".apk", ".ipa", ".deb", ".rpm", - - // Directory - ".directory" - ], - }) - - // -------------------------------------------------------- - // Dragster - // -------------------------------------------------------- - let drop_area_content = drop_area_placeholder; - - $('.drop-area').dragster({ - enter: function (dragsterEvent, event) { - drop_area_content = $('.drop-area').html(); - $('.drop-area').addClass('drop-area-hover'); - $('.drop-area').html(drop_area_placeholder); - }, - leave: function (dragsterEvent, event) { - $('.drop-area').html(drop_area_content); - $('.drop-area').removeClass('drop-area-hover'); - }, - drop: async function (dragsterEvent, event) { - const e = event.originalEvent; - e.stopPropagation(); - e.preventDefault(); - - // hide previous success message - $('.deploy-success-msg').fadeOut(); - - // remove hover class - $('.drop-area').removeClass('drop-area-hover'); - - //---------------------------------------------------- - // Puter items dropped - //---------------------------------------------------- - if (e.detail?.items?.length > 0) { - let items = e.detail.items; - - // ---------------------------------------------------- - // One Puter file dropped - // ---------------------------------------------------- - if (items.length === 1 && !items[0].isDirectory) { - if (items[0].name.toLowerCase() === 'index.html') { - dropped_items = items[0].path; - $('.drop-area').removeClass('drop-area-hover'); - $('.drop-area').addClass('drop-area-ready-to-deploy'); - drop_area_content = `

index.html

Ready to deploy 🚀

Cancel

`; - $('.drop-area').html(drop_area_content); - - // enable deploy button - $('.deploy-btn').removeClass('disabled'); - - } else { - puter.ui.alert(`You need to have an index.html file in your deployment.`, [ - { - label: 'Ok', - }, - ]); - $('.drop-area').removeClass('drop-area-ready-to-deploy'); - $('.deploy-btn').addClass('disabled'); - dropped_items = []; - } - return; - } - // ---------------------------------------------------- - // Multiple Puter files dropped - // ---------------------------------------------------- - else if (items.length > 1) { - let hasIndexHtml = false; - for (let item of items) { - if (item.name.toLowerCase() === 'index.html') { - hasIndexHtml = true; - break; - } - } - - if (hasIndexHtml) { - dropped_items = items; - $('.drop-area').removeClass('drop-area-hover'); - $('.drop-area').addClass('drop-area-ready-to-deploy'); - drop_area_content = `

${items.length} items

Ready to deploy 🚀

Cancel

`; - $('.drop-area').html(drop_area_content); - - // enable deploy button - $('.deploy-btn').removeClass('disabled'); - } else { - puter.ui.alert(`You need to have an index.html file in your deployment.`, [ - { - label: 'Ok', - }, - ]); - $('.drop-area').removeClass('drop-area-ready-to-deploy'); - $('.drop-area').removeClass('drop-area-hover'); - $('.deploy-btn').addClass('disabled'); - dropped_items = []; - } - return; - } - // ---------------------------------------------------- - // One Puter directory dropped - // ---------------------------------------------------- - else if (items.length === 1 && items[0].isDirectory) { - let children = await puter.fs.readdir(items[0].path); - // check if index.html exists, if found, deploy entire directory - for (let child of children) { - if (child.name === 'index.html') { - // deploy(currently_editing_app, items[0].path); - dropped_items = items[0].path; - let rootItems = ''; - - if (children.length === 1) - rootItems = children[0].name; - else if (children.length === 2) - rootItems = children[0].name + ', ' + children[1].name; - else if (children.length === 3) - rootItems = children[0].name + ', ' + children[1].name + ', and' + children[1].name; - else if (children.length > 3) - rootItems = children[0].name + ', ' + children[1].name + ', and ' + (children.length - 2) + ' more item' + (children.length - 2 > 1 ? 's' : ''); - - $('.drop-area').removeClass('drop-area-hover'); - $('.drop-area').addClass('drop-area-ready-to-deploy'); - drop_area_content = `

${rootItems}

Ready to deploy 🚀

Cancel

`; - $('.drop-area').html(drop_area_content); - - // enable deploy button - $('.deploy-btn').removeClass('disabled'); - return; - } - } - - // no index.html in directory - puter.ui.alert(index_missing_error, [ - { - label: 'Ok', - }, - ]); - $('.drop-area').removeClass('drop-area-ready-to-deploy'); - $('.deploy-btn').addClass('disabled'); - dropped_items = []; - } - - return false; - } - - //----------------------------------------------------------------------------- - // Local items dropped - //----------------------------------------------------------------------------- - if (!e.dataTransfer || !e.dataTransfer.items || e.dataTransfer.items.length === 0) - return; - - // get dropped items - dropped_items = await puter.ui.getEntriesFromDataTransferItems(e.dataTransfer.items); - - // generate a flat array of full paths from the dropped items - let paths = []; - for (let item of dropped_items) { - paths.push('/' + (item.fullPath ?? item.filepath)); - } - - // generate a directory tree from the paths - let tree = generateDirTree(paths); - - dropped_items = setRootDirTree(tree, dropped_items); - - // alert if no index.html in root - if (!hasRootIndexHtml(tree)) { - puter.ui.alert(index_missing_error, [ - { - label: 'Ok', - }, - ]); - $('.drop-area').removeClass('drop-area-ready-to-deploy'); - $('.deploy-btn').addClass('disabled'); - dropped_items = []; - return; - } - - // Get all keys (directories and files) in the root - const rootKeys = Object.keys(tree); - - // generate a list of items in the root in the form of a string (e.g. /index.html, /css/style.css) with maximum of 3 items - let rootItems = ''; - - if (rootKeys.length === 1) - rootItems = rootKeys[0]; - else if (rootKeys.length === 2) - rootItems = rootKeys[0] + ', ' + rootKeys[1]; - else if (rootKeys.length === 3) - rootItems = rootKeys[0] + ', ' + rootKeys[1] + ', and' + rootKeys[1]; - else if (rootKeys.length > 3) - rootItems = rootKeys[0] + ', ' + rootKeys[1] + ', and ' + (rootKeys.length - 2) + ' more item' + (rootKeys.length - 2 > 1 ? 's' : ''); - - rootItems = html_encode(rootItems); - $('.drop-area').removeClass('drop-area-hover'); - $('.drop-area').addClass('drop-area-ready-to-deploy'); - drop_area_content = `

${rootItems}

Ready to deploy 🚀

Cancel

`; - $('.drop-area').html(drop_area_content); - - // enable deploy button - $('.deploy-btn').removeClass('disabled'); - - return false; - } - }); - - // Focus on the first input - $('#edit-app-title').focus(); - - try { - activate_tippy(); - } catch (e) { - console.log('no tippy:', e); - } - - // Custom function to handle bulk pasting of file extensions - if (tagify) { - // Create a completely separate paste handler - const handleBulkPaste = function(e) { - const clipboardData = e.clipboardData || window.clipboardData; - if (!clipboardData) return; - - const pastedText = clipboardData.getData('text'); - if (!pastedText) return; - - // Check if the pasted text contains delimiters - if (/[,;\t\s]/.test(pastedText)) { - e.stopPropagation(); - e.preventDefault(); - - // Process the pasted text to extract extensions - const extensions = pastedText.split(/[,;\t\s]+/) - .map(ext => ext.trim()) - .filter(ext => ext && (ext.startsWith('.') || ext.includes('/'))); - - if (extensions.length > 0) { - // Get existing values to prevent duplicates - const existingValues = tagify.value.map(tag => tag.value); - - // Only add extensions that don't already exist - const newExtensions = extensions.filter(ext => !existingValues.includes(ext)); - - if (newExtensions.length > 0) { - // Add the new tags - tagify.addTags(newExtensions); - - // Update the UI - setTimeout(() => { - toggleSaveButton(); - toggleResetButton(); - }, 10); - } - } - - // Clear the input element to prevent any text concatenation - setTimeout(() => { - if (tagify.DOM.input) { - tagify.DOM.input.textContent = ''; - } - }, 10); - } - }; - - // Add the paste handler directly to the tagify wrapper element - const tagifyWrapper = tagify.DOM.scope; - if (tagifyWrapper) { - tagifyWrapper.addEventListener('paste', handleBulkPaste, true); - } - - // Also add it to the input element for better coverage - if (tagify.DOM.input) { - tagify.DOM.input.addEventListener('paste', handleBulkPaste, true); - } - - // Add a comma key handler to support adding tags with comma - tagify.DOM.input.addEventListener('keydown', function(e) { - if (e.key === ',' && tagify.DOM.input.textContent.trim()) { - e.preventDefault(); - - const text = tagify.DOM.input.textContent.trim(); - - // Only add valid extensions - if ((text.startsWith('.') || text.includes('/')) && - tagify.settings.pattern.test(text)) { - - // Check for duplicates - const existingValues = tagify.value.map(tag => tag.value); - - if (!existingValues.includes(text)) { - tagify.addTags([text]); - - // Update UI - setTimeout(() => { - toggleSaveButton(); - toggleResetButton(); - }, 10); - } - - // Always clear the input - tagify.DOM.input.textContent = ''; - } - } - }); - } -} - $('.jip-submit-btn').on('click', async function (e) { const first_name = $('#jip-first-name').val(); const last_name = $('#jip-last-name').val(); @@ -1288,201 +220,6 @@ $('.jip-submit-btn').on('click', async function (e) { }) }) -$(document).on('click', '.edit-app-save-btn', async function (e) { - const title = $('#edit-app-title').val(); - const name = $('#edit-app-name').val(); - const index_url = $('#edit-app-index-url').val(); - const description = $('#edit-app-description').val(); - const uid = $('#edit-app-uid').val(); - const height = $('#edit-app-window-height').val(); - const width = $('#edit-app-window-width').val(); - const top = $('#edit-app-window-top').val(); - const left = $('#edit-app-window-left').val(); - const category = $('#edit-app-category').val(); - - let filetype_associations = $('#edit-app-filetype-associations').val(); - - let icon; - - let error; - - //validation - if (title === '') - error = `Title is required.`; - else if (title.length > 60) - error = `Title cannot be longer than ${60}.`; - else if (name === '') - error = `Name is required.`; - else if (name.length > 60) - error = `Name cannot be longer than ${60}.`; - else if (index_url === '') - error = `Index URL is required.`; - else if (!name.match(/^[a-zA-Z0-9-_-]+$/)) - error = `Name can only contain letters, numbers, dash (-) and underscore (_).`; - else if (!is_valid_url(index_url)) - error = `Index URL must be a valid url.`; - else if (!index_url.toLowerCase().startsWith('https://') && !index_url.toLowerCase().startsWith('http://')) - error = `Index URL must start with 'https://' or 'http://'.`; - // height must be a number - else if (isNaN(height)) - error = `Window Height must be a number.`; - // height must be greater than 0 - else if (height <= 0) - error = `Window Height must be greater than 0.`; - // width must be a number - else if (isNaN(width)) - error = `Window Width must be a number.`; - // width must be greater than 0 - else if (width <= 0) - error = `Window Width must be greater than 0.`; - // top must be a number - else if (top && isNaN(top)) - error = `Window Top must be a number.`; - // left must be a number - else if (left && isNaN(left)) - error = `Window Left must be a number.`; - - // download icon from URL - else { - let icon_url = $('#edit-app-icon').attr('data-url'); - let icon_base64 = $('#edit-app-icon').attr('data-base64'); - - if(icon_base64){ - icon = icon_base64; - }else if (icon_url) { - icon = await getBase64ImageFromUrl(icon_url); - let app_max_icon_size = 5 * 1024 * 1024; - if (icon.length > app_max_icon_size) - error = `Icon cannot be larger than ${byte_format(app_max_icon_size)}`; - // make sure icon is an image - else if (!icon.startsWith('data:image/') && !icon.startsWith('data:application/octet-stream')) - error = `Icon must be an image.`; - }else{ - icon = null; - } - } - - // parse filetype_associations - if(filetype_associations !== ''){ - filetype_associations = JSON.parse(filetype_associations); - filetype_associations = filetype_associations.map((type) => { - const fileType = type.value; - if ( - !fileType || - fileType === "." || - fileType === "/" - ) { - error = `File Association Type must be valid.`; - return null; // Return null for invalid cases - } - const lower = fileType.toLocaleLowerCase(); - - if (fileType.includes("/")) { - return lower; - } else if (fileType.includes(".")) { - return "." + lower.split(".")[1]; - } else { - return "." + lower; - } - }).filter(Boolean); - } - - // error? - if (error) { - $('#edit-app-error').show(); - $('#edit-app-error').html(error); - document.body.scrollTop = document.documentElement.scrollTop = 0; - return; - } - - // show working spinner - puter.ui.showSpinner(); - - // disable submit button - $('.edit-app-save-btn').prop('disabled', true); - - let socialImageUrl = null; - if ($('#edit-app-social-image').attr('data-base64')) { - socialImageUrl = await handleSocialImageUpload(name, $('#edit-app-social-image').attr('data-base64')); - } else if ($('#edit-app-social-image').attr('data-url')) { - socialImageUrl = $('#edit-app-social-image').attr('data-url'); - } - - puter.apps.update(currently_editing_app.name, { - title: title, - name: name, - indexURL: index_url, - icon: icon, - description: description, - maximizeOnStart: $('#edit-app-maximize-on-start').is(":checked"), - background: $('#edit-app-background').is(":checked"), - metadata: { - fullpage_on_landing: $('#edit-app-fullpage-on-landing').is(":checked"), - social_image: socialImageUrl, - category: category || null, - window_size: { - width: width ?? 800, - height: height ?? 600, - }, - window_position: { - top: top, - left: left, - }, - window_resizable: $('#edit-app-window-resizable').is(":checked"), - hide_titlebar: $('#edit-app-hide-titlebar').is(":checked"), - locked: $(`#edit-app-locked`).is(":checked") ?? false, - }, - filetypeAssociations: filetype_associations, - }).then(async (app) => { - refresh_app_list(); - - currently_editing_app = app; - trackOriginalValues(); // Update original values after save - toggleSaveButton(); //Disable Save Button after succesful save - toggleResetButton(); //DIsable Reset Button after succesful save - $('#edit-app-error').hide(); - $('#edit-app-success').show(); - document.body.scrollTop = document.documentElement.scrollTop = 0; - // Update open-app-btn - $(`.open-app-btn[data-app-uid="${uid}"]`).attr('data-app-name', app.name); - $(`.open-app[data-uid="${uid}"]`).attr('data-app-name', app.name); - // Update title - $(`.app-title[data-uid="${uid}"]`).html(html_encode(app.title)); - // Update app link - $(`.app-url[data-uid="${uid}"]`).html(applink(app)); - $(`.app-url[data-uid="${uid}"]`).attr('href', applink(app)); - // Update icons - $(`.app-icon[data-uid="${uid}"]`).attr('src', html_encode(app.icon ? app.icon : './img/app.svg')); - $(`[data-app-uid="${uid}"]`).attr('data-app-title', html_encode(app.title)); - $(`[data-app-name="${uid}"]`).attr('data-app-name', html_encode(app.name)); - }).catch((err) => { - $('#edit-app-success').hide(); - $('#edit-app-error').show(); - $('#edit-app-error').html(err.error?.message); - // scroll to top so that user sees error message - document.body.scrollTop = document.documentElement.scrollTop = 0; - // re-enable submit button - $('.edit-app-save-btn').prop('disabled', false); - }).finally(() => { - puter.ui.hideSpinner(); - }) -}) - -$(document).on('input change', '#edit-app input, #edit-app textarea, #edit-app select', () => { - toggleSaveButton(); - toggleResetButton(); -}); - -$(document).on('click', '.edit-app-reset-btn', function () { - resetToOriginalValues(); - toggleSaveButton(); // Disable Save button since values are reverted to original - toggleResetButton(); //Disable Reset button since values are reverted to original -}); - -$(document).on('click', '.open-app-btn', async function (e) { - puter.ui.launchApp($(this).attr('data-app-name')) -}) - $('#earn-money-c2a-close').click(async function (e) { $('#earn-money').get(0).close(); puter.kv.set('earn-money-c2a-closed', 'true') @@ -1494,127 +231,8 @@ $('#earn-money::backdrop').click(async function (e) { puter.kv.set('earn-money-c2a-closed', 'true') }) -$(document).on('click', '.edit-app-open-app-btn', async function (e) { - puter.ui.launchApp($(this).attr('data-app-name')) -}) - -$(document).on('click', '.delete-app-settings', async function (e) { - let app_uid = $(this).attr('data-app-uid'); - let app_name = $(this).attr('data-app-name'); - let app_title = $(this).attr('data-app-title'); - - // check if app is locked - const app_data = await puter.apps.get(app_name, {icon_size: 16}); - - if(app_data.metadata?.locked){ - puter.ui.alert(`${app_data.title} is locked and cannot be deleted.`, [ - { - label: 'Ok', - }, - ], { - type: 'warning', - }); - return; - } - - // confirm delete - const alert_resp = await puter.ui.alert(`Are you sure you want to premanently delete ${html_encode(app_title)}?`, - [ - { - label: 'Yes, delete permanently', - value: 'delete', - type: 'danger', - }, - { - label: 'Cancel' - }, - ] - ); - - if (alert_resp === 'delete') { - let init_ts = Date.now(); - puter.ui.showSpinner(); - puter.apps.delete(app_name).then(async (app) => { - setTimeout(() => { - puter.ui.hideSpinner(); - $('.back-to-main-btn').trigger('click'); - }, - // make sure the modal was shown for at least 2 seconds - (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); - // get app directory - puter.fs.stat({ - path: `/${authUsername}/AppData/${dev_center_uid}/${app_uid}`, - returnSubdomains: true, - }).then(async (stat) => { - // delete subdomain associated with the app dir - puter.hosting.delete(stat.subdomains[0].subdomain) - // delete app directory - puter.fs.delete( - `/${authUsername}/AppData/${dev_center_uid}/${app_uid}`, - { recursive: true } - ) - }) - }).catch(async (err) => { - setTimeout(() => { - puter.ui.hideSpinner(); - puter.ui.alert(err?.message, [ - { - label: 'Ok', - }, - ]); - }, - (Date.now() - init_ts) > 2000 ? 1 : (2000 - (Date.now() - init_ts))); - }) - } -}) - -$(document).on('click', '.edit-app', async function (e) { - $('#edit-app-uid').val($(this).attr('data-app-uid')); -}) - -$(document).on('click', '.back-to-main-btn', function (e) { - $('section:not(.sidebar)').hide(); - $('.tab-btn').removeClass('active'); - $('.tab-btn[data-tab="apps"]').addClass('active'); - - // get apps - puter.ui.showSpinner(); - setTimeout(function () { - puter.apps.list({icon_size: 64}).then((apps_res) => { - // uncheck the select all checkbox - $('.select-all-apps').prop('checked', false); - - puter.ui.hideSpinner(); - apps = apps_res; - if (apps.length > 0) { - if (activeTab === 'apps') { - $('#no-apps-notice').hide(); - $('#app-list').show(); - } - $('.app-card').remove(); - apps.forEach(app => { - $('#app-list-table > tbody').append(generate_app_card(app)); - }); - count_apps(); - sort_apps(); - activate_tippy(); - } else - $('#no-apps-notice').show(); - }) - }, 1000); -}) - -function count_apps() { - let count = 0; - $('.app-card').each(function () { - count++; - }) - $('.app-count').html(count); - return count; -} - // https://stackoverflow.com/a/43467144/1764493 -function is_valid_url(string) { +window.is_valid_url = (string) => { let url; try { @@ -1626,47 +244,7 @@ function is_valid_url(string) { return url.protocol === "http:" || url.protocol === "https:"; } -$(document).on('click', '#edit-app-icon-delete', async function (e) { - $('#edit-app-icon').css('background-image', ``); - $('#edit-app-icon').removeAttr('data-url'); - $('#edit-app-icon').removeAttr('data-base64'); - $('#edit-app-icon-delete').hide(); - - toggleSaveButton(); - toggleResetButton(); -}) - -$(document).on('click', '#edit-app-icon', async function (e) { - const res2 = await puter.ui.showOpenFilePicker({ - accept: "image/*", - }); - - const icon = await puter.fs.read(res2.path); - // convert blob to base64 - const reader = new FileReader(); - reader.readAsDataURL(icon); - - reader.onloadend = function () { - let image = reader.result; - // Get file extension - let fileExtension = res2.name.split('.').pop(); - - // Get MIME type - let mimeType = getMimeType(fileExtension); - - // Replace MIME type in the data URL - image = image.replace('data:application/octet-stream;base64', `data:${mimeType};base64`); - - $('#edit-app-icon').css('background-image', `url(${image})`); - $('#edit-app-icon').attr('data-base64', image); - $('#edit-app-icon-delete').show(); - - toggleSaveButton(); - toggleResetButton(); - } -}) - -async function getBase64ImageFromUrl(imageUrl) { +window.getBase64ImageFromUrl = async (imageUrl) => { var res = await fetch(imageUrl); var blob = await res.blob(); @@ -1683,161 +261,6 @@ async function getBase64ImageFromUrl(imageUrl) { }) } -/** - * Generates HTML for an individual app card in the app list. - * - * @param {Object} app - The app object containing details of the app. - * * - * @returns {string} HTML string representing the app card. - * - * @description - * This function creates an HTML string for an app card, which includes: - * - Checkbox for app selection - * - App icon and title - * - Links to open, edit, add to desktop, or delete the app - * - Display of app statistics (user count, open count) - * - Creation date - * - Incentive program status badge (if applicable) - * - * The generated HTML is designed to be inserted into the app list table. - * It includes data attributes for various interactive features and - * event handling. - * - * @example - * const appCardHTML = generate_app_card(myAppObject); - * $('#app-list-table > tbody').append(appCardHTML); - */ -function generate_app_card(app) { - let h = ``; -h += ``; - // check box - h += ``; - h += `
`; - h += ``; - h += `
`; - h += ``; - // App info (title, category, toolbar) -h += ``; - - // Wrapper for icon + content side by side -h += `
`; - - // Icon - h += `
`; - - // App info content - h += `
`; - - // Info block with fixed layout -h += `
`; - - // Title - h += `

- ${html_encode(app.title)}${app.metadata?.locked ? lock_svg_tippy : ''} -

`; - - // Category (optional) - if (app.metadata?.category) { - const category = APP_CATEGORIES.find(c => c.id === app.metadata.category); - if (category) { - h += `${html_encode(category.label)}`; - } - } - - // Link - h += `${html_encode(applink(app))}`; - -h += `
`; - - - - // Toolbar - h += `
`; - h += `
`; // remove inline styles - - h += `Open`; - h += `Settings`; - h += `Add Shortcut to Desktop`; - h += `Delete`; - h += `
`; -h += `
`; - h += `
`; // end info column - h += `
`; // end row -h += ``; - - - // users count - h += ``; - h += `${number_format((app.stats.referral_count ?? 0) + app.stats.user_count)}`; - h += ``; - - // opens - h += ``; - h += `${number_format(app.stats.open_count)}`; - h += ``; - - // Created - h += ``; - h += `${moment(app.created_at).format('MMM Do, YYYY')}`; - h += ``; - - h += ``; - h += `
`; - // "Approved for listing" - h += ``; - - // "Approved for opening items" - h += ``; - - // "Approved for incentive program" - h += ``; - h += `
`; - h += ``; - h += ``; - return h; -} - /** * Formats a binary-byte integer into the human-readable form with units. * @@ -1854,7 +277,7 @@ window.byte_format = (bytes) => { /** * check if a string is a valid email address */ -function validateEmail(email) { +window.validateEmail = (email) => { var re = /\S+@\S+\.\S+/; return re.test(email); } @@ -1869,7 +292,7 @@ function validateEmail(email) { * @returns {string} The formatted number with grouped thousands, using the specified decimal point and thousands separator characters. * @throws {TypeError} If the `number` parameter cannot be converted to a finite number, or if the `decimals` parameter is non-finite and cannot be converted to an absolute number. */ -function number_format(number, decimals, dec_point, thousands_sep) { +window.number_format = (number, decimals, dec_point, thousands_sep) => { // Strip all characters but numerical ones. number = (number + '').replace(/[^0-9+\-Ee.]/g, ''); var n = !isFinite(+number) ? 0 : +number, @@ -1897,420 +320,6 @@ $(document).on('click', '.close-message', function () { $($(this).attr('data-target')).fadeOut(); }); -$('th.sort').on('click', function (e) { - // determine what column to sort by - const sortByColumn = $(this).attr('data-column'); - - // toggle sort direction - if (sortByColumn === sortBy) { - if (sortDirection === 'asc') - sortDirection = 'desc'; - else - sortDirection = 'asc'; - } - else { - sortBy = sortByColumn; - sortDirection = 'desc'; - } - - // update arrow - $('.sort-arrow').css('display', 'none'); - $('#app-list-table').find('th').removeClass('sorted'); - $(this).find('.sort-arrow-' + sortDirection).css('display', 'inline'); - $(this).addClass('sorted'); - - sort_apps(); -}); - -function sort_apps() { - let sorted_apps; - - // sort - if (sortDirection === 'asc'){ - sorted_apps = apps.sort((a, b) => { - if(sortBy === 'name'){ - return a[sortBy].localeCompare(b[sortBy]); - }else if(sortBy === 'created_at'){ - return new Date(a[sortBy]) - new Date(b[sortBy]); - } else if(sortBy === 'user_count' || sortBy === 'open_count'){ - return a.stats[sortBy] - b.stats[sortBy]; - }else{ - a[sortBy] > b[sortBy] ? 1 : -1 - } - }); - }else{ - sorted_apps = apps.sort((a, b) => { - if(sortBy === 'name'){ - return b[sortBy].localeCompare(a[sortBy]); - }else if(sortBy === 'created_at'){ - return new Date(b[sortBy]) - new Date(a[sortBy]); - } else if(sortBy === 'user_count' || sortBy === 'open_count'){ - return b.stats[sortBy] - a.stats[sortBy]; - }else{ - b[sortBy] > a[sortBy] ? 1 : -1 - } - }); - } - // refresh app list - $('.app-card').remove(); - sorted_apps.forEach(app => { - $('#app-list-table > tbody').append(generate_app_card(app)); - }); - - count_apps(); - - // show apps that match search_query and hide apps that don't - if (search_query) { - // show apps that match search_query and hide apps that don't - apps.forEach((app) => { - if (app.title.toLowerCase().includes(search_query.toLowerCase())) { - $(`.app-card[data-name="${html_encode(app.name)}"]`).show(); - } else { - $(`.app-card[data-name="${html_encode(app.name)}"]`).hide(); - } - }) - } -} - -/** - * Checks if the items being deployed contain a .git directory - * @param {Array|string} items - Items to check (can be path string or array of items) - * @returns {Promise} - True if .git directory is found - */ -async function hasGitDirectory(items) { - // Case 1: Single Puter path - if (typeof items === 'string' && (items.startsWith('/') || items.startsWith('~'))) { - const stat = await puter.fs.stat(items); - if (stat.is_dir) { - const files = await puter.fs.readdir(items); - return files.some(file => file.name === '.git' && file.is_dir); - } - return false; - } - - // Case 2: Array of Puter items - if (Array.isArray(items) && items[0]?.uid) { - return items.some(item => item.name === '.git' && item.is_dir); - } - - // Case 3: Local items (DataTransferItems) - if (Array.isArray(items)) { - for (let item of items) { - if (item.fullPath?.includes('/.git/') || - item.path?.includes('/.git/') || - item.filepath?.includes('/.git/')) { - return true; - } - } - } - - return false; -} - -/** - * Shows a warning dialog about .git directory deployment - * @returns {Promise} - True if the user wants to proceed with deployment - */ -async function showGitWarningDialog() { - try { - // Check if the user has chosen to skip the warning - const skipWarning = await puter.kv.get('skip-git-warning'); - - // Log retrieved value for debugging - console.log('Retrieved skip-git-warning:', skipWarning); - - // If the user opted to skip the warning, proceed without showing it - if (skipWarning === true) { - return true; - } - } catch (error) { - console.error('Error accessing KV store:', error); - // If KV store access fails, fall back to showing the dialog - } - - // Create the modal dialog - const modal = document.createElement('div'); - modal.innerHTML = ` -
-

Warning: Git Repository Detected

-

A .git directory was found in your deployment files. Deploying .git directories may:

-
    -
  • Expose sensitive information like commit history and configuration
  • -
  • Significantly increase deployment size
  • -
-
- - -
-
- - -
-
-
- `; - document.body.appendChild(modal); - - return new Promise((resolve) => { - // Handle "Continue Deployment" - document.getElementById('continue-deployment').addEventListener('click', async () => { - try { - const skipChecked = document.getElementById('skip-git-warning')?.checked; - if (skipChecked) { - console.log("Saving 'skip-git-warning' preference as true"); - await puter.kv.set('skip-git-warning', true); - } - } catch (error) { - console.error('Error saving user preference to KV store:', error); - } finally { - document.body.removeChild(modal); - resolve(true); // Continue deployment - } - }); - - // Handle "Cancel Deployment" - document.getElementById('cancel-deployment').addEventListener('click', () => { - document.body.removeChild(modal); - resolve(false); // Cancel deployment - }); - }); -} - -window.deploy = async function (app, items) { - // Check for .git directory before proceeding - try { - if (await hasGitDirectory(items)) { - const shouldProceed = await showGitWarningDialog(); - if (!shouldProceed) { - reset_drop_area(); - return; - } - } - } catch (err) { - console.error('Error checking for .git directory:', err); - } - let appdata_dir, current_app_dir; - - // disable deploy button - $('.deploy-btn').addClass('disabled'); - - // change drop area text - $('.drop-area').html(deploying_spinner + '
Deploying (0%)
'); - - if (typeof items === 'string' && (items.startsWith('/') || items.startsWith('~'))) { - $('.drop-area').removeClass('drop-area-hover'); - $('.drop-area').addClass('drop-area-ready-to-deploy'); - } - - // -------------------------------------------------------------------- - // Get current directory, we need to delete the existing hostname - // later on - // -------------------------------------------------------------------- - try { - current_app_dir = await puter.fs.stat({ - path: `/${authUsername}/AppData/${dev_center_uid}/${app.uid ?? app.uuid}`, - returnSubdomains: true - }); - } catch (err) { - console.log(err); - } - - // -------------------------------------------------------------------- - // Delete existing hostnames attached to this app directory if they exist - // -------------------------------------------------------------------- - if (current_app_dir?.subdomains.length > 0) { - for (let subdomain of current_app_dir?.subdomains) { - puter.hosting.delete(subdomain.subdomain) - } - } - - // -------------------------------------------------------------------- - // Delete existing app directory - // -------------------------------------------------------------------- - try { - await puter.fs.delete(current_app_dir.path) - } catch (err) { - console.log(err); - } - - // -------------------------------------------------------------------- - // Make an app directory under AppData - // if the directory already exists, it should be overwritten - // -------------------------------------------------------------------- - try { - appdata_dir = await puter.fs.mkdir( - // path - `/${authUsername}/AppData/${dev_center_uid}/${app.uid ?? app.uuid}`, - // options - { overwrite: true, recursive: true, rename: false } - ) - } catch (err) { - console.log(err); - } - - // -------------------------------------------------------------------- - // (A) One Puter Item: If 'items' is a string and starts with /, it's a path to a Puter item - // -------------------------------------------------------------------- - if (typeof items === 'string' && (items.startsWith('/') || items.startsWith('~'))) { - // perform stat on 'items' - const stat = await puter.fs.stat(items); - - // -------------------------------------------------------------------- - // Puter Directory - // -------------------------------------------------------------------- - // Perform readdir on 'items' - // todo there is apparently a bug in Puter where sometimes path is literally missing from the items - // returned by readdir. This is the 'path' that readdit didn't return a path for: "~/Desktop/particle-clicker-master" - if (stat.is_dir) { - const files = await puter.fs.readdir(items); - // copy the 'files' to the app directory - if (files.length > 0) { - for (let file of files) { - // perform copy - await puter.fs.copy( - file.path, - appdata_dir.path, - { overwrite: true } - ); - // update progress - $('.deploy-percent').text(`(${Math.round((files.indexOf(file) / files.length) * 100)}%)`); - } - } - } - // -------------------------------------------------------------------- - // Puter File - // -------------------------------------------------------------------- - else { - // copy the 'files' to the app directory - await puter.fs.copy( - items, - appdata_dir.path, - { overwrite: true } - ); - } - - // generate new hostname with a random suffix - let hostname = `${currently_editing_app.name}-${(Math.random() + 1).toString(36).substring(7)}`; - - // -------------------------------------------------------------------- - // Create a router for the app with the fresh hostname - // we change hostname every time to prevent caching issues - // -------------------------------------------------------------------- - puter.hosting.create(hostname, appdata_dir.path).then(async (res) => { - // TODO this endpoint needs to be able to update only the specified fields - puter.apps.update(currently_editing_app.name, { - indexURL: protocol + `://${hostname}.` + static_hosting_domain, - title: currently_editing_app.title, - name: currently_editing_app.name, - icon: currently_editing_app.icon, - description: currently_editing_app.description, - maximizeOnStart: currently_editing_app.maximize_on_start, - background: currently_editing_app.background, - filetypeAssociations: currently_editing_app.filetype_associations, - }) - // set the 'Index URL' field for the 'Settings' tab - $('#edit-app-index-url').val(protocol + `://${hostname}.` + static_hosting_domain); - // show success message - $('.deploy-success-msg').show(); - // reset drop area - reset_drop_area(); - }) - } - // -------------------------------------------------------------------- - // (B) Multiple Puter Items: If `items` is an Array `items[0]` has `uid` - // then it's a Puter Item Array. - // -------------------------------------------------------------------- - else if (Array.isArray(items) && items[0].uid) { - // If there's no index.html in the root, return - if (!hasRootIndexHtml) - return; - - // copy the 'files' to the app directory - for (let item of items) { - // perform copy - await puter.fs.copy( - item.fullPath ? item.fullPath : item.path ? item.path : item.filepath, - appdata_dir.path, - { overwrite: true } - ); - // update progress - $('.deploy-percent').text(`(${Math.round((items.indexOf(item) / items.length) * 100)}%)`); - } - - // generate new hostname with a random suffix - let hostname = `${currently_editing_app.name}-${(Math.random() + 1).toString(36).substring(7)}`; - - // -------------------------------------------------------------------- - // Create a router for the app with the fresh hostname - // we change hostname every time to prevent caching issues - // -------------------------------------------------------------------- - puter.hosting.create(hostname, appdata_dir.path).then(async (res) => { - // TODO this endpoint needs to be able to update only the specified fields - puter.apps.update(currently_editing_app.name, { - indexURL: protocol + `://${hostname}.` + static_hosting_domain, - title: currently_editing_app.title, - name: currently_editing_app.name, - icon: currently_editing_app.icon, - description: currently_editing_app.description, - maximizeOnStart: currently_editing_app.maximize_on_start, - background: currently_editing_app.background, - filetypeAssociations: currently_editing_app.filetype_associations, - }) - // set the 'Index URL' field for the 'Settings' tab - $('#edit-app-index-url').val(protocol + `://${hostname}.` + static_hosting_domain); - // show success message - $('.deploy-success-msg').show(); - // reset drop area - reset_drop_area(); - }) - } - - // -------------------------------------------------------------------- - // (C) Local Items: Upload new deploy - // -------------------------------------------------------------------- - else { - puter.fs.upload( - items, - `/${authUsername}/AppData/${dev_center_uid}/${currently_editing_app.uid}`, - { - dedupeName: false, - overwrite: false, - parsedDataTransferItems: true, - createMissingAncestors: true, - progress: function (operation_id, op_progress) { - $('.deploy-percent').text(`(${op_progress}%)`); - }, - }).then(async (uploaded) => { - // new hostname - let hostname = `${currently_editing_app.name}-${(Math.random() + 1).toString(36).substring(7)}`; - - // ---------------------------------------- - // Create a router for the app with a fresh hostname - // we change hostname every time to prevent caching issues - // ---------------------------------------- - puter.hosting.create(hostname, appdata_dir.path).then(async (res) => { - // TODO this endpoint needs to be able to update only the specified fields - puter.apps.update(currently_editing_app.name, { - indexURL: protocol + `://${hostname}.` + static_hosting_domain, - title: currently_editing_app.title, - name: currently_editing_app.name, - icon: currently_editing_app.icon, - description: currently_editing_app.description, - maximizeOnStart: currently_editing_app.maximize_on_start, - background: currently_editing_app.background, - filetypeAssociations: currently_editing_app.filetype_associations, - }) - // set the 'Index URL' field for the 'Settings' tab - $('#edit-app-index-url').val(protocol + `://${hostname}.` + static_hosting_domain); - // show success message - $('.deploy-success-msg').show(); - // reset drop area - reset_drop_area() - }) - }) - } -} - $(document).on('click', '.section-tab-btn', function (e) { // hide all tabs $('.section-tab').hide(); @@ -2322,207 +331,10 @@ $(document).on('click', '.section-tab-btn', function (e) { $(this).addClass('active'); }) -function generateDirTree(paths) { - const root = {}; - - for (let path of paths) { - let parts = path.split('/'); - let currentNode = root; - for (let part of parts) { - if (!part) continue; // skip empty parts, especially leading one - if (!currentNode[part]) { - currentNode[part] = {}; - } - currentNode = currentNode[part]; - } - } - - return root; -} - -function setRootDirTree(tree, items) { - // Get all keys (directories and files) in the root - const rootKeys = Object.keys(tree); - - // If there's only one object in the root, check if it's non-empty and return it - if (rootKeys.length === 1 && typeof tree[rootKeys[0]] === 'object' && Object.keys(tree[rootKeys[0]]).length > 0) { - let newItems = []; - for (let item of items) { - if (item.fullPath) - item.finalPath = item.fullPath.replace(rootKeys[0], ''); - else if (item.path) - item.path = item.path.replace(rootKeys[0], ''); - else - item.filepath = item.filepath.replace(rootKeys[0], ''); - - newItems.push(item); - } - return newItems; - } else { - return items; - } -} - -function hasRootIndexHtml(tree) { - // Check if index.html exists in the root - if (tree['index.html']) { - return true; - } - - // Get all keys (directories and files) in the root - const rootKeys = Object.keys(tree); - - // If there's only one directory in the root, check if index.html exists in that directory - if (rootKeys.length === 1 && typeof tree[rootKeys[0]] === 'object' && tree[rootKeys[0]]['index.html']) { - return true; - } - - return false; -} - $(document).on('click', '.close-success-msg', function (e) { $(this).closest('div').fadeOut(); }) -$(document).on('click', '.open-app', function (e) { - puter.ui.launchApp($(this).attr('data-app-name')); -}) - -$(document).on('click', '.insta-deploy-to-new-app', async function (e) { - $('.insta-deploy-modal').get(0).close(); - let title = await puter.ui.prompt('Please enter a title for your app:', 'My Awesome App'); - - if (title.length > 60) { - puter.ui.alert(`Title cannot be longer than 60.`, [ - { - label: 'Ok', - }, - ]); - // todo go back to create an app prompt and prefill the title input with the title the user entered - $('.insta-deploy-modal').get(0).showModal(); - } - else if (title) { - if (source_path) { - create_app(title, source_path); - source_path = null; - } else { - create_app(title, null, dropped_items); - dropped_items = null; - } - } else - $('.insta-deploy-modal').get(0).showModal(); - - return; - -}) - -$(document).on('click', '.insta-deploy-to-existing-app', function (e) { - $('.insta-deploy-modal').get(0).close(); - $('.insta-deploy-existing-app-select').get(0).showModal(); - $('.insta-deploy-existing-app-list').html(`
${loading_spinner}
`); - puter.apps.list({ icon_size: 64 }).then((apps) => { - setTimeout(() => { - $('.insta-deploy-existing-app-list').html(''); - if (apps.length === 0) - $('.insta-deploy-existing-app-list').html(` -
- - You have no existing apps. -
- `); - else { - for (let app of apps) { - $('.insta-deploy-existing-app-list').append( - `
- - ${html_encode(app.title)} -
- ${number_format((app.stats.referral_count ?? 0) + app.stats.user_count)} - ${number_format(app.stats.open_count)} -
-
` - ); - } - } - }, 500); - }) - - // todo reset .insta-deploy-existing-app-list on close -}) - -$(document).on('click', '.insta-deploy-app-selector', function (e) { - $('.insta-deploy-app-selector').removeClass('active'); - $(this).addClass('active'); - - // enable deploy button - $('.insta-deploy-existing-app-deploy-btn').removeClass('disabled'); -}) - -$(document).on('click', '.insta-deploy-existing-app-deploy-btn', function (e) { - $('.insta-deploy-existing-app-deploy-btn').addClass('disabled'); - $('.insta-deploy-existing-app-select')?.get(0)?.close(); - - const app_item = $('.insta-deploy-app-selector.active'); - - // load the 'App Settings' section - edit_app_section(app_item.attr('data-name')); - - $('.drop-area').removeClass('drop-area-hover'); - $('.drop-area').addClass('drop-area-ready-to-deploy'); - let drop_area_content = `

Ready to deploy 🚀

Cancel

`; - $('.drop-area').html(drop_area_content); - - // deploy - console.log('data uid is present?', $(e.target).attr('data-uid'), app_item.attr('data-uid')); - deploy({ uid: app_item.attr('data-uid') }, source_path ?? dropped_items); - $('.insta-deploy-existing-app-list').html(''); -}) - -$(document).on('click', '.insta-deploy-cancel', function (e) { - $(this).closest('dialog')?.get(0)?.close(); -}) -$(document).on('click', '.insta-deploy-existing-app-back', function (e) { - $('.insta-deploy-existing-app-select')?.get(0)?.close(); - $('.insta-deploy-modal')?.get(0)?.showModal(); - // disable deploy button - $('.insta-deploy-existing-app-deploy-btn').addClass('disabled'); - - // todo disable the 'an existing app' option if there are no existing apps -}) - - -$(document).on('click', '.add-app-to-desktop', function (e) { - let app_title = $(this).attr('data-app-title'); - let app_uid = $(this).attr('data-app-uid'); - - puter.fs.upload( - new File([], app_title), - `/${authUsername}/Desktop`, - { - name: app_title, - dedupeName: true, - overwrite: false, - appUID: app_uid, - }).then(async (uploaded) => { - puter.ui.alert(`${app_title} shortcut has been added to your desktop.`, [ - { - label: 'Ok', - type: 'primary', - }, - ], { - type: 'success', - }); - }) - -}) - -function reset_drop_area() { - dropped_items = null; - $('.drop-area').html(drop_area_placeholder); - $('.drop-area').removeClass('drop-area-ready-to-deploy'); - $('.deploy-btn').addClass('disabled'); -} - $('body').on('dragover', function (event) { // skip if the user is dragging something over the drop area if ($(event.target).hasClass('drop-area')) @@ -2610,253 +422,13 @@ $('body').on('drop', async function (event) { $('.insta-deploy-item-name').html(html_encode(rootItems)); }); -$('.insta-deploy-existing-app-select').on('close', function (e) { - $('.insta-deploy-existing-app-list').html(''); -}) - -$('.refresh-app-list').on('click', function (e) { - puter.ui.showSpinner(); - - puter.apps.list({ icon_size: 64 }).then((resp) => { - setTimeout(() => { - apps = resp; - - $('.app-card').remove(); - apps.forEach(app => { - $('#app-list-table > tbody').append(generate_app_card(app)); - }); - - count_apps(); - - // preserve search query - if (search_query) { - // show apps that match search_query and hide apps that don't - apps.forEach((app) => { - if (app.title.toLowerCase().includes(search_query.toLowerCase())) { - $(`.app-card[data-name="${app.name}"]`).show(); - } else { - $(`.app-card[data-name="${app.name}"]`).hide(); - } - }) - } - - // preserve sort - sort_apps(); - activate_tippy(); - - puter.ui.hideSpinner(); - }, 1000); - }) -}) - -$(document).on('click', '.search', function (e) { - e.stopPropagation(); - e.preventDefault(); - // don't let click bubble up to window - e.stopImmediatePropagation(); -}) - -$(document).on('input change keyup keypress keydown paste cut', '.search', function (e) { - // search apps for query - search_query = $(this).val().toLowerCase(); - if (search_query === '') { - // hide 'clear search' button - $('.search-clear').hide(); - // show all apps again - $(`.app-card`).show(); - } else { - // show 'clear search' button - $('.search-clear').show(); - // show apps that match search_query and hide apps that don't - apps.forEach((app) => { - if ( - app.title.toLowerCase().includes(search_query.toLowerCase()) - || app.name.toLowerCase().includes(search_query.toLowerCase()) - || app.description.toLowerCase().includes(search_query.toLowerCase()) - || app.uid.toLowerCase().includes(search_query.toLowerCase()) - ) - { - $(`.app-card[data-name="${app.name}"]`).show(); - } else { - $(`.app-card[data-name="${app.name}"]`).hide(); - } - }) - } -}) - -$(document).on('click', '.search-clear', function (e) { - $('.search').val(''); - $('.search').trigger('change'); - $('.search').focus(); - search_query = ''; -}) - -$(document).on('change', '.app-checkbox', function (e) { - // determine if select-all checkbox should be checked, indeterminate, or unchecked - if ($('.app-checkbox:checked').length === $('.app-checkbox').length) { - $('.select-all-apps').prop('indeterminate', false); - $('.select-all-apps').prop('checked', true); - } else if ($('.app-checkbox:checked').length > 0) { - $('.select-all-apps').prop('indeterminate', true); - $('.select-all-apps').prop('checked', false); - } - else { - $('.select-all-apps').prop('indeterminate', false); - $('.select-all-apps').prop('checked', false); - } - - // activate row - if ($(this).is(':checked')) - $(this).closest('tr').addClass('active'); - else - $(this).closest('tr').removeClass('active'); - - // enable delete button if at least one checkbox is checked - if ($('.app-checkbox:checked').length > 0) - $('.delete-apps-btn').removeClass('disabled'); - else - $('.delete-apps-btn').addClass('disabled'); - -}) - -$(document).on('click', '.delete-apps-btn', async function (e) { - // show confirmation alert - let resp = await puter.ui.alert(`Are you sure you want to delete the selected apps?`, [ - { - label: 'Delete', - type: 'danger', - value: 'delete', - }, - { - label: 'Cancel', - }, - ], { - type: 'warning', - }); - - if (resp === 'delete') { - // disable delete button - // $('.delete-apps-btn').addClass('disabled'); - - // show 'deleting' modal - puter.ui.showSpinner(); - - let start_ts = Date.now(); - const apps = $('.app-checkbox:checked').toArray(); - - // delete all checked apps - for (let app of apps) { - // get app uid - const app_uid = $(app).attr('data-app-uid'); - const app_name = $(app).attr('data-app-name'); - - // get app - const app_data = await puter.apps.get(app_name, {icon_size: 64 }); - - if(app_data.metadata?.locked){ - if(apps.length === 1){ - puter.ui.alert(`${app_data.title} is locked and cannot be deleted.`, [ - { - label: 'Ok', - }, - ], { - type: 'warning', - }); - - break; - } - - let resp = await puter.ui.alert(`${app_data.title} is locked and cannot be deleted.`, [ - { - label: 'Skip and Continue', - value: 'Continue', - type: 'primary' - }, - { - label: 'Cancel', - }, - ], { - type: 'warning', - }); - - if(resp === 'Cancel') - break; - else if(resp === 'Continue') - continue; - else - continue; - } - - // delete app - await puter.apps.delete(app_name) - - // remove app card - $(`.app-card[data-uid="${app_uid}"]`).fadeOut(200, function name(params) { - $(this).remove(); - if ($(`.app-card`).length === 0) { - $('section:not(.sidebar)').hide(); - $('#no-apps-notice').show(); - } else { - $('section:not(.sidebar)').hide(); - $('#app-list').show(); - } - count_apps(); - }); - - try{ - // get app directory - const stat = await puter.fs.stat({ - path: `/${authUsername}/AppData/${dev_center_uid}/${app_uid}`, - returnSubdomains: true - }); - // delete subdomain associated with the app directory - if(stat?.subdomains[0]?.subdomain){ - await puter.hosting.delete(stat.subdomains[0].subdomain) - } - // delete app directory - await puter.fs.delete( - `/${authUsername}/AppData/${dev_center_uid}/${app_uid}`, - { recursive: true } - ) - count_apps(); - } catch(err) { - console.log(err); - } - } - - // close 'deleting' modal - setTimeout(() => { - puter.ui.hideSpinner(); - if($('.app-checkbox:checked').length === 0){ - // disable delete button - $('.delete-apps-btn').addClass('disabled'); - // reset the 'select all' checkbox - $('.select-all-apps').prop('indeterminate', false); - $('.select-all-apps').prop('checked', false); - } - }, (start_ts - Date.now()) > 500 ? 0 : 500); - } -}) - -$(document).on('change', '.select-all-apps', function (e) { - if ($(this).is(':checked')) { - $('.app-checkbox').prop('checked', true); - $('.app-card').addClass('active'); - $('.delete-apps-btn').removeClass('disabled'); - } else { - $('.app-checkbox').prop('checked', false); - $('.app-card').removeClass('active'); - $('.delete-apps-btn').addClass('disabled'); - } -}) - /** * Get the MIME type for a given file extension. * * @param {string} extension - The file extension (with or without leading dot). * @returns {string} The corresponding MIME type, or 'application/octet-stream' if not found. */ -function getMimeType(extension) { +window.getMimeType = (extension) => { const mimeTypes = { jpg: 'image/jpeg', jpeg: 'image/jpeg', @@ -2876,287 +448,12 @@ function getMimeType(extension) { return mimeTypes[cleanExtension] || 'application/octet-stream'; } -// if edit-app-maximize-on-start is checked, disable window size and position fields -$(document).on('change', '#edit-app-maximize-on-start', function (e) { - if ($(this).is(':checked')) { - $('#edit-app-window-width, #edit-app-window-height').prop('disabled', true); - $('#edit-app-window-top, #edit-app-window-left').prop('disabled', true); - } else { - $('#edit-app-window-width, #edit-app-window-height').prop('disabled', false); - $('#edit-app-window-top, #edit-app-window-left').prop('disabled', false); - } -}) - -$(document).on('change', '#edit-app-background', function (e) { - if($('#edit-app-background').is(":checked")){ - disable_window_settings() - }else{ - enable_window_settings() - } -}) - -function disable_window_settings(){ - $('#edit-app-maximize-on-start').prop('disabled', true); - $('#edit-app-fullpage-on-landing').prop('disabled', true); - $('#edit-app-window-width, #edit-app-window-height').prop('disabled', true); - $('#edit-app-window-top, #edit-app-window-left').prop('disabled', true); - $('#edit-app-window-resizable').prop('disabled', true); - $('#edit-app-hide-titlebar').prop('disabled', true); -} - -function enable_window_settings(){ - $('#edit-app-maximize-on-start').prop('disabled', false); - $('#edit-app-fullpage-on-landing').prop('disabled', false); - $('#edit-app-window-width, #edit-app-window-height').prop('disabled', false); - $('#edit-app-window-top, #edit-app-window-left').prop('disabled', false); - $('#edit-app-window-resizable').prop('disabled', false); - $('#edit-app-hide-titlebar').prop('disabled', false); -} - -$(document).on('click', '.reset-deploy', function (e) { - reset_drop_area(); -}) - $(document).on('click', '.sidebar-toggle', function (e) { $('.sidebar').toggleClass('open'); $('body').toggleClass('sidebar-open'); }) -async function initializeAssetsDirectory() { - try { - // Check if assets_url exists - const existingURL = await puter.kv.get('assets_url'); - if (!existingURL) { - // Create assets directory - const assetsDir = await puter.fs.mkdir( - `/${authUsername}/AppData/${dev_center_uid}/assets`, - { overwrite: false } - ); - - // Publish the directory - const hostname = `assets-${Math.random().toString(36).substring(2)}`; - const route = await puter.hosting.create(hostname, assetsDir.path); - - // Store the URL - await puter.kv.set('assets_url', `https://${hostname}.puter.site`); - } - } catch (err) { - console.error('Error initializing assets directory:', err); - } -} - -function generateSocialImageSection(app) { - return ` - - - Remove social image - - `; -} - - -$(document).on('click', '#edit-app-social-image', async function(e) { - const res = await puter.ui.showOpenFilePicker({ - accept: "image/*", - }); - - const socialImage = await puter.fs.read(res.path); - // Convert blob to base64 for preview - const reader = new FileReader(); - reader.readAsDataURL(socialImage); - - reader.onloadend = function() { - let image = reader.result; - // Get file extension - let fileExtension = res.name.split('.').pop(); - // Get MIME type - let mimeType = getMimeType(fileExtension); - // Replace MIME type in the data URL - image = image.replace('data:application/octet-stream;base64', `data:image/${mimeType};base64`); - - $('#edit-app-social-image').css('background-image', `url(${image})`); - $('#edit-app-social-image').attr('data-base64', image); - $('#edit-app-social-image-delete').show(); - - toggleSaveButton(); - toggleResetButton(); - } -}); - -$(document).on('click', '#edit-app-social-image-delete', async function(e) { - $('#edit-app-social-image').css('background-image', ''); - $('#edit-app-social-image').removeAttr('data-url'); - $('#edit-app-social-image').removeAttr('data-base64'); - $('#edit-app-social-image-delete').hide(); -}); - -async function handleSocialImageUpload(app_name, socialImageData) { - if (!socialImageData) return null; - - try { - const assets_url = await puter.kv.get('assets_url'); - if (!assets_url) throw new Error('Assets URL not found'); - - // Convert base64 to blob - const base64Response = await fetch(socialImageData); - const blob = await base64Response.blob(); - - // Get assets directory path - const assetsDir = `/${authUsername}/AppData/${dev_center_uid}/assets`; - - // Upload new image - await puter.fs.upload( - new File([blob], `${app_name}.png`, { type: 'image/png' }), - assetsDir, - { overwrite: true } - ); - - return `${assets_url}/${app_name}.png`; - } catch (err) { - console.error('Error uploading social image:', err); - throw err; - } -} - -$(document).on('click', '.copy-app-uid', function(e) { - const appUID = $('#edit-app-uid').val(); - navigator.clipboard.writeText(appUID); - // change to 'copied' - $(this).html('Copied'); - setTimeout(() => { - $(this).html(copy_svg); - }, 2000); -}); - -$(document).on('change', '#analytics-period', async function(e) { - let period = $(this).val(); - render_analytics(period); -}); - -async function render_analytics(period){ - puter.ui.showSpinner(); - - // set a sensible stats_grouping based on the selected period - let stats_grouping; - - if (period === 'today' || period === 'yesterday') { - stats_grouping = 'hour'; - } - else if (period === 'this_week' || period === 'last_week' || period === 'this_month' || period === 'last_month' || period === '7d' || period === '30d') { - stats_grouping = 'day'; - } - else if (period === 'this_year' || period === 'last_year' || period === '12m' || period === 'all') { - stats_grouping = 'month'; - } - - const app = await puter.apps.get( - currently_editing_app.name, - { - icon_size: 16, - stats_period: period, - stats_grouping: stats_grouping, - } - ); - - $('#analytics-users .count').html(number_format(app.stats.user_count)); - $('#analytics-opens .count').html(number_format(app.stats.open_count)); - - // Clear existing chart if any - $('#analytics-chart').remove(); - $('.analytics-container').remove(); - - // Create new canvas - const container = $('
'); - const canvas = $(''); - container.append(canvas); - $('#analytics-opens').parent().after(container); - - // Format the data - const labels = app.stats.grouped_stats.open_count.map(item => { - let date; - if (stats_grouping === 'month') { - // Handle YYYY-MM format explicitly - const [year, month] = item.period.split('-'); - date = new Date(parseInt(year), parseInt(month) - 1); // month is 0-based in JS - } else { - date = new Date(item.period); - } - - if (stats_grouping === 'hour') { - return date.toLocaleString('en-US', { hour: 'numeric', hour12: true }).toLowerCase(); - } else if (stats_grouping === 'day') { - return date.toLocaleString('en-US', { month: 'short', day: 'numeric' }); - } else { - return date.toLocaleString('en-US', { month: 'short', year: 'numeric' }); - } - }); - const openData = app.stats.grouped_stats.open_count.map(item => item.count); - const userData = app.stats.grouped_stats.user_count.map(item => item.count); - - // Create chart - const ctx = document.getElementById('analytics-chart').getContext('2d'); - new Chart(ctx, { - type: 'line', - data: { - labels: labels, - datasets: [ - { - label: 'Opens', - data: openData, - borderColor: '#346beb', - tension: 0, - fill: false - }, - { - label: 'Users', - data: userData, - borderColor: '#27cc32', - tension: 0, - fill: false - } - ] - }, - options: { - responsive: true, - maintainAspectRatio: false, - scales: { - x: { - display: true, - title: { - display: true, - text: 'Period' - }, - ticks: { - maxRotation: 45, - minRotation: 45 - } - }, - y: { - display: true, - beginAtZero: true, - title: { - display: true, - text: 'Count' - }, - ticks: { - precision: 0, // Show whole numbers only - stepSize: 1 // Increment by 1 - } - } - }, - } - }); - - puter.ui.hideSpinner(); -} - -$(document).on('click', '.stats-cell', function(e) { - edit_app_section($(this).attr('data-app-name'), 'analytics'); -}) - -function activate_tippy(){ +window.activate_tippy = () => { tippy('.tippy', { content(reference) { return reference.getAttribute('title'); diff --git a/src/dev-center/js/html-entities.js b/src/dev-center/js/libs/html-entities.js similarity index 100% rename from src/dev-center/js/html-entities.js rename to src/dev-center/js/libs/html-entities.js diff --git a/src/dev-center/js/jquery-3.6.0.min.js b/src/dev-center/js/libs/jquery-3.6.0.min.js similarity index 100% rename from src/dev-center/js/jquery-3.6.0.min.js rename to src/dev-center/js/libs/jquery-3.6.0.min.js diff --git a/src/dev-center/js/jquery.dragster.js b/src/dev-center/js/libs/jquery.dragster.js similarity index 100% rename from src/dev-center/js/jquery.dragster.js rename to src/dev-center/js/libs/jquery.dragster.js diff --git a/src/dev-center/js/slugify.js b/src/dev-center/js/libs/slugify.js similarity index 100% rename from src/dev-center/js/slugify.js rename to src/dev-center/js/libs/slugify.js diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js new file mode 100644 index 000000000..e896bbb27 --- /dev/null +++ b/src/dev-center/js/websites.js @@ -0,0 +1,4 @@ +async function init_websites() { +} + +export default init_websites; \ No newline at end of file diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js new file mode 100644 index 000000000..2ad8b9a4d --- /dev/null +++ b/src/dev-center/js/workers.js @@ -0,0 +1,63 @@ +window.workers = []; + +window.create_worker = async (name) => { + let worker = await puter.workers.create(name, window.default_worker_file); + + return worker; +} + +window.refresh_worker_list = (show_loading = false) => { + if (show_loading) + puter.ui.showSpinner(); + // get workers + setTimeout(function () { + puter.workers.list().then((workers_res) => { + workers = workers_res; + puter.ui.hideSpinner(); + }) + }, 1000); +} + + +async function init_workers() { +} + +$(document).on('click', '.create-a-worker-btn', async function (e) { + let name = await puter.ui.prompt('Please enter a name for your worker:', 'my-awesome-worker'); + + if (name) { + console.log(name); + await create_worker(name); + } +}) + +window.createDefaultWorkerFile = async () => { + window.default_worker_file = `/${auth_username}/AppData/${dev_center_uid}/default_worker_file.js`; + let existingFile; + try { + // Check if default_worker_file exists + existingFile = await puter.fs.read(default_worker_file); + } catch (err) { + console.error('Error creating default worker file:', err); + } + + if (!existingFile) { + // Create default_worker_file + await puter.fs.write(default_worker_file, `// This is an example application for Puter Workers + +router.get('/', ({request}) => { +return 'Hello World'; // returns a string +}); +router.get('/api/hello', ({request}) => { +return {'msg': 'hello'}; // returns a JSON object +}); +router.get('/*page', ({request, params}) => { +return new Response(\`Page \${params.page} not found\`, {status: 404}); +});`); + } + +} + + + +export default init_workers; \ No newline at end of file From 970ba0bd7720a036816f31c3bb3b13d12df24962 Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sat, 2 Aug 2025 15:39:57 -0700 Subject: [PATCH 03/46] enable checkboxes for workers table --- src/dev-center/css/style.css | 51 ++++++++++++++---------- src/dev-center/js/apps.js | 69 +++++++++++++-------------------- src/dev-center/js/dev-center.js | 5 +++ src/dev-center/js/workers.js | 38 ++++++++++++++++++ 4 files changed, 102 insertions(+), 61 deletions(-) diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index 73f461c90..eac0e6fc1 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -1230,6 +1230,17 @@ dialog{ display: inline-block; background-color: #e3f2fd; color: #1976d2; + background-color: #f3f4f6; + color: #374151; + padding: 1px 6px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + margin: 1px 0 0 0; + max-width: fit-content; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .category-select { @@ -1254,28 +1265,28 @@ dialog{ width: 18px; margin-right: 5px; margin-bottom: -4px; } .category-badge { - display: inline-block; - background-color: #f2f2f2; - color: #555; - font-size: 12px; - font-weight: 500; - padding: 2px 6px; - margin-left: 8px; - border-radius: 6px; - vertical-align: middle; + display: inline-block; + background-color: #f2f2f2; + color: #555; + font-size: 12px; + font-weight: 500; + padding: 2px 6px; + margin-left: 8px; + border-radius: 6px; + vertical-align: middle; } .app-category { - display: inline-block; - background-color: #f3f4f6; - color: #374151; - padding: 1px 6px; - border-radius: 4px; - font-size: 11px; - font-weight: 500; - margin-top: 2px; - max-height: 18px; - line-height: 1.2; - white-space: nowrap; + display: inline-block; + background-color: #f3f4f6; + color: #374151; + padding: 1px 6px; + border-radius: 4px; + font-size: 11px; + font-weight: 500; + margin-top: 2px; + max-height: 18px; + line-height: 1.2; + white-space: nowrap; } .app-row-toolbar span { diff --git a/src/dev-center/js/apps.js b/src/dev-center/js/apps.js index 3c86d521a..4da813110 100644 --- a/src/dev-center/js/apps.js +++ b/src/dev-center/js/apps.js @@ -273,7 +273,7 @@ $(document).on('click', '.deploy-btn', function (e) { deploy(currently_editing_app, dropped_items); }) -$(document).on('click', '.edit-app, .got-to-edit-app', function (e) { +$(document).on('click', '.edit-app, .go-to-edit-app', function (e) { const cur_app_name = $(this).attr('data-app-name') edit_app_section(cur_app_name); }) @@ -1460,21 +1460,22 @@ $(document).on('click', '#edit-app-icon', async function (e) { */ function generate_app_card(app) { let h = ``; -h += ``; - // check box - h += ``; - h += `
`; - h += ``; - h += `
`; - h += ``; - // App info (title, category, toolbar) -h += ``; + h += ``; + // check box + h += ``; + h += `
`; + h += ``; + h += `
`; + h += ``; + + // App info (title, category, toolbar) + h += ``; - // Wrapper for icon + content side by side -h += `
`; + // Wrapper for icon + content side by side + h += `
`; // Icon - h += `
`; // Info block with fixed layout -h += `
`; + h += `
`; - // Title - h += `

c.id === app.metadata.category); if (category) { - h += `${html_encode(category.label)}`; + h += `${html_encode(category.label)}`; } } @@ -1543,18 +1532,17 @@ h += `

`; - // Toolbar - h += `
`; - h += `
`; // remove inline styles - - h += `Open`; - h += `Settings`; - h += `Add Shortcut to Desktop`; - h += `Delete`; - h += `
`; -h += `
`; + // Toolbar + h += `
`; + h += `
`; // remove inline styles + h += `Open`; + h += `Settings`; + h += `Add Shortcut to Desktop`; + h += `Delete`; + h += `
`; + h += `
`; h += `
`; // end info column - h += `
`; // end row + h += `
`; // end row h += ``; @@ -2305,7 +2293,6 @@ $(document).on('change', '.app-checkbox', function (e) { $('.delete-apps-btn').removeClass('disabled'); else $('.delete-apps-btn').addClass('disabled'); - }) $(document).on('click', '.delete-apps-btn', async function (e) { diff --git a/src/dev-center/js/dev-center.js b/src/dev-center/js/dev-center.js index bec57515a..56f14d378 100644 --- a/src/dev-center/js/dev-center.js +++ b/src/dev-center/js/dev-center.js @@ -123,9 +123,14 @@ $(document).on('click', '.tab-btn', async function (e) { // append row to worker-list-table $('#worker-list-table > tbody').append(` + + + ${name} + ${worker.deployed_at} + `); diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index 2ad8b9a4d..47d23f40a 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -58,6 +58,44 @@ return new Response(\`Page \${params.page} not found\`, {status: 404}); } +$(document).on('change', '.worker-checkbox', function (e) { + // determine if select-all checkbox should be checked, indeterminate, or unchecked + if ($('.worker-checkbox:checked').length === $('.worker-checkbox').length) { + $('.select-all-workers').prop('indeterminate', false); + $('.select-all-workers').prop('checked', true); + } else if ($('.worker-checkbox:checked').length > 0) { + $('.select-all-workers').prop('indeterminate', true); + $('.select-all-workers').prop('checked', false); + } + else { + $('.select-all-workers').prop('indeterminate', false); + $('.select-all-workers').prop('checked', false); + } + + // activate row + if ($(this).is(':checked')) + $(this).closest('tr').addClass('active'); + else + $(this).closest('tr').removeClass('active'); + + // enable delete button if at least one checkbox is checked + if ($('.worker-checkbox:checked').length > 0) + $('.delete-workers-btn').removeClass('disabled'); + else + $('.delete-workers-btn').addClass('disabled'); +}) + +$(document).on('change', '.select-all-workers', function (e) { + if ($(this).is(':checked')) { + $('.worker-checkbox').prop('checked', true); + $('.worker-card').addClass('active'); + $('.delete-workers-btn').removeClass('disabled'); + } else { + $('.worker-checkbox').prop('checked', false); + $('.worker-card').removeClass('active'); + $('.delete-workers-btn').addClass('disabled'); + } +}) export default init_workers; \ No newline at end of file From 2d4b5984ed1b7ed31d596b1d4ad5d751b1576107 Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sat, 2 Aug 2025 16:50:58 -0700 Subject: [PATCH 04/46] Implement 'Delete' and 'Delete Selected' for workers --- src/dev-center/index.html | 34 +++- src/dev-center/js/dev-center.js | 28 +-- src/dev-center/js/workers.js | 299 +++++++++++++++++++++++++++++++- 3 files changed, 322 insertions(+), 39 deletions(-) diff --git a/src/dev-center/index.html b/src/dev-center/index.html index c0ac90dc2..ccc306825 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -267,10 +267,10 @@
- + - + @@ -281,10 +281,36 @@ - +
-

My Websites

+
+

My Websites

+ + +
+ +
+ + +
+ + +
+
Worker▼▲Deployed▼▲Created▼▲
+ + + + + + + + + + + +
Website▼▲Created▼▲
+
diff --git a/src/dev-center/js/dev-center.js b/src/dev-center/js/dev-center.js index 56f14d378..c25cfb59c 100644 --- a/src/dev-center/js/dev-center.js +++ b/src/dev-center/js/dev-center.js @@ -111,34 +111,8 @@ $(document).on('click', '.tab-btn', async function (e) { // Workers tab // --------------------------------------------------------------- else if ($(this).attr('data-tab') === 'workers') { + refresh_worker_list(); activeTab = 'workers'; - // puter.workers.list() returns an object with each worker as a key and the worker object as the value - // so we need to convert it to an array, key is the worker name and value is the worker object - window.workers = Object.entries(await puter.workers.list()); - // Get workers - if (activeTab === 'workers' && window.workers.length > 0) { - $('#no-workers-notice').hide(); - $('#worker-list').show(); - window.workers.forEach(([name, worker]) => { - // append row to worker-list-table - $('#worker-list-table > tbody').append(` - - - - - ${name} - ${worker.deployed_at} - - - - - - `); - }); - } else { - $('#no-workers-notice').show(); - $('#worker-list').hide(); - } } // --------------------------------------------------------------- // Payout Method tab diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index 47d23f40a..ed64f3402 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -1,4 +1,7 @@ +let sortBy = 'created_at'; +let sortDirection = 'desc'; window.workers = []; +let search_query; window.create_worker = async (name) => { let worker = await puter.workers.create(name, window.default_worker_file); @@ -6,16 +9,26 @@ window.create_worker = async (name) => { return worker; } -window.refresh_worker_list = (show_loading = false) => { +window.refresh_worker_list = async (show_loading = false) => { if (show_loading) puter.ui.showSpinner(); - // get workers - setTimeout(function () { - puter.workers.list().then((workers_res) => { - workers = workers_res; - puter.ui.hideSpinner(); - }) - }, 1000); + + // puter.workers.list() returns an array of worker objects + window.workers = await puter.workers.list(); + + // Get workers + if (window.activeTab === 'workers' && window.workers.length > 0) { + $('.worker-card').remove(); + $('#no-workers-notice').hide(); + $('#worker-list').show(); + window.workers.forEach((worker) => { + // append row to worker-list-table + $('#worker-list-table > tbody').append(generate_worker_card(worker)); + }); + } else { + $('#no-workers-notice').show(); + $('#worker-list').hide(); + } } @@ -97,5 +110,275 @@ $(document).on('change', '.select-all-workers', function (e) { } }) +$('.refresh-worker-list').on('click', function (e) { + puter.ui.showSpinner(); + refresh_worker_list(); + + puter.ui.hideSpinner(); +}) + +$('th.sort').on('click', function (e) { + // determine what column to sort by + const sortByColumn = $(this).attr('data-column'); + + // toggle sort direction + if (sortByColumn === sortBy) { + if (sortDirection === 'asc') + sortDirection = 'desc'; + else + sortDirection = 'asc'; + } + else { + sortBy = sortByColumn; + sortDirection = 'desc'; + } + + // update arrow + $('.sort-arrow').css('display', 'none'); + $('#worker-list-table').find('th').removeClass('sorted'); + $(this).find('.sort-arrow-' + sortDirection).css('display', 'inline'); + $(this).addClass('sorted'); + + sort_workers(); +}); + +function sort_workers() { + let sorted_workers; + + // sort + if (sortDirection === 'asc'){ + sorted_workers = workers.sort((a, b) => { + if(sortBy === 'name'){ + return a[sortBy].localeCompare(b[sortBy]); + }else if(sortBy === 'created_at'){ + return new Date(a[sortBy]) - new Date(b[sortBy]); + } else if(sortBy === 'user_count' || sortBy === 'open_count'){ + return a.stats[sortBy] - b.stats[sortBy]; + }else{ + a[sortBy] > b[sortBy] ? 1 : -1 + } + }); + }else{ + sorted_workers = workers.sort((a, b) => { + if(sortBy === 'name'){ + return b[sortBy].localeCompare(a[sortBy]); + }else if(sortBy === 'created_at'){ + return new Date(b[sortBy]) - new Date(a[sortBy]); + } else if(sortBy === 'user_count' || sortBy === 'open_count'){ + return b.stats[sortBy] - a.stats[sortBy]; + }else{ + b[sortBy] > a[sortBy] ? 1 : -1 + } + }); + } + // refresh worker list + $('.worker-card').remove(); + sorted_workers.forEach(worker => { + $('#worker-list-table > tbody').append(generate_worker_card(worker)); + }); + + count_workers(); + + // show workers that match search_query and hide workers that don't + if (search_query) { + // show workers that match search_query and hide workers that don't + workers.forEach((worker) => { + if (worker.name.toLowerCase().includes(search_query.toLowerCase())) { + $(`.worker-card[data-name="${html_encode(worker.name)}"]`).show(); + } else { + $(`.worker-card[data-name="${html_encode(worker.name)}"]`).hide(); + } + }) + } +} + +function count_workers() { + let count = 0; + $('.worker-card').each(function () { + count++; + }) + $('.worker-count').html(count); + return count; +} + +function generate_worker_card(worker) { + return ` + + + + + ${worker.name} + ${worker.created_at} + + + + + `; +} + +$(document).on('input change keyup keypress keydown paste cut', '.search', function (e) { + // search apps for query + search_query = $(this).val().toLowerCase(); + if (search_query === '') { + // hide 'clear search' button + $('.search-clear').hide(); + // show all apps again + $(`.worker-card`).show(); + } else { + // show 'clear search' button + $('.search-clear').show(); + // show apps that match search_query and hide apps that don't + workers.forEach((worker) => { + if ( + worker.name.toLowerCase().includes(search_query.toLowerCase()) + ) + { + $(`.worker-card[data-name="${worker.name}"]`).show(); + } else { + $(`.worker-card[data-name="${worker.name}"]`).hide(); + } + }) + } +}) + +$(document).on('click', '.delete-workers-btn', async function (e) { + // show confirmation alert + let resp = await puter.ui.alert(`Are you sure you want to delete the selected workers?`, [ + { + label: 'Delete', + type: 'danger', + value: 'delete', + }, + { + label: 'Cancel', + }, + ], { + type: 'warning', + }); + + if (resp === 'delete') { + // disable delete button + $('.delete-workers-btn').addClass('disabled'); + + // show 'deleting' modal + puter.ui.showSpinner(); + + let start_ts = Date.now(); + const workers = $('.worker-checkbox:checked').toArray(); + + // delete all checked workers + for (let worker of workers) { + let worker_name = $(worker).attr('data-worker-name'); + // delete worker + await puter.workers.delete(worker_name) + + // remove worker card + $(`.worker-card[data-name="${worker_name}"]`).fadeOut(200, function name(params) { + $(this).remove(); + if ($(`.worker-card`).length === 0) { + $('section:not(.sidebar)').hide(); + $('#no-workers-notice').show(); + } else { + $('section:not(.sidebar)').hide(); + $('#worker-list').show(); + } + count_workers(); + }); + + try{ + count_workers(); + } catch(err) { + console.log(err); + } + } + + // close 'deleting' modal + setTimeout(() => { + puter.ui.hideSpinner(); + if($('.worker-checkbox:checked').length === 0){ + // disable delete button + $('.delete-workers-btn').addClass('disabled'); + // reset the 'select all' checkbox + $('.select-all-workers').prop('indeterminate', false); + $('.select-all-workers').prop('checked', false); + } + }, (start_ts - Date.now()) > 500 ? 0 : 500); + } +}) + +$(document).on('click', '.delete-worker-btn', async function (e) { + let worker_name = $(this).attr('data-worker-name'); + + // get worker + const worker_data = await puter.workers.get(worker_name); + + if(worker_data.metadata?.locked){ + puter.ui.alert(`${worker_data.name} is locked and cannot be deleted.`, [ + { + label: 'Ok', + }, + ], { + type: 'warning', + }); + return; + } + + // confirm delete + const alert_resp = await puter.ui.alert(`Are you sure you want to premanently delete ${html_encode(worker_data.name)}?`, + [ + { + label: 'Yes, delete permanently', + value: 'delete', + type: 'danger', + }, + { + label: 'Cancel' + }, + ] + ); + + if (alert_resp === 'delete') { + let init_ts = Date.now(); + puter.ui.showSpinner(); + puter.workers.delete(worker_name).then(async (worker) => { + setTimeout(() => { + puter.ui.hideSpinner(); + $(`.worker-card[data-name="${worker_name}"]`).fadeOut(200, function name(params) { + $(this).remove(); + if ($(`.worker-card`).length === 0) { + $('section:not(.sidebar)').hide(); + $('#no-workers-notice').show(); + } else { + $('section:not(.sidebar)').hide(); + $('#worker-list').show(); + } + count_workers(); + }); + }, + // make sure the modal was shown for at least 2 seconds + (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); + + // delete worker directory + puter.fs.delete( + `/${auth_username}/AppData/${dev_center_uid}/${worker_name}`, + { recursive: true } + ) + + }).catch(async (err) => { + setTimeout(() => { + puter.ui.hideSpinner(); + puter.ui.alert(err?.message, [ + { + label: 'Ok', + }, + ]); + }, + // make sure the modal was shown for at least 2 seconds + (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); + }) + } +}) + + export default init_workers; \ No newline at end of file From 32bdd70ed0e8392ed275f67f1ff4466e33a18f28 Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sat, 2 Aug 2025 17:17:51 -0700 Subject: [PATCH 05/46] improve worker counting --- src/dev-center/css/style.css | 6 +++--- src/dev-center/js/workers.js | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index eac0e6fc1..d56eb20d0 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -37,7 +37,7 @@ body{ flex: 1; } -h1 .app-count{ +h1 .app-count, h1 .worker-count{ font-size: 20px; color: #6d767d; font-weight: 400; @@ -499,7 +499,7 @@ label { background-color: #3273dc; } -.sidebar-nav > li .app-count{ +.sidebar-nav > li .app-count, .sidebar-nav > li .worker-count { font-size: 14px; color: #6d767d; font-weight: 400; @@ -507,7 +507,7 @@ label { float: right; } -.tab-btn.active .app-count{ +.tab-btn.active .app-count, .tab-btn.active .worker-count{ color: #f6f6f6; } diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index ed64f3402..072108ffe 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -29,6 +29,8 @@ window.refresh_worker_list = async (show_loading = false) => { $('#no-workers-notice').show(); $('#worker-list').hide(); } + + count_workers(); } From c5159a6da2e4832d7de56a5dba5a5057bf5d3cbb Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sat, 2 Aug 2025 17:36:47 -0700 Subject: [PATCH 06/46] Update style.css --- src/dev-center/css/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index d56eb20d0..64caf5944 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -1287,6 +1287,7 @@ dialog{ max-height: 18px; line-height: 1.2; white-space: nowrap; + border: 1px solid #CCC; } .app-row-toolbar span { From 4b43b24e50766325991c77404b140e8e8b3dee4e Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sat, 2 Aug 2025 19:06:40 -0700 Subject: [PATCH 07/46] multiple style improvements --- src/dev-center/css/style.css | 62 ++++++++++++++++++++++++++---------- src/dev-center/index.html | 28 ++++++++-------- src/dev-center/js/apps.js | 4 +++ 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index 64caf5944..56180f5e0 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -26,7 +26,6 @@ html { width: 100vw; height: 100vh; - background-color: #eff1f5 } body{ @@ -206,6 +205,23 @@ h1 .app-count, h1 .worker-count{ padding: 0 24px; } +.refresh-app-list, .refresh-worker-list { + width:40px; + padding: 10px; + float: right; + margin-right: 10px; + background-color: white; + border: none; + color: #0d6efd; + font-size: 13px; + cursor: pointer; + opacity: 0.9; +} + +.refresh-app-list:hover, .refresh-worker-list:hover { + opacity: 1; +} + a { color: #0d6efd; text-decoration: none; @@ -231,12 +247,15 @@ section { display: none; } #app-list-table > thead, #website-list-table > thead, #worker-list-table > thead{ - font-size:14px; background-color: #f7fafc; border-top: 1px solid #DDD; text-transform: uppercase; color: #6d767d; + font-size:14px; + border-top: 1px solid #DDD; + text-transform: uppercase; + color: #6d767d; cursor: default; } .app-card, .website-card, .worker-card { padding: 12px; - border-top: 1px solid #d8dce5; + border-top: 1px solid #f1f2f5; border-radius: 0; clear: both; background: white; @@ -246,10 +265,10 @@ section { } .app-card:hover, .website-card:hover, .worker-card:hover { - background-color: #e8eff66e; + background-color: #f0f5fb6e; } .app-card.active, .website-card.active, .worker-card.active { - background-color: #e8eff6; + background-color: #ebeff39a; } @@ -442,14 +461,16 @@ label { padding: 30px 16px; overflow-x: hidden; overflow-y: auto; - background-color: white; - border-right: 1px solid #e1e1e1; + background-color: rgb(249, 249, 249); + border-right: 1px solid #eeeeee; color: rgb(51, 51, 51); font-weight: 400; width: 25px; } .sidebar .sidebar-content{ display: none; + position: relative; + height: 100%; } .sidebar.open{ width: 250px; @@ -492,11 +513,16 @@ label { display: block; padding: 10px 20px; cursor: pointer; + margin-bottom: 15px; +} + +.sidebar-nav>li:hover { + background-color: #656c771b; } .sidebar-nav>li.active { color: #fff; - background-color: #3273dc; + background-color: #3a85ff; } .sidebar-nav > li .app-count, .sidebar-nav > li .worker-count { @@ -537,14 +563,12 @@ label { .main>section { background-color: white; - box-shadow: 0px 0px 3px #E1E1E1; - border: 1px solid #e1e1e1; border-radius: 3px; } .link-to-docs{ color: #586373; - font-size: 15px; + font-size: 14px; } .link-to-docs, .link-to-docs:hover { text-decoration: none; @@ -640,7 +664,7 @@ table { .table>thead>tr>th { vertical-align: bottom; - border-bottom: 2px solid #ddd; + border-bottom: 1px solid #ddd; } .table>thead>tr>th, .table>tbody>tr>th, .table>tfoot>tr>th, .table>thead>tr>td, .table>tbody>tr>td, .table>tfoot>tr>td { @@ -730,7 +754,7 @@ th.sorted{ user-select: none !important; } -.my-apps-title { +.my-apps-title, .my-workers-title { font-size: 27px; margin-top: 0px; float: left; @@ -953,7 +977,7 @@ ol li:before { .ip-terms-notice{ font-size: 12px; margin-top: 20px; margin-bottom: 0; } -.app-list-nav{ +.app-list-nav, .worker-list-nav{ overflow: hidden; margin-bottom: 40px; margin-top: 10px; } .back-to-main-btn{ @@ -1114,9 +1138,15 @@ dialog{ background-position-x: 5px; padding-left: 35px; padding-right: 35px; - border: 2px solid #CCC; + border: 2px solid #ebebeb; +} +.search:focus, .search:focus-within, .search:active, .search:focus-visible { + border: 2px solid #cbcbcb !important; + outline: none !important; +} +.search.has-value{ + border: 2px solid #0066d2 !important; } - .search-clear { display: none; position: absolute; diff --git a/src/dev-center/index.html b/src/dev-center/index.html index ccc306825..0470af8f3 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -77,6 +77,7 @@ @@ -220,7 +222,7 @@

My Apps

- +
@@ -255,7 +257,6 @@

My Workers

-
@@ -264,6 +265,7 @@
+
diff --git a/src/dev-center/js/apps.js b/src/dev-center/js/apps.js index 4da813110..97b47ee52 100644 --- a/src/dev-center/js/apps.js +++ b/src/dev-center/js/apps.js @@ -2241,6 +2241,8 @@ $(document).on('input change keyup keypress keydown paste cut', '.search', funct $('.search-clear').hide(); // show all apps again $(`.app-card`).show(); + // remove 'has-value' class from search input + $('.search').removeClass('has-value'); } else { // show 'clear search' button $('.search-clear').show(); @@ -2258,6 +2260,8 @@ $(document).on('input change keyup keypress keydown paste cut', '.search', funct $(`.app-card[data-name="${app.name}"]`).hide(); } }) + // add 'has-value' class to search input + $('.search').addClass('has-value'); } }) From 2f30ce3d4d8a4d2b1e7da38314bcd31cc7783b09 Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sat, 2 Aug 2025 19:12:22 -0700 Subject: [PATCH 08/46] layout fix --- src/dev-center/index.html | 2 +- src/dev-center/js/websites.js | 386 +++++++++++++++++++++++++++++++++- 2 files changed, 385 insertions(+), 3 deletions(-) diff --git a/src/dev-center/index.html b/src/dev-center/index.html index 0470af8f3..adec95ec8 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -222,7 +222,6 @@

My Apps

-
@@ -231,6 +230,7 @@
+
diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index e896bbb27..9e37ce3f4 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -1,4 +1,386 @@ -async function init_websites() { +let sortBy = 'created_at'; +let sortDirection = 'desc'; +window.websites = []; +let search_query; + +window.create_website = async (subdomain) => { + let website = await puter.hosting.create(subdomain); + + return website; } -export default init_websites; \ No newline at end of file +window.refresh_website_list = async (show_loading = false) => { + if (show_loading) + puter.ui.showSpinner(); + + // puter.hosting.list() returns an array of website objects + window.websites = await puter.hosting.list(); + + // Get workers + if (window.activeTab === 'websites' && window.websites.length > 0) { + $('.website-card').remove(); + $('#no-websites-notice').hide(); + $('#website-list').show(); + window.websites.forEach((website) => { + // append row to website-list-table + $('#website-list-table > tbody').append(generate_website_card(website)); + }); + } else { + $('#no-websites-notice').show(); + $('#website-list').hide(); + } + + count_workers(); +} + + +async function init_workers() { +} + +$(document).on('click', '.create-a-worker-btn', async function (e) { + let name = await puter.ui.prompt('Please enter a name for your worker:', 'my-awesome-worker'); + + if (name) { + console.log(name); + await create_worker(name); + } +}) + +window.createDefaultWorkerFile = async () => { + window.default_worker_file = `/${auth_username}/AppData/${dev_center_uid}/default_worker_file.js`; + let existingFile; + try { + // Check if default_worker_file exists + existingFile = await puter.fs.read(default_worker_file); + } catch (err) { + console.error('Error creating default worker file:', err); + } + + if (!existingFile) { + // Create default_worker_file + await puter.fs.write(default_worker_file, `// This is an example application for Puter Workers + +router.get('/', ({request}) => { +return 'Hello World'; // returns a string +}); +router.get('/api/hello', ({request}) => { +return {'msg': 'hello'}; // returns a JSON object +}); +router.get('/*page', ({request, params}) => { +return new Response(\`Page \${params.page} not found\`, {status: 404}); +});`); + } + +} + +$(document).on('change', '.worker-checkbox', function (e) { + // determine if select-all checkbox should be checked, indeterminate, or unchecked + if ($('.worker-checkbox:checked').length === $('.worker-checkbox').length) { + $('.select-all-workers').prop('indeterminate', false); + $('.select-all-workers').prop('checked', true); + } else if ($('.worker-checkbox:checked').length > 0) { + $('.select-all-workers').prop('indeterminate', true); + $('.select-all-workers').prop('checked', false); + } + else { + $('.select-all-workers').prop('indeterminate', false); + $('.select-all-workers').prop('checked', false); + } + + // activate row + if ($(this).is(':checked')) + $(this).closest('tr').addClass('active'); + else + $(this).closest('tr').removeClass('active'); + + // enable delete button if at least one checkbox is checked + if ($('.worker-checkbox:checked').length > 0) + $('.delete-workers-btn').removeClass('disabled'); + else + $('.delete-workers-btn').addClass('disabled'); +}) + +$(document).on('change', '.select-all-workers', function (e) { + if ($(this).is(':checked')) { + $('.worker-checkbox').prop('checked', true); + $('.worker-card').addClass('active'); + $('.delete-workers-btn').removeClass('disabled'); + } else { + $('.worker-checkbox').prop('checked', false); + $('.worker-card').removeClass('active'); + $('.delete-workers-btn').addClass('disabled'); + } +}) + +$('.refresh-worker-list').on('click', function (e) { + puter.ui.showSpinner(); + refresh_worker_list(); + + puter.ui.hideSpinner(); +}) + +$('th.sort').on('click', function (e) { + // determine what column to sort by + const sortByColumn = $(this).attr('data-column'); + + // toggle sort direction + if (sortByColumn === sortBy) { + if (sortDirection === 'asc') + sortDirection = 'desc'; + else + sortDirection = 'asc'; + } + else { + sortBy = sortByColumn; + sortDirection = 'desc'; + } + + // update arrow + $('.sort-arrow').css('display', 'none'); + $('#worker-list-table').find('th').removeClass('sorted'); + $(this).find('.sort-arrow-' + sortDirection).css('display', 'inline'); + $(this).addClass('sorted'); + + sort_workers(); +}); + +function sort_workers() { + let sorted_workers; + + // sort + if (sortDirection === 'asc'){ + sorted_workers = workers.sort((a, b) => { + if(sortBy === 'name'){ + return a[sortBy].localeCompare(b[sortBy]); + }else if(sortBy === 'created_at'){ + return new Date(a[sortBy]) - new Date(b[sortBy]); + } else if(sortBy === 'user_count' || sortBy === 'open_count'){ + return a.stats[sortBy] - b.stats[sortBy]; + }else{ + a[sortBy] > b[sortBy] ? 1 : -1 + } + }); + }else{ + sorted_workers = workers.sort((a, b) => { + if(sortBy === 'name'){ + return b[sortBy].localeCompare(a[sortBy]); + }else if(sortBy === 'created_at'){ + return new Date(b[sortBy]) - new Date(a[sortBy]); + } else if(sortBy === 'user_count' || sortBy === 'open_count'){ + return b.stats[sortBy] - a.stats[sortBy]; + }else{ + b[sortBy] > a[sortBy] ? 1 : -1 + } + }); + } + // refresh worker list + $('.worker-card').remove(); + sorted_workers.forEach(worker => { + $('#worker-list-table > tbody').append(generate_worker_card(worker)); + }); + + count_workers(); + + // show workers that match search_query and hide workers that don't + if (search_query) { + // show workers that match search_query and hide workers that don't + workers.forEach((worker) => { + if (worker.name.toLowerCase().includes(search_query.toLowerCase())) { + $(`.worker-card[data-name="${html_encode(worker.name)}"]`).show(); + } else { + $(`.worker-card[data-name="${html_encode(worker.name)}"]`).hide(); + } + }) + } +} + +function count_workers() { + let count = 0; + $('.worker-card').each(function () { + count++; + }) + $('.worker-count').html(count); + return count; +} + +function generate_worker_card(worker) { + return ` + + + + + + + `; +} + +$(document).on('input change keyup keypress keydown paste cut', '.search', function (e) { + // search apps for query + search_query = $(this).val().toLowerCase(); + if (search_query === '') { + // hide 'clear search' button + $('.search-clear').hide(); + // show all apps again + $(`.worker-card`).show(); + } else { + // show 'clear search' button + $('.search-clear').show(); + // show apps that match search_query and hide apps that don't + workers.forEach((worker) => { + if ( + worker.name.toLowerCase().includes(search_query.toLowerCase()) + ) + { + $(`.worker-card[data-name="${worker.name}"]`).show(); + } else { + $(`.worker-card[data-name="${worker.name}"]`).hide(); + } + }) + } +}) + +$(document).on('click', '.delete-workers-btn', async function (e) { + // show confirmation alert + let resp = await puter.ui.alert(`Are you sure you want to delete the selected workers?`, [ + { + label: 'Delete', + type: 'danger', + value: 'delete', + }, + { + label: 'Cancel', + }, + ], { + type: 'warning', + }); + + if (resp === 'delete') { + // disable delete button + $('.delete-workers-btn').addClass('disabled'); + + // show 'deleting' modal + puter.ui.showSpinner(); + + let start_ts = Date.now(); + const workers = $('.worker-checkbox:checked').toArray(); + + // delete all checked workers + for (let worker of workers) { + let worker_name = $(worker).attr('data-worker-name'); + // delete worker + await puter.workers.delete(worker_name) + + // remove worker card + $(`.worker-card[data-name="${worker_name}"]`).fadeOut(200, function name(params) { + $(this).remove(); + if ($(`.worker-card`).length === 0) { + $('section:not(.sidebar)').hide(); + $('#no-workers-notice').show(); + } else { + $('section:not(.sidebar)').hide(); + $('#worker-list').show(); + } + count_workers(); + }); + + try{ + count_workers(); + } catch(err) { + console.log(err); + } + } + + // close 'deleting' modal + setTimeout(() => { + puter.ui.hideSpinner(); + if($('.worker-checkbox:checked').length === 0){ + // disable delete button + $('.delete-workers-btn').addClass('disabled'); + // reset the 'select all' checkbox + $('.select-all-workers').prop('indeterminate', false); + $('.select-all-workers').prop('checked', false); + } + }, (start_ts - Date.now()) > 500 ? 0 : 500); + } +}) + +$(document).on('click', '.delete-worker-btn', async function (e) { + let worker_name = $(this).attr('data-worker-name'); + + // get worker + const worker_data = await puter.workers.get(worker_name); + + if(worker_data.metadata?.locked){ + puter.ui.alert(`${worker_data.name} is locked and cannot be deleted.`, [ + { + label: 'Ok', + }, + ], { + type: 'warning', + }); + return; + } + + // confirm delete + const alert_resp = await puter.ui.alert(`Are you sure you want to premanently delete ${html_encode(worker_data.name)}?`, + [ + { + label: 'Yes, delete permanently', + value: 'delete', + type: 'danger', + }, + { + label: 'Cancel' + }, + ] + ); + + if (alert_resp === 'delete') { + let init_ts = Date.now(); + puter.ui.showSpinner(); + puter.workers.delete(worker_name).then(async (worker) => { + setTimeout(() => { + puter.ui.hideSpinner(); + $(`.worker-card[data-name="${worker_name}"]`).fadeOut(200, function name(params) { + $(this).remove(); + if ($(`.worker-card`).length === 0) { + $('section:not(.sidebar)').hide(); + $('#no-workers-notice').show(); + } else { + $('section:not(.sidebar)').hide(); + $('#worker-list').show(); + } + count_workers(); + }); + }, + // make sure the modal was shown for at least 2 seconds + (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); + + // delete worker directory + puter.fs.delete( + `/${auth_username}/AppData/${dev_center_uid}/${worker_name}`, + { recursive: true } + ) + + }).catch(async (err) => { + setTimeout(() => { + puter.ui.hideSpinner(); + puter.ui.alert(err?.message, [ + { + label: 'Ok', + }, + ]); + }, + // make sure the modal was shown for at least 2 seconds + (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); + }) + } +}) + + + +export default init_workers; \ No newline at end of file From e0d7a4aa17c4ba4fb8399c4c115168d53e613bfb Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sat, 2 Aug 2025 20:25:46 -0700 Subject: [PATCH 09/46] Refactor app and worker management UI with context menus and improved delete functionality --- src/dev-center/css/style.css | 23 +- src/dev-center/img/options.svg | 2 + src/dev-center/index.html | 3 +- src/dev-center/js/apps.js | 274 +++++++++++++---------- src/dev-center/js/websites.js | 382 --------------------------------- src/dev-center/js/workers.js | 59 +++-- 6 files changed, 225 insertions(+), 518 deletions(-) create mode 100644 src/dev-center/img/options.svg diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index 56180f5e0..b37c23d03 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -24,7 +24,6 @@ } html { - width: 100vw; height: 100vh; } @@ -439,7 +438,7 @@ label { } .app-card-link:hover{ - text-decoration: underline; + text-decoration: underline !important; } .edit-app img { @@ -1334,4 +1333,24 @@ dialog{ color: #000; /* Optional: darken on hover */ } +.options-icon { + cursor: pointer; + opacity: 0.7; + width: 30px; + height: 30px; + border-radius: 3px; +} +.options-icon:hover { + opacity: 1; + background-color: #ededed; +} + +.worker-checkbox, .app-checkbox { + width: 20px; + height: 20px; +} + +.no-hover:hover { + background-color: transparent !important; +} \ No newline at end of file diff --git a/src/dev-center/img/options.svg b/src/dev-center/img/options.svg new file mode 100644 index 000000000..6d0301c53 --- /dev/null +++ b/src/dev-center/img/options.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/dev-center/index.html b/src/dev-center/index.html index adec95ec8..7a2a752ae 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -88,7 +88,7 @@ + diff --git a/src/dev-center/js/apps.js b/src/dev-center/js/apps.js index 97b47ee52..f30799c03 100644 --- a/src/dev-center/js/apps.js +++ b/src/dev-center/js/apps.js @@ -279,85 +279,6 @@ $(document).on('click', '.edit-app, .go-to-edit-app', function (e) { }) $(document).on('click', '.delete-app', async function (e) { - let app_uid = $(this).attr('data-app-uid'); - let app_title = $(this).attr('data-app-title'); - let app_name = $(this).attr('data-app-name'); - - // get app - const app_data = await puter.apps.get(app_name, { icon_size: 16 }); - - if(app_data.metadata?.locked){ - puter.ui.alert(`${app_data.title} is locked and cannot be deleted.`, [ - { - label: 'Ok', - }, - ], { - type: 'warning', - }); - return; - } - - // confirm delete - const alert_resp = await puter.ui.alert(`Are you sure you want to premanently delete ${html_encode(app_title)}?`, - [ - { - label: 'Yes, delete permanently', - value: 'delete', - type: 'danger', - }, - { - label: 'Cancel' - }, - ] - ); - - if (alert_resp === 'delete') { - let init_ts = Date.now(); - puter.ui.showSpinner(); - puter.apps.delete(app_name).then(async (app) => { - setTimeout(() => { - puter.ui.hideSpinner(); - $(`.app-card[data-uid="${app_uid}"]`).fadeOut(200, function name(params) { - $(this).remove(); - if ($(`.app-card`).length === 0) { - $('section:not(.sidebar)').hide(); - $('#no-apps-notice').show(); - } else { - $('section:not(.sidebar)').hide(); - $('#app-list').show(); - } - count_apps(); - }); - }, - // make sure the modal was shown for at least 2 seconds - (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); - - // get app directory - puter.fs.stat({ - path: `/${auth_username}/AppData/${dev_center_uid}/${app_uid}`, - returnSubdomains: true, - }).then(async (stat) => { - // delete subdomain associated with the app dir - puter.hosting.delete(stat.subdomains[0].subdomain) - // delete app directory - puter.fs.delete( - `/${auth_username}/AppData/${dev_center_uid}/${app_uid}`, - { recursive: true } - ) - }) - }).catch(async (err) => { - setTimeout(() => { - puter.ui.hideSpinner(); - puter.ui.alert(err?.message, [ - { - label: 'Ok', - }, - ]); - }, - // make sure the modal was shown for at least 2 seconds - (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); - }) - } }) // generate app link @@ -1532,15 +1453,6 @@ h += ``; - // Toolbar - h += `
`; - h += `
`; // remove inline styles - h += `Open`; - h += `Settings`; - h += `Add Shortcut to Desktop`; - h += `Delete`; - h += `
`; - h += `
`; h += ``; // end info column h += ``; // end row h += ``; @@ -1573,6 +1485,10 @@ h += ``; h += ``; h += ``; h += ``; + + // options + h += ``; + h += ``; return h; } @@ -2161,32 +2077,6 @@ $(document).on('click', '.insta-deploy-existing-app-back', function (e) { -$(document).on('click', '.add-app-to-desktop', function (e) { - let app_title = $(this).attr('data-app-title'); - let app_uid = $(this).attr('data-app-uid'); - - puter.fs.upload( - new File([], app_title), - `/${auth_username}/Desktop`, - { - name: app_title, - dedupeName: true, - overwrite: false, - appUID: app_uid, - }).then(async (uploaded) => { - puter.ui.alert(`${app_title} shortcut has been added to your desktop.`, [ - { - label: 'Ok', - type: 'primary', - }, - ], { - type: 'success', - }); - }) - -}) - - $('.insta-deploy-existing-app-select').on('close', function (e) { $('.insta-deploy-existing-app-list').html(''); }) @@ -2706,4 +2596,160 @@ async function render_analytics(period){ $(document).on('click', '.stats-cell', function(e) { edit_app_section($(this).attr('data-app-name'), 'analytics'); }) + +function app_context_menu(app_name, app_title, app_uid) { + puter.ui.contextMenu({ + items: [ + { + label: 'Open', + type: 'primary', + action: () => { + puter.ui.launchApp(app_name); + }, + }, + { + label: 'Edit', + type: 'primary', + action: () => { + edit_app_section(app_name, 'edit'); + }, + }, + { + label: 'Add Shortcut to Desktop', + type: 'primary', + action: () => { + puter.fs.upload( + new File([], app_title), + `/${auth_username}/Desktop`, + { + name: app_title, + dedupeName: true, + overwrite: false, + appUID: app_uid, + }).then(async (uploaded) => { + puter.ui.alert(`${app_title} shortcut has been added to your desktop.`, [ + { + label: 'Ok', + type: 'primary', + }, + ], { + type: 'success', + }); + }) + + }, + }, + '-', + { + label: 'Delete', + type: 'danger', + action: () => { + attempt_delete_app(app_name, app_title, app_uid); + }, + }, + ], + }); + +} +$(document).on('click', '.options-icon-app', function(e) { + let app_name = $(this).attr('data-app-name'); + let app_title = $(this).attr('data-app-title'); + let app_uid = $(this).attr('data-app-uid'); + + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + app_context_menu(app_name, app_title, app_uid); +}) + +$(document).on('contextmenu', '.app-card', function(e) { + let app_name = $(this).attr('data-name'); + let app_title = $(this).attr('data-title'); + let app_uid = $(this).attr('data-uid'); + + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + app_context_menu(app_name, app_title, app_uid); +}) + +async function attempt_delete_app(app_name, app_title, app_uid) { + // get app + const app_data = await puter.apps.get(app_name, { icon_size: 16 }); + + if(app_data.metadata?.locked){ + puter.ui.alert(`${app_data.title} is locked and cannot be deleted.`, [ + { + label: 'Ok', + }, + ], { + type: 'warning', + }); + return; + } + + // confirm delete + const alert_resp = await puter.ui.alert(`Are you sure you want to premanently delete ${html_encode(app_title)}?`, + [ + { + label: 'Yes, delete permanently', + value: 'delete', + type: 'danger', + }, + { + label: 'Cancel' + }, + ] + ); + + if (alert_resp === 'delete') { + let init_ts = Date.now(); + puter.ui.showSpinner(); + puter.apps.delete(app_name).then(async (app) => { + setTimeout(() => { + puter.ui.hideSpinner(); + $(`.app-card[data-uid="${app_uid}"]`).fadeOut(200, function name(params) { + $(this).remove(); + if ($(`.app-card`).length === 0) { + $('section:not(.sidebar)').hide(); + $('#no-apps-notice').show(); + } else { + $('section:not(.sidebar)').hide(); + $('#app-list').show(); + } + count_apps(); + }); + }, + // make sure the modal was shown for at least 2 seconds + (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); + + // get app directory + puter.fs.stat({ + path: `/${auth_username}/AppData/${dev_center_uid}/${app_uid}`, + returnSubdomains: true, + }).then(async (stat) => { + // delete subdomain associated with the app dir + puter.hosting.delete(stat.subdomains[0].subdomain) + // delete app directory + puter.fs.delete( + `/${auth_username}/AppData/${dev_center_uid}/${app_uid}`, + { recursive: true } + ) + }) + }).catch(async (err) => { + setTimeout(() => { + puter.ui.hideSpinner(); + puter.ui.alert(err?.message, [ + { + label: 'Ok', + }, + ]); + }, + // make sure the modal was shown for at least 2 seconds + (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); + }) + } + +} + export default init_apps; \ No newline at end of file diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index 9e37ce3f4..0bb9f1062 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -1,386 +1,4 @@ -let sortBy = 'created_at'; -let sortDirection = 'desc'; -window.websites = []; -let search_query; - -window.create_website = async (subdomain) => { - let website = await puter.hosting.create(subdomain); - - return website; -} - -window.refresh_website_list = async (show_loading = false) => { - if (show_loading) - puter.ui.showSpinner(); - - // puter.hosting.list() returns an array of website objects - window.websites = await puter.hosting.list(); - - // Get workers - if (window.activeTab === 'websites' && window.websites.length > 0) { - $('.website-card').remove(); - $('#no-websites-notice').hide(); - $('#website-list').show(); - window.websites.forEach((website) => { - // append row to website-list-table - $('#website-list-table > tbody').append(generate_website_card(website)); - }); - } else { - $('#no-websites-notice').show(); - $('#website-list').hide(); - } - - count_workers(); -} - - async function init_workers() { } -$(document).on('click', '.create-a-worker-btn', async function (e) { - let name = await puter.ui.prompt('Please enter a name for your worker:', 'my-awesome-worker'); - - if (name) { - console.log(name); - await create_worker(name); - } -}) - -window.createDefaultWorkerFile = async () => { - window.default_worker_file = `/${auth_username}/AppData/${dev_center_uid}/default_worker_file.js`; - let existingFile; - try { - // Check if default_worker_file exists - existingFile = await puter.fs.read(default_worker_file); - } catch (err) { - console.error('Error creating default worker file:', err); - } - - if (!existingFile) { - // Create default_worker_file - await puter.fs.write(default_worker_file, `// This is an example application for Puter Workers - -router.get('/', ({request}) => { -return 'Hello World'; // returns a string -}); -router.get('/api/hello', ({request}) => { -return {'msg': 'hello'}; // returns a JSON object -}); -router.get('/*page', ({request, params}) => { -return new Response(\`Page \${params.page} not found\`, {status: 404}); -});`); - } - -} - -$(document).on('change', '.worker-checkbox', function (e) { - // determine if select-all checkbox should be checked, indeterminate, or unchecked - if ($('.worker-checkbox:checked').length === $('.worker-checkbox').length) { - $('.select-all-workers').prop('indeterminate', false); - $('.select-all-workers').prop('checked', true); - } else if ($('.worker-checkbox:checked').length > 0) { - $('.select-all-workers').prop('indeterminate', true); - $('.select-all-workers').prop('checked', false); - } - else { - $('.select-all-workers').prop('indeterminate', false); - $('.select-all-workers').prop('checked', false); - } - - // activate row - if ($(this).is(':checked')) - $(this).closest('tr').addClass('active'); - else - $(this).closest('tr').removeClass('active'); - - // enable delete button if at least one checkbox is checked - if ($('.worker-checkbox:checked').length > 0) - $('.delete-workers-btn').removeClass('disabled'); - else - $('.delete-workers-btn').addClass('disabled'); -}) - -$(document).on('change', '.select-all-workers', function (e) { - if ($(this).is(':checked')) { - $('.worker-checkbox').prop('checked', true); - $('.worker-card').addClass('active'); - $('.delete-workers-btn').removeClass('disabled'); - } else { - $('.worker-checkbox').prop('checked', false); - $('.worker-card').removeClass('active'); - $('.delete-workers-btn').addClass('disabled'); - } -}) - -$('.refresh-worker-list').on('click', function (e) { - puter.ui.showSpinner(); - refresh_worker_list(); - - puter.ui.hideSpinner(); -}) - -$('th.sort').on('click', function (e) { - // determine what column to sort by - const sortByColumn = $(this).attr('data-column'); - - // toggle sort direction - if (sortByColumn === sortBy) { - if (sortDirection === 'asc') - sortDirection = 'desc'; - else - sortDirection = 'asc'; - } - else { - sortBy = sortByColumn; - sortDirection = 'desc'; - } - - // update arrow - $('.sort-arrow').css('display', 'none'); - $('#worker-list-table').find('th').removeClass('sorted'); - $(this).find('.sort-arrow-' + sortDirection).css('display', 'inline'); - $(this).addClass('sorted'); - - sort_workers(); -}); - -function sort_workers() { - let sorted_workers; - - // sort - if (sortDirection === 'asc'){ - sorted_workers = workers.sort((a, b) => { - if(sortBy === 'name'){ - return a[sortBy].localeCompare(b[sortBy]); - }else if(sortBy === 'created_at'){ - return new Date(a[sortBy]) - new Date(b[sortBy]); - } else if(sortBy === 'user_count' || sortBy === 'open_count'){ - return a.stats[sortBy] - b.stats[sortBy]; - }else{ - a[sortBy] > b[sortBy] ? 1 : -1 - } - }); - }else{ - sorted_workers = workers.sort((a, b) => { - if(sortBy === 'name'){ - return b[sortBy].localeCompare(a[sortBy]); - }else if(sortBy === 'created_at'){ - return new Date(b[sortBy]) - new Date(a[sortBy]); - } else if(sortBy === 'user_count' || sortBy === 'open_count'){ - return b.stats[sortBy] - a.stats[sortBy]; - }else{ - b[sortBy] > a[sortBy] ? 1 : -1 - } - }); - } - // refresh worker list - $('.worker-card').remove(); - sorted_workers.forEach(worker => { - $('#worker-list-table > tbody').append(generate_worker_card(worker)); - }); - - count_workers(); - - // show workers that match search_query and hide workers that don't - if (search_query) { - // show workers that match search_query and hide workers that don't - workers.forEach((worker) => { - if (worker.name.toLowerCase().includes(search_query.toLowerCase())) { - $(`.worker-card[data-name="${html_encode(worker.name)}"]`).show(); - } else { - $(`.worker-card[data-name="${html_encode(worker.name)}"]`).hide(); - } - }) - } -} - -function count_workers() { - let count = 0; - $('.worker-card').each(function () { - count++; - }) - $('.worker-count').html(count); - return count; -} - -function generate_worker_card(worker) { - return ` - - - - - - - `; -} - -$(document).on('input change keyup keypress keydown paste cut', '.search', function (e) { - // search apps for query - search_query = $(this).val().toLowerCase(); - if (search_query === '') { - // hide 'clear search' button - $('.search-clear').hide(); - // show all apps again - $(`.worker-card`).show(); - } else { - // show 'clear search' button - $('.search-clear').show(); - // show apps that match search_query and hide apps that don't - workers.forEach((worker) => { - if ( - worker.name.toLowerCase().includes(search_query.toLowerCase()) - ) - { - $(`.worker-card[data-name="${worker.name}"]`).show(); - } else { - $(`.worker-card[data-name="${worker.name}"]`).hide(); - } - }) - } -}) - -$(document).on('click', '.delete-workers-btn', async function (e) { - // show confirmation alert - let resp = await puter.ui.alert(`Are you sure you want to delete the selected workers?`, [ - { - label: 'Delete', - type: 'danger', - value: 'delete', - }, - { - label: 'Cancel', - }, - ], { - type: 'warning', - }); - - if (resp === 'delete') { - // disable delete button - $('.delete-workers-btn').addClass('disabled'); - - // show 'deleting' modal - puter.ui.showSpinner(); - - let start_ts = Date.now(); - const workers = $('.worker-checkbox:checked').toArray(); - - // delete all checked workers - for (let worker of workers) { - let worker_name = $(worker).attr('data-worker-name'); - // delete worker - await puter.workers.delete(worker_name) - - // remove worker card - $(`.worker-card[data-name="${worker_name}"]`).fadeOut(200, function name(params) { - $(this).remove(); - if ($(`.worker-card`).length === 0) { - $('section:not(.sidebar)').hide(); - $('#no-workers-notice').show(); - } else { - $('section:not(.sidebar)').hide(); - $('#worker-list').show(); - } - count_workers(); - }); - - try{ - count_workers(); - } catch(err) { - console.log(err); - } - } - - // close 'deleting' modal - setTimeout(() => { - puter.ui.hideSpinner(); - if($('.worker-checkbox:checked').length === 0){ - // disable delete button - $('.delete-workers-btn').addClass('disabled'); - // reset the 'select all' checkbox - $('.select-all-workers').prop('indeterminate', false); - $('.select-all-workers').prop('checked', false); - } - }, (start_ts - Date.now()) > 500 ? 0 : 500); - } -}) - -$(document).on('click', '.delete-worker-btn', async function (e) { - let worker_name = $(this).attr('data-worker-name'); - - // get worker - const worker_data = await puter.workers.get(worker_name); - - if(worker_data.metadata?.locked){ - puter.ui.alert(`${worker_data.name} is locked and cannot be deleted.`, [ - { - label: 'Ok', - }, - ], { - type: 'warning', - }); - return; - } - - // confirm delete - const alert_resp = await puter.ui.alert(`Are you sure you want to premanently delete ${html_encode(worker_data.name)}?`, - [ - { - label: 'Yes, delete permanently', - value: 'delete', - type: 'danger', - }, - { - label: 'Cancel' - }, - ] - ); - - if (alert_resp === 'delete') { - let init_ts = Date.now(); - puter.ui.showSpinner(); - puter.workers.delete(worker_name).then(async (worker) => { - setTimeout(() => { - puter.ui.hideSpinner(); - $(`.worker-card[data-name="${worker_name}"]`).fadeOut(200, function name(params) { - $(this).remove(); - if ($(`.worker-card`).length === 0) { - $('section:not(.sidebar)').hide(); - $('#no-workers-notice').show(); - } else { - $('section:not(.sidebar)').hide(); - $('#worker-list').show(); - } - count_workers(); - }); - }, - // make sure the modal was shown for at least 2 seconds - (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); - - // delete worker directory - puter.fs.delete( - `/${auth_username}/AppData/${dev_center_uid}/${worker_name}`, - { recursive: true } - ) - - }).catch(async (err) => { - setTimeout(() => { - puter.ui.hideSpinner(); - puter.ui.alert(err?.message, [ - { - label: 'Ok', - }, - ]); - }, - // make sure the modal was shown for at least 2 seconds - (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); - }) - } -}) - - - export default init_workers; \ No newline at end of file diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index 072108ffe..c2964f2ea 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -41,7 +41,6 @@ $(document).on('click', '.create-a-worker-btn', async function (e) { let name = await puter.ui.prompt('Please enter a name for your worker:', 'my-awesome-worker'); if (name) { - console.log(name); await create_worker(name); } }) @@ -207,13 +206,11 @@ function generate_worker_card(worker) { return ` - + `; } @@ -308,8 +305,41 @@ $(document).on('click', '.delete-workers-btn', async function (e) { } }) -$(document).on('click', '.delete-worker-btn', async function (e) { - let worker_name = $(this).attr('data-worker-name'); +$(document).on('contextmenu', '.worker-card', function (e) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + puter.ui.contextMenu({ + items: [ + { + label: 'Delete', + type: 'danger', + action: () => { + attempt_delete_worker($(this).attr('data-name')); + }, + }, + ], + }); +}) + +$(document).on('click', '.options-icon-worker', function (e) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + puter.ui.contextMenu({ + items: [ + { + label: 'Delete', + type: 'danger', + action: () => { + attempt_delete_worker($(this).attr('data-worker-name')); + }, + }, + ], + }); +}) + +async function attempt_delete_worker(worker_name) { // get worker const worker_data = await puter.workers.get(worker_name); @@ -357,15 +387,8 @@ $(document).on('click', '.delete-worker-btn', async function (e) { count_workers(); }); }, - // make sure the modal was shown for at least 2 seconds - (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); - - // delete worker directory - puter.fs.delete( - `/${auth_username}/AppData/${dev_center_uid}/${worker_name}`, - { recursive: true } - ) - + // make sure the modal was shown for at least 2 seconds + (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); }).catch(async (err) => { setTimeout(() => { puter.ui.hideSpinner(); @@ -379,8 +402,6 @@ $(document).on('click', '.delete-worker-btn', async function (e) { (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); }) } -}) - - +} export default init_workers; \ No newline at end of file From 7c30472bdfe0026b1da96f6533677f572ad4f442 Mon Sep 17 00:00:00 2001 From: jelveh Date: Sat, 2 Aug 2025 23:19:49 -0700 Subject: [PATCH 10/46] add websites tab --- src/dev-center/css/style.css | 38 ++-- src/dev-center/index.html | 2 +- src/dev-center/js/apps.js | 3 +- src/dev-center/js/dev-center.js | 7 + src/dev-center/js/websites.js | 349 +++++++++++++++++++++++++++++++- src/dev-center/js/workers.js | 2 +- 6 files changed, 375 insertions(+), 26 deletions(-) diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index b37c23d03..0b90ffff6 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -35,7 +35,7 @@ body{ flex: 1; } -h1 .app-count, h1 .worker-count{ +h1 .app-count, h1 .worker-count, h1 .website-count{ font-size: 20px; color: #6d767d; font-weight: 400; @@ -70,10 +70,6 @@ h1 .app-count, h1 .worker-count{ border-style: solid; border-width: 1px; line-height: 35px; - background: -webkit-gradient(linear, left top, left bottom, from(#f6f6f6), to(#e1e1e1)); - background: linear-gradient(#f6f6f6, #e1e1e1); - -webkit-box-shadow: inset 0px 1px 0px rgb(255 255 255 / 30%), 0 1px 2px rgb(0 0 0 / 15%); - box-shadow: inset 0px 1px 0px rgb(255 255 255 / 30%), 0 1px 2px rgb(0 0 0 / 15%); border-radius: 4px; outline: none; @@ -204,7 +200,7 @@ h1 .app-count, h1 .worker-count{ padding: 0 24px; } -.refresh-app-list, .refresh-worker-list { +.refresh-app-list, .refresh-worker-list, .refresh-website-list { width:40px; padding: 10px; float: right; @@ -217,7 +213,7 @@ h1 .app-count, h1 .worker-count{ opacity: 0.9; } -.refresh-app-list:hover, .refresh-worker-list:hover { +.refresh-app-list:hover, .refresh-worker-list:hover, .refresh-website-list:hover { opacity: 1; } @@ -260,7 +256,6 @@ section { background: white; overflow: hidden; position: relative; - } .app-card:hover, .website-card:hover, .worker-card:hover { @@ -457,7 +452,7 @@ label { left: 0; z-index: 1000; display: block; - padding: 30px 16px; + padding: 30px 16px 0; overflow-x: hidden; overflow-y: auto; background-color: rgb(249, 249, 249); @@ -524,15 +519,15 @@ label { background-color: #3a85ff; } -.sidebar-nav > li .app-count, .sidebar-nav > li .worker-count { - font-size: 14px; +.sidebar-nav > li .app-count, .sidebar-nav > li .worker-count, .sidebar-nav > li .website-count { + font-size: 16px; color: #6d767d; font-weight: 400; margin-left: 10px; float: right; } -.tab-btn.active .app-count, .tab-btn.active .worker-count{ +.tab-btn.active .app-count, .tab-btn.active .worker-count, .tab-btn.active .website-count{ color: #f6f6f6; } @@ -753,7 +748,7 @@ th.sorted{ user-select: none !important; } -.my-apps-title, .my-workers-title { +.my-apps-title, .my-workers-title, .my-websites-title { font-size: 27px; margin-top: 0px; float: left; @@ -952,18 +947,19 @@ ol li:before { background-color: rgba(0, 0, 0, 0.4); } -.create-an-app-btn{ - float:right; margin-bottom: 10px; +.create-an-app-btn, .create-a-website-btn{ + float:right; + margin-bottom: 10px; } -.create-an-app-btn img{ +.create-an-app-btn img, .create-a-website-btn img{ width: 20px; margin-bottom: -5px; margin-right: 10px; } -.create-a-worker-btn{ +.create-a-worker-btn, .create-a-website-btn{ float:right; margin-bottom: 10px; } -.create-a-worker-btn img{ +.create-a-worker-btn img, .create-a-website-btn img{ width: 20px; margin-bottom: -5px; margin-right: 5px; @@ -976,7 +972,7 @@ ol li:before { .ip-terms-notice{ font-size: 12px; margin-top: 20px; margin-bottom: 0; } -.app-list-nav, .worker-list-nav{ +.app-list-nav, .worker-list-nav, .website-list-nav{ overflow: hidden; margin-bottom: 40px; margin-top: 10px; } .back-to-main-btn{ @@ -989,7 +985,7 @@ ol li:before { display: flex; align-items: center; } -.app-title{ +.app-title, .website-title{ margin-top:0px; margin-bottom: 0; } @@ -1346,7 +1342,7 @@ dialog{ background-color: #ededed; } -.worker-checkbox, .app-checkbox { +.worker-checkbox, .app-checkbox, .website-checkbox { width: 20px; height: 20px; } diff --git a/src/dev-center/index.html b/src/dev-center/index.html index 7a2a752ae..a87c52662 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -290,7 +290,6 @@

My Websites

-
@@ -299,6 +298,7 @@
+
+ + ${worker.name}${worker.created_at} + +
Opens▼▲ Created▼▲
- - ${worker.name}${worker.created_at} - -
- + ${worker.name} ${worker.created_at} - -
diff --git a/src/dev-center/js/apps.js b/src/dev-center/js/apps.js index f30799c03..68ab0ca40 100644 --- a/src/dev-center/js/apps.js +++ b/src/dev-center/js/apps.js @@ -2601,12 +2601,13 @@ function app_context_menu(app_name, app_title, app_uid) { puter.ui.contextMenu({ items: [ { - label: 'Open', + label: 'Open App', type: 'primary', action: () => { puter.ui.launchApp(app_name); }, }, + '-', { label: 'Edit', type: 'primary', diff --git a/src/dev-center/js/dev-center.js b/src/dev-center/js/dev-center.js index c25cfb59c..7eacdc8dd 100644 --- a/src/dev-center/js/dev-center.js +++ b/src/dev-center/js/dev-center.js @@ -115,6 +115,13 @@ $(document).on('click', '.tab-btn', async function (e) { activeTab = 'workers'; } // --------------------------------------------------------------- + // Websites tab + // --------------------------------------------------------------- + else if ($(this).attr('data-tab') === 'websites') { + refresh_websites_list(); + activeTab = 'websites'; + } + // --------------------------------------------------------------- // Payout Method tab // --------------------------------------------------------------- else if ($(this).attr('data-tab') === 'payout-method') { diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index 0bb9f1062..c7f0d052b 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -1,4 +1,349 @@ -async function init_workers() { +let sortBy = 'created_at'; +let sortDirection = 'desc'; +window.websites = []; +let search_query; + +window.create_website = async (name) => { + let website = await puter.hosting.create(name, window.default_website_file); + + return website; } -export default init_workers; \ No newline at end of file +window.refresh_websites_list = async (show_loading = false) => { + if (show_loading) + puter.ui.showSpinner(); + + // puter.hosting.list() returns an array of website objects + window.websites = await puter.hosting.list(); + + // Get websites + if (window.activeTab === 'websites' && window.websites.length > 0) { + $('.website-card').remove(); + $('#no-websites-notice').hide(); + $('#website-list').show(); + window.websites.forEach((website) => { + // append row to website-list-table + $('#website-list-table > tbody').append(generate_website_card(website)); + }); + } else { + $('#no-websites-notice').show(); + $('#website-list').hide(); + } + + count_websites(); +} + + +async function init_websites() { +} + +$(document).on('click', '.create-a-website-btn', async function (e) { + let name = await puter.ui.prompt('Please enter a name for your website:', 'my-awesome-website'); + + if (name) { + await create_website(name); + } +}) + +$(document).on('change', '.website-checkbox', function (e) { + // determine if select-all checkbox should be checked, indeterminate, or unchecked + if ($('.website-checkbox:checked').length === $('.website-checkbox').length) { + $('.select-all-websites').prop('indeterminate', false); + $('.select-all-websites').prop('checked', true); + } else if ($('.website-checkbox:checked').length > 0) { + $('.select-all-websites').prop('indeterminate', true); + $('.select-all-websites').prop('checked', false); + } + else { + $('.select-all-websites').prop('indeterminate', false); + $('.select-all-websites').prop('checked', false); + } + + // activate row + if ($(this).is(':checked')) + $(this).closest('tr').addClass('active'); + else + $(this).closest('tr').removeClass('active'); + + // enable delete button if at least one checkbox is checked + if ($('.website-checkbox:checked').length > 0) + $('.delete-websites-btn').removeClass('disabled'); + else + $('.delete-websites-btn').addClass('disabled'); +}) + +$(document).on('change', '.select-all-websites', function (e) { + if ($(this).is(':checked')) { + $('.website-checkbox').prop('checked', true); + $('.website-card').addClass('active'); + $('.delete-websites-btn').removeClass('disabled'); + } else { + $('.website-checkbox').prop('checked', false); + $('.website-card').removeClass('active'); + $('.delete-websites-btn').addClass('disabled'); + } +}) + +$('.refresh-website-list').on('click', function (e) { + puter.ui.showSpinner(); + refresh_website_list(); + + puter.ui.hideSpinner(); +}) + +$('th.sort').on('click', function (e) { + // determine what column to sort by + const sortByColumn = $(this).attr('data-column'); + + // toggle sort direction + if (sortByColumn === sortBy) { + if (sortDirection === 'asc') + sortDirection = 'desc'; + else + sortDirection = 'asc'; + } + else { + sortBy = sortByColumn; + sortDirection = 'desc'; + } + + // update arrow + $('.sort-arrow').css('display', 'none'); + $('#website-list-table').find('th').removeClass('sorted'); + $(this).find('.sort-arrow-' + sortDirection).css('display', 'inline'); + $(this).addClass('sorted'); + + sort_websites(); +}); + +function sort_websites() { + let sorted_websites; + + // sort + if (sortDirection === 'asc'){ + sorted_websites = websites.sort((a, b) => { + if(sortBy === 'name'){ + return a[sortBy].localeCompare(b[sortBy]); + }else if(sortBy === 'created_at'){ + return new Date(a[sortBy]) - new Date(b[sortBy]); + } else if(sortBy === 'user_count' || sortBy === 'open_count'){ + return a.stats[sortBy] - b.stats[sortBy]; + }else{ + a[sortBy] > b[sortBy] ? 1 : -1 + } + }); + }else{ + sorted_websites = websites.sort((a, b) => { + if(sortBy === 'name'){ + return b[sortBy].localeCompare(a[sortBy]); + }else if(sortBy === 'created_at'){ + return new Date(b[sortBy]) - new Date(a[sortBy]); + } else if(sortBy === 'user_count' || sortBy === 'open_count'){ + return b.stats[sortBy] - a.stats[sortBy]; + }else{ + b[sortBy] > a[sortBy] ? 1 : -1 + } + }); + } + // refresh website list + $('.website-card').remove(); + sorted_websites.forEach(website => { + $('#website-list-table > tbody').append(generate_website_card(website)); + }); + + count_websites(); + + // show websites that match search_query and hide websites that don't + if (search_query) { + // show websites that match search_query and hide websites that don't + websites.forEach((website) => { + if (website.name.toLowerCase().includes(search_query.toLowerCase())) { + $(`.website-card[data-name="${html_encode(website.name)}"]`).show(); + } else { + $(`.website-card[data-name="${html_encode(website.name)}"]`).hide(); + } + }) + } +} + +function count_websites() { + let count = 0; + $('.website-card').each(function () { + count++; + }) + $('.website-count').html(count); + return count; +} + +function generate_website_card(website) { + return ` + + + + + + + `; +} + +$(document).on('input change keyup keypress keydown paste cut', '.search', function (e) { + // search apps for query + search_query = $(this).val().toLowerCase(); + if (search_query === '') { + // hide 'clear search' button + $('.search-clear').hide(); + // show all apps again + $(`.website-card`).show(); + } else { + // show 'clear search' button + $('.search-clear').show(); + // show apps that match search_query and hide apps that don't + websites.forEach((website) => { + if ( + website.name.toLowerCase().includes(search_query.toLowerCase()) + ) + { + $(`.website-card[data-name="${website.name}"]`).show(); + } else { + $(`.website-card[data-name="${website.name}"]`).hide(); + } + }) + } +}) + +$(document).on('click', '.delete-websites-btn', async function (e) { + // show confirmation alert + let resp = await puter.ui.alert(`Are you sure you want to delete the selected websites?`, [ + { + label: 'Delete', + type: 'danger', + value: 'delete', + }, + { + label: 'Cancel', + }, + ], { + type: 'warning', + }); + + if (resp === 'delete') { + // disable delete button + $('.delete-websites-btn').addClass('disabled'); + + // show 'deleting' modal + puter.ui.showSpinner(); + + let start_ts = Date.now(); + const websites = $('.website-checkbox:checked').toArray(); + + // delete all checked websites + for (let website of websites) { + let website_name = $(website).attr('data-website-name'); + // delete website + await puter.hosting.delete(website_name) + + // remove website card + $(`.website-card[data-name="${website_name}"]`).fadeOut(200, function name(params) { + $(this).remove(); + if ($(`.website-card`).length === 0) { + $('section:not(.sidebar)').hide(); + $('#no-websites-notice').show(); + } else { + $('section:not(.sidebar)').hide(); + $('#website-list').show(); + } + count_websites(); + }); + + try{ + count_websites(); + } catch(err) { + console.log(err); + } + } + + // close 'deleting' modal + setTimeout(() => { + puter.ui.hideSpinner(); + if($('.website-checkbox:checked').length === 0){ + // disable delete button + $('.delete-websites-btn').addClass('disabled'); + // reset the 'select all' checkbox + $('.select-all-websites').prop('indeterminate', false); + $('.select-all-websites').prop('checked', false); + } + }, (start_ts - Date.now()) > 500 ? 0 : 500); + } +}) + +$(document).on('contextmenu', '.website-card', function (e) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + puter.ui.contextMenu({ + items: [ + { + label: 'Delete', + type: 'danger', + action: () => { + attempt_delete_website($(this).attr('data-name')); + }, + }, + ], + }); +}) + +$(document).on('click', '.options-icon-website', function (e) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + puter.ui.contextMenu({ + items: [ + { + label: 'Delete', + type: 'danger', + action: () => { + attempt_delete_website($(this).attr('data-website-name')); + }, + }, + ], + }); +}) + +async function attempt_delete_website(website_name) { + // confirm delete + const alert_resp = await puter.ui.alert(`Are you sure you want to premanently delete ${html_encode(website_name)}.puter.site?`, + [ + { + label: 'Yes, delete permanently', + value: 'delete', + type: 'danger', + }, + { + label: 'Cancel' + }, + ] + ); + + if (alert_resp === 'delete') { + // remove website card and update website count + $(`.website-card[data-name="${website_name}"]`).fadeOut(200, function name(params) { + $(this).remove(); + if ($(`.website-card`).length === 0) { + $('section:not(.sidebar)').hide(); + $('#no-websites-notice').show(); + } else { + $('section:not(.sidebar)').hide(); + $('#website-list').show(); + } + count_websites(); + }); + + // delete website + puter.hosting.delete(website_name); + } +} + +export default init_websites; \ No newline at end of file diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index c2964f2ea..bd244dc1a 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -205,7 +205,7 @@ function count_workers() { function generate_worker_card(worker) { return ` - From 82339b69a3be6a1190c813f18ed9d88306920670 Mon Sep 17 00:00:00 2001 From: jelveh Date: Sat, 2 Aug 2025 23:37:34 -0700 Subject: [PATCH 11/46] Add "No websites notice" --- src/dev-center/css/style.css | 14 +++++++++----- src/dev-center/img/websites-placeholder.svg | 2 ++ src/dev-center/index.html | 15 ++++++++++++--- 3 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 src/dev-center/img/websites-placeholder.svg diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index 0b90ffff6..616dfeaef 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -621,7 +621,7 @@ label { box-sizing: border-box; } -#no-apps-notice, #no-workers-notice, #loading { +#no-apps-notice, #no-workers-notice, #no-websites-notice, #loading { display: flex; flex-direction: column; justify-content: center; @@ -1127,13 +1127,17 @@ dialog{ width: 100%; box-sizing: border-box; background-color: white; - padding: 5px; - background-size: 20px; + padding: 8px; + background-size: 15px; background-position-y: center; - background-position-x: 5px; + background-position-x: 9px; padding-left: 35px; padding-right: 35px; border: 2px solid #ebebeb; + font-size: 14px; +} +.search::placeholder { + opacity: 0.7; } .search:focus, .search:focus-within, .search:active, .search:focus-visible { border: 2px solid #cbcbcb !important; @@ -1146,7 +1150,7 @@ dialog{ display: none; position: absolute; right: 6px; - top: 6px; + top: 8px; opacity: 0.3; height: 20px; } diff --git a/src/dev-center/img/websites-placeholder.svg b/src/dev-center/img/websites-placeholder.svg new file mode 100644 index 000000000..06fb067b8 --- /dev/null +++ b/src/dev-center/img/websites-placeholder.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/dev-center/index.html b/src/dev-center/index.html index a87c52662..86bd84b27 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -189,6 +189,15 @@

You haven't created any workers yet.

+ + + + + @@ -225,7 +234,7 @@
- +
@@ -261,7 +270,7 @@
- +
@@ -293,7 +302,7 @@
- +
From 7fceb804a754abce503e877b8253698c33d6b1ff Mon Sep 17 00:00:00 2001 From: jelveh Date: Sun, 3 Aug 2025 00:25:06 -0700 Subject: [PATCH 12/46] better interaction when deleting websites etc --- src/dev-center/css/style.css | 13 ++++---- src/dev-center/index.html | 14 ++++---- src/dev-center/js/apps.js | 39 ++++++++-------------- src/dev-center/js/websites.js | 3 +- src/dev-center/js/workers.js | 61 ++++++++++++----------------------- 5 files changed, 49 insertions(+), 81 deletions(-) diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index 616dfeaef..e08df526d 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -452,7 +452,7 @@ label { left: 0; z-index: 1000; display: block; - padding: 30px 16px 0; + padding: 40px 16px 0; overflow-x: hidden; overflow-y: auto; background-color: rgb(249, 249, 249); @@ -480,15 +480,16 @@ label { right: 0; top: 0; padding: 10px; - background-size: 15px; + background-size: 25px; background-repeat: no-repeat; background-position: center; cursor: pointer; - background-image: url("data:image/svg+xml,%3Csvg%20enable-background%3D%22new%200%200%2024%2024%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22m3%2023-3-3%208-8-8-8%203-3%2011%2011zm10%200-3-3%208-8-8-8%203-3%2011%2011z%22%2F%3E%3C%2Fsvg%3E"); - opacity: 0.3; + background-image: url("data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%3C!--%20Uploaded%20to%3A%20SVG%20Repo%2C%20www.svgrepo.com%2C%20Generator%3A%20SVG%20Repo%20Mixer%20Tools%20--%3E%3Csvg%20fill%3D%22%23000000%22%20width%3D%22800px%22%20height%3D%22800px%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M15.2928932%2C12%20L12.1464466%2C8.85355339%20C11.9511845%2C8.65829124%2011.9511845%2C8.34170876%2012.1464466%2C8.14644661%20C12.3417088%2C7.95118446%2012.6582912%2C7.95118446%2012.8535534%2C8.14644661%20L16.8535534%2C12.1464466%20C17.0488155%2C12.3417088%2017.0488155%2C12.6582912%2016.8535534%2C12.8535534%20L12.8535534%2C16.8535534%20C12.6582912%2C17.0488155%2012.3417088%2C17.0488155%2012.1464466%2C16.8535534%20C11.9511845%2C16.6582912%2011.9511845%2C16.3417088%2012.1464466%2C16.1464466%20L15.2928932%2C13%20L4.5%2C13%20C4.22385763%2C13%204%2C12.7761424%204%2C12.5%20C4%2C12.2238576%204.22385763%2C12%204.5%2C12%20L15.2928932%2C12%20Z%20M19%2C5.5%20C19%2C5.22385763%2019.2238576%2C5%2019.5%2C5%20C19.7761424%2C5%2020%2C5.22385763%2020%2C5.5%20L20%2C19.5%20C20%2C19.7761424%2019.7761424%2C20%2019.5%2C20%20C19.2238576%2C20%2019%2C19.7761424%2019%2C19.5%20L19%2C5.5%20Z%22%2F%3E%3C%2Fsvg%3E"); + opacity: 0.6; + margin-top: 5px; } .open .sidebar-toggle{ - background-image: url("data:image/svg+xml,%3Csvg%20enable-background%3D%22new%200%200%2024%2024%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22m21%201%203%203-8%208%208%208-3%203-11-11zm-10%200%203%203-8%208%208%208-3%203-11-11z%22%2F%3E%3C%2Fsvg%3E"); + background-image: url("data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%3C!--%20Uploaded%20to%3A%20SVG%20Repo%2C%20www.svgrepo.com%2C%20Generator%3A%20SVG%20Repo%20Mixer%20Tools%20--%3E%3Csvg%20fill%3D%22%23000000%22%20width%3D%22800px%22%20height%3D%22800px%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M8.70710678%2C12%20L19.5%2C12%20C19.7761424%2C12%2020%2C12.2238576%2020%2C12.5%20C20%2C12.7761424%2019.7761424%2C13%2019.5%2C13%20L8.70710678%2C13%20L11.8535534%2C16.1464466%20C12.0488155%2C16.3417088%2012.0488155%2C16.6582912%2011.8535534%2C16.8535534%20C11.6582912%2C17.0488155%2011.3417088%2C17.0488155%2011.1464466%2C16.8535534%20L7.14644661%2C12.8535534%20C6.95118446%2C12.6582912%206.95118446%2C12.3417088%207.14644661%2C12.1464466%20L11.1464466%2C8.14644661%20C11.3417088%2C7.95118446%2011.6582912%2C7.95118446%2011.8535534%2C8.14644661%20C12.0488155%2C8.34170876%2012.0488155%2C8.65829124%2011.8535534%2C8.85355339%20L8.70710678%2C12%20L8.70710678%2C12%20Z%20M4%2C5.5%20C4%2C5.22385763%204.22385763%2C5%204.5%2C5%20C4.77614237%2C5%205%2C5.22385763%205%2C5.5%20L5%2C19.5%20C5%2C19.7761424%204.77614237%2C20%204.5%2C20%20C4.22385763%2C20%204%2C19.7761424%204%2C19.5%20L4%2C5.5%20Z%22%2F%3E%3C%2Fsvg%3E"); } .sidebar-toggle:hover{ opacity: 1; @@ -507,7 +508,7 @@ label { display: block; padding: 10px 20px; cursor: pointer; - margin-bottom: 15px; + margin-bottom: 10px; } .sidebar-nav>li:hover { diff --git a/src/dev-center/index.html b/src/dev-center/index.html index 86bd84b27..0014e9b63 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -92,10 +92,10 @@ @@ -178,7 +178,7 @@ @@ -187,7 +187,7 @@ @@ -196,7 +196,7 @@ diff --git a/src/dev-center/js/apps.js b/src/dev-center/js/apps.js index 68ab0ca40..7bc003bac 100644 --- a/src/dev-center/js/apps.js +++ b/src/dev-center/js/apps.js @@ -2205,9 +2205,6 @@ $(document).on('click', '.delete-apps-btn', async function (e) { }); if (resp === 'delete') { - // disable delete button - // $('.delete-apps-btn').addClass('disabled'); - // show 'deleting' modal puter.ui.showSpinner(); @@ -2704,26 +2701,20 @@ async function attempt_delete_app(app_name, app_title, app_uid) { ); if (alert_resp === 'delete') { - let init_ts = Date.now(); - puter.ui.showSpinner(); - puter.apps.delete(app_name).then(async (app) => { - setTimeout(() => { - puter.ui.hideSpinner(); - $(`.app-card[data-uid="${app_uid}"]`).fadeOut(200, function name(params) { - $(this).remove(); - if ($(`.app-card`).length === 0) { - $('section:not(.sidebar)').hide(); - $('#no-apps-notice').show(); - } else { - $('section:not(.sidebar)').hide(); - $('#app-list').show(); - } - count_apps(); - }); - }, - // make sure the modal was shown for at least 2 seconds - (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); + $(`.app-card[data-uid="${app_uid}"]`).fadeOut(200, function name(params) { + $(this).remove(); + if ($(`.app-card`).length === 0) { + $('section:not(.sidebar)').hide(); + $('#no-apps-notice').show(); + } else { + $('section:not(.sidebar)').hide(); + $('#app-list').show(); + } + count_apps(); + }); + // delete app + puter.apps.delete(app_name).then(async (app) => { // get app directory puter.fs.stat({ path: `/${auth_username}/AppData/${dev_center_uid}/${app_uid}`, @@ -2738,16 +2729,12 @@ async function attempt_delete_app(app_name, app_title, app_uid) { ) }) }).catch(async (err) => { - setTimeout(() => { puter.ui.hideSpinner(); puter.ui.alert(err?.message, [ { label: 'Ok', }, ]); - }, - // make sure the modal was shown for at least 2 seconds - (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); }) } diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index c7f0d052b..b8e509882 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -42,6 +42,7 @@ $(document).on('click', '.create-a-website-btn', async function (e) { if (name) { await create_website(name); + refresh_websites_list(); } }) @@ -86,7 +87,7 @@ $(document).on('change', '.select-all-websites', function (e) { $('.refresh-website-list').on('click', function (e) { puter.ui.showSpinner(); - refresh_website_list(); + refresh_websites_list(); puter.ui.hideSpinner(); }) diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index bd244dc1a..2e89d06a9 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -344,17 +344,6 @@ async function attempt_delete_worker(worker_name) { // get worker const worker_data = await puter.workers.get(worker_name); - if(worker_data.metadata?.locked){ - puter.ui.alert(`${worker_data.name} is locked and cannot be deleted.`, [ - { - label: 'Ok', - }, - ], { - type: 'warning', - }); - return; - } - // confirm delete const alert_resp = await puter.ui.alert(`Are you sure you want to premanently delete ${html_encode(worker_data.name)}?`, [ @@ -370,37 +359,27 @@ async function attempt_delete_worker(worker_name) { ); if (alert_resp === 'delete') { - let init_ts = Date.now(); - puter.ui.showSpinner(); - puter.workers.delete(worker_name).then(async (worker) => { - setTimeout(() => { - puter.ui.hideSpinner(); - $(`.worker-card[data-name="${worker_name}"]`).fadeOut(200, function name(params) { - $(this).remove(); - if ($(`.worker-card`).length === 0) { - $('section:not(.sidebar)').hide(); - $('#no-workers-notice').show(); - } else { - $('section:not(.sidebar)').hide(); - $('#worker-list').show(); - } - count_workers(); - }); + // remove worker card and update worker count + $(`.worker-card[data-name="${worker_name}"]`).fadeOut(200, function name(params) { + $(this).remove(); + if ($(`.worker-card`).length === 0) { + $('section:not(.sidebar)').hide(); + $('#no-workers-notice').show(); + } else { + $('section:not(.sidebar)').hide(); + $('#worker-list').show(); + } + count_workers(); + }); + + // delete worker + puter.workers.delete(worker_name).then().catch(async (err) => { + puter.ui.alert(err?.message, [ + { + label: 'Ok', }, - // make sure the modal was shown for at least 2 seconds - (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); - }).catch(async (err) => { - setTimeout(() => { - puter.ui.hideSpinner(); - puter.ui.alert(err?.message, [ - { - label: 'Ok', - }, - ]); - }, - // make sure the modal was shown for at least 2 seconds - (Date.now() - init_ts) > 2000 ? 1 : 2000 - (Date.now() - init_ts)); - }) + ]); + }) } } From bb7bb532abcc4f3958a3cd9208621fbb215d24f1 Mon Sep 17 00:00:00 2001 From: jelveh Date: Sun, 3 Aug 2025 00:52:33 -0700 Subject: [PATCH 13/46] wip --- src/dev-center/css/style.css | 27 +++++++++++++++++---------- src/dev-center/img/plus.svg | 6 ++++++ src/dev-center/index.html | 25 ++++++++++++++----------- src/dev-center/js/websites.js | 2 +- src/dev-center/js/workers.js | 8 ++++++-- 5 files changed, 44 insertions(+), 24 deletions(-) create mode 100644 src/dev-center/img/plus.svg diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index e08df526d..345ce6c9e 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -216,7 +216,9 @@ h1 .app-count, h1 .worker-count, h1 .website-count{ .refresh-app-list:hover, .refresh-worker-list:hover, .refresh-website-list:hover { opacity: 1; } - +.refresh-icon{ + width: 18px; height: 18px; margin-top: -2px; display: block; +} a { color: #0d6efd; text-decoration: none; @@ -952,19 +954,18 @@ ol li:before { float:right; margin-bottom: 10px; } -.create-an-app-btn img, .create-a-website-btn img{ - width: 20px; +.create-an-app-btn img, .create-a-website-btn img, .create-a-worker-btn img{ + width: 25px; + height: 25px; margin-bottom: -5px; margin-right: 10px; + margin-bottom: -7px; + margin-right: 4px; } .create-a-worker-btn, .create-a-website-btn{ float:right; margin-bottom: 10px; } -.create-a-worker-btn img, .create-a-website-btn img{ - width: 20px; - margin-bottom: -5px; - margin-right: 5px; -} + .jip-submit-btn{ float: left; margin-top: 10px; @@ -1137,6 +1138,12 @@ dialog{ border: 2px solid #ebebeb; font-size: 14px; } +.search-container{ + margin-bottom: 10px; + position: relative; + width: 300px; + float:left; +} .search::placeholder { opacity: 0.7; } @@ -1339,12 +1346,12 @@ dialog{ opacity: 0.7; width: 30px; height: 30px; - border-radius: 3px; + border-radius: 5px; } .options-icon:hover { opacity: 1; - background-color: #ededed; + background-color: #edededb4; } .worker-checkbox, .app-checkbox, .website-checkbox { diff --git a/src/dev-center/img/plus.svg b/src/dev-center/img/plus.svg new file mode 100644 index 000000000..7ed5e22e6 --- /dev/null +++ b/src/dev-center/img/plus.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/dev-center/index.html b/src/dev-center/index.html index 0014e9b63..d02d84378 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -19,6 +19,9 @@ + + + @@ -178,7 +181,7 @@ @@ -187,7 +190,7 @@ @@ -196,7 +199,7 @@ @@ -230,16 +233,16 @@

My Apps

- +
-
+
- +
+ + ${website.subdomain}.puter.site${website.created_at}
+ ${worker.name}
@@ -269,13 +272,13 @@ -
+
- +
@@ -298,16 +301,16 @@

My Websites

- +
-
+
- +
diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index b8e509882..143e1435e 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -172,7 +172,7 @@ function count_websites() { $('.website-card').each(function () { count++; }) - $('.website-count').html(count); + $('.website-count').html(count ? count : ''); return count; } diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index 2e89d06a9..d6ad910e4 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -14,7 +14,11 @@ window.refresh_worker_list = async (show_loading = false) => { puter.ui.showSpinner(); // puter.workers.list() returns an array of worker objects - window.workers = await puter.workers.list(); + try { + window.workers = await puter.workers.list(); + } catch (err) { + console.error('Error refreshing worker list:', err); + } // Get workers if (window.activeTab === 'workers' && window.workers.length > 0) { @@ -198,7 +202,7 @@ function count_workers() { $('.worker-card').each(function () { count++; }) - $('.worker-count').html(count); + $('.worker-count').html(count ? count : ''); return count; } From 615de5ec1138041aeb36c3c691de82f79acc94ba Mon Sep 17 00:00:00 2001 From: jelveh Date: Sun, 3 Aug 2025 01:03:17 -0700 Subject: [PATCH 14/46] wip --- src/dev-center/css/style.css | 3 +++ src/dev-center/js/websites.js | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index 345ce6c9e..74108872b 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -511,6 +511,9 @@ label { padding: 10px 20px; cursor: pointer; margin-bottom: 10px; + margin-left: 10px; + margin-right: 10px; + border-radius: 7px; } .sidebar-nav>li:hover { diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index 143e1435e..b1cb2ed3a 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -4,7 +4,12 @@ window.websites = []; let search_query; window.create_website = async (name) => { - let website = await puter.hosting.create(name, window.default_website_file); + let website + try { + website = await puter.hosting.create(name, window.default_website_file); + } catch (error) { + puter.ui.alert(`Error creating website: ${error.error.message}`); + } return website; } From 36a2f2f7b8e16d256e966197a0249d91c53a6caf Mon Sep 17 00:00:00 2001 From: jelveh Date: Sun, 3 Aug 2025 01:23:16 -0700 Subject: [PATCH 15/46] fix issue with deplot area not showing up --- src/dev-center/js/apps.js | 4 ++-- src/dev-center/js/dev-center.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dev-center/js/apps.js b/src/dev-center/js/apps.js index 7bc003bac..eb0aa81be 100644 --- a/src/dev-center/js/apps.js +++ b/src/dev-center/js/apps.js @@ -2608,8 +2608,8 @@ function app_context_menu(app_name, app_title, app_uid) { { label: 'Edit', type: 'primary', - action: () => { - edit_app_section(app_name, 'edit'); + action: () => { + edit_app_section(app_name); }, }, { diff --git a/src/dev-center/js/dev-center.js b/src/dev-center/js/dev-center.js index 7eacdc8dd..9c3d729b9 100644 --- a/src/dev-center/js/dev-center.js +++ b/src/dev-center/js/dev-center.js @@ -310,7 +310,7 @@ $(document).on('click', '.section-tab-btn', function (e) { // hide all tabs $('.section-tab').hide(); // show section - $('.section-tab[data-tab="' + $(this).attr('data-tab') + '"]').show(); + $(`.section-tab[data-tab="${$(this).attr('data-tab')}"]`).show(); // remove active class from all tab buttons $('.section-tab-btn').removeClass('active'); // add active class to clicked tab button From f0dba7ca60a39c36f36d8328a473d780f389eb84 Mon Sep 17 00:00:00 2001 From: jelveh Date: Sun, 3 Aug 2025 12:53:07 -0700 Subject: [PATCH 16/46] Show connected directory for websites --- src/dev-center/css/style.css | 1 + src/dev-center/index.html | 3 ++- src/dev-center/js/websites.js | 15 ++++++++------- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index 74108872b..1f6b11f61 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -44,6 +44,7 @@ h1 .app-count, h1 .worker-count, h1 .website-count{ padding: 2px 10px; border-radius: 3px; } + /* ------------------------------------ Button ------------------------------------*/ diff --git a/src/dev-center/index.html b/src/dev-center/index.html index d02d84378..984744512 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -91,7 +91,7 @@ + diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index b1cb2ed3a..76d1efd2e 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -163,10 +163,10 @@ function sort_websites() { if (search_query) { // show websites that match search_query and hide websites that don't websites.forEach((website) => { - if (website.name.toLowerCase().includes(search_query.toLowerCase())) { - $(`.website-card[data-name="${html_encode(website.name)}"]`).show(); + if (website.subdomain.toLowerCase().includes(search_query.toLowerCase())) { + $(`.website-card[data-name="${html_encode(website.subdomain)}"]`).show(); } else { - $(`.website-card[data-name="${html_encode(website.name)}"]`).hide(); + $(`.website-card[data-name="${html_encode(website.subdomain)}"]`).hide(); } }) } @@ -184,10 +184,11 @@ function count_websites() { function generate_website_card(website) { return ` - + @@ -208,12 +209,12 @@ $(document).on('input change keyup keypress keydown paste cut', '.search', funct // show apps that match search_query and hide apps that don't websites.forEach((website) => { if ( - website.name.toLowerCase().includes(search_query.toLowerCase()) + website.subdomain.toLowerCase().includes(search_query.toLowerCase()) ) { - $(`.website-card[data-name="${website.name}"]`).show(); + $(`.website-card[data-name="${website.subdomain}"]`).show(); } else { - $(`.website-card[data-name="${website.name}"]`).hide(); + $(`.website-card[data-name="${website.subdomain}"]`).hide(); } }) } From 99739173d69b8a21f4d97d75bee1183e38a89b8a Mon Sep 17 00:00:00 2001 From: jelveh Date: Sun, 3 Aug 2025 13:03:49 -0700 Subject: [PATCH 17/46] Show connected directory on websites list --- src/dev-center/css/style.css | 4 ++++ src/dev-center/js/websites.js | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index 1f6b11f61..7febab428 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -1365,4 +1365,8 @@ dialog{ .no-hover:hover { background-color: transparent !important; +} + +.root-dir-name{ + cursor: pointer; } \ No newline at end of file diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index 76d1efd2e..74169ff28 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -188,7 +188,7 @@ function generate_website_card(website) { - + @@ -353,4 +353,17 @@ async function attempt_delete_website(website_name) { } } +$(document).on('click', '.root-dir-name', function (e) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + + const root_dir_path = $(this).attr('data-root-dir-path'); + + if(root_dir_path){ + puter.ui.launchApp('explorer', { + path: root_dir_path, + }); + } +}) export default init_websites; \ No newline at end of file From 3b707c31a74a0ab68c23f05267ea7e50e833c55c Mon Sep 17 00:00:00 2001 From: jelveh Date: Sun, 3 Aug 2025 13:08:30 -0700 Subject: [PATCH 18/46] Search websites supports conenct directory matching --- src/dev-center/js/websites.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index 74169ff28..328573318 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -209,7 +209,8 @@ $(document).on('input change keyup keypress keydown paste cut', '.search', funct // show apps that match search_query and hide apps that don't websites.forEach((website) => { if ( - website.subdomain.toLowerCase().includes(search_query.toLowerCase()) + website.subdomain.toLowerCase().includes(search_query.toLowerCase()) || + website.root_dir?.name?.toLowerCase().includes(search_query.toLowerCase()) ) { $(`.website-card[data-name="${website.subdomain}"]`).show(); From 30db54f06294c393758ae69d5d00941399625bf7 Mon Sep 17 00:00:00 2001 From: jelveh Date: Sun, 3 Aug 2025 14:50:02 -0700 Subject: [PATCH 19/46] Remove context menu from entries --- src/dev-center/css/style.css | 21 ++++++++++++++------- src/dev-center/js/apps.js | 11 ----------- src/dev-center/js/websites.js | 17 ----------------- src/dev-center/js/workers.js | 26 ++++++++------------------ 4 files changed, 22 insertions(+), 53 deletions(-) diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index 7febab428..7c0679093 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -237,8 +237,8 @@ section { padding: 10px; overflow: hidden; width: 100%; - padding-right: 20px; - padding-left: 20px; + padding-right: 40px; + padding-left: 40px; box-sizing: border-box; } #app-list, #website-list, #worker-list { @@ -756,12 +756,13 @@ th.sorted{ } .my-apps-title, .my-workers-title, .my-websites-title { - font-size: 27px; + font-size: 22px; margin-top: 0px; float: left; font-weight: 500; - margin-bottom: 15px; + margin-bottom: 0; color: #394254; + flex-grow: 1; } .app-row-toolbar { @@ -956,7 +957,7 @@ ol li:before { .create-an-app-btn, .create-a-website-btn{ float:right; - margin-bottom: 10px; + margin-bottom: 0px; } .create-an-app-btn img, .create-a-website-btn img, .create-a-worker-btn img{ width: 25px; @@ -967,7 +968,8 @@ ol li:before { margin-right: 4px; } .create-a-worker-btn, .create-a-website-btn{ - float:right; margin-bottom: 10px; + float:right; + margin-bottom: 0px; } .jip-submit-btn{ @@ -979,7 +981,12 @@ ol li:before { font-size: 12px; margin-top: 20px; margin-bottom: 0; } .app-list-nav, .worker-list-nav, .website-list-nav{ - overflow: hidden; margin-bottom: 40px; margin-top: 10px; + overflow: hidden; + margin-bottom: 40px; + margin-top: 10px; + display: flex; + flex-direction: row; + align-items: center; } .back-to-main-btn{ float:right; margin-bottom: 10px; diff --git a/src/dev-center/js/apps.js b/src/dev-center/js/apps.js index eb0aa81be..504123753 100644 --- a/src/dev-center/js/apps.js +++ b/src/dev-center/js/apps.js @@ -2660,17 +2660,6 @@ $(document).on('click', '.options-icon-app', function(e) { app_context_menu(app_name, app_title, app_uid); }) -$(document).on('contextmenu', '.app-card', function(e) { - let app_name = $(this).attr('data-name'); - let app_title = $(this).attr('data-title'); - let app_uid = $(this).attr('data-uid'); - - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - app_context_menu(app_name, app_title, app_uid); -}) - async function attempt_delete_app(app_name, app_title, app_uid) { // get app const app_data = await puter.apps.get(app_name, { icon_size: 16 }); diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index 328573318..ba66153e6 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -286,23 +286,6 @@ $(document).on('click', '.delete-websites-btn', async function (e) { } }) -$(document).on('contextmenu', '.website-card', function (e) { - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - puter.ui.contextMenu({ - items: [ - { - label: 'Delete', - type: 'danger', - action: () => { - attempt_delete_website($(this).attr('data-name')); - }, - }, - ], - }); -}) - $(document).on('click', '.options-icon-website', function (e) { e.preventDefault(); e.stopPropagation(); diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index d6ad910e4..e94cb6eee 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -4,7 +4,14 @@ window.workers = []; let search_query; window.create_worker = async (name) => { - let worker = await puter.workers.create(name, window.default_worker_file); + let worker; + + try { + worker = await puter.workers.create(name, window.default_worker_file); + } catch (err) { + console.log(err); + console.error('Error creating worker:', err.error); + } return worker; } @@ -309,23 +316,6 @@ $(document).on('click', '.delete-workers-btn', async function (e) { } }) -$(document).on('contextmenu', '.worker-card', function (e) { - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - puter.ui.contextMenu({ - items: [ - { - label: 'Delete', - type: 'danger', - action: () => { - attempt_delete_worker($(this).attr('data-name')); - }, - }, - ], - }); -}) - $(document).on('click', '.options-icon-worker', function (e) { e.preventDefault(); e.stopPropagation(); From 2d0d253f9663d87065f461d16b0eebab58b99234 Mon Sep 17 00:00:00 2001 From: jelveh Date: Sun, 3 Aug 2025 15:03:48 -0700 Subject: [PATCH 20/46] Update apps.js --- src/dev-center/js/apps.js | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/dev-center/js/apps.js b/src/dev-center/js/apps.js index 504123753..139250fee 100644 --- a/src/dev-center/js/apps.js +++ b/src/dev-center/js/apps.js @@ -188,7 +188,6 @@ async function create_app(title, source_path = null, items = null) { window_resizable: true, fullpage_on_landing: true, }, - }) .then(async (app) => { let app_dir; @@ -209,27 +208,26 @@ async function create_app(title, source_path = null, items = null) { // Update the app with the new hostname // ---------------------------------------------------- puter.apps.update(app.name, { - title: title, - indexURL: source_path ? protocol + `://${subdomain}.` + static_hosting_domain : 'https://dev-center.puter.com/coming-soon.html', - icon: icon, - description: ' ', - maximizeOnStart: false, - background: false, - metadata: { - category: null, // default category on creation - window_resizable: true, - fullpage_on_landing: true, - } -}).then(async (app) => { + title: title, + indexURL: source_path ? protocol + `://${subdomain}.` + static_hosting_domain : 'https://dev-center.puter.com/coming-soon.html', + icon: icon, + description: ' ', + maximizeOnStart: false, + background: false, + metadata: { + category: null, // default category on creation + window_resizable: true, + fullpage_on_landing: true, + } + }).then(async (app) => { // refresh app list puter.apps.list({ icon_size: 64 }).then(async (resp) => { apps = resp; - // Close the 'Creting new app...' modal + // Close the 'Creating new app...' modal // but make sure it was shown for at least 2 seconds setTimeout(() => { // open edit app section edit_app_section(app.name); - refresh_app_list(); // ✅ Refreshes main app list // set drop area if source_path was provided or items were dropped if (source_path || items) { From 4550e4f4fd62429ca4dbba01908452ecda5f3e16 Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sun, 3 Aug 2025 16:12:27 -0700 Subject: [PATCH 21/46] remove unnecessary console.logs in Login --- src/gui/src/UI/UIWindowLogin.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/gui/src/UI/UIWindowLogin.js b/src/gui/src/UI/UIWindowLogin.js index 2d98d4523..463ae61a1 100644 --- a/src/gui/src/UI/UIWindowLogin.js +++ b/src/gui/src/UI/UIWindowLogin.js @@ -185,7 +185,6 @@ async function UIWindowLogin(options){ // Disable the login button to prevent multiple submissions $(el_window).find('.login-btn').prop('disabled', true); - console.log('Sending login AJAX request with async: true'); $.ajax({ url: window.gui_origin + "/login", type: 'POST', @@ -194,7 +193,6 @@ async function UIWindowLogin(options){ contentType: "application/json", data: data, success: async function (data){ - console.log('Login request successful'); // Keep the button disabled on success since we're redirecting or closing let p = Promise.resolve(); if ( data.next_step === 'otp' ) { @@ -252,7 +250,6 @@ async function UIWindowLogin(options){ p.resolve(); } catch (e) { // keeping this log; useful in screenshots - console.log('2FA Login Error', e); component.set('error', i18n(error_i18n_key)); component.set('is_checking_code', false); } @@ -319,7 +316,6 @@ async function UIWindowLogin(options){ p.resolve(); } catch (e) { // keeping this log; useful in screenshots - console.log('2FA Recovery Error', e); component.set('error', i18n(error_i18n_key)); } } @@ -366,7 +362,6 @@ async function UIWindowLogin(options){ if(options.reload_on_success){ sessionStorage.setItem('playChimeNextUpdate', 'yes'); window.onbeforeunload = null; - console.log('About to redirect, checking URL parameters:', window.location.search); // Replace with a clean URL to prevent password leakage const cleanUrl = window.location.origin + window.location.pathname; window.location.replace(cleanUrl); From 80ccb2d48b6974e5d78c4e07ba858d888c6846b0 Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sun, 3 Aug 2025 16:36:18 -0700 Subject: [PATCH 22/46] Update workers.js --- src/dev-center/js/workers.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index e94cb6eee..016edf1b8 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -334,12 +334,8 @@ $(document).on('click', '.options-icon-worker', function (e) { }) async function attempt_delete_worker(worker_name) { - - // get worker - const worker_data = await puter.workers.get(worker_name); - // confirm delete - const alert_resp = await puter.ui.alert(`Are you sure you want to premanently delete ${html_encode(worker_data.name)}?`, + const alert_resp = await puter.ui.alert(`Are you sure you want to premanently delete ${html_encode(worker_name)}?`, [ { label: 'Yes, delete permanently', From 8f18a841b3c67301c1835b74e3aee6cdad0bfcfa Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sun, 3 Aug 2025 17:15:46 -0700 Subject: [PATCH 23/46] Add file column to worker table and enable file path launching --- src/dev-center/index.html | 1 + src/dev-center/js/{libs => }/html-entities.js | 0 .../js/{libs => }/jquery-3.6.0.min.js | 0 .../js/{libs => }/jquery.dragster.js | 0 src/dev-center/js/{libs => }/slugify.js | 0 src/dev-center/js/workers.js | 16 +++++ src/gui/src/services/ExecService.js | 65 +++++++++++++++++-- src/puter-js/src/modules/UI.js | 15 ++++- 8 files changed, 92 insertions(+), 5 deletions(-) rename src/dev-center/js/{libs => }/html-entities.js (100%) rename src/dev-center/js/{libs => }/jquery-3.6.0.min.js (100%) rename src/dev-center/js/{libs => }/jquery.dragster.js (100%) rename src/dev-center/js/{libs => }/slugify.js (100%) diff --git a/src/dev-center/index.html b/src/dev-center/index.html index 984744512..e38a34d18 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -285,6 +285,7 @@ + diff --git a/src/dev-center/js/libs/html-entities.js b/src/dev-center/js/html-entities.js similarity index 100% rename from src/dev-center/js/libs/html-entities.js rename to src/dev-center/js/html-entities.js diff --git a/src/dev-center/js/libs/jquery-3.6.0.min.js b/src/dev-center/js/jquery-3.6.0.min.js similarity index 100% rename from src/dev-center/js/libs/jquery-3.6.0.min.js rename to src/dev-center/js/jquery-3.6.0.min.js diff --git a/src/dev-center/js/libs/jquery.dragster.js b/src/dev-center/js/jquery.dragster.js similarity index 100% rename from src/dev-center/js/libs/jquery.dragster.js rename to src/dev-center/js/jquery.dragster.js diff --git a/src/dev-center/js/libs/slugify.js b/src/dev-center/js/slugify.js similarity index 100% rename from src/dev-center/js/libs/slugify.js rename to src/dev-center/js/slugify.js diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index 016edf1b8..5b5592b81 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -220,6 +220,7 @@ function generate_worker_card(worker) { + @@ -373,4 +374,19 @@ async function attempt_delete_worker(worker_name) { } } +$(document).on('click', '.file-path', function (e) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + + const file_path = $(this).attr('data-file-path'); + + if(file_path){ + puter.ui.launchApp({ + name: 'editor', + file_paths: [file_path], + }); + } +}) + export default init_workers; \ No newline at end of file diff --git a/src/gui/src/services/ExecService.js b/src/gui/src/services/ExecService.js index 16aee4791..a2c310284 100644 --- a/src/gui/src/services/ExecService.js +++ b/src/gui/src/services/ExecService.js @@ -48,7 +48,8 @@ export class ExecService extends Service { } // This method is exposed to apps via IPCService. - async launchApp ({ app_name, args, pseudonym }, { ipc_context, msg_id } = {}) { + async launchApp ({ app_name, args, pseudonym, file_paths }, { ipc_context, msg_id } = {}) { + console.log('launchApp', { app_name, args, pseudonym, file_paths }); const app = ipc_context?.caller?.app; const process = ipc_context?.caller?.process; @@ -68,8 +69,8 @@ export class ExecService extends Service { Object.assign(params, provider()); } - // The "body" of this method is in a separate file - const child_process = await launch_app({ + // Handle file paths if provided and caller is in godmode + let launch_options = { launched_by_exec_service: true, name: app_name, pseudonym, @@ -80,7 +81,63 @@ export class ExecService extends Service { ...(connection ? { parent_pseudo_id: connection.backward.uuid, } : {}), - }); + }; + + // Check if file_paths are provided and caller has godmode permissions + if (file_paths && Array.isArray(file_paths) && file_paths.length > 0 && process) { + console.log('file_paths', file_paths); + try { + // Get caller app info to check godmode status + const caller_app_name = process.name; + const caller_app_info = await window.get_apps(caller_app_name); + + // Check if caller is in godmode + if (caller_app_info && caller_app_info.godmode === 1) { + this.log.info(`⚠️ GODMODE app ${caller_app_name} launching ${app_name} with files:`, file_paths); + + // Get target app info to create file signatures + const target_app_info = await puter.apps.get(app_name); + + // For the first file, create a file signature and set it up like opening a file + if (file_paths.length > 0) { + const first_file_path = file_paths[0]; + + try { + // Get file stats to verify it exists + const file_stat = await puter.fs.stat(first_file_path); + + // Create file signature for the target app + const file_signature_result = await puter.fs.sign(target_app_info.uuid, { + path: first_file_path, + action: 'write' + }); + + // Set up launch options with file information + launch_options.file_signature = file_signature_result.items; + launch_options.file_path = first_file_path; + launch_options.token = file_signature_result.token; + + // Add all file paths to args for the target app + launch_options.args.file_paths = file_paths; + + } catch (file_error) { + this.log.warn(`Failed to process file ${first_file_path}:`, file_error); + // Continue with launch but without file signature + } + } + + } else { + console.log(`⚠️ App ${caller_app_name} attempted to launch ${app_name} with files but does not have godmode permissions`); + // Continue with normal launch, ignoring file_paths + } + } catch (error) { + console.log('Error checking godmode permissions:', error); + // Continue with normal launch + } + } + + // The "body" of this method is in a separate file + const child_process = await launch_app(launch_options); const send_child_launched_msg = (...a) => { if ( ! process ) return; diff --git a/src/puter-js/src/modules/UI.js b/src/puter-js/src/modules/UI.js index ebc0995fb..b32c7239e 100644 --- a/src/puter-js/src/modules/UI.js +++ b/src/puter-js/src/modules/UI.js @@ -1047,7 +1047,19 @@ class UI extends EventListener { // Returns a Promise launchApp = async function launchApp(app_name, args, callback) { let pseudonym = undefined; - if ( app_name.includes('#(as)') ) { + let file_paths = undefined; + + // Handle case where app_name is an options object + if (typeof app_name === 'object' && app_name !== null) { + const options = app_name; + app_name = options.name || options.app_name; + file_paths = options.file_paths; + args = args || options.args; + callback = callback || options.callback; + pseudonym = options.pseudonym; + } + + if ( app_name && app_name.includes('#(as)') ) { [app_name, pseudonym] = app_name.split('#(as)'); } const app_info = await this.#ipc_stub({ @@ -1055,6 +1067,7 @@ class UI extends EventListener { callback, parameters: { app_name, + file_paths, pseudonym, args, }, From 12f574ed70a6aa466dff3b863d3a50f43994ef1d Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sun, 3 Aug 2025 17:21:13 -0700 Subject: [PATCH 24/46] move libs back to original location --- src/dev-center/js/{ => libs}/html-entities.js | 0 src/dev-center/js/{ => libs}/jquery-3.6.0.min.js | 0 src/dev-center/js/{ => libs}/jquery.dragster.js | 0 src/dev-center/js/{ => libs}/slugify.js | 0 src/gui/src/services/ExecService.js | 2 -- 5 files changed, 2 deletions(-) rename src/dev-center/js/{ => libs}/html-entities.js (100%) rename src/dev-center/js/{ => libs}/jquery-3.6.0.min.js (100%) rename src/dev-center/js/{ => libs}/jquery.dragster.js (100%) rename src/dev-center/js/{ => libs}/slugify.js (100%) diff --git a/src/dev-center/js/html-entities.js b/src/dev-center/js/libs/html-entities.js similarity index 100% rename from src/dev-center/js/html-entities.js rename to src/dev-center/js/libs/html-entities.js diff --git a/src/dev-center/js/jquery-3.6.0.min.js b/src/dev-center/js/libs/jquery-3.6.0.min.js similarity index 100% rename from src/dev-center/js/jquery-3.6.0.min.js rename to src/dev-center/js/libs/jquery-3.6.0.min.js diff --git a/src/dev-center/js/jquery.dragster.js b/src/dev-center/js/libs/jquery.dragster.js similarity index 100% rename from src/dev-center/js/jquery.dragster.js rename to src/dev-center/js/libs/jquery.dragster.js diff --git a/src/dev-center/js/slugify.js b/src/dev-center/js/libs/slugify.js similarity index 100% rename from src/dev-center/js/slugify.js rename to src/dev-center/js/libs/slugify.js diff --git a/src/gui/src/services/ExecService.js b/src/gui/src/services/ExecService.js index a2c310284..8d26221fd 100644 --- a/src/gui/src/services/ExecService.js +++ b/src/gui/src/services/ExecService.js @@ -49,7 +49,6 @@ export class ExecService extends Service { // This method is exposed to apps via IPCService. async launchApp ({ app_name, args, pseudonym, file_paths }, { ipc_context, msg_id } = {}) { - console.log('launchApp', { app_name, args, pseudonym, file_paths }); const app = ipc_context?.caller?.app; const process = ipc_context?.caller?.process; @@ -85,7 +84,6 @@ export class ExecService extends Service { // Check if file_paths are provided and caller has godmode permissions if (file_paths && Array.isArray(file_paths) && file_paths.length > 0 && process) { - console.log('file_paths', file_paths); try { // Get caller app info to check godmode status const caller_app_name = process.name; From 1bc8de8112a7711db84a12f22aa694e23ddb161f Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sun, 3 Aug 2025 17:52:14 -0700 Subject: [PATCH 25/46] make workers sortable by file path --- src/dev-center/css/style.css | 4 ++++ src/dev-center/js/workers.js | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index 7c0679093..8944e5b5e 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -1376,4 +1376,8 @@ dialog{ .root-dir-name{ cursor: pointer; +} + +.worker-file-path{ + cursor: pointer; } \ No newline at end of file diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index 5b5592b81..f8b585e8f 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -164,9 +164,10 @@ function sort_workers() { return a[sortBy].localeCompare(b[sortBy]); }else if(sortBy === 'created_at'){ return new Date(a[sortBy]) - new Date(b[sortBy]); - } else if(sortBy === 'user_count' || sortBy === 'open_count'){ - return a.stats[sortBy] - b.stats[sortBy]; - }else{ + }else if(sortBy === 'file_path'){ + return a[sortBy].localeCompare(b[sortBy]); + } + else{ a[sortBy] > b[sortBy] ? 1 : -1 } }); @@ -176,9 +177,9 @@ function sort_workers() { return b[sortBy].localeCompare(a[sortBy]); }else if(sortBy === 'created_at'){ return new Date(b[sortBy]) - new Date(a[sortBy]); - } else if(sortBy === 'user_count' || sortBy === 'open_count'){ - return b.stats[sortBy] - a.stats[sortBy]; - }else{ + }else if(sortBy === 'file_path'){ + return b[sortBy].localeCompare(a[sortBy]); + } else{ b[sortBy] > a[sortBy] ? 1 : -1 } }); @@ -220,7 +221,7 @@ function generate_worker_card(worker) { - + @@ -374,12 +375,12 @@ async function attempt_delete_worker(worker_name) { } } -$(document).on('click', '.file-path', function (e) { +$(document).on('click', '.worker-file-path', function (e) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); - const file_path = $(this).attr('data-file-path'); + const file_path = $(this).attr('data-worker-file-path'); if(file_path){ puter.ui.launchApp({ From 2691818f6eb78fc2a5ec7f535a3cb6d15d32ed3c Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sun, 3 Aug 2025 18:16:08 -0700 Subject: [PATCH 26/46] Update index.html --- src/dev-center/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dev-center/index.html b/src/dev-center/index.html index e38a34d18..06ba22aed 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -269,7 +269,7 @@

My Workers

- +
@@ -285,7 +285,7 @@
- + From 3df0bac9dc7dfc230e2d5b4e32ad563b606d7ab4 Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sun, 3 Aug 2025 18:39:14 -0700 Subject: [PATCH 27/46] count websites and workers at start --- src/dev-center/js/websites.js | 9 +++++---- src/dev-center/js/workers.js | 7 +++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index ba66153e6..d67685551 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -40,6 +40,10 @@ window.refresh_websites_list = async (show_loading = false) => { async function init_websites() { + puter.hosting.list().then((websites) => { + window.websites = websites; + count_websites(); + }); } $(document).on('click', '.create-a-website-btn', async function (e) { @@ -173,10 +177,7 @@ function sort_websites() { } function count_websites() { - let count = 0; - $('.website-card').each(function () { - count++; - }) + let count = window.websites.length; $('.website-count').html(count ? count : ''); return count; } diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index f8b585e8f..e7fe0b0b4 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -46,6 +46,8 @@ window.refresh_worker_list = async (show_loading = false) => { async function init_workers() { + window.workers = await puter.workers.list(); + count_workers(); } $(document).on('click', '.create-a-worker-btn', async function (e) { @@ -206,10 +208,7 @@ function sort_workers() { } function count_workers() { - let count = 0; - $('.worker-card').each(function () { - count++; - }) + let count = window.workers.length; $('.worker-count').html(count ? count : ''); return count; } From 59c297db49ef1525d4f787033edfa4ca25c19dd6 Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sun, 3 Aug 2025 19:06:45 -0700 Subject: [PATCH 28/46] search is now isolated to the active tab --- src/dev-center/index.html | 12 +++++----- src/dev-center/js/apps.js | 32 ++++++++++++++++----------- src/dev-center/js/dev-center.js | 39 +++++++++++++++++++++++++++++++++ src/dev-center/js/websites.js | 30 +++++++++++++++++++------ src/dev-center/js/workers.js | 29 +++++++++++++++++------- 5 files changed, 108 insertions(+), 34 deletions(-) diff --git a/src/dev-center/index.html b/src/dev-center/index.html index 06ba22aed..a5a91224f 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -237,8 +237,8 @@
- - + +
@@ -273,8 +273,8 @@
- - + +
@@ -306,8 +306,8 @@
- - + +
diff --git a/src/dev-center/js/apps.js b/src/dev-center/js/apps.js index 139250fee..a0da57602 100644 --- a/src/dev-center/js/apps.js +++ b/src/dev-center/js/apps.js @@ -2114,26 +2114,30 @@ $('.refresh-app-list').on('click', function (e) { }) }) -$(document).on('click', '.search', function (e) { +$(document).on('click', '.search-apps', function (e) { e.stopPropagation(); e.preventDefault(); // don't let click bubble up to window e.stopImmediatePropagation(); }) -$(document).on('input change keyup keypress keydown paste cut', '.search', function (e) { +$(document).on('input change keyup keypress keydown paste cut', '.search-apps', function (e) { + search_apps(); +}) + +window.search_apps = function() { // search apps for query - search_query = $(this).val().toLowerCase(); + search_query = $('.search-apps').val().toLowerCase(); if (search_query === '') { // hide 'clear search' button - $('.search-clear').hide(); + $('.search-clear-apps').hide(); // show all apps again $(`.app-card`).show(); // remove 'has-value' class from search input - $('.search').removeClass('has-value'); + $('.search-apps').removeClass('has-value'); } else { // show 'clear search' button - $('.search-clear').show(); + $('.search-clear-apps').show(); // show apps that match search_query and hide apps that don't apps.forEach((app) => { if ( @@ -2149,15 +2153,17 @@ $(document).on('input change keyup keypress keydown paste cut', '.search', funct } }) // add 'has-value' class to search input - $('.search').addClass('has-value'); - } -}) + $('.search-apps').addClass('has-value'); + } +} -$(document).on('click', '.search-clear', function (e) { - $('.search').val(''); - $('.search').trigger('change'); - $('.search').focus(); +$(document).on('click', '.search-clear-apps', function (e) { + $('.search-apps').val(''); + $('.search-apps').trigger('change'); + $('.search-apps').focus(); search_query = ''; + // remove 'has-value' class from search input + $('.search-apps').removeClass('has-value'); }) $(document).on('change', '.app-checkbox', function (e) { diff --git a/src/dev-center/js/dev-center.js b/src/dev-center/js/dev-center.js index 9c3d729b9..fa9ab8047 100644 --- a/src/dev-center/js/dev-center.js +++ b/src/dev-center/js/dev-center.js @@ -106,6 +106,8 @@ $(document).on('click', '.tab-btn', async function (e) { if ($(this).attr('data-tab') === 'apps') { refresh_app_list(); activeTab = 'apps'; + // Reset apps search when tab is activated + resetAppsSearch(); } // --------------------------------------------------------------- // Workers tab @@ -113,6 +115,8 @@ $(document).on('click', '.tab-btn', async function (e) { else if ($(this).attr('data-tab') === 'workers') { refresh_worker_list(); activeTab = 'workers'; + // Reset workers search when tab is activated + resetWorkersSearch(); } // --------------------------------------------------------------- // Websites tab @@ -120,6 +124,8 @@ $(document).on('click', '.tab-btn', async function (e) { else if ($(this).attr('data-tab') === 'websites') { refresh_websites_list(); activeTab = 'websites'; + // Reset websites search when tab is activated + resetWebsitesSearch(); } // --------------------------------------------------------------- // Payout Method tab @@ -439,6 +445,39 @@ $(document).on('click', '.sidebar-toggle', function (e) { $('body').toggleClass('sidebar-open'); }) +// --------------------------------------------------------------- +// Search Reset Functions +// --------------------------------------------------------------- +window.resetAppsSearch = () => { + $('.search-apps').val(''); + $('.search-clear-apps').hide(); + $('.search-apps').removeClass('has-value'); + // Reset search query in apps.js scope if search_apps function is available + if (typeof search_apps === 'function') { + search_apps(); + } +} + +window.resetWorkersSearch = () => { + $('.search-workers').val(''); + $('.search-clear-workers').hide(); + $('.search-workers').removeClass('has-value'); + // Reset search query in workers.js scope if search_workers function is available + if (typeof search_workers === 'function') { + search_workers(); + } +} + +window.resetWebsitesSearch = () => { + $('.search-websites').val(''); + $('.search-clear-websites').hide(); + $('.search-websites').removeClass('has-value'); + // Reset search query in websites.js scope if search_websites function is available + if (typeof search_websites === 'function') { + search_websites(); + } +} + window.activate_tippy = () => { tippy('.tippy', { content(reference) { diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index d67685551..d8338c8a2 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -196,18 +196,22 @@ function generate_website_card(website) { `; } -$(document).on('input change keyup keypress keydown paste cut', '.search', function (e) { - // search apps for query - search_query = $(this).val().toLowerCase(); +$(document).on('input change keyup keypress keydown paste cut', '.search-websites', function (e) { + search_websites(); +}) + +window.search_websites = function() { + // search websites for query + search_query = $('.search-websites').val().toLowerCase(); if (search_query === '') { // hide 'clear search' button - $('.search-clear').hide(); - // show all apps again + $('.search-clear-websites').hide(); + // show all websites again $(`.website-card`).show(); } else { // show 'clear search' button - $('.search-clear').show(); - // show apps that match search_query and hide apps that don't + $('.search-clear-websites').show(); + // show websites that match search_query and hide websites that don't websites.forEach((website) => { if ( website.subdomain.toLowerCase().includes(search_query.toLowerCase()) || @@ -219,7 +223,19 @@ $(document).on('input change keyup keypress keydown paste cut', '.search', funct $(`.website-card[data-name="${website.subdomain}"]`).hide(); } }) + + // add 'has-value' class to search input + $('.search-websites').addClass('has-value'); } +} + +$(document).on('click', '.search-clear-websites', function (e) { + $('.search-websites').val(''); + $('.search-websites').trigger('change'); + $('.search-websites').focus(); + search_query = ''; + // remove 'has-value' class from search input + $('.search-websites').removeClass('has-value'); }) $(document).on('click', '.delete-websites-btn', async function (e) { diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index e7fe0b0b4..053a5cb35 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -227,18 +227,22 @@ function generate_worker_card(worker) { `; } -$(document).on('input change keyup keypress keydown paste cut', '.search', function (e) { - // search apps for query - search_query = $(this).val().toLowerCase(); +$(document).on('input change keyup keypress keydown paste cut', '.search-workers', function (e) { + search_workers(); +}) + +window.search_workers = function() { + // search workers for query + search_query = $('.search-workers').val().toLowerCase(); if (search_query === '') { // hide 'clear search' button - $('.search-clear').hide(); - // show all apps again + $('.search-clear-workers').hide(); + // show all workers again $(`.worker-card`).show(); } else { // show 'clear search' button - $('.search-clear').show(); - // show apps that match search_query and hide apps that don't + $('.search-clear-workers').show(); + // show workers that match search_query and hide workers that don't workers.forEach((worker) => { if ( worker.name.toLowerCase().includes(search_query.toLowerCase()) @@ -249,7 +253,16 @@ $(document).on('input change keyup keypress keydown paste cut', '.search', funct $(`.worker-card[data-name="${worker.name}"]`).hide(); } }) - } + } +} + +$(document).on('click', '.search-clear-workers', function (e) { + $('.search-workers').val(''); + $('.search-workers').trigger('change'); + $('.search-workers').focus(); + search_query = ''; + // remove 'has-value' class from search input + $('.search-workers').removeClass('has-value'); }) $(document).on('click', '.delete-workers-btn', async function (e) { From b59b19f8b3c2f712342fc81522926ad50ef5b3b0 Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sun, 3 Aug 2025 19:25:18 -0700 Subject: [PATCH 29/46] Creating workers or websites start with selecting a file/folder --- src/dev-center/js/websites.js | 30 ++++++++++++++++++++++++------ src/dev-center/js/workers.js | 31 ++++++++++++++++++++++++++----- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index d8338c8a2..85c3fdd10 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -3,10 +3,14 @@ let sortDirection = 'desc'; window.websites = []; let search_query; -window.create_website = async (name) => { +window.create_website = async (name, directoryPath = null) => { let website + + // Use provided directory path or default to the default website file + const websiteDir = directoryPath || window.default_website_file; + try { - website = await puter.hosting.create(name, window.default_website_file); + website = await puter.hosting.create(name, websiteDir); } catch (error) { puter.ui.alert(`Error creating website: ${error.error.message}`); } @@ -47,11 +51,25 @@ async function init_websites() { } $(document).on('click', '.create-a-website-btn', async function (e) { - let name = await puter.ui.prompt('Please enter a name for your website:', 'my-awesome-website'); + // Step 1: Show directory picker + let selectedDirectory; + try { + selectedDirectory = await puter.ui.showDirectoryPicker(); + } catch (err) { + // User cancelled directory picker or there was an error + console.log('Directory picker cancelled or error:', err); + return; + } - if (name) { - await create_website(name); - refresh_websites_list(); + // Step 2: Ask for website name + if (selectedDirectory && selectedDirectory.path) { + let name = await puter.ui.prompt('Please enter a name for your website:', 'my-awesome-website'); + + // Step 3: Create website with selected directory + if (name) { + await create_website(name, selectedDirectory.path); + refresh_websites_list(); + } } }) diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index 053a5cb35..5a51a4907 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -3,11 +3,14 @@ let sortDirection = 'desc'; window.workers = []; let search_query; -window.create_worker = async (name) => { +window.create_worker = async (name, filePath = null) => { let worker; + // Use provided file path or default to the default worker file + const workerFile = filePath || window.default_worker_file; + try { - worker = await puter.workers.create(name, window.default_worker_file); + worker = await puter.workers.create(name, workerFile); } catch (err) { console.log(err); console.error('Error creating worker:', err.error); @@ -51,10 +54,28 @@ async function init_workers() { } $(document).on('click', '.create-a-worker-btn', async function (e) { - let name = await puter.ui.prompt('Please enter a name for your worker:', 'my-awesome-worker'); + // Step 1: Show file picker limited to .js files + let selectedFile; + try { + selectedFile = await puter.ui.showOpenFilePicker({ + accept: ".js", + }); + } catch (err) { + // User cancelled file picker or there was an error + console.log('File picker cancelled or error:', err); + return; + } - if (name) { - await create_worker(name); + // Step 2: Ask for worker name + if (selectedFile && selectedFile.path) { + let name = await puter.ui.prompt('Please enter a name for your worker:', 'my-awesome-worker'); + + // Step 3: Create worker with selected file + if (name) { + await create_worker(name, selectedFile.path); + // Refresh the worker list to show the new worker + await refresh_worker_list(); + } } }) From a340c9a0b756a6402ff8097e17ef235ce3020067 Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sun, 3 Aug 2025 19:47:45 -0700 Subject: [PATCH 30/46] fix sorting in websites tab --- src/dev-center/js/websites.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index 85c3fdd10..748aa305b 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -151,25 +151,33 @@ function sort_websites() { if (sortDirection === 'asc'){ sorted_websites = websites.sort((a, b) => { if(sortBy === 'name'){ - return a[sortBy].localeCompare(b[sortBy]); + return a.subdomain.localeCompare(b.subdomain); }else if(sortBy === 'created_at'){ return new Date(a[sortBy]) - new Date(b[sortBy]); } else if(sortBy === 'user_count' || sortBy === 'open_count'){ return a.stats[sortBy] - b.stats[sortBy]; + } else if(sortBy === 'root_dir'){ + const aRootDir = a.root_dir?.name || ''; + const bRootDir = b.root_dir?.name || ''; + return aRootDir.localeCompare(bRootDir); }else{ - a[sortBy] > b[sortBy] ? 1 : -1 + return a[sortBy] > b[sortBy] ? 1 : -1; } }); }else{ sorted_websites = websites.sort((a, b) => { if(sortBy === 'name'){ - return b[sortBy].localeCompare(a[sortBy]); + return b.subdomain.localeCompare(a.subdomain); }else if(sortBy === 'created_at'){ return new Date(b[sortBy]) - new Date(a[sortBy]); } else if(sortBy === 'user_count' || sortBy === 'open_count'){ return b.stats[sortBy] - a.stats[sortBy]; + } else if(sortBy === 'root_dir'){ + const aRootDir = a.root_dir?.name || ''; + const bRootDir = b.root_dir?.name || ''; + return bRootDir.localeCompare(aRootDir); }else{ - b[sortBy] > a[sortBy] ? 1 : -1 + return b[sortBy] > a[sortBy] ? 1 : -1; } }); } From 1d1b484bca8f4f430270a09724273e4b3fa8cec3 Mon Sep 17 00:00:00 2001 From: jelveh Date: Mon, 4 Aug 2025 00:04:11 -0700 Subject: [PATCH 31/46] Update workers.js --- src/dev-center/js/workers.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index 5a51a4907..adba39b3e 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -12,8 +12,7 @@ window.create_worker = async (name, filePath = null) => { try { worker = await puter.workers.create(name, workerFile); } catch (err) { - console.log(err); - console.error('Error creating worker:', err.error); + puter.ui.alert(`Error creating worker: ${err.error?.message}`); } return worker; From dc8b7ecb34a34184bac5a41875387525d2ca93e8 Mon Sep 17 00:00:00 2001 From: jelveh Date: Mon, 4 Aug 2025 13:09:50 -0700 Subject: [PATCH 32/46] update app count when a new app is created --- src/dev-center/index.html | 1 + src/dev-center/js/apps.js | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/dev-center/index.html b/src/dev-center/index.html index a5a91224f..1af4219dd 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -333,6 +333,7 @@ + diff --git a/src/dev-center/js/apps.js b/src/dev-center/js/apps.js index a0da57602..f107e1518 100644 --- a/src/dev-center/js/apps.js +++ b/src/dev-center/js/apps.js @@ -259,6 +259,11 @@ async function create_app(title, source_path = null, items = null) { appUID: app.uid, } ) + //---------------------------------------------------- + // Increment app count + //---------------------------------------------------- + $('.app-count').html(parseInt($('.app-count').html() ?? 0) + 1); + }).catch(async (err) => { $('#create-app-error').show(); $('#create-app-error').html(err.message); @@ -1308,7 +1313,7 @@ function count_apps() { $('.app-card').each(function () { count++; }) - $('.app-count').html(count); + $('.app-count').html(count ? count : ''); return count; } From 39bd8258667c2591f5f8029c599566244d4335c1 Mon Sep 17 00:00:00 2001 From: jelveh Date: Mon, 4 Aug 2025 13:24:36 -0700 Subject: [PATCH 33/46] fix search functionality by managing 'has-value' class for input fields in websites and workers search --- src/dev-center/js/websites.js | 2 ++ src/dev-center/js/workers.js | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index 748aa305b..f75298265 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -234,6 +234,8 @@ window.search_websites = function() { $('.search-clear-websites').hide(); // show all websites again $(`.website-card`).show(); + // remove 'has-value' class from search input + $('.search-websites').removeClass('has-value'); } else { // show 'clear search' button $('.search-clear-websites').show(); diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index adba39b3e..470b4cf70 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -259,6 +259,8 @@ window.search_workers = function() { $('.search-clear-workers').hide(); // show all workers again $(`.worker-card`).show(); + // remove 'has-value' class from search input + $('.search-workers').removeClass('has-value'); } else { // show 'clear search' button $('.search-clear-workers').show(); @@ -273,6 +275,8 @@ window.search_workers = function() { $(`.worker-card[data-name="${worker.name}"]`).hide(); } }) + // add 'has-value' class to search input + $('.search-workers').addClass('has-value'); } } From ab8cdb2d57b3a7b1b27808634079cae12574df5e Mon Sep 17 00:00:00 2001 From: jelveh Date: Mon, 4 Aug 2025 14:11:55 -0700 Subject: [PATCH 34/46] Add `requestEmailConfirmation` to `puter.ui` --- src/dev-center/js/workers.js | 8 ++++++ src/gui/src/IPC.js | 51 ++++++++++++++++++++++++++++++++++ src/puter-js/src/modules/UI.js | 6 ++++ 3 files changed, 65 insertions(+) diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index 470b4cf70..e88249ce7 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -53,6 +53,14 @@ async function init_workers() { } $(document).on('click', '.create-a-worker-btn', async function (e) { + // if user doesn't have an email, request it + if(!window.user?.email || !window.user?.email_confirmed){ + const email_confirm_resp = await puter.ui.requestEmailConfirmation(); + if(!email_confirm_resp) + UIAlert('Email confirmation required to create a worker.'); + return; + } + // Step 1: Show file picker limited to .js files let selectedFile; try { diff --git a/src/gui/src/IPC.js b/src/gui/src/IPC.js index 4ba4e484e..1b531b8e7 100644 --- a/src/gui/src/IPC.js +++ b/src/gui/src/IPC.js @@ -32,6 +32,8 @@ import update_mouse_position from './helpers/update_mouse_position.js'; import item_icon from './helpers/item_icon.js'; import UIPopover from './UI/UIPopover.js'; import socialLink from './helpers/socialLink.js'; +import UIWindowEmailConfirmationRequired from './UI/UIWindowEmailConfirmationRequired.js'; +import UIWindowSaveAccount from './UI/UIWindowSaveAccount.js'; import { PROCESS_IPC_ATTACHED } from './definitions.js'; @@ -157,6 +159,55 @@ const ipc_listener = async (event, handled) => { // TODO: Respond to this } //-------------------------------------------------------- + // requestEmailConfirmation + //-------------------------------------------------------- + else if(event.data.msg === 'requestEmailConfirmation'){ + // If the user has an email and it is confirmed, respond with success + if(window.user.email && window.user.email_confirmed){ + target_iframe.contentWindow.postMessage({ + original_msg_id: msg_id, + msg: 'requestEmailConfirmationResponded', + response: true, + }, '*'); + } + + // If the user is a temporary user, show the save account window + if(window.user.is_temp && + !await UIWindowSaveAccount({ + send_confirmation_code: true, + message: 'Please create an account to proceed.', + window_options: { + backdrop: true, + close_on_backdrop_click: false, + } + })){ + target_iframe.contentWindow.postMessage({ + original_msg_id: msg_id, + msg: 'requestEmailConfirmationResponded', + response: false, + }, '*'); + return; + } + else if(!window.user.email_confirmed && !await UIWindowEmailConfirmationRequired()){ + target_iframe.contentWindow.postMessage({ + original_msg_id: msg_id, + msg: 'requestEmailConfirmationResponded', + response: false, + }, '*'); + return; + } + + const email_confirm_resp = await UIWindowEmailConfirmationRequired({ + email: window.user.email, + }); + + target_iframe.contentWindow.postMessage({ + original_msg_id: msg_id, + msg: 'requestEmailConfirmationResponded', + response: email_confirm_resp, + }, '*'); + } + //-------------------------------------------------------- // ALERT //-------------------------------------------------------- else if(event.data.msg === 'ALERT' && event.data.message !== undefined){ diff --git a/src/puter-js/src/modules/UI.js b/src/puter-js/src/modules/UI.js index b32c7239e..ade763ff5 100644 --- a/src/puter-js/src/modules/UI.js +++ b/src/puter-js/src/modules/UI.js @@ -629,6 +629,12 @@ class UI extends EventListener { this.#onLaunchedWithItems = callback; } + requestEmailConfirmation = function() { + return new Promise((resolve, reject) => { + this.#postMessageWithCallback('requestEmailConfirmation', resolve, { }); + }); + } + alert = function(message, buttons, options, callback) { return new Promise((resolve) => { this.#postMessageWithCallback('ALERT', resolve, { message, buttons, options }); From 0d6423a8b1c87575ee1f43b6cc0308fc10bc8d2c Mon Sep 17 00:00:00 2001 From: jelveh Date: Mon, 4 Aug 2025 14:35:28 -0700 Subject: [PATCH 35/46] Enforce email confirmation via GUI for deploying workers --- src/gui/src/UI/UIItem.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/gui/src/UI/UIItem.js b/src/gui/src/UI/UIItem.js index 48dfd3341..78b48b5b5 100644 --- a/src/gui/src/UI/UIItem.js +++ b/src/gui/src/UI/UIItem.js @@ -1145,6 +1145,19 @@ function UIItem(options){ menu_items.push({ html: i18n('publish_as_serverless_worker'), onClick: async function(){ + if(window.user.is_temp && + !await UIWindowSaveAccount({ + send_confirmation_code: true, + message: 'Please create an account to proceed.', + window_options: { + backdrop: true, + close_on_backdrop_click: false, + } + })) + return; + else if(!window.user.email_confirmed && !await UIWindowEmailConfirmationRequired()) + return; + UIWindowPublishWorker(options.uid, $(el_item).attr('data-name'), $(el_item).attr('data-path')); } }); From a6db0f1092ef01192fd236cd7215d72c0f1e5733 Mon Sep 17 00:00:00 2001 From: jelveh Date: Mon, 4 Aug 2025 15:20:37 -0700 Subject: [PATCH 36/46] Update style.css --- src/gui/src/css/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/src/css/style.css b/src/gui/src/css/style.css index 24bff3f2b..0c49e8dca 100644 --- a/src/gui/src/css/style.css +++ b/src/gui/src/css/style.css @@ -2591,7 +2591,7 @@ label { .taskbar.taskbar-position-right .taskbar-item[data-app="separator"]{ max-height: 10px; min-height: 10px !important; - padding: 5px !important; + padding: 5px 3px 5px 7px !important; } .taskbar.taskbar-position-left .taskbar-item[data-app="separator"] .taskbar-icon, .taskbar.taskbar-position-right .taskbar-item[data-app="separator"] .taskbar-icon{ From 9f8c0d322720fbc99060aacc45504af0385be6d3 Mon Sep 17 00:00:00 2001 From: jelveh Date: Mon, 4 Aug 2025 15:25:14 -0700 Subject: [PATCH 37/46] Update index.html --- src/dev-center/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dev-center/index.html b/src/dev-center/index.html index 1af4219dd..65a2e0c9e 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -188,7 +188,7 @@ @@ -197,7 +197,7 @@ From b6fcb78f6e64fd064675be34eaab3a5eef884465 Mon Sep 17 00:00:00 2001 From: jelveh Date: Mon, 4 Aug 2025 22:08:32 -0700 Subject: [PATCH 38/46] wip --- src/dev-center/js/dev-center.js | 5 ++++- src/dev-center/js/workers.js | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/dev-center/js/dev-center.js b/src/dev-center/js/dev-center.js index fa9ab8047..5784d99c7 100644 --- a/src/dev-center/js/dev-center.js +++ b/src/dev-center/js/dev-center.js @@ -27,10 +27,11 @@ window.auth_username = null; window.dev_center_uid = puter.appID; window.developer; window.activeTab = 'apps'; +window.user = null; // auth_username (async () => { - let user = await puter.auth.getUser(); + window.user = await puter.auth.getUser(); if (user?.username) { window.auth_username = user.username; @@ -198,6 +199,8 @@ $('.jip-submit-btn').on('click', async function (e) { $('.jip-submit-btn').prop('disabled', false); // update dev profile $('#payout-method-email').html(paypal); + // show separator + $('.tab-btn-separator').show(); // show payout method tab $('.tab-btn[data-tab="payout-method"]').show(); }, diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index e88249ce7..a03565865 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -61,6 +61,9 @@ $(document).on('click', '.create-a-worker-btn', async function (e) { return; } + // refresh user data + window.user = await puter.auth.getUser(); + // Step 1: Show file picker limited to .js files let selectedFile; try { From 59dade2736028c2ab3a8c8e6508df3033ad045c2 Mon Sep 17 00:00:00 2001 From: jelveh Date: Mon, 4 Aug 2025 22:18:22 -0700 Subject: [PATCH 39/46] fix the bug with directory picker selecting the current directory --- src/gui/src/UI/UIWindow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/src/UI/UIWindow.js b/src/gui/src/UI/UIWindow.js index 46e311206..e7627d033 100644 --- a/src/gui/src/UI/UIWindow.js +++ b/src/gui/src/UI/UIWindow.js @@ -824,7 +824,7 @@ async function UIWindow(options) { // No item selected, return current directory // ------------------------------------------------ if(selected_els.length === 0){ - selected_dirs = await puter.fs.sign(options.initiating_app_uuid, {uid: $(el_window).attr('data-uid'), action: 'write'}) + selected_dirs = await puter.fs.sign(options.initiating_app_uuid, {uid: $(el_window).attr('data-uid'), action: 'write', path: $(el_window).attr('data-path')}) selected_dirs = selected_dirs.items; } From 162aa485d09029e478326165267076f1504ac26d Mon Sep 17 00:00:00 2001 From: jelveh Date: Mon, 4 Aug 2025 22:27:07 -0700 Subject: [PATCH 40/46] Add directory change functionality for websites and update UI alert handling --- src/dev-center/index.html | 3 +- src/dev-center/js/websites.js | 71 +++++++++++++++++++++++++++++++++++ src/gui/src/UI/UIAlert.js | 4 ++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/dev-center/index.html b/src/dev-center/index.html index 65a2e0c9e..a5118890e 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -85,6 +85,7 @@
  • Apps
  • Websites
  • Workers
  • +
    @@ -332,7 +333,7 @@ - + diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index f75298265..77e18a178 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -337,6 +337,12 @@ $(document).on('click', '.options-icon-website', function (e) { e.stopImmediatePropagation(); puter.ui.contextMenu({ items: [ + { + label: 'Change Directory', + action: () => { + change_website_directory($(this).attr('data-website-name')); + }, + }, { label: 'Delete', type: 'danger', @@ -382,6 +388,71 @@ async function attempt_delete_website(website_name) { } } +async function change_website_directory(website_name) { + try { + // Step 1: Show directory picker + const selectedDirectory = await puter.ui.showDirectoryPicker(); + + if (!selectedDirectory || !selectedDirectory.path) { + return; // User cancelled + } + + // Step 2: Confirm the change since it will replace the current website + const confirmResp = await puter.ui.alert( + `Are you sure you want to change the directory for ${html_encode(website_name)}.puter.site?

    This will update the website to serve files from the new directory.`, + [ + { + label: 'Yes, change directory', + value: 'change', + type: 'primary', + }, + { + label: 'Cancel' + }, + ], + { + type: 'info', + } + ); + + if (confirmResp !== 'change') { + return; + } + + // Step 3: Show loading spinner + puter.ui.showSpinner(); + + try { + // Step 4: Delete the existing website + await puter.hosting.delete(website_name); + + // Step 5: Create a new website with the same name but new directory + await puter.hosting.create(website_name, selectedDirectory.path); + + // Step 6: Refresh the websites list to show the updated directory + await refresh_websites_list(); + + // Step 7: Show success message + puter.ui.alert(`Website directory changed successfully! ${html_encode(website_name)}.puter.site now serves files from the new directory.`, [], { + type: 'success', + }); + + } catch (error) { + // If there's an error, show error message + puter.ui.alert(`Error changing website directory: ${error.error?.message || error.message || 'Unknown error'}`, [], { + type: 'error', + }); + } finally { + // Hide loading spinner + puter.ui.hideSpinner(); + } + + } catch (error) { + // Handle directory picker error + console.log('Directory picker cancelled or error:', error); + } +} + $(document).on('click', '.root-dir-name', function (e) { e.preventDefault(); e.stopPropagation(); diff --git a/src/gui/src/UI/UIAlert.js b/src/gui/src/UI/UIAlert.js index 2ab565ae7..1aa683cbb 100644 --- a/src/gui/src/UI/UIAlert.js +++ b/src/gui/src/UI/UIAlert.js @@ -97,6 +97,10 @@ function UIAlert(options) { santized_message = santized_message.replace(/<p>/g, '

    '); santized_message = santized_message.replace(/<\/p>/g, '

    '); + // replace sanitized
    with
    + santized_message = santized_message.replace(/<br>/g, '
    '); + santized_message = santized_message.replace(/<\/br>/g, '
    '); + let h = ''; // icon h += ``; From 90f1232b870f8034af86902dc3ff1e94d4017b71 Mon Sep 17 00:00:00 2001 From: jelveh Date: Mon, 4 Aug 2025 22:47:17 -0700 Subject: [PATCH 41/46] Refactor dropped items handling in dev-center.js to use window scope and improve UI updates; minor UI adjustment in websites.js and handle empty file paths in workers.js --- src/dev-center/js/dev-center.js | 16 ++++++++-------- src/dev-center/js/websites.js | 1 + src/dev-center/js/workers.js | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/dev-center/js/dev-center.js b/src/dev-center/js/dev-center.js index 5784d99c7..37a61c8fe 100644 --- a/src/dev-center/js/dev-center.js +++ b/src/dev-center/js/dev-center.js @@ -351,14 +351,14 @@ $('body').on('drop', async function (event) { // retrieve puter items from the event if (event.detail?.items?.length > 0) { - dropped_items = event.detail.items; - source_path = dropped_items[0].path; + window.dropped_items = event.detail.items; + window.source_path = window.dropped_items[0].path; // by deploying an existing Puter folder. So we create the app and deploy it. - if (source_path) { + if (window.source_path) { // todo if there are no apps, go straight to creating a new app $('.insta-deploy-modal').get(0).showModal(); // set item name - $('.insta-deploy-item-name').html(html_encode(dropped_items[0].name)); + $('.insta-deploy-item-name').html(html_encode(window.dropped_items[0].name)); } } //----------------------------------------------------------------------------- @@ -369,18 +369,18 @@ $('body').on('drop', async function (event) { return; // Get dropped items - dropped_items = await puter.ui.getEntriesFromDataTransferItems(e.dataTransfer.items); + window.dropped_items = await puter.ui.getEntriesFromDataTransferItems(e.dataTransfer.items); // Generate a flat array of full paths from the dropped items let paths = []; - for (let item of dropped_items) { + for (let item of window.dropped_items) { paths.push('/' + (item.fullPath ?? item.filepath)); } // Generate a directory tree from the paths let tree = generateDirTree(paths); - dropped_items = setRootDirTree(tree, dropped_items); + window.dropped_items = setRootDirTree(tree, window.dropped_items); // Alert if no index.html in root if (!hasRootIndexHtml(tree)) { @@ -391,7 +391,7 @@ $('body').on('drop', async function (event) { ]); $('.drop-area').removeClass('drop-area-ready-to-deploy'); $('.deploy-btn').addClass('disabled'); - dropped_items = []; + window.dropped_items = []; return; } diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index 77e18a178..16075dd3d 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -343,6 +343,7 @@ $(document).on('click', '.options-icon-website', function (e) { change_website_directory($(this).attr('data-website-name')); }, }, + '-', { label: 'Delete', type: 'danger', diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index a03565865..d8642417d 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -251,7 +251,7 @@ function generate_worker_card(worker) {
    - + From 636bf6b8b377820fb18b47cfbeafdd6616d67e31 Mon Sep 17 00:00:00 2001 From: jelveh Date: Mon, 4 Aug 2025 23:18:01 -0700 Subject: [PATCH 42/46] Implement shift-click functionality for checkbox selection in apps, websites, and workers; improve UI responsiveness by updating row activation based on checkbox state. --- src/dev-center/js/apps.js | 24 +++++++++++++++++++++++- src/dev-center/js/websites.js | 24 +++++++++++++++++++++++- src/dev-center/js/workers.js | 25 ++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/dev-center/js/apps.js b/src/dev-center/js/apps.js index f107e1518..eb0821c51 100644 --- a/src/dev-center/js/apps.js +++ b/src/dev-center/js/apps.js @@ -2171,7 +2171,26 @@ $(document).on('click', '.search-clear-apps', function (e) { $('.search-apps').removeClass('has-value'); }) -$(document).on('change', '.app-checkbox', function (e) { +$(document).on('click', '.app-checkbox', function (e) { + // was shift key pressed? + if (e.originalEvent && e.originalEvent.shiftKey) { + // select all checkboxes in range + const currentIndex = $('.app-checkbox').index(this); + const startIndex = Math.min(window.last_clicked_app_checkbox_index, currentIndex); + const endIndex = Math.max(window.last_clicked_app_checkbox_index, currentIndex); + + // set all checkboxes in range to the same state as current checkbox + for (let i = startIndex; i <= endIndex; i++) { + const checkbox = $('.app-checkbox').eq(i); + checkbox.prop('checked', $(this).is(':checked')); + // activate row + if ($(checkbox).is(':checked')) + $(checkbox).closest('tr').addClass('active'); + else + $(checkbox).closest('tr').removeClass('active'); + } + } + // determine if select-all checkbox should be checked, indeterminate, or unchecked if ($('.app-checkbox:checked').length === $('.app-checkbox').length) { $('.select-all-apps').prop('indeterminate', false); @@ -2196,6 +2215,9 @@ $(document).on('change', '.app-checkbox', function (e) { $('.delete-apps-btn').removeClass('disabled'); else $('.delete-apps-btn').addClass('disabled'); + + // store the index of the last clicked checkbox + window.last_clicked_app_checkbox_index = $('.app-checkbox').index(this); }) $(document).on('click', '.delete-apps-btn', async function (e) { diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index 16075dd3d..d83708fdf 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -73,7 +73,26 @@ $(document).on('click', '.create-a-website-btn', async function (e) { } }) -$(document).on('change', '.website-checkbox', function (e) { +$(document).on('click', '.website-checkbox', function (e) { + // was shift key pressed? + if (e.originalEvent && e.originalEvent.shiftKey) { + // select all checkboxes in range + const currentIndex = $('.website-checkbox').index(this); + const startIndex = Math.min(window.last_clicked_website_checkbox_index, currentIndex); + const endIndex = Math.max(window.last_clicked_website_checkbox_index, currentIndex); + + // set all checkboxes in range to the same state as current checkbox + for (let i = startIndex; i <= endIndex; i++) { + const checkbox = $('.website-checkbox').eq(i); + checkbox.prop('checked', $(this).is(':checked')); + // activate row + if ($(checkbox).is(':checked')) + $(checkbox).closest('tr').addClass('active'); + else + $(checkbox).closest('tr').removeClass('active'); + } + } + // determine if select-all checkbox should be checked, indeterminate, or unchecked if ($('.website-checkbox:checked').length === $('.website-checkbox').length) { $('.select-all-websites').prop('indeterminate', false); @@ -98,6 +117,9 @@ $(document).on('change', '.website-checkbox', function (e) { $('.delete-websites-btn').removeClass('disabled'); else $('.delete-websites-btn').addClass('disabled'); + + // store the index of the last clicked checkbox + window.last_clicked_website_checkbox_index = $('.website-checkbox').index(this); }) $(document).on('change', '.select-all-websites', function (e) { diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index d8642417d..e1a8a1bd5 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -116,7 +116,27 @@ return new Response(\`Page \${params.page} not found\`, {status: 404}); } -$(document).on('change', '.worker-checkbox', function (e) { +$(document).on('click', '.worker-checkbox', function (e) { + // was shift key pressed? + if (e.originalEvent && e.originalEvent.shiftKey) { + // select all checkboxes in range + const currentIndex = $('.worker-checkbox').index(this); + const startIndex = Math.min(window.last_clicked_worker_checkbox_index, currentIndex); + const endIndex = Math.max(window.last_clicked_worker_checkbox_index, currentIndex); + + // set all checkboxes in range to the same state as current checkbox + for (let i = startIndex; i <= endIndex; i++) { + const checkbox = $('.worker-checkbox').eq(i); + checkbox.prop('checked', $(this).is(':checked')); + + // activate row + if ($(checkbox).is(':checked')) + $(checkbox).closest('tr').addClass('active'); + else + $(checkbox).closest('tr').removeClass('active'); + } + } + // determine if select-all checkbox should be checked, indeterminate, or unchecked if ($('.worker-checkbox:checked').length === $('.worker-checkbox').length) { $('.select-all-workers').prop('indeterminate', false); @@ -141,6 +161,9 @@ $(document).on('change', '.worker-checkbox', function (e) { $('.delete-workers-btn').removeClass('disabled'); else $('.delete-workers-btn').addClass('disabled'); + + // store the index of the last clicked checkbox + window.last_clicked_worker_checkbox_index = $('.worker-checkbox').index(this); }) $(document).on('change', '.select-all-workers', function (e) { From 5ccdd627ee3b4cf492ab72c42f798c10009b77e9 Mon Sep 17 00:00:00 2001 From: jelveh Date: Tue, 5 Aug 2025 08:49:30 -0700 Subject: [PATCH 43/46] Add functionality to remove app, website, and worker cards with dedicated functions; streamline deletion process and improve UI updates for empty states. --- src/dev-center/index.html | 1 + src/dev-center/js/apps.js | 62 ++++++++++++------------ src/dev-center/js/dev-center.js | 3 -- src/dev-center/js/images.js | 7 +++ src/dev-center/js/websites.js | 61 +++++++++++++---------- src/dev-center/js/workers.js | 86 +++++++++++++-------------------- 6 files changed, 108 insertions(+), 112 deletions(-) create mode 100644 src/dev-center/js/images.js diff --git a/src/dev-center/index.html b/src/dev-center/index.html index a5118890e..9f1baffca 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -340,6 +340,7 @@ + diff --git a/src/dev-center/js/apps.js b/src/dev-center/js/apps.js index eb0821c51..278d8821a 100644 --- a/src/dev-center/js/apps.js +++ b/src/dev-center/js/apps.js @@ -25,15 +25,6 @@ const APP_CATEGORIES = [ { id: 'lifestyle', label: 'Lifestyle' }, ]; -const deploying_spinner = ``; -const loading_spinner = ``; -const drop_area_placeholder = `

    Drop your app folder and files here to deploy.

    HTML, JS, CSS, ...

    `; -const index_missing_error = `Please upload an 'index.html' file or if you're uploading a directory, make sure it contains an 'index.html' file at its root.`; -const lock_svg = ` `; -const lock_svg_tippy = ` `; - -const copy_svg = ` `; - async function init_apps() { setTimeout(async function () { puter.ui.onLaunchedWithItems(async function (items) { @@ -2220,6 +2211,35 @@ $(document).on('click', '.app-checkbox', function (e) { window.last_clicked_app_checkbox_index = $('.app-checkbox').index(this); }) +function remove_app_card(app_uid, callback = null) { + $(`.app-card[data-uid="${app_uid}"]`).fadeOut(200, function() { + $(this).remove(); + if ($(`.app-card`).length === 0) { + $('section:not(.sidebar)').hide(); + $('#no-apps-notice').show(); + } else { + $('section:not(.sidebar)').hide(); + $('#app-list').show(); + } + + // update select-all-apps checkbox's state + if($('.app-checkbox:checked').length === 0){ + $('.select-all-apps').prop('indeterminate', false); + $('.select-all-apps').prop('checked', false); + } + else if($('.app-checkbox:checked').length === $('.app-card').length){ + $('.select-all-apps').prop('indeterminate', false); + $('.select-all-apps').prop('checked', true); + } + else{ + $('.select-all-apps').prop('indeterminate', true); + } + + count_apps(); + if (callback) callback(); + }); +} + $(document).on('click', '.delete-apps-btn', async function (e) { // show confirmation alert let resp = await puter.ui.alert(`Are you sure you want to delete the selected apps?`, [ @@ -2289,17 +2309,7 @@ $(document).on('click', '.delete-apps-btn', async function (e) { await puter.apps.delete(app_name) // remove app card - $(`.app-card[data-uid="${app_uid}"]`).fadeOut(200, function name(params) { - $(this).remove(); - if ($(`.app-card`).length === 0) { - $('section:not(.sidebar)').hide(); - $('#no-apps-notice').show(); - } else { - $('section:not(.sidebar)').hide(); - $('#app-list').show(); - } - count_apps(); - }); + remove_app_card(app_uid); try{ // get app directory @@ -2721,17 +2731,7 @@ async function attempt_delete_app(app_name, app_title, app_uid) { ); if (alert_resp === 'delete') { - $(`.app-card[data-uid="${app_uid}"]`).fadeOut(200, function name(params) { - $(this).remove(); - if ($(`.app-card`).length === 0) { - $('section:not(.sidebar)').hide(); - $('#no-apps-notice').show(); - } else { - $('section:not(.sidebar)').hide(); - $('#app-list').show(); - } - count_apps(); - }); + remove_app_card(app_uid); // delete app puter.apps.delete(app_name).then(async (app) => { diff --git a/src/dev-center/js/dev-center.js b/src/dev-center/js/dev-center.js index 37a61c8fe..9f5784dab 100644 --- a/src/dev-center/js/dev-center.js +++ b/src/dev-center/js/dev-center.js @@ -79,9 +79,6 @@ $(document).ready(async function () { // initialize assets directory await initializeAssetsDirectory(); - // create default worker file - await createDefaultWorkerFile(); - puter.ui.showSpinner(); init_apps(); diff --git a/src/dev-center/js/images.js b/src/dev-center/js/images.js new file mode 100644 index 000000000..c027ddddb --- /dev/null +++ b/src/dev-center/js/images.js @@ -0,0 +1,7 @@ +window.deploying_spinner = ``; +window.loading_spinner = ``; +window.drop_area_placeholder = `

    Drop your app folder and files here to deploy.

    HTML, JS, CSS, ...

    `; +window.index_missing_error = `Please upload an 'index.html' file or if you're uploading a directory, make sure it contains an 'index.html' file at its root.`; +window.lock_svg = ` `; +window.lock_svg_tippy = ` `; +window.copy_svg = ` `; \ No newline at end of file diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js index d83708fdf..fd4ae9a05 100644 --- a/src/dev-center/js/websites.js +++ b/src/dev-center/js/websites.js @@ -4,7 +4,7 @@ window.websites = []; let search_query; window.create_website = async (name, directoryPath = null) => { - let website + let website; // Use provided directory path or default to the default website file const websiteDir = directoryPath || window.default_website_file; @@ -42,7 +42,6 @@ window.refresh_websites_list = async (show_loading = false) => { count_websites(); } - async function init_websites() { puter.hosting.list().then((websites) => { window.websites = websites; @@ -288,6 +287,35 @@ $(document).on('click', '.search-clear-websites', function (e) { $('.search-websites').removeClass('has-value'); }) +function remove_website_card(website_name, callback = null) { + $(`.website-card[data-name="${website_name}"]`).fadeOut(200, function() { + $(this).remove(); + if ($(`.website-card`).length === 0) { + $('section:not(.sidebar)').hide(); + $('#no-websites-notice').show(); + } else { + $('section:not(.sidebar)').hide(); + $('#website-list').show(); + } + + // update select-all-websites checkbox's state + if($('.website-checkbox:checked').length === 0){ + $('.select-all-websites').prop('indeterminate', false); + $('.select-all-websites').prop('checked', false); + } + else if($('.website-checkbox:checked').length === $('.website-card').length){ + $('.select-all-websites').prop('indeterminate', false); + $('.select-all-websites').prop('checked', true); + } + else{ + $('.select-all-websites').prop('indeterminate', true); + } + + count_websites(); + if (callback) callback(); + }); +} + $(document).on('click', '.delete-websites-btn', async function (e) { // show confirmation alert let resp = await puter.ui.alert(`Are you sure you want to delete the selected websites?`, [ @@ -320,17 +348,7 @@ $(document).on('click', '.delete-websites-btn', async function (e) { await puter.hosting.delete(website_name) // remove website card - $(`.website-card[data-name="${website_name}"]`).fadeOut(200, function name(params) { - $(this).remove(); - if ($(`.website-card`).length === 0) { - $('section:not(.sidebar)').hide(); - $('#no-websites-notice').show(); - } else { - $('section:not(.sidebar)').hide(); - $('#website-list').show(); - } - count_websites(); - }); + remove_website_card(website_name); try{ count_websites(); @@ -370,14 +388,14 @@ $(document).on('click', '.options-icon-website', function (e) { label: 'Delete', type: 'danger', action: () => { - attempt_delete_website($(this).attr('data-website-name')); + attempt_website_deletion($(this).attr('data-website-name')); }, }, ], }); }) -async function attempt_delete_website(website_name) { +async function attempt_website_deletion(website_name) { // confirm delete const alert_resp = await puter.ui.alert(`Are you sure you want to premanently delete ${html_encode(website_name)}.puter.site?`, [ @@ -394,17 +412,7 @@ async function attempt_delete_website(website_name) { if (alert_resp === 'delete') { // remove website card and update website count - $(`.website-card[data-name="${website_name}"]`).fadeOut(200, function name(params) { - $(this).remove(); - if ($(`.website-card`).length === 0) { - $('section:not(.sidebar)').hide(); - $('#no-websites-notice').show(); - } else { - $('section:not(.sidebar)').hide(); - $('#website-list').show(); - } - count_websites(); - }); + remove_website_card(website_name); // delete website puter.hosting.delete(website_name); @@ -489,4 +497,5 @@ $(document).on('click', '.root-dir-name', function (e) { }); } }) + export default init_websites; \ No newline at end of file diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js index e1a8a1bd5..9e32354f3 100644 --- a/src/dev-center/js/workers.js +++ b/src/dev-center/js/workers.js @@ -7,7 +7,7 @@ window.create_worker = async (name, filePath = null) => { let worker; // Use provided file path or default to the default worker file - const workerFile = filePath || window.default_worker_file; + const workerFile = filePath; try { worker = await puter.workers.create(name, workerFile); @@ -89,33 +89,6 @@ $(document).on('click', '.create-a-worker-btn', async function (e) { } }) -window.createDefaultWorkerFile = async () => { - window.default_worker_file = `/${auth_username}/AppData/${dev_center_uid}/default_worker_file.js`; - let existingFile; - try { - // Check if default_worker_file exists - existingFile = await puter.fs.read(default_worker_file); - } catch (err) { - console.error('Error creating default worker file:', err); - } - - if (!existingFile) { - // Create default_worker_file - await puter.fs.write(default_worker_file, `// This is an example application for Puter Workers - -router.get('/', ({request}) => { -return 'Hello World'; // returns a string -}); -router.get('/api/hello', ({request}) => { -return {'msg': 'hello'}; // returns a JSON object -}); -router.get('/*page', ({request, params}) => { -return new Response(\`Page \${params.page} not found\`, {status: 404}); -});`); - } - -} - $(document).on('click', '.worker-checkbox', function (e) { // was shift key pressed? if (e.originalEvent && e.originalEvent.shiftKey) { @@ -323,6 +296,35 @@ $(document).on('click', '.search-clear-workers', function (e) { $('.search-workers').removeClass('has-value'); }) +function remove_worker_card(worker_name, callback = null) { + $(`.worker-card[data-name="${worker_name}"]`).fadeOut(200, function() { + $(this).remove(); + if ($(`.worker-card`).length === 0) { + $('section:not(.sidebar)').hide(); + $('#no-workers-notice').show(); + } else { + $('section:not(.sidebar)').hide(); + $('#worker-list').show(); + } + + // update select-all-workers checkbox's state + if($('.worker-checkbox:checked').length === 0){ + $('.select-all-workers').prop('indeterminate', false); + $('.select-all-workers').prop('checked', false); + } + else if($('.worker-checkbox:checked').length === $('.worker-card').length){ + $('.select-all-workers').prop('indeterminate', false); + $('.select-all-workers').prop('checked', true); + } + else{ + $('.select-all-workers').prop('indeterminate', true); + } + + count_workers(); + if (callback) callback(); + }); +} + $(document).on('click', '.delete-workers-btn', async function (e) { // show confirmation alert let resp = await puter.ui.alert(`Are you sure you want to delete the selected workers?`, [ @@ -355,17 +357,7 @@ $(document).on('click', '.delete-workers-btn', async function (e) { await puter.workers.delete(worker_name) // remove worker card - $(`.worker-card[data-name="${worker_name}"]`).fadeOut(200, function name(params) { - $(this).remove(); - if ($(`.worker-card`).length === 0) { - $('section:not(.sidebar)').hide(); - $('#no-workers-notice').show(); - } else { - $('section:not(.sidebar)').hide(); - $('#worker-list').show(); - } - count_workers(); - }); + remove_worker_card(worker_name); try{ count_workers(); @@ -398,14 +390,14 @@ $(document).on('click', '.options-icon-worker', function (e) { label: 'Delete', type: 'danger', action: () => { - attempt_delete_worker($(this).attr('data-worker-name')); + attempt_worker_deletion($(this).attr('data-worker-name')); }, }, ], }); }) -async function attempt_delete_worker(worker_name) { +async function attempt_worker_deletion(worker_name) { // confirm delete const alert_resp = await puter.ui.alert(`Are you sure you want to premanently delete ${html_encode(worker_name)}?`, [ @@ -422,17 +414,7 @@ async function attempt_delete_worker(worker_name) { if (alert_resp === 'delete') { // remove worker card and update worker count - $(`.worker-card[data-name="${worker_name}"]`).fadeOut(200, function name(params) { - $(this).remove(); - if ($(`.worker-card`).length === 0) { - $('section:not(.sidebar)').hide(); - $('#no-workers-notice').show(); - } else { - $('section:not(.sidebar)').hide(); - $('#worker-list').show(); - } - count_workers(); - }); + remove_worker_card(worker_name); // delete worker puter.workers.delete(worker_name).then().catch(async (err) => { From 6bccf7dc7d4ab676db55d6cf629d2c3f2fb73494 Mon Sep 17 00:00:00 2001 From: jelveh Date: Tue, 5 Aug 2025 09:03:35 -0700 Subject: [PATCH 44/46] Update style.css --- src/dev-center/css/style.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index 8944e5b5e..81f13c82e 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -570,10 +570,11 @@ label { .link-to-docs{ color: #586373; font-size: 14px; -} -.link-to-docs, .link-to-docs:hover { text-decoration: none; } +.link-to-docs:hover { + text-decoration: underline !important; +} .link-to-docs img{ width: 12px; margin-bottom: -1px; From 64a5de3cc41d37b46abd18ecff416144346889f9 Mon Sep 17 00:00:00 2001 From: jelveh Date: Tue, 5 Aug 2025 09:09:41 -0700 Subject: [PATCH 45/46] fix dev docs link --- src/dev-center/css/style.css | 5 +++++ src/dev-center/index.html | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css index 81f13c82e..f80780c9a 100644 --- a/src/dev-center/css/style.css +++ b/src/dev-center/css/style.css @@ -578,6 +578,7 @@ label { .link-to-docs img{ width: 12px; margin-bottom: -1px; + margin-left: 5px; } .tab-btn { background-size: 20px; @@ -1381,4 +1382,8 @@ dialog{ .worker-file-path{ cursor: pointer; +} + +.tab-btn-separator{ + display: none; } \ No newline at end of file diff --git a/src/dev-center/index.html b/src/dev-center/index.html index 9f1baffca..fca93b091 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -92,7 +92,7 @@
    Website▼▲Connected Directory▼▲ Created▼▲
    + ${website.subdomain}.puter.site${website.root_dir ? website.root_dir.name : ''} ${website.created_at}
    ${website.subdomain}.puter.site${website.root_dir ? website.root_dir.name : ''} ${website.root_dir ? website.root_dir.name : ''} ${website.created_at}
    Worker▼▲File▼▲ Created▼▲
    ${worker.name}${worker.file_path} ${worker.created_at}
    ${worker.name}${worker.file_path}${worker.file_path} ${worker.created_at}
    Worker▼▲File▼▲File▼▲ Created▼▲
    ${worker.name}${worker.file_path}${worker.file_path ? worker.file_path : ''} ${worker.created_at}
    - + @@ -284,7 +284,7 @@
    App▼▲ Users▼▲ Opens▼▲
    - + @@ -317,7 +317,7 @@
    Worker▼▲ File▼▲ Created▼▲
    - + diff --git a/src/dev-center/js/apps.js b/src/dev-center/js/apps.js index 278d8821a..74d28c64a 100644 --- a/src/dev-center/js/apps.js +++ b/src/dev-center/js/apps.js @@ -1379,7 +1379,7 @@ function generate_app_card(app) { // check box h += ``;
    Website▼▲ Connected Directory▼▲ Created▼▲`; h += `
    `; - h += ``; + h += ``; h += `
    `; h += `