adrift/frontend/src/App.svelte

539 lines
16 KiB
Svelte

<script lang="ts">
import { setBareClientImplementation } from "bare-client-custom";
import {
AdriftBareClient,
Connection,
DevWsTransport,
RTCTransport,
SignalFirebase,
downloadShortcut,
} from "client";
import {
Button,
Card,
CircularProgressIndeterminate,
Dialog,
RadioAnim3,
SnackbarAnim,
StyleFromScheme,
TextField,
} from "m3-svelte";
import iconDiscord from "@iconify-icons/ic/outline-discord";
import iconGithub from "@iconify-icons/bi/github";
import Icon from "@iconify/svelte";
import type { Transport } from "protocol";
import Proxy from "./Proxy.svelte";
import { initializeApp } from "firebase/app";
import TrackerList from "tracker-list";
import {
browserLocalPersistence,
createUserWithEmailAndPassword,
getAuth,
setPersistence,
signInWithEmailAndPassword,
} from "firebase/auth";
import logo from "./logo.png";
import AccountCreation from "./AccountCreation.svelte";
import { SnackbarIn } from "m3-svelte/package/containers/Snackbar.svelte";
import { goOffline } from "firebase/database";
enum ReadyState {
Idle,
Connecting,
Connected,
AccountCreation,
}
let state = ReadyState.Idle;
let transport: Transport;
let rtctransport: RTCTransport | undefined;
let email = "";
let password = "";
let connectionState = "";
let showSwarmWarning = false;
let showLogin = false;
let chosenTracker: keyof typeof TrackerList | undefined;
let showTrackerList = false;
let createaccount = false;
let snackbar: (data: SnackbarIn) => void;
async function onTransportOpen() {
console.log("Transport opened");
let connection = new Connection(transport);
// TODO: error handling here
await connection.initialize();
let bare = new AdriftBareClient(connection);
console.log(setBareClientImplementation);
setBareClientImplementation(bare);
state = ReadyState.Connected;
}
function onTransportClose() {
console.warn("Transport closed");
}
function createRTCTransport() {
let transport = new RTCTransport(
onTransportOpen,
onTransportClose,
() => {
connectionState = `Connection ${transport.peer.connectionState}...`;
},
() => {
connectionState = `Signaling ${transport.peer.connectionState}...`;
},
() => {
if (transport.peer.connectionState == "new") {
connectionState = `Creating an offer...`;
} else {
connectionState = `Gathering ${transport.peer.connectionState}...`;
}
}
);
return transport;
}
async function initFirebase() {
if (!chosenTracker) return;
let tracker = TrackerList[chosenTracker];
let app = initializeApp(tracker.firebase);
}
async function connectAccount() {
rtctransport = transport = createRTCTransport();
state = ReadyState.Connecting;
let offer = await rtctransport.createOffer();
connectionState = "Finding your node...";
let answer = await SignalFirebase.signalAccount(JSON.stringify(offer));
connectionState = "Linking to node...";
await new Promise((r) => {
setTimeout(r, 1000);
});
rtctransport.answer(answer.answer, answer.candidates);
}
async function connectSwarm() {
await initFirebase();
state = ReadyState.Connecting;
rtctransport = transport = createRTCTransport();
let offer = await rtctransport.createOffer();
connectionState = "Routing you to an available node...";
try {
let answer = await SignalFirebase.signalSwarm(JSON.stringify(offer));
connectionState = "Linking to node...";
await new Promise((r) => {
setTimeout(r, 500);
});
rtctransport.answer(answer.answer, answer.candidates);
} catch (e) {
console.error(e);
connectionState = e;
}
}
async function connectDevHttp() {
rtctransport = transport = createRTCTransport();
let offer = await rtctransport.createOffer();
console.log("offer created", offer);
console.log(JSON.stringify(offer));
const r = await fetch("http://localhost:3000/connect", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(offer),
});
if (r.status != 200) {
throw new Error("connect: " + r.status + " " + r.statusText);
}
const { answer, candidates } = await r.json();
await rtctransport.answer(answer, candidates);
}
async function connectDevWS() {
transport = new DevWsTransport(onTransportOpen, () =>
console.log("onclose")
);
}
</script>
{#if state == ReadyState.Connected}
<Proxy />
{:else if state == ReadyState.Connecting}
<div class="h-full w-full flex justify-center items-center">
<Card type="outlined">
<div class="flex items-center p-2">
<CircularProgressIndeterminate />
<div class="p-5" />
<h2 class="text-xl">
{connectionState}
</h2>
</div>
<br />
<p class="text-sm opacity-70">
Adrift is routing you to a server available to take your requests.<br
/>The initial connection may take several minutes depending on server
load
</p>
</Card>
</div>
{:else if !import.meta.env.VITE_ADRIFT_DEV}
<div class="flex flex-col h-full">
<div id="topbar" class="flex justify-between items-center p-4">
<div id="logo">
<Card type="">
<div class="flex items-center text-3xl">
<Icon icon="material-symbols:sailing" />
<p class="text-2xl ml-3">Adrift</p>
</div>
</Card>
</div>
<div id="nav" />
<div id="links">
<Card type="elevated">
<div class="flex">
<a href="https://discord.gg/bAgNyGpXSx">
<Icon icon={iconDiscord} class="icon" />
</a>
<spacer />
<a href="https://github.com/MercuryWorkshop/adrift">
<Icon icon={iconGithub} class="icon" />
</a>
</div>
</Card>
</div>
</div>
<div class="flex flex-col flex-1">
<div class="flex m-4">
<Card
type="elevated"
extraOptions={{ class: "m3-container type-elevated w-9/12" }}
>
<div class="w-full">
<h2 class="text-6xl">Surf the web, Adrift</h2>
<h2 class="text-2xl">
A fast and modern decentralized proxy network
</h2>
</div>
<div class="mt-5 flex justify-between">
<Button type="filled" on:click={() => (showTrackerList = true)}
>Start Browsing</Button
>
{#if !import.meta.env.VITE_ADRIFT_SINGLEFILE}
<Button
type="text"
on:click={() => {
downloadShortcut("adrift.html", "Homework");
}}>Get Shortcut</Button
>
{/if}
</div>
</Card>
</div>
<div
class="flex h-full justify-end m-4 transition-all"
class:opacity-0={!showTrackerList}
>
<Card
type="elevated"
extraOptions={{
class: "m3-container type-elevated w-9/12 flex flex-col",
}}
>
<h2 class="text-4xl">Select a Tracker</h2>
<h2 class="text-1xl">Trackers allow you to connect to Adrift</h2>
<div class="mt-5">
{#each Object.keys(TrackerList) as tracker}
<label>
<div class="flex items-center">
<svelte:component this={RadioAnim3}>
<input
type="radio"
id={tracker}
name="tabs"
value={tracker}
bind:group={chosenTracker}
/>
</svelte:component>
<p class="m-3 text-xl">
{tracker}
</p>
</div>
<p>
{TrackerList[tracker].description}
</p>
</label>
{/each}
</div>
<div class="flex-1" />
<div class="mt-5 flex">
{#if chosenTracker}
<Button type="elevated" on:click={() => (showSwarmWarning = true)}
>Connect to the swarm</Button
>
<Button
type="filled"
on:click={async () => {
await initFirebase();
let auth = getAuth();
await setPersistence(auth, browserLocalPersistence);
if (!auth.currentUser) {
showLogin = true;
} else {
await connectAccount();
}
}}>Connect with login</Button
>
{/if}
</div>
<Dialog headline="WARNING" bind:open={showSwarmWarning}>
<h2 class="text-2xl">
TLS has not currently been implemented for the Adrift Swarm. It
will later, but until then your data will not be private, and you
should not enter any sensitive information on any page
</h2>
<br />
<Button type="filled" on:click={() => (showLogin = false)}
>Cancel</Button
>
<Button type="outlined" on:click={connectSwarm}
>I understand, Connect</Button
>
</Dialog>
<Dialog headline="Log in to connect" bind:open={showLogin}>
<button
class="text-primary my-3"
on:click={() => ((createaccount = true), (showLogin = false))}
>New here? Create an account</button
>
<br />
<TextField name="email" bind:value={email} />
<TextField
name="password"
bind:value={password}
extraOptions={{ type: "password" }}
/>
<div class="flex mt-5">
<Button type="outlined" on:click={() => (showLogin = false)}
>Cancel</Button
>
<Button
type="filled"
on:click={async () => {
try {
await signInWithEmailAndPassword(
getAuth(),
email,
password
);
connectAccount();
} catch (e) {
snackbar({ message: e, closable: true });
}
}}>Connect</Button
>
</div>
</Dialog>
<Dialog bind:open={createaccount} headline="Create an account">
<TextField name="email" bind:value={email} />
<TextField
name="password"
bind:value={password}
extraOptions={{ type: "password" }}
/>
<p>
Note: to be able to connect, you'll need to download an exit node
and run it on a computer with an uncensored internet connection
</p>
<div class="flex mt-5">
<Button
type="filled"
on:click={async () => {
try {
await createUserWithEmailAndPassword(
getAuth(),
email,
password
);
createaccount = false;
} catch (e) {
snackbar({ message: e, closable: true });
}
}}>Create Account</Button
>
</div></Dialog
>
</Card>
</div>
<div class="flex m-4">
<Card
type="elevated"
extraOptions={{ class: "m3-container type-elevated w-full" }}
>
<div class="flex space-x-10">
<a class="text-1xl" href="https://mercurywork.shop"
>© 2023 Mercury Workshop</a
>
<div class="space-x-3">
<a class="text-1xl" href="https://discord.gg/bAgNyGpXSx"
>discord</a
>
<a class="text-1xl" href="https://github.com/MercuryWorkshop"
>github</a
>
</div>
<div />
</div>
</Card>
</div>
</div>
</div>
{:else}
<div class="flex items-center justify-center h-full">
<Card type="elevated">
<div class="flex flex-col h-full">
<h2 class="m3-font-headline-large m-3">Adrift DEV</h2>
<div class="flex space-evenly pad-children">
<Button type="filled" on:click={connectDevHttp}
>Connect with WebRTC transport over localhost HTTP signaling</Button
>
<Button type="filled" on:click={connectDevWS}
>Connect with localhost websocket transport</Button
>
</div>
</div>
</Card>
</div>
{/if}
<svelte:component this={SnackbarAnim} bind:show={snackbar} />
<StyleFromScheme
lightScheme={{
primary: 1284831119,
onPrimary: 4294967295,
primaryContainer: 4293516799,
onPrimaryContainer: 4280291399,
inversePrimary: 4291804670,
secondary: 4284636017,
onSecondary: 4294967295,
secondaryContainer: 4293451512,
onSecondaryContainer: 4280162603,
tertiary: 4286468704,
onTertiary: 4294967295,
tertiaryContainer: 4294957539,
onTertiaryContainer: 4281405469,
error: 4290386458,
onError: 4294967295,
errorContainer: 4294957782,
onErrorContainer: 4282449922,
background: 4294834175,
onBackground: 4280097568,
surface: 4294834175,
onSurface: 4280097568,
surfaceVariant: 4293386475,
onSurfaceVariant: 4282991950,
inverseSurface: 4281478965,
inverseOnSurface: 4294307831,
outline: 4286215551,
outlineVariant: 4291478735,
shadow: 4278190080,
scrim: 4278190080,
surfaceDim: 4292794592,
surfaceBright: 4294834175,
surfaceContainerLowest: 4294967295,
surfaceContainerLow: 4294505210,
surfaceContainer: 4294110452,
surfaceContainerHigh: 4293715694,
surfaceContainerHighest: 4293320937,
surfaceTint: 4284831119,
}}
darkScheme={{
primary: 1291804670,
onPrimary: 4281739101,
primaryContainer: 4283252085,
onPrimaryContainer: 4293516799,
inversePrimary: 4284831119,
secondary: 4291543771,
onSecondary: 4281544001,
secondaryContainer: 4283057240,
onSecondaryContainer: 4293451512,
tertiary: 4293900488,
onTertiary: 4283049266,
tertiaryContainer: 4284693320,
onTertiaryContainer: 4294957539,
error: 4294948011,
onError: 4285071365,
errorContainer: 4287823882,
onErrorContainer: 4294957782,
background: 4279505432,
onBackground: 4293320937,
surface: 4279505432,
onSurface: 4293320937,
surfaceVariant: 4282991950,
onSurfaceVariant: 4291478735,
inverseSurface: 4293320937,
inverseOnSurface: 4281478965,
outline: 4287926169,
outlineVariant: 4282991950,
shadow: 4278190080,
scrim: 4278190080,
surfaceDim: 4279505432,
surfaceBright: 4282071102,
surfaceContainerLowest: 4279176467,
surfaceContainerLow: 4280097568,
surfaceContainer: 4280360740,
surfaceContainerHigh: 4281018671,
surfaceContainerHighest: 4281742394,
surfaceTint: 4291804670,
}}
/>
<style>
:global(.icon) {
font-size: 2em;
}
:global(.pad-children > *) {
margin: 2rem;
}
:global(#nav > *) {
padding: 0.5em;
}
spacer {
margin: 1em;
}
:global(body, html, #app) {
width: 100vw;
height: 100vh;
padding: 0;
margin: 0;
background-color: rgb(var(--m3-scheme-background));
color: rgb(var(--m3-scheme-on-background));
}
</style>