diff --git a/src/gui/src/UI/UIDesktop.js b/src/gui/src/UI/UIDesktop.js index 1dab0d5a..fabbd1cf 100644 --- a/src/gui/src/UI/UIDesktop.js +++ b/src/gui/src/UI/UIDesktop.js @@ -30,6 +30,7 @@ import UIWindowFeedback from "./UIWindowFeedback.js" import UIWindowLogin from "./UIWindowLogin.js" import UIWindowQR from "./UIWindowQR.js" import UIWindowRefer from "./UIWindowRefer.js" +import UIWindowProgress from "./UIWindowProgress.js" import UITaskbar from "./UITaskbar.js" import new_context_menu_item from "../helpers/new_context_menu_item.js" import refresh_item_container from "../helpers/refresh_item_container.js" @@ -1304,8 +1305,7 @@ async function UIDesktop(options) { // load from direct app URLs: /app/app-name // --------------------------------------------- else if (window.app_launched_from_url) { - let qparams = new URLSearchParams(window.location.search); - if (!qparams.has('c')) { + if (!window.url_query_params.has('c')) { let posargs = undefined; if (window.app_query_params && window.app_query_params.posargs) { posargs = JSON.parse(window.app_query_params.posargs); @@ -1313,8 +1313,8 @@ async function UIDesktop(options) { launch_app({ app: window.app_launched_from_url.name, app_obj: window.app_launched_from_url, - readURL: qparams.get('readURL'), - maximized: qparams.get('maximized'), + readURL: window.url_query_params.get('readURL'), + maximized: window.url_query_params.get('maximized'), params: window.app_query_params ?? [], ...(posargs ? { args: { @@ -1678,11 +1678,11 @@ async function UIDesktop(options) { // i.e. https://puter.com/@ //-------------------------------------------------------------------------------------- const url_paths = window.location.pathname.split('/').filter(element => element); - if (url_paths[0]?.startsWith('@')) { - const username = url_paths[0].substring(1); + if (window.url_paths[0]?.startsWith('@')) { + const username = window.url_paths[0].substring(1); let item_path = '/' + username + '/Public'; - if ( url_paths.length > 1 ) { - item_path += '/' + url_paths.slice(1).join('/'); + if ( window.url_paths.length > 1 ) { + item_path += '/' + window.url_paths.slice(1).join('/'); } // GUARD: avoid invalid user directories @@ -1781,6 +1781,117 @@ async function UIDesktop(options) { app: 'explorer', }); } + + //-------------------------------------------------------------------------------------- + // Direct download link + // i.e. https://puter.com/?download= + //-------------------------------------------------------------------------------------- + if (window.url_paths.length === 0 && window.url_query_params.has('download')) { + const url = window.url_query_params.get('download'); + let file_name = url.split('/').pop().split('?')[0]; + + let response = await UIAlert({ + message: i18n('confirm_download_file_to_desktop', file_name), + type: 'confirm', + buttons: [ + { label: i18n('alert_yes'), value: true, type: "primary" }, + { label: i18n('alert_no'), value: false, type: "secondary" } + ], + }); + + if (!response) + return; + + + + let cancelled = false; + let upload_xhr = null; + const abort_controller = new AbortController(); + + // create progressbar dialog + let progwin = await UIWindowProgress({ + title: i18n('downloading'), + icon: window.icons[`app-icon-uploader.svg`], + operation_id: window.uuidv4(), + show_progress: true, + on_cancel: () => { + cancelled = true; + abort_controller.abort(); + if (upload_xhr) { + upload_xhr.abort(); + } + } + }); + progwin?.set_status(i18n('downloading_file', file_name)); + + (async () => { + try { + // download the file + const response = await puter.net.fetch(url, { + signal: abort_controller.signal, + }); + + const total = Number(response.headers.get('content-length')); + const reader = response.body.getReader(); + + const chunks = []; + let received = 0; + + while (true) { + const { done, value } = await reader.read(); + if (done || cancelled) break; + + if (value) { + // store the chunk + chunks.push(value); + received += value.length; + // calculate progress + const progress = Number.isFinite(total) && total > 0 + ? received / total + : 0; + // update progressbar + progwin?.set_progress(Math.floor(progress * 100)); + } + } + + if (cancelled) { + progwin?.close(); + return; + } + + // combine chunks into a blob + let blob = new Blob(chunks, { + type: response.headers.get('content-type') ?? 'application/octet-stream', + }); + + // reset progressbar + progwin?.set_progress(0); + progwin?.set_status(i18n('uploading_file', file_name)); + + // upload to user's desktop + await puter.fs.write(`~/Desktop/${file_name}`, blob, { + dedupeName: true, + progress: (_, percent) => { + // update progressbar + progwin?.set_progress(percent); + }, + init: (_, xhr) => { + upload_xhr = xhr; + } + }); + } catch (e) { + // alert the user if there's a genuine error + if (!cancelled && e.name !== 'AbortError') { + await UIAlert({ + message: i18n('error_download_failed') + ': ' + e.message, + type: 'error', + }); + } + } + // close progress window + progwin?.close(); + })(); + } } $(document).on('contextmenu taphold', '.taskbar', function (event) { diff --git a/src/gui/src/i18n/translations/en.js b/src/gui/src/i18n/translations/en.js index fb3f81c9..2b1ac407 100644 --- a/src/gui/src/i18n/translations/en.js +++ b/src/gui/src/i18n/translations/en.js @@ -116,8 +116,11 @@ const en = { documents: 'Documents', dont_allow: 'Don\'t Allow', download: 'Download', + confirm_download_file_to_desktop: 'Are you sure you want to download %% to your Desktop?', download_file: 'Download File', downloading: "Downloading", + downloading_file: "Downloading %%", + error_download_failed: "Failed to download file", email: "Email", email_change_confirmation_sent: "A confirmation email has been sent to your new email address. Please check your inbox and follow the instructions to complete the process.", email_invalid: 'Email is invalid.', @@ -324,6 +327,8 @@ const en = { untar: "Untar", untarring: "Untarring %strong%", upload: 'Upload', + uploading: "Uploading", + uploading_file: "Uploading %%", upload_here: 'Upload here', used_of: '{{used}} used of {{available}}', usage: 'Usage',