diff --git a/src/packager/PackagerOptions.svelte b/src/packager/PackagerOptions.svelte index 9bbcaeb..d5f9fdf 100644 --- a/src/packager/PackagerOptions.svelte +++ b/src/packager/PackagerOptions.svelte @@ -15,7 +15,7 @@ const packager = new Packager(); packager.project = projectData.project; - const cloudVariables = Object.values(projectData.stageVariables) + const cloudVariables = Object.values(projectData.project.analysis.stageVariables) .filter(i => i.isCloud) .map(i => i.name); const canUseCloudVariableServer = !!projectData.projectId; diff --git a/src/packager/SelectProject.svelte b/src/packager/SelectProject.svelte index f8ed423..d402aff 100644 --- a/src/packager/SelectProject.svelte +++ b/src/packager/SelectProject.svelte @@ -3,11 +3,9 @@ import Button from './Button.svelte'; import writablePersistentStore from './persistent-store'; import {error, progress} from './stores'; - import xhr from './lib/xhr'; import {UserError} from './errors'; - import {readAsArrayBuffer} from './lib/readers'; - import * as Comlink from 'comlink'; - import DownloadWorker from 'worker-loader?name=downloader.worker.js!./downloader.worker.js'; + import loadProject from './load-project'; + import getProjectTitle from './lib/get-project-meta.js'; export let projectData = null; const type = writablePersistentStore('SelectProject.type', 'id'); @@ -39,10 +37,15 @@ reset(); $progress.visible = true; - let data; let uniqueId = ''; let id = null; let projectTitle = ''; + let project; + + const progressCallback = (loadedAssets, totalAssets) => { + $progress.text = `Loading assets (${loadedAssets}/${totalAssets})`; + $progress.progress = loadedAssets / totalAssets; + }; if ($type === 'id') { id = getId(); @@ -50,28 +53,10 @@ throw new UserError('Invalid project ID'); } uniqueId = `#${id}`; - $progress.text = 'Loading project metadata'; - try { - const meta = await xhr({ - url: `https://trampoline.turbowarp.org/proxy/projects/${id}`, - timeout: 5000, - type: 'json' - }); - projectTitle = meta.title; - } catch (e) { - // Happens commonly when loading unshared projects, not something to worry about - console.warn(e); - } - - $progress.text = 'Loading project data'; - data = await xhr({ - url: `https://projects.scratch.mit.edu/${id}`, - type: 'arraybuffer', - progressCallback: (p) => { - $progress.progress = p; - } - }); + projectTitle = await getProjectTitle(id); + $progress.text = 'Loading project'; + project = await loadProject.fromID(id, progressCallback); } else { if (!files) { throw new UserError('No file selected'); @@ -80,23 +65,14 @@ uniqueId = `@${file.name}`; projectTitle = file.name; $progress.text = 'Reading project'; - data = await readAsArrayBuffer(file); + project = await loadProject.fromFile(file); } - const progressCallback = (loadedAssets, totalAssets) => { - $progress.text = `Loading assets (${loadedAssets}/${totalAssets})`; - $progress.progress = loadedAssets / totalAssets; - }; - - const worker = Comlink.wrap(new DownloadWorker()); - const project = await worker.downloadProject(data, Comlink.proxy(progressCallback)); - projectData = { projectId: id, uniqueId, title: projectTitle, project, - stageVariables: project.stageVariables }; } catch (e) { $error = e; diff --git a/src/packager/lib/download-project.js b/src/packager/lib/download-project.js index f9331da..0f9b90f 100644 --- a/src/packager/lib/download-project.js +++ b/src/packager/lib/download-project.js @@ -11,6 +11,19 @@ const identifyProjectType = (projectData) => { return null; }; +const analyzeScratch2 = (projectData) => { + const stageVariables = {}; + for (const {name, value, isPersistent} of projectData.variables) { + stageVariables[name] = { + name, + isCloud: isPersistent + }; + } + return { + stageVariables + }; +}; + const loadScratch2 = (projectData, progressTarget) => { const zip = new JSZip(); @@ -106,20 +119,28 @@ const loadScratch2 = (projectData, progressTarget) => { .then(() => { // We must add the project JSON at the end because it is changed during the loading due to updating asset IDs zip.file('project.json', JSON.stringify(projectData)); - const stageVariables = {}; - for (const {name, value, isPersistent} of projectData.variables) { - stageVariables[name] = { - name, - isCloud: isPersistent - }; - } return { zip, - stageVariables: {} + analysis: analyzeScratch2(projectData) }; }); }; +const analyzeScratch3 = (projectData) => { + const stage = projectData.targets[0]; + const stageVariables = {}; + for (const id of Object.keys(stage.variables)) { + const [name, value, cloud] = stage.variables[id]; + stageVariables[id] = { + name, + isCloud: !!cloud + }; + } + return { + stageVariables + }; +}; + const loadScratch3 = (projectData, progressTarget) => { const zip = new JSZip(); @@ -164,18 +185,9 @@ const loadScratch3 = (projectData, progressTarget) => { return Promise.all(assets.map((a) => addFile(a))) .then(() => { - const stage = targets[0]; - const stageVariables = {}; - for (const id of Object.keys(stage.variables)) { - const [name, value, cloud] = stage.variables[id]; - stageVariables[id] = { - name, - isCloud: !!cloud - }; - } return { zip, - stageVariables + analysis: analyzeScratch3(projectData) }; }); }; @@ -192,8 +204,13 @@ const downloadJSONProject = (json, progressTarget) => { }; const downloadProject = async (data, progressCallback) => { + let type; + let blob; + let analysis; + const bufferView = new Uint8Array(data); if (bufferView[0] === '{'.charCodeAt(0)) { + // JSON project, we have to download everything else const progressTarget = new EventTarget(); let totalAssets = 0; let loadedAssets = 0; @@ -206,26 +223,53 @@ const downloadProject = async (data, progressCallback) => { progressCallback(loadedAssets, totalAssets); }); - // Looks like it's JSON const text = new TextDecoder().decode(data); const json = JSON.parse(text); - const type = identifyProjectType(json); - const result = await downloadJSONProject(json, progressTarget); - const blob = await result.zip.generateAsync({ + type = identifyProjectType(json); + const downloaded = await downloadJSONProject(json, progressTarget); + blob = await downloaded.zip.generateAsync({ type: 'blob', compression: 'DEFLATE' }); + analysis = downloaded.analysis; + } else { + // It's a binary blob + blob = new Blob([data]); + let zip; + try { + zip = await JSZip.loadAsync(data); + } catch (e) { + console.warn(e); + } + if (zip) { + const projectDataFile = zip.file('project.json'); + const projectDataText = await projectDataFile.async('text'); + const projectData = JSON.parse(projectDataText); + type = identifyProjectType(projectData); + if (type === 'sb3') { + analysis = analyzeScratch3(projectData); + } else { + analysis = analyzeScratch2(projectData); + } + } else { + type = 'sb'; + analysis = { + stageVariables: {} + }; + } + } + + if (type === 'sb3') { return { - type: type === 'sb3' ? 'sb3' : 'blob', + type: 'sb3', blob, - stageVariables: result.stageVariables - }; + analysis + } } - // It's a binary blob return { type: 'blob', - blob: new Blob([data]), - stageVariables: {} + blob, + analysis }; }; diff --git a/src/packager/lib/get-project-meta.js b/src/packager/lib/get-project-meta.js new file mode 100644 index 0000000..b8eb679 --- /dev/null +++ b/src/packager/lib/get-project-meta.js @@ -0,0 +1,17 @@ +import xhr from './xhr'; + +const getProjectTitle = async (id) => { + try { + const meta = await xhr({ + url: `https://trampoline.turbowarp.org/proxy/projects/${id}`, + timeout: 5000, + type: 'json' + }); + return meta.title; + } catch (e) { + // Happens commonly when loading unshared projects, not something to worry about + return ''; + } +}; + +export default getProjectTitle; diff --git a/src/packager/load-project.js b/src/packager/load-project.js new file mode 100644 index 0000000..07d1527 --- /dev/null +++ b/src/packager/load-project.js @@ -0,0 +1,28 @@ +import * as Comlink from 'comlink'; +import DownloadWorker from 'worker-loader?name=downloader.worker.js!./downloader.worker.js'; +import {readAsArrayBuffer} from './lib/readers'; +import xhr from './lib/xhr'; + +const fromID = async (id, progressCallback) => { + const buffer = await xhr({ + url: `https://projects.scratch.mit.edu/${id}`, + type: 'arraybuffer' + // TODO: bubble progress + }); + const worker = Comlink.wrap(new DownloadWorker()); + const project = await worker.downloadProject(Comlink.transfer(buffer, [buffer]), Comlink.proxy(progressCallback)); + return project; +}; + +const fromFile = async (file) => { + const worker = Comlink.wrap(new DownloadWorker()); + const buffer = await readAsArrayBuffer(file); + const progressCallback = () => {}; + const project = await worker.downloadProject(Comlink.transfer(buffer, [buffer]), Comlink.proxy(progressCallback)); + return project; +}; + +export default { + fromID, + fromFile +};