Spaces:
Sleeping
Sleeping
<script> | |
import {onMount, tick} from 'svelte'; | |
import {_} from '../locales'; | |
import Section from './Section.svelte'; | |
import Button from './Button.svelte'; | |
import DropArea from './DropArea.svelte'; | |
import ComplexMessage from './ComplexMessage.svelte'; | |
import ImportingProject from './ImportProject.svelte'; | |
import writablePersistentStore from './persistent-store'; | |
import {progress, currentTask} from './stores'; | |
import {UserError} from '../common/errors'; | |
import getProjectMetadata from './get-project-metadata'; | |
import loadProject from '../packager/load-project'; | |
import {extractProjectId, isValidURL, getTitleFromURL} from './url-utils'; | |
import Task from './task'; | |
import importExternalProject from './import-external-project'; | |
const defaultProjectId = '163240'; | |
const type = writablePersistentStore('SelectProject.type', 'id'); | |
const projectId = writablePersistentStore('SelectProject.id', defaultProjectId); | |
const projectUrl = writablePersistentStore('SelectProject.url', ''); | |
const projectIdInURL = /^#\d+$/.test(location.hash) ? location.hash.substring(1) : null; | |
if (projectIdInURL) { | |
$type = 'id'; | |
$projectId = projectIdInURL; | |
} | |
let isImportingProject = false; | |
importExternalProject({ | |
onStartImporting: () => { | |
isImportingProject = true; | |
}, | |
onCancelImporting: () => { | |
isImportingProject = false; | |
}, | |
onFinishImporting: (files) => { | |
if (!isImportingProject) { | |
// Import was cancelled. | |
return; | |
} | |
$type = 'file'; | |
isImportingProject = false; | |
fileInputElement.files = files; | |
setFiles(files); | |
} | |
}); | |
export let projectData = null; | |
const resetProjectAndCancelTask = () => { | |
projectData = null; | |
currentTask.abort(); | |
}; | |
// Reset project when input changes | |
$: $projectId, $type, resetProjectAndCancelTask(); | |
// just incase some non-number string was stored from older versions | |
$projectId = extractProjectId($projectId); | |
const getDisplayedProjectURL = () => `https://studio.penguinmod.com/#${$projectId}`; | |
const submitOnEnter = (e) => { | |
if (e.key === 'Enter') { | |
load(); | |
} | |
}; | |
const handleInput = (e) => { | |
$projectId = extractProjectId(e.target.value); | |
e.target.value = getDisplayedProjectURL(); | |
}; | |
const handleFocus = (e) => { | |
e.target.select(); | |
}; | |
let fileInputElement; | |
const copyFileList = (files) => { | |
const transfer = new DataTransfer(); | |
for (const file of files) { | |
transfer.items.add(file); | |
} | |
return transfer.files; | |
}; | |
// This is used to remember files between reloads in some browsers (currently only Firefox) | |
// This element is defined in the HTML of template.ejs because some browsers won't | |
// autocomplete file inputs generated by JS. | |
const inputForRememberingProjectFile = document.querySelector('.input-for-remembering-project-file'); | |
if (inputForRememberingProjectFile) { | |
// Check for autocompleted files after mount so that fileInputElement is defined. | |
onMount(() => { | |
const storedFiles = inputForRememberingProjectFile.files; | |
if (storedFiles.length) { | |
fileInputElement.files = copyFileList(storedFiles); | |
} | |
}); | |
} | |
const setFiles = (files) => { | |
resetProjectAndCancelTask(); | |
if (fileInputElement.files !== files) { | |
fileInputElement.files = files; | |
} | |
if (inputForRememberingProjectFile) { | |
inputForRememberingProjectFile.files = copyFileList(files); | |
} | |
if (files.length && $type === 'file') { | |
// if $type was updated before calling this function, wait for the current task to get | |
// cancelled before we start the next one | |
tick().then(load); | |
} | |
}; | |
const handleDrop = ({detail: dataTransfer}) => { | |
const name = dataTransfer.files[0].name; | |
if (name.endsWith('.sb') || name.endsWith('.sb2') || name.endsWith('.sb3') || name.endsWith('.pm') || name.endsWith('.pmp') || name.endsWith('.s4s.txt')) { | |
$type = 'file'; | |
setFiles(dataTransfer.files); | |
} | |
}; | |
const handleFileInputChange = (e) => { | |
setFiles(e.target.files); | |
}; | |
const internalLoad = async (task) => { | |
let uniqueId = ''; | |
let id = null; | |
let projectTitle = ''; | |
let project; | |
const progressCallback = (type, a, b) => { | |
if (type === 'fetch') { | |
task.setProgress(a); | |
} else if (type === 'assets') { | |
task.setProgressText( | |
$_('progress.loadingAssets') | |
.replace('{complete}', a) | |
.replace('{total}', b) | |
); | |
task.setProgress(a / b); | |
} else if (type === 'compress') { | |
task.setProgressText($_('progress.compressingProject')); | |
task.setProgress(a); | |
} | |
}; | |
if ($type === 'id') { | |
id = $projectId; | |
if (!id) { | |
throw new UserError($_('select.invalidId')); | |
} | |
uniqueId = `#${id}`; | |
task.setProgressText($_('progress.loadingProjectMetadata')); | |
const metadata = await getProjectMetadata(id); | |
const token = metadata.token; | |
projectTitle = metadata.title; | |
task.setProgressText($_('progress.loadingProjectData')); | |
const {promise: loadProjectPromise, terminate} = await loadProject.fromID(id, token, progressCallback); | |
task.whenAbort(terminate); | |
project = await loadProjectPromise; | |
} else if ($type === 'file') { | |
const files = fileInputElement.files; | |
const file = files && files[0]; | |
if (!file) { | |
throw new UserError($_('select.noFileSelected')); | |
} | |
uniqueId = `@${file.name}`; | |
projectTitle = file.name; | |
task.setProgressText($_('progress.compressingProject')); | |
project = await (await loadProject.fromFile(file, progressCallback)).promise; | |
} else if ($type === 'url') { | |
const url = $projectUrl; | |
if (!isValidURL(url)) { | |
throw new UserError($_('select.invalidUrl')); | |
} | |
uniqueId = `$${url}`; | |
projectTitle = getTitleFromURL(url); | |
task.setProgressText($_('progress.loadingProjectData')); | |
project = await (await loadProject.fromURL(url, progressCallback)).promise; | |
} else { | |
throw new Error('Unknown type'); | |
} | |
return { | |
projectId: id, | |
uniqueId, | |
title: projectTitle, | |
project, | |
}; | |
}; | |
const load = async () => { | |
resetProjectAndCancelTask(); | |
const task = new Task(); | |
projectData = await task.do(internalLoad(task)); | |
task.done(); | |
}; | |
</script> | |
<style> | |
input[type="text"] { | |
max-width: 300px; | |
flex-grow: 1; | |
} | |
.options { | |
margin: 12px 0; | |
} | |
.option { | |
min-height: 25px; | |
display: flex; | |
align-items: center; | |
flex-wrap: wrap; | |
} | |
input[type="text"], input[type="file"] { | |
margin-left: 4px; | |
} | |
</style> | |
{#if isImportingProject} | |
<ImportingProject on:cancel={() => { | |
isImportingProject = false; | |
}} /> | |
{/if} | |
<DropArea on:drop={handleDrop}> | |
<Section accent="#4C97FF"> | |
<h2>{$_('select.select')}</h2> | |
<p>{$_('select.selectHelp')}</p> | |
<div class="options"> | |
<div class="option"> | |
<label> | |
<input type="radio" name="project-type" bind:group={$type} value="id"> | |
{$_('select.id')} | |
</label> | |
{#if $type === "id"} | |
<input type="text" value={getDisplayedProjectURL()} spellcheck="false" on:keypress={submitOnEnter} on:input={handleInput} on:focus={handleFocus}> | |
{/if} | |
</div> | |
<!-- TurboWarp Desktop looks for the file-input-option class for special handling, so be careful when modifying this. --> | |
<div class="option file-input-option"> | |
<label> | |
<input type="radio" name="project-type" bind:group={$type} value="file"> | |
{$_('select.file')} | |
</label> | |
<input hidden={$type !== "file"} on:change={handleFileInputChange} bind:this={fileInputElement} type="file" accept=".sb,.sb2,.sb3, .pm, .pmp, .s4s.txt, .goobert"> | |
</div> | |
<div class="option"> | |
<label> | |
<input type="radio" name="project-type" bind:group={$type} value="url"> | |
{$_('select.url')} | |
</label> | |
{#if $type === "url"} | |
<input type="text" bind:value={$projectUrl} spellcheck="false" placeholder="https://..." on:keypress={submitOnEnter}> | |
{/if} | |
</div> | |
</div> | |
{#if $type === "id"} | |
<p> | |
{$_('select.unsharedProjects')} | |
</p> | |
{/if} | |
<Button on:click={load} text={$_('select.loadProject')} /> | |
</Section> | |
</DropArea> | |
{#if !$progress.visible && !projectData} | |
<Section caption> | |
<p>{$_('select.loadToContinue')}</p> | |
</Section> | |
{/if} | |