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} | |