s4s-packager / src /p4 /SelectProject.svelte
soiz1's picture
Update src/p4/SelectProject.svelte
b43ac90 verified
<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}