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 @@
`; - // 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 ` - - - - - ${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 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 ` - + ${worker.name} ${worker.created_at} - - - + `; } @@ -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