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.
svelte/site/src/routes/repl/_components/AppControls/index.svelte

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>&nbsp;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>