/** * 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 . */ 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(); 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); } }); $('.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 = `First Name cannot be longer than ${100}.`; } else if ( last_name.length > 100 ) { error = `Last Name cannot be longer than ${100}.`; } else if ( paypal.length > 100 ) { error = `Paypal 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 ) { 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 ( 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(window.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 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 window.dropped_items ) { paths.push(`/${ item.fullPath ?? item.filepath}`); } // Generate a directory tree from the paths let tree = generateDirTree(paths); window.dropped_items = setRootDirTree(tree, window.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'); window.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, }); };