Spaces:
Sleeping
Sleeping
| /** | |
| * 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(); | |
| 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 = `<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 ) { | |
| 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, | |
| }); | |
| }; |