mirror of https://github.com/sveltejs/svelte
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
313 lines
6.7 KiB
313 lines
6.7 KiB
<script>
|
|
import { createEventDispatcher } from 'svelte';
|
|
import UserMenu from './UserMenu.svelte';
|
|
import Icon from '../../../../components/Icon.svelte';
|
|
import * as doNotZip from 'do-not-zip';
|
|
import downloadBlob from '../../_utils/downloadBlob.js';
|
|
import { user } from '../../../../user.js';
|
|
import { enter } from '../../../../utils/events.js';
|
|
|
|
const dispatch = createEventDispatcher();
|
|
|
|
export let repl;
|
|
export let gist;
|
|
export let name;
|
|
export let zen_mode;
|
|
export let bundle;
|
|
|
|
let saving = false;
|
|
let downloading = false;
|
|
let justSaved = false;
|
|
let justForked = false;
|
|
|
|
const isMac = typeof navigator !== 'undefined' && navigator.platform === 'MacIntel';
|
|
|
|
function wait(ms) {
|
|
return new Promise(f => setTimeout(f, ms));
|
|
}
|
|
|
|
let canSave;
|
|
$: canSave = !!$user && !!gist && !!gist.owner && $user.id == gist.owner.id; // comparing number and string
|
|
|
|
function handleKeydown(event) {
|
|
if (event.which === 83 && (isMac ? event.metaKey : event.ctrlKey)) {
|
|
event.preventDefault();
|
|
save();
|
|
}
|
|
}
|
|
|
|
function login(event) {
|
|
event.preventDefault();
|
|
const loginWindow = window.open(`${window.location.origin}/auth/login`, 'login', 'width=600,height=400');
|
|
|
|
const handleLogin = event => {
|
|
loginWindow.close();
|
|
user.set(event.data.user);
|
|
window.removeEventListener('message', handleLogin);
|
|
};
|
|
|
|
window.addEventListener('message', handleLogin);
|
|
}
|
|
|
|
async function fork(intentWasSave) {
|
|
saving = true;
|
|
|
|
const { components } = repl.toJSON();
|
|
|
|
try {
|
|
const r = await fetch(`gist/create`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
name,
|
|
components
|
|
})
|
|
});
|
|
|
|
if (r.status < 200 || r.status >= 300) {
|
|
const { error } = await r.json();
|
|
throw new Error(`Received an HTTP ${r.status} response: ${error}`);
|
|
}
|
|
|
|
const gist = await r.json();
|
|
dispatch('forked', { gist });
|
|
|
|
if (intentWasSave) {
|
|
justSaved = true;
|
|
await wait(600);
|
|
justSaved = false;
|
|
} else {
|
|
justForked = true;
|
|
await wait(600);
|
|
justForked = false;
|
|
}
|
|
} catch (err) {
|
|
if (navigator.onLine) {
|
|
alert(err.message);
|
|
} else {
|
|
alert(`It looks like you're offline! Find the internet and try again`);
|
|
}
|
|
}
|
|
|
|
saving = false;
|
|
}
|
|
|
|
async function save() {
|
|
if (saving) return;
|
|
|
|
if (!canSave) {
|
|
fork(true);
|
|
return;
|
|
}
|
|
|
|
saving = true;
|
|
|
|
const { components } = repl.toJSON();
|
|
|
|
try {
|
|
const files = {};
|
|
|
|
// null out any deleted files
|
|
const set = new Set(components.map(m => `${m.name}.${m.type}`));
|
|
Object.keys(gist.files).forEach(file => {
|
|
if (/\.(svelte|html|js)$/.test(file)) {
|
|
if (!set.has(file)) files[file] = null;
|
|
}
|
|
});
|
|
|
|
components.forEach(module => {
|
|
const file = `${module.name}.${module.type}`;
|
|
if (!module.source.trim()) {
|
|
throw new Error(`GitHub does not allow saving gists with empty files - ${file}`);
|
|
}
|
|
|
|
if (!gist.files[file] || module.source !== gist.files[file].content) {
|
|
files[file] = { content: module.source };
|
|
}
|
|
});
|
|
|
|
const r = await fetch(`gist/${gist.id}`, {
|
|
method: 'PATCH',
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
description: name,
|
|
files
|
|
})
|
|
});
|
|
|
|
if (r.status < 200 || r.status >= 300) {
|
|
const { error } = await r.json();
|
|
throw new Error(`Received an HTTP ${r.status} response: ${error}`);
|
|
}
|
|
|
|
const result = await r.json();
|
|
|
|
justSaved = true;
|
|
await wait(600);
|
|
justSaved = false;
|
|
} catch (err) {
|
|
if (navigator.onLine) {
|
|
alert(err.message);
|
|
} else {
|
|
alert(`It looks like you're offline! Find the internet and try again`);
|
|
}
|
|
}
|
|
|
|
saving = false;
|
|
}
|
|
|
|
async function download() {
|
|
downloading = true;
|
|
|
|
const { components, imports } = repl.toJSON();
|
|
|
|
const files = await (await fetch('/svelte-app.json')).json();
|
|
|
|
if (imports.length > 0) {
|
|
const idx = files.findIndex(({ path }) => path === 'package.json');
|
|
const pkg = JSON.parse(files[idx].data);
|
|
const deps = {};
|
|
imports.forEach(mod => {
|
|
const match = /^(@[^\/]+\/)?[^@\/]+/.exec(mod);
|
|
deps[match[0]] = 'latest';
|
|
});
|
|
pkg.dependencies = deps;
|
|
files[idx].data = JSON.stringify(pkg, null, ' ');
|
|
}
|
|
|
|
files.push(...components.map(component => ({ path: `src/${component.name}.${component.type}`, data: component.source })));
|
|
files.push({
|
|
path: `src/main.js`, data: `import App from './App.svelte';
|
|
|
|
var app = new App({
|
|
target: document.body
|
|
});
|
|
|
|
export default app;` });
|
|
|
|
downloadBlob(doNotZip.toBlob(files), 'svelte-app.zip');
|
|
|
|
downloading = false;
|
|
}
|
|
</script>
|
|
|
|
<svelte:window on:keydown={handleKeydown} />
|
|
|
|
<div class="app-controls">
|
|
<input
|
|
bind:value={name}
|
|
on:focus="{e => e.target.select()}"
|
|
use:enter="{e => e.target.blur()}"
|
|
>
|
|
|
|
<div style="text-align: right; margin-right:.4rem">
|
|
<button class="icon" on:click="{() => zen_mode = !zen_mode}" title="fullscreen editor">
|
|
{#if zen_mode}
|
|
<Icon name="close" />
|
|
{:else}
|
|
<Icon name="maximize" />
|
|
{/if}
|
|
</button>
|
|
|
|
<button class="icon" disabled={downloading} on:click={download} title="download zip file">
|
|
<Icon name="download" />
|
|
</button>
|
|
|
|
{#if $user}
|
|
<button class="icon" disabled="{saving || !$user}" on:click={fork} title="fork">
|
|
{#if justForked}
|
|
<Icon name="check" />
|
|
{:else}
|
|
<Icon name="git-branch" />
|
|
{/if}
|
|
</button>
|
|
|
|
<button class="icon" disabled="{saving || !$user}" on:click={save} title="save">
|
|
{#if justSaved}
|
|
<Icon name="check" />
|
|
{:else}
|
|
<Icon name="save" />
|
|
{/if}
|
|
</button>
|
|
{/if}
|
|
|
|
{#if gist}
|
|
<a class="icon" href={gist.html_url} title="link to gist">
|
|
<Icon name="link" />
|
|
</a>
|
|
{/if}
|
|
|
|
{#if $user}
|
|
<UserMenu />
|
|
{:else}
|
|
<button class="icon" on:click={login}>
|
|
<Icon name="log-in" />
|
|
<span> Log in to save</span>
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.app-controls {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: var(--app-controls-h);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: .6rem var(--side-nav);
|
|
background-color: var(--second);
|
|
color: white;
|
|
}
|
|
|
|
.icon {
|
|
position: relative;
|
|
top: -0.1rem;
|
|
display: inline-block;
|
|
padding: 0.2em;
|
|
opacity: .7;
|
|
transition: opacity .3s;
|
|
font-family: var(--font);
|
|
font-size: 1.6rem;
|
|
/* width: 1.6em;
|
|
height: 1.6em; */
|
|
line-height: 1;
|
|
margin: 0 0 0 0.2em;
|
|
}
|
|
|
|
.icon:hover { opacity: 1 }
|
|
.icon:disabled { opacity: .3 }
|
|
|
|
.icon[title^='fullscreen'] { display: none }
|
|
|
|
input {
|
|
background: transparent;
|
|
border: none;
|
|
color: currentColor;
|
|
font-family: var(--font);
|
|
font-size: 1.6rem;
|
|
opacity: 0.7;
|
|
outline: none;
|
|
flex: 1;
|
|
}
|
|
|
|
input:focus {
|
|
opacity: 1;
|
|
}
|
|
|
|
button span {
|
|
display: none;
|
|
}
|
|
|
|
@media (min-width: 600px) {
|
|
.icon[title^='fullscreen'] { display: inline }
|
|
|
|
button span {
|
|
display: inline-block;
|
|
}
|
|
}
|
|
</style>
|