Add upload and share functionality and minor improvements

This commit is contained in:
Yeicor
2025-08-02 12:50:20 +02:00
parent 56fc556c0f
commit 06a95d4875
8 changed files with 483 additions and 323 deletions

View File

@@ -12,7 +12,8 @@ import {
mdiFolderOpen,
mdiPlay,
mdiReload,
mdiShare
mdiShare,
mdiUpload
} from "@mdi/js";
import {VBtn, VCard, VCardText, VSlider, VSpacer, VToolbar, VToolbarTitle, VTooltip} from "vuetify/components";
// @ts-expect-error
@@ -25,6 +26,7 @@ import {NetworkUpdateEvent, NetworkUpdateEventModel} from "../misc/network.ts";
import {settings} from "../misc/settings.ts";
// @ts-expect-error
import playgroundStartupCode from './PlaygroundStartup.py?raw';
import {uploadFile} from "./upload-file.ts";
const model = defineModel<{ code: string, firstTime: boolean }>({required: true}); // Initial code should only be set on first load!
const emit = defineEmits<{ close: [], updateModel: [NetworkUpdateEvent] }>()
@@ -139,9 +141,13 @@ function onModelData(modelData: string) {
const binaryData = Base64.toUint8Array(modelData.slice(i + 1)); // Extract the base64 part
console.assert(binaryData.slice(0, 4).toString() == "103,108,84,70", // Ugly...
"Invalid GLTF binary data received: " + binaryData.slice(0, 4).toString());
// - Save for upload and share link feature
builtModelsGlb[modelMetadata.name] = binaryData;
// - Create a Blob from the binary data to be used as a URL
const blob = new Blob([binaryData], {type: 'model/gltf-binary'});
modelMetadata.url = URL.createObjectURL(blob); // Set the hacked URL in the model metadata XXX: revoked on App.vue
} else {
delete builtModelsGlb[modelMetadata.name]; // Remove from built models if it's a remove request
}
// - Emit the event with the model metadata and URL
let networkUpdateEvent = new NetworkUpdateEvent([modelMetadata], () => {
@@ -158,15 +164,14 @@ function resetWorker(loadSnapshot: Uint8Array | undefined = undefined) {
setupPyodide(false, loadSnapshot); // Reinitialize Pyodide
}
function shareLink() {
function shareLinkCommon(added: Record<string, string>, forgotten: Array<string>) {
const baseUrl = window.location
const searchParams = new URLSearchParams(baseUrl.search);
searchParams.delete('pg_code_url'); // Remove any existing pg_code parameter
searchParams.delete('pg_code'); // Remove any existing pg_code parameter
for (const k of forgotten) searchParams.delete(k);
const hashParams = new URLSearchParams(baseUrl.hash.slice(1)); // Keep all previous URL parameters
hashParams.delete('pg_code_url') // Would overwrite the pg_code parameter
hashParams.set('pg_code', b64UrlEncode(gzip(model.value.code, {level: 9}))); // Compress and encode the code
const shareUrl = `${baseUrl.origin}${baseUrl.pathname}?${searchParams}#${hashParams}`; // Prefer hash to GET
for (const k of forgotten) hashParams.delete(k);
for (const k in added) hashParams.append(k, added[k]); // Prefer hash to GET
const shareUrl = `${baseUrl.origin}${baseUrl.pathname}?${searchParams}#${hashParams}`;
output(`Share link ready: ${shareUrl}\n`)
if (navigator.clipboard?.writeText === undefined) {
output("Clipboard API not available. Please copy the link manually.\n");
@@ -178,6 +183,35 @@ function shareLink() {
}
}
function shareLink() {
shareLinkCommon({'pg_code': b64UrlEncode(gzip(model.value.code, {level: 9}))}, ['pg_code']);
}
const builtModelsGlb: Record<string, Uint8Array> = {}; // Store built models to support uploading
async function uploadAndShareLink() {
try {
output("Uploading files...\n");
// Upload code.py
const codeBlob = new Blob([model.value.code], {type: 'text/x-python'});
const newParams: Record<string, string> = {
'pg_code': await uploadFile('code.py', new Uint8Array(await codeBlob.arrayBuffer()))
};
// Upload all models
for (const name in builtModelsGlb) {
const glb: any = builtModelsGlb[name];
newParams['preload'] = await uploadFile(name + '.glb', glb);
}
// Build share URL
return shareLinkCommon(newParams, ['pg_code'])
} catch (e) {
output(`Error uploading/sharing files: ${e}. Falling back to private share link.\n`);
return shareLink(); // Fallback to private share link if upload fails
}
}
function saveSnapshot() {
throw new Error("Not implemented yet!"); // TODO: Implement snapshot saving
}
@@ -230,38 +264,48 @@ onMounted(() => {
location="bottom">Opacity of the editor (0 = hidden, 1 = fully visible)</v-tooltip>
</span>
<span style="margin-right: -8px;"><!-- This span is only needed to force tooltip to work while button is disabled -->
<span style="padding-left: 12px; width: 48px;"><!-- This span is only needed to force tooltip to work while button is disabled -->
<v-btn icon disabled @click="saveSnapshot()">
<svg-icon :path="mdiContentSave" type="mdi"/>
</v-btn>
<v-tooltip activator="parent"
location="bottom">Save current state to a snapshot for fast startup (WIP)</v-tooltip>
</span>
<span style="margin-left: -8px"><!-- This span is only needed to force tooltip to work while button is disabled -->
<span style="padding-right: 12px; width: 48px;"><!-- This span is only needed to force tooltip to work while button is disabled -->
<v-btn icon disabled @click="loadSnapshot()">
<svg-icon :path="mdiFolderOpen" type="mdi"/>
</v-btn>
<v-tooltip activator="parent" location="bottom">Load snapshot for fast startup (WIP)</v-tooltip>
</span>
<v-btn icon @click="resetWorker()">
<v-btn icon @click="shareLink()" style="padding-left: 12px;">
<svg-icon :path="mdiShare" type="mdi"/>
<v-tooltip activator="parent" location="bottom">Share link that automatically runs the code.<br/>Only people
with the link can see the code.
</v-tooltip>
</v-btn>
<v-btn icon @click="uploadAndShareLink()" style="padding-right: 12px">
<svg-icon :path="mdiShare" type="mdi" style="position: absolute; scale: 75%; top: 6px;"/>
<svg-icon :path="mdiUpload" type="mdi" style="position: absolute; scale: 75%; bottom: 6px;"/>
<v-tooltip activator="parent" location="bottom">Uploads all models and code and then shares a link to them.<br/>Useful
to view the models while the playground loads, but uses third-party storage.
</v-tooltip>
</v-btn>
<v-btn icon @click="resetWorker()" style="padding-left: 12px;">
<svg-icon :path="mdiReload" type="mdi"/>
<v-tooltip activator="parent" location="bottom">Reset Pyodide worker (this forgets all previous state and will
take a little while)
</v-tooltip>
</v-btn>
<v-btn icon @click="runCode()" :disabled="running">
<v-btn icon @click="runCode()" :disabled="running" style="padding-right: 12px">
<svg-icon :path="mdiPlay" type="mdi"/>
<Loading v-if="running" style="position: absolute; top: -16%; left: -16%"/><!-- Ugly positioning -->
<Loading v-if="running" style="position: absolute; top: -16%; left: -28%"/><!-- Ugly positioning -->
<v-tooltip activator="parent" location="bottom">Run code</v-tooltip>
</v-btn>
<v-btn icon @click="shareLink()">
<svg-icon :path="mdiShare" type="mdi"/>
<v-tooltip activator="parent" location="bottom">Share link that auto-runs the code</v-tooltip>
</v-btn>
<v-btn icon @click="emit('close')">
<svg-icon :path="mdiClose" type="mdi"/>
<v-tooltip activator="parent" location="bottom">Close (Pyodide remains loaded)</v-tooltip>