Files
puter/src/dev-center/js/dev-center.js
T
2025-08-04 22:08:32 -07:00

496 lines
17 KiB
JavaScript

/**
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import init_apps from './apps.js';
import init_workers from './workers.js';
import init_websites from './websites.js';
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';
window.user = null;
// auth_username
(async () => {
window.user = await puter.auth.getUser();
if (user?.username) {
window.auth_username = user.username;
}
})()
// domain and APIOrigin
if (window.url_params.has('puter.domain')) {
window.domain = window.url_params.get('puter.domain')
}
// static hosting domain
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 (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
window.protocol = 'https';
if (window.url_params.has('puter.protocol') && window.url_params.get('puter.protocol') === 'http')
window.protocol = 'http';
// 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'));
}
// 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
await initializeAssetsDirectory();
// create default worker file
await createDefaultWorkerFile();
puter.ui.showSpinner();
init_apps();
init_websites();
init_workers();
puter.ui.hideSpinner();
});
// ---------------------------------------------------------------
// Tab Buttons
// ---------------------------------------------------------------
$(document).on('click', '.tab-btn', async function (e) {
puter.ui.showSpinner();
$('section:not(.sidebar)').hide();
$('.tab-btn').removeClass('active');
$(this).addClass('active');
$('section[data-tab="' + $(this).attr('data-tab') + '"]').show();
// ---------------------------------------------------------------
// Apps tab
// ---------------------------------------------------------------
if ($(this).attr('data-tab') === 'apps') {
refresh_app_list();
activeTab = 'apps';
// Reset apps search when tab is activated
resetAppsSearch();
}
// ---------------------------------------------------------------
// Workers tab
// ---------------------------------------------------------------
else if ($(this).attr('data-tab') === 'workers') {
refresh_worker_list();
activeTab = 'workers';
// Reset workers search when tab is activated
resetWorkersSearch();
}
// ---------------------------------------------------------------
// Websites tab
// ---------------------------------------------------------------
else if ($(this).attr('data-tab') === 'websites') {
refresh_websites_list();
activeTab = 'websites';
// Reset websites search when tab is activated
resetWebsitesSearch();
}
// ---------------------------------------------------------------
// Payout Method tab
// ---------------------------------------------------------------
else if ($(this).attr('data-tab') === 'payout-method') {
activeTab = 'payout-method';
puter.ui.showSpinner();
setTimeout(function () {
puter.apps.getDeveloperProfile(function (dev_profile) {
// show payout method tab if dev has joined incentive program
if (dev_profile.joined_incentive_program) {
$('#payout-method-email').html(dev_profile.paypal);
}
puter.ui.hideSpinner();
if (activeTab === 'payout-method')
$('#tab-payout-method').show();
})
}, 1000);
}
puter.ui.hideSpinner();
})
$('.jip-submit-btn').on('click', async function (e) {
const first_name = $('#jip-first-name').val();
const last_name = $('#jip-last-name').val();
const paypal = $('#jip-paypal').val();
let error;
if (first_name === '' || last_name === '' || paypal === '')
error = `All fields are required.`;
else if (first_name.length > 100)
error = `<strong>First Name</strong> cannot be longer than ${100}.`;
else if (last_name.length > 100)
error = `<strong>Last Name</strong> cannot be longer than ${100}.`;
else if (paypal.length > 100)
error = `<strong>Paypal</strong> cannot be longer than ${100}.`;
// check if email is valid
else if (!validateEmail(paypal))
error = `Paypal email must be a valid email address.`;
// error?
if (error) {
$('#jip-error').show();
$('#jip-error').html(error);
document.body.scrollTop = document.documentElement.scrollTop = 0;
return;
}
// disable submit button
$('.jip-submit-btn').prop('disabled', true);
$.ajax({
url: puter.APIOrigin + "/jip",
type: 'POST',
async: true,
contentType: "application/json",
data: JSON.stringify({
first_name: first_name,
last_name: last_name,
paypal: paypal,
}),
headers: {
"Authorization": "Bearer " + puter.authToken
},
success: function () {
$('#jip-success').show();
$('#jip-form').hide();
//enable submit button
$('.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();
},
error: function (err) {
$('#jip-error').show();
$('#jip-error').html(err.message);
// scroll to top so that user sees error message
document.body.scrollTop = document.documentElement.scrollTop = 0;
// enable submit button
$('.jip-submit-btn').prop('disabled', false);
}
})
})
$('#earn-money-c2a-close').click(async function (e) {
$('#earn-money').get(0).close();
puter.kv.set('earn-money-c2a-closed', 'true')
})
$('#earn-money::backdrop').click(async function (e) {
alert();
$('#earn-money').get(0).close();
puter.kv.set('earn-money-c2a-closed', 'true')
})
// https://stackoverflow.com/a/43467144/1764493
window.is_valid_url = (string) => {
let url;
try {
url = new URL(string);
} catch (_) {
return false;
}
return url.protocol === "http:" || url.protocol === "https:";
}
window.getBase64ImageFromUrl = async (imageUrl) => {
var res = await fetch(imageUrl);
var blob = await res.blob();
return new Promise((resolve, reject) => {
var reader = new FileReader();
reader.addEventListener("load", function () {
resolve(reader.result);
}, false);
reader.onerror = () => {
return reject(this);
};
reader.readAsDataURL(blob);
})
}
/**
* Formats a binary-byte integer into the human-readable form with units.
*
* @param {integer} bytes
* @returns
*/
window.byte_format = (bytes) => {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes === 0) return '0 Byte';
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
};
/**
* check if a string is a valid email address
*/
window.validateEmail = (email) => {
var re = /\S+@\S+\.\S+/;
return re.test(email);
}
/**
* Formats a number with grouped thousands.
*
* @param {number|string} number - The number to be formatted. If a string is provided, it must only contain numerical characters, plus and minus signs, and the letter 'E' or 'e' (for scientific notation).
* @param {number} decimals - The number of decimal points. If a non-finite number is provided, it defaults to 0.
* @param {string} [dec_point='.'] - The character used for the decimal point. Defaults to '.' if not provided.
* @param {string} [thousands_sep=','] - The character used for the thousands separator. Defaults to ',' if not provided.
* @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.
*/
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,
prec = !isFinite(+decimals) ? 0 : Math.abs(decimals),
sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep,
dec = (typeof dec_point === 'undefined') ? '.' : dec_point,
s = '',
toFixedFix = function (n, prec) {
var k = Math.pow(10, prec);
return '' + Math.round(n * k) / k;
};
// Fix for IE parseFloat(0.55).toFixed(0) = 0;
s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
if (s[0].length > 3) {
s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep);
}
if ((s[1] || '').length < prec) {
s[1] = s[1] || '';
s[1] += new Array(prec - s[1].length + 1).join('0');
}
return s.join(dec);
}
$(document).on('click', '.close-message', function () {
$($(this).attr('data-target')).fadeOut();
});
$(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();
// remove active class from all tab buttons
$('.section-tab-btn').removeClass('active');
// add active class to clicked tab button
$(this).addClass('active');
})
$(document).on('click', '.close-success-msg', function (e) {
$(this).closest('div').fadeOut();
})
$('body').on('dragover', function (event) {
// skip if the user is dragging something over the drop area
if ($(event.target).hasClass('drop-area'))
return;
event.preventDefault(); // Prevent the default behavior
event.stopPropagation(); // Stop the event from propagating
});
// Developers can drop items anywhere on the page to deploy them
$('body').on('drop', async function (event) {
// skip if the user is dragging something over the drop area
if ($(event.target).hasClass('drop-area'))
return;
// prevent default behavior
event.preventDefault();
event.stopPropagation();
// retrieve puter items from the event
if (event.detail?.items?.length > 0) {
dropped_items = event.detail.items;
source_path = dropped_items[0].path;
// 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(dropped_items[0].name));
}
}
//-----------------------------------------------------------------------------
// Local items dropped
//-----------------------------------------------------------------------------
const e = event.originalEvent;
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' : '');
// Show insta-deploy modal
$('.insta-deploy-modal').get(0)?.showModal();
// Set item name
$('.insta-deploy-item-name').html(html_encode(rootItems));
});
/**
* 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.
*/
window.getMimeType = (extension) => {
const mimeTypes = {
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
bmp: 'image/bmp',
webp: 'image/webp',
svg: 'image/svg+xml',
tiff: 'image/tiff',
ico: 'image/x-icon'
};
// Remove leading dot if present and convert to lowercase
const cleanExtension = extension.replace(/^\./, '').toLowerCase();
// Return the MIME type if found, otherwise return 'application/octet-stream'
return mimeTypes[cleanExtension] || 'application/octet-stream';
}
$(document).on('click', '.sidebar-toggle', function (e) {
$('.sidebar').toggleClass('open');
$('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) {
return reference.getAttribute('title');
},
onMount(instance) {
// Remove the default title to prevent double tooltips
instance.reference.removeAttribute('title');
},
placement: 'top',
arrow: true,
});
}